File indexing completed on 2024-04-28 04:57:10

0001 /**
0002  * SPDX-FileCopyrightText: 2015 Holger Kaelberer <holger.k@elberer.de>
0003  * SPDX-FileCopyrightText: 2018 Richard Liebscher <richard.liebscher@gmail.com>
0004  *
0005  * SPDX-License-Identifier: GPL-2.0-or-later
0006  */
0007 
0008 #include "dbusnotificationslistener.h"
0009 
0010 #include <limits>
0011 
0012 #include <QBuffer>
0013 #include <QFile>
0014 #include <QImage>
0015 
0016 #include <kiconloader.h>
0017 #include <kicontheme.h>
0018 
0019 #include "plugin_sendnotifications_debug.h"
0020 #include <core/kdeconnectplugin.h>
0021 #include <core/kdeconnectpluginconfig.h>
0022 
0023 namespace
0024 {
0025 // https://specifications.freedesktop.org/notification-spec/notification-spec-latest.html
0026 inline constexpr const char *NOTIFY_SIGNATURE = "susssasa{sv}i";
0027 inline constexpr const char *IMAGE_DATA_SIGNATURE = "iiibiiay";
0028 
0029 QString becomeMonitor(DBusConnection *conn, const char *match)
0030 {
0031     // message
0032     DBusMessage *msg = dbus_message_new_method_call(DBUS_SERVICE_DBUS, DBUS_PATH_DBUS, DBUS_INTERFACE_MONITORING, "BecomeMonitor");
0033     Q_ASSERT(msg != nullptr);
0034 
0035     // arguments
0036     const char *matches[] = {match};
0037     const char **matches_ = matches;
0038     dbus_uint32_t flags = 0;
0039 
0040     bool success = dbus_message_append_args(msg, DBUS_TYPE_ARRAY, DBUS_TYPE_STRING, &matches_, 1, DBUS_TYPE_UINT32, &flags, DBUS_TYPE_INVALID);
0041     if (!success) {
0042         dbus_message_unref(msg);
0043         return QStringLiteral("Failed to call dbus_message_append_args");
0044     }
0045 
0046     // send
0047     // TODO: wait and check for error: dbus_connection_send_with_reply_and_block
0048     success = dbus_connection_send(conn, msg, nullptr);
0049     if (!success) {
0050         dbus_message_unref(msg);
0051         return QStringLiteral("Failed to call dbus_connection_send");
0052     }
0053 
0054     dbus_message_unref(msg);
0055 
0056     return QString();
0057 }
0058 
0059 extern "C" DBusHandlerResult handleMessageFromC(DBusConnection *, DBusMessage *message, void *user_data)
0060 {
0061     auto *self = static_cast<DBusNotificationsListenerThread *>(user_data);
0062     if (dbus_message_is_method_call(message, "org.freedesktop.Notifications", "Notify")) {
0063         self->handleNotifyCall(message);
0064     }
0065     // Monitors must not allow libdbus to reply to messages, so we eat the message.
0066     return DBUS_HANDLER_RESULT_HANDLED;
0067 }
0068 
0069 unsigned nextUnsigned(DBusMessageIter *iter)
0070 {
0071     Q_ASSERT(dbus_message_iter_get_arg_type(iter) == DBUS_TYPE_UINT32);
0072     DBusBasicValue value;
0073     dbus_message_iter_get_basic(iter, &value);
0074     dbus_message_iter_next(iter);
0075     return value.u32;
0076 }
0077 
0078 int nextInt(DBusMessageIter *iter)
0079 {
0080     Q_ASSERT(dbus_message_iter_get_arg_type(iter) == DBUS_TYPE_INT32);
0081     DBusBasicValue value;
0082     dbus_message_iter_get_basic(iter, &value);
0083     dbus_message_iter_next(iter);
0084     return value.i32;
0085 }
0086 
0087 QString nextString(DBusMessageIter *iter)
0088 {
0089     Q_ASSERT(dbus_message_iter_get_arg_type(iter) == DBUS_TYPE_STRING);
0090     DBusBasicValue value;
0091     dbus_message_iter_get_basic(iter, &value);
0092     dbus_message_iter_next(iter);
0093     return QString::fromUtf8(value.str);
0094 }
0095 
0096 QStringList nextStringList(DBusMessageIter *iter)
0097 {
0098     DBusMessageIter sub;
0099     dbus_message_iter_recurse(iter, &sub);
0100     dbus_message_iter_next(iter);
0101 
0102     QStringList list;
0103     while (dbus_message_iter_get_arg_type(&sub) != DBUS_TYPE_INVALID) {
0104         list.append(nextString(&sub));
0105     }
0106     return list;
0107 }
0108 
0109 QVariant nextVariant(DBusMessageIter *iter)
0110 {
0111     int type = dbus_message_iter_get_arg_type(iter);
0112     if (type != DBUS_TYPE_VARIANT) {
0113         return QVariant();
0114     }
0115 
0116     DBusMessageIter sub;
0117     dbus_message_iter_recurse(iter, &sub);
0118     dbus_message_iter_next(iter);
0119 
0120     type = dbus_message_iter_get_arg_type(&sub);
0121     if (dbus_type_is_basic(type)) {
0122         DBusBasicValue value;
0123         dbus_message_iter_get_basic(&sub, &value);
0124         switch (type) {
0125         case DBUS_TYPE_BOOLEAN:
0126             return QVariant(value.bool_val);
0127         case DBUS_TYPE_INT16:
0128             return QVariant(value.i16);
0129         case DBUS_TYPE_INT32:
0130             return QVariant(value.i32);
0131         case DBUS_TYPE_INT64:
0132             return QVariant((qlonglong)value.i64);
0133         case DBUS_TYPE_UINT16:
0134             return QVariant(value.u16);
0135         case DBUS_TYPE_UINT32:
0136             return QVariant(value.u32);
0137         case DBUS_TYPE_UINT64:
0138             return QVariant((qulonglong)value.u64);
0139         case DBUS_TYPE_BYTE:
0140             return QVariant(value.byt);
0141         case DBUS_TYPE_DOUBLE:
0142             return QVariant(value.dbl);
0143         case DBUS_TYPE_STRING:
0144             return QVariant(QString::fromUtf8(value.str));
0145         case DBUS_STRUCT_BEGIN_CHAR: {
0146         }
0147         default:
0148             break;
0149         }
0150     }
0151 
0152     qCWarning(KDECONNECT_PLUGIN_SENDNOTIFICATIONS) << "Unimplemented conversation of type" << QChar(type) << type;
0153 
0154     return QVariant();
0155 }
0156 
0157 static QVariantMap nextVariantMap(DBusMessageIter *iter)
0158 {
0159     DBusMessageIter sub;
0160     dbus_message_iter_recurse(iter, &sub);
0161     dbus_message_iter_next(iter);
0162 
0163     QVariantMap map;
0164     while (dbus_message_iter_get_arg_type(&sub) != DBUS_TYPE_INVALID) {
0165         DBusMessageIter entry;
0166         dbus_message_iter_recurse(&sub, &entry);
0167         dbus_message_iter_next(&sub);
0168         QString key = nextString(&entry);
0169         QVariant value = nextVariant(&entry);
0170         map.insert(key, value);
0171     }
0172     return map;
0173 }
0174 }
0175 
0176 void DBusNotificationsListenerThread::run()
0177 {
0178     DBusError err = DBUS_ERROR_INIT;
0179     m_connection = dbus_bus_get_private(DBUS_BUS_SESSION, &err);
0180     if (dbus_error_is_set(&err)) {
0181         qCWarning(KDECONNECT_PLUGIN_SENDNOTIFICATIONS) << "D-Bus connection failed" << err.message;
0182         dbus_error_free(&err);
0183         return;
0184     }
0185 
0186     Q_ASSERT(m_connection != nullptr);
0187 
0188     dbus_connection_set_route_peer_messages(m_connection, true);
0189     dbus_connection_set_exit_on_disconnect(m_connection, false);
0190     dbus_connection_add_filter(m_connection, handleMessageFromC, this, nullptr);
0191 
0192     QString error = becomeMonitor(m_connection,
0193                                   "interface='org.freedesktop.Notifications',"
0194                                   "member='Notify'");
0195 
0196     if (!error.isEmpty()) {
0197         qCWarning(KDECONNECT_PLUGIN_SENDNOTIFICATIONS).noquote() << "Failed to become a DBus monitor."
0198                                                                  << "No notifictions will be sent. Error:" << error;
0199     }
0200 
0201     // wake up every minute to see if we are still connected
0202     while (m_connection != nullptr) {
0203         dbus_connection_read_write_dispatch(m_connection, 60 * 1000);
0204     }
0205 
0206     deleteLater();
0207 }
0208 
0209 void DBusNotificationsListenerThread::stop()
0210 {
0211     if (m_connection) {
0212         dbus_connection_close(m_connection);
0213         dbus_connection_unref(m_connection);
0214         m_connection = nullptr;
0215     }
0216 }
0217 
0218 void DBusNotificationsListenerThread::handleNotifyCall(DBusMessage *message)
0219 {
0220     DBusMessageIter iter;
0221     dbus_message_iter_init(message, &iter);
0222 
0223     if (!dbus_message_has_signature(message, NOTIFY_SIGNATURE)) {
0224         qCWarning(KDECONNECT_PLUGIN_SENDNOTIFICATIONS).nospace()
0225             << "Call to Notify has wrong signature. Expected " << NOTIFY_SIGNATURE << ", got " << dbus_message_get_signature(message);
0226         return;
0227     }
0228 
0229     QString appName = nextString(&iter);
0230     uint replacesId = nextUnsigned(&iter);
0231     QString appIcon = nextString(&iter);
0232     QString summary = nextString(&iter);
0233     QString body = nextString(&iter);
0234     QStringList actions = nextStringList(&iter);
0235     QVariantMap hints = nextVariantMap(&iter);
0236     int timeout = nextInt(&iter);
0237 
0238     Q_EMIT notificationReceived(appName, replacesId, appIcon, summary, body, actions, hints, timeout);
0239 }
0240 
0241 DBusNotificationsListener::DBusNotificationsListener(KdeConnectPlugin *aPlugin)
0242     : NotificationsListener(aPlugin)
0243     , m_thread(new DBusNotificationsListenerThread)
0244 {
0245     connect(m_thread, &DBusNotificationsListenerThread::notificationReceived, this, &DBusNotificationsListener::onNotify);
0246     m_thread->start();
0247 }
0248 
0249 DBusNotificationsListener::~DBusNotificationsListener()
0250 {
0251     m_thread->stop();
0252     m_thread->quit();
0253 }
0254 
0255 void DBusNotificationsListener::onNotify(const QString &appName,
0256                                          uint replacesId,
0257                                          const QString &appIcon,
0258                                          const QString &summary,
0259                                          const QString &body,
0260                                          const QStringList &actions,
0261                                          const QVariantMap &hints,
0262                                          int timeout)
0263 {
0264     Q_UNUSED(actions);
0265 
0266     // qCDebug(KDECONNECT_PLUGIN_SENDNOTIFICATIONS) << "Got notification appName=" << appName << "replacesId=" << replacesId
0267     // << "appIcon=" << appIcon << "summary=" << summary << "body=" << body << "actions=" << actions << "hints=" << hints << "timeout=" << timeout;
0268 
0269     auto *config = m_plugin->config();
0270     if (timeout > 0 && config->getBool(QStringLiteral("generalPersistent"), false)) {
0271         return;
0272     }
0273 
0274     if (!checkApplicationName(appName, appIcon)) {
0275         return;
0276     }
0277 
0278     int urgency = -1;
0279     auto urgencyHint = hints.constFind(QStringLiteral("urgency"));
0280     if (urgencyHint != hints.cend()) {
0281         bool ok = false;
0282         urgency = urgencyHint->toInt(&ok);
0283         if (!ok) {
0284             urgency = -1;
0285         }
0286     }
0287     if (urgency > -1 && urgency < config->getInt(QStringLiteral("generalUrgency"), 0)) {
0288         return;
0289     }
0290 
0291     if (summary.isEmpty()) {
0292         return;
0293     }
0294 
0295     const bool includeBody = config->getBool(QStringLiteral("generalIncludeBody"), true);
0296 
0297     QString ticker = summary;
0298     if (!body.isEmpty() && includeBody) {
0299         ticker += QLatin1String(": ") + body;
0300     }
0301 
0302     if (checkIsInBlacklist(appName, ticker)) {
0303         return;
0304     }
0305 
0306     // qCDebug(KDECONNECT_PLUGIN_SENDNOTIFICATIONS) << "Sending notification from" << appName << ":" <<ticker << "; appIcon=" << appIcon;
0307 
0308     static unsigned id = 0;
0309     if (id == std::numeric_limits<unsigned>::max()) {
0310         id = 0;
0311     }
0312     NetworkPacket np(PACKET_TYPE_NOTIFICATION,
0313                      {
0314                          {QStringLiteral("id"), replacesId > 0 ? replacesId : id++},
0315                          {QStringLiteral("appName"), appName},
0316                          {QStringLiteral("ticker"), ticker},
0317                          {QStringLiteral("isClearable"), timeout == -1},
0318                          {QStringLiteral("title"), summary},
0319                          {QStringLiteral("silent"), false},
0320                      });
0321 
0322     if (!body.isEmpty() && includeBody) {
0323         np.set(QStringLiteral("text"), body);
0324     }
0325 
0326     // Only send icon on first notify (replacesId == 0)
0327     if (config->getBool(QStringLiteral("generalSynchronizeIcons"), true) && replacesId == 0) {
0328         QSharedPointer<QIODevice> iconSource;
0329         // try different image sources according to priorities in notifications-spec version 1.2:
0330         auto it = hints.constFind(QStringLiteral("image-data"));
0331         if (it != hints.cend() || (it = hints.constFind(QStringLiteral("image_data"))) != hints.cend()) {
0332             iconSource = iconForImageData(it.value());
0333         } else if ((it = hints.constFind(QStringLiteral("image-path"))) != hints.cend()
0334                    || (it = hints.constFind(QStringLiteral("image_path"))) != hints.cend()) {
0335             iconSource = iconForIconName(it.value().toString());
0336         } else if (!appIcon.isEmpty()) {
0337             iconSource = iconForIconName(appIcon);
0338         } else if ((it = hints.constFind(QStringLiteral("icon_data"))) != hints.cend()) {
0339             iconSource = iconForImageData(it.value());
0340         }
0341         if (iconSource) {
0342             np.setPayload(iconSource, iconSource->size());
0343         }
0344     }
0345 
0346     m_plugin->sendPacket(np);
0347 }
0348 
0349 bool DBusNotificationsListener::parseImageDataArgument(const QVariant &argument,
0350                                                        int &width,
0351                                                        int &height,
0352                                                        int &rowStride,
0353                                                        int &bitsPerSample,
0354                                                        int &channels,
0355                                                        bool &hasAlpha,
0356                                                        QByteArray &imageData) const
0357 {
0358     // FIXME
0359     // if (!argument.canConvert<QDBusArgument>()) {
0360     //     return false;
0361     // }
0362     // const QDBusArgument dbusArg = argument.value<QDBusArgument>();
0363     // dbusArg.beginStructure();
0364     // dbusArg >> width >> height >> rowStride >> hasAlpha >> bitsPerSample >> channels >> imageData;
0365     // dbusArg.endStructure();
0366     return true;
0367 }
0368 
0369 QSharedPointer<QIODevice> DBusNotificationsListener::iconForImageData(const QVariant &argument) const
0370 {
0371     int width, height, rowStride, bitsPerSample, channels;
0372     bool hasAlpha;
0373     QByteArray imageData;
0374 
0375     if (!parseImageDataArgument(argument, width, height, rowStride, bitsPerSample, channels, hasAlpha, imageData))
0376         return QSharedPointer<QIODevice>();
0377 
0378     if (bitsPerSample != 8) {
0379         qCWarning(KDECONNECT_PLUGIN_SENDNOTIFICATIONS) << "Unsupported image format:"
0380                                                        << "width=" << width << "height=" << height << "rowStride=" << rowStride
0381                                                        << "bitsPerSample=" << bitsPerSample << "channels=" << channels << "hasAlpha=" << hasAlpha;
0382         return QSharedPointer<QIODevice>();
0383     }
0384 
0385     QImage image(reinterpret_cast<uchar *>(imageData.data()), width, height, rowStride, hasAlpha ? QImage::Format_ARGB32 : QImage::Format_RGB32);
0386     if (hasAlpha) {
0387         image = std::move(image).rgbSwapped(); // RGBA --> ARGB
0388     }
0389 
0390     QSharedPointer<QIODevice> buffer = iconFromQImage(image);
0391     if (!buffer) {
0392         qCWarning(KDECONNECT_PLUGIN_SENDNOTIFICATIONS) << "Could not initialize image buffer";
0393         return QSharedPointer<QIODevice>();
0394     }
0395 
0396     return buffer;
0397 }
0398 
0399 QSharedPointer<QIODevice> DBusNotificationsListener::iconForIconName(const QString &iconName) const
0400 {
0401     int size = KIconLoader::SizeHuge; // use big size to allow for good quality on high-DPI mobile devices
0402     QString iconPath = iconName;
0403     if (!QFile::exists(iconName)) {
0404         const KIconTheme *iconTheme = KIconLoader::global()->theme();
0405         if (iconTheme) {
0406             iconPath = iconTheme->iconPath(iconName + QLatin1String(".png"), size, KIconLoader::MatchBest);
0407             if (iconPath.isEmpty()) {
0408                 iconPath = iconTheme->iconPath(iconName + QLatin1String(".svg"), size, KIconLoader::MatchBest);
0409                 if (iconPath.isEmpty()) {
0410                     iconPath = iconTheme->iconPath(iconName + QLatin1String(".svgz"), size, KIconLoader::MatchBest);
0411                 }
0412             }
0413         } else {
0414             qCWarning(KDECONNECT_PLUGIN_SENDNOTIFICATIONS) << "KIconLoader has no theme set";
0415         }
0416     }
0417     if (iconPath.isEmpty()) {
0418         qCWarning(KDECONNECT_PLUGIN_SENDNOTIFICATIONS) << "Could not find notification icon:" << iconName;
0419         return QSharedPointer<QIODevice>();
0420     } else if (iconPath.endsWith(QLatin1String(".png"))) {
0421         return QSharedPointer<QIODevice>(new QFile(iconPath));
0422     } else {
0423         // TODO: cache icons
0424         return iconFromQImage(QImage(iconPath));
0425     }
0426 }
0427 
0428 #include "moc_dbusnotificationslistener.cpp"