File indexing completed on 2024-04-28 16:54:37

0001 /*
0002     SPDX-FileCopyrightText: 2018-2019 Kai Uwe Broulik <kde@privat.broulik.de>
0003 
0004     SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
0005 */
0006 
0007 #include "server_p.h"
0008 
0009 #include "debug.h"
0010 
0011 #include "notificationmanageradaptor.h"
0012 #include "notificationsadaptor.h"
0013 
0014 #include "notification_p.h"
0015 
0016 #include "server.h"
0017 #include "serverinfo.h"
0018 
0019 #include "utils_p.h"
0020 
0021 #include <QDBusConnection>
0022 #include <QDBusServiceWatcher>
0023 
0024 #include <KConfigGroup>
0025 #include <KService>
0026 #include <KSharedConfig>
0027 #include <KUser>
0028 
0029 using namespace NotificationManager;
0030 
0031 ServerPrivate::ServerPrivate(QObject *parent)
0032     : QObject(parent)
0033     , m_inhibitionWatcher(new QDBusServiceWatcher(this))
0034     , m_notificationWatchers(new QDBusServiceWatcher(this))
0035 {
0036     m_inhibitionWatcher->setConnection(QDBusConnection::sessionBus());
0037     m_inhibitionWatcher->setWatchMode(QDBusServiceWatcher::WatchForUnregistration);
0038     connect(m_inhibitionWatcher, &QDBusServiceWatcher::serviceUnregistered, this, &ServerPrivate::onInhibitionServiceUnregistered);
0039 
0040     m_notificationWatchers->setConnection(QDBusConnection::sessionBus());
0041     m_notificationWatchers->setWatchMode(QDBusServiceWatcher::WatchForUnregistration);
0042     connect(m_notificationWatchers, &QDBusServiceWatcher::serviceUnregistered, [=](const QString &service) {
0043         m_notificationWatchers->removeWatchedService(service);
0044     });
0045 }
0046 
0047 ServerPrivate::~ServerPrivate() = default;
0048 
0049 QString ServerPrivate::notificationServiceName()
0050 {
0051     return QStringLiteral("org.freedesktop.Notifications");
0052 }
0053 
0054 QString ServerPrivate::notificationServicePath()
0055 {
0056     return QStringLiteral("/org/freedesktop/Notifications");
0057 }
0058 
0059 QString ServerPrivate::notificationServiceInterface()
0060 {
0061     return notificationServiceName();
0062 }
0063 
0064 ServerInfo *ServerPrivate::currentOwner() const
0065 {
0066     if (!m_currentOwner) {
0067         m_currentOwner.reset(new ServerInfo());
0068     }
0069 
0070     return m_currentOwner.get();
0071 }
0072 
0073 bool ServerPrivate::init()
0074 {
0075     if (m_valid) {
0076         return true;
0077     }
0078 
0079     new NotificationsAdaptor(this);
0080     new NotificationManagerAdaptor(this);
0081 
0082     if (!m_dbusObjectValid) { // if already registered, don't fail here
0083         m_dbusObjectValid = QDBusConnection::sessionBus().registerObject(notificationServicePath(), this);
0084     }
0085 
0086     if (!m_dbusObjectValid) {
0087         qCWarning(NOTIFICATIONMANAGER) << "Failed to register Notification DBus object";
0088         return false;
0089     }
0090 
0091     // Only the "dbus master" (effectively plasmashell) should be the true owner of notifications
0092     const bool master = Utils::isDBusMaster();
0093 
0094     QDBusConnectionInterface *dbusIface = QDBusConnection::sessionBus().interface();
0095 
0096     if (!master) {
0097         // NOTE this connects to whether the application lost ownership of given service
0098         // This is not a wildcard listener for all unregistered services on the bus!
0099         connect(dbusIface, &QDBusConnectionInterface::serviceUnregistered, this, &ServerPrivate::onServiceOwnershipLost, Qt::UniqueConnection);
0100     }
0101 
0102     auto registration = dbusIface->registerService(notificationServiceName(),
0103                                                    master ? QDBusConnectionInterface::ReplaceExistingService : QDBusConnectionInterface::DontQueueService,
0104                                                    master ? QDBusConnectionInterface::DontAllowReplacement : QDBusConnectionInterface::AllowReplacement);
0105     if (registration.value() != QDBusConnectionInterface::ServiceRegistered) {
0106         qCWarning(NOTIFICATIONMANAGER) << "Failed to register Notification service on DBus";
0107         return false;
0108     }
0109 
0110     connect(this, &ServerPrivate::inhibitedChanged, this, &ServerPrivate::onInhibitedChanged, Qt::UniqueConnection);
0111 
0112     qCDebug(NOTIFICATIONMANAGER) << "Registered Notification service on DBus";
0113 
0114     KConfigGroup config(KSharedConfig::openConfig(), QStringLiteral("Notifications"));
0115     const bool broadcastsEnabled = config.readEntry("ListenForBroadcasts", false);
0116 
0117     if (broadcastsEnabled) {
0118         qCDebug(NOTIFICATIONMANAGER) << "Notification server is configured to listen for broadcasts";
0119         // NOTE Keep disconnect() call in onServiceOwnershipLost in sync if you change this!
0120         QDBusConnection::systemBus().connect({},
0121                                              {},
0122                                              QStringLiteral("org.kde.BroadcastNotifications"),
0123                                              QStringLiteral("Notify"),
0124                                              this,
0125                                              SLOT(onBroadcastNotification(QMap<QString, QVariant>)));
0126     }
0127 
0128     m_valid = true;
0129     Q_EMIT validChanged();
0130 
0131     return true;
0132 }
0133 
0134 uint ServerPrivate::Notify(const QString &app_name,
0135                            uint replaces_id,
0136                            const QString &app_icon,
0137                            const QString &summary,
0138                            const QString &body,
0139                            const QStringList &actions,
0140                            const QVariantMap &hints,
0141                            int timeout)
0142 {
0143     const bool wasReplaced = replaces_id > 0;
0144     uint notificationId = 0;
0145     if (wasReplaced) {
0146         notificationId = replaces_id;
0147     } else {
0148         // Avoid wrapping around to 0 in case of overflow
0149         if (!m_highestNotificationId) {
0150             ++m_highestNotificationId;
0151         }
0152         notificationId = m_highestNotificationId;
0153         ++m_highestNotificationId;
0154     }
0155 
0156     Notification notification(notificationId);
0157     notification.setDBusService(message().service());
0158     notification.setSummary(summary);
0159     notification.setBody(body);
0160     notification.setApplicationName(app_name);
0161 
0162     notification.setActions(actions);
0163 
0164     notification.setTimeout(timeout);
0165 
0166     // might override some of the things we set above (like application name)
0167     notification.d->processHints(hints);
0168 
0169     // If we didn't get a pixmap, load the app_icon instead
0170     if (notification.d->image.isNull()) {
0171         notification.setIcon(app_icon);
0172     }
0173 
0174     uint pid = 0;
0175     if (notification.desktopEntry().isEmpty() || notification.applicationName().isEmpty()) {
0176         if (notification.desktopEntry().isEmpty() && notification.applicationName().isEmpty()) {
0177             qCInfo(NOTIFICATIONMANAGER) << "Notification from service" << message().service()
0178                                         << "didn't contain any identification information, this is an application bug!";
0179         }
0180         QDBusReply<uint> pidReply = connection().interface()->servicePid(message().service());
0181         if (pidReply.isValid()) {
0182             pid = pidReply.value();
0183         }
0184     }
0185 
0186     // No desktop entry? Try to read the BAMF_DESKTOP_FILE_HINT in the environment of snaps
0187     if (notification.desktopEntry().isEmpty() && pid > 0) {
0188         const QString desktopEntry = Utils::desktopEntryFromPid(pid);
0189         if (!desktopEntry.isEmpty()) {
0190             qCDebug(NOTIFICATIONMANAGER) << "Resolved notification to be from desktop entry" << desktopEntry;
0191             notification.setDesktopEntry(desktopEntry);
0192 
0193             // No application name? Set it to the service name, which is nicer than the process name fallback below
0194             // Also if the title looks like it's just the desktop entry, use the nicer service name
0195             if (notification.applicationName().isEmpty() || notification.applicationName() == desktopEntry) {
0196                 KService::Ptr service = KService::serviceByDesktopName(desktopEntry);
0197                 if (service) {
0198                     notification.setApplicationName(service->name());
0199                 }
0200             }
0201         }
0202     }
0203 
0204     // No application name? Try to figure out the process name using the sender's PID
0205     if (notification.applicationName().isEmpty() && pid > 0) {
0206         const QString processName = Utils::processNameFromPid(pid);
0207         if (!processName.isEmpty()) {
0208             qCDebug(NOTIFICATIONMANAGER) << "Resolved notification to be from process name" << processName;
0209             notification.setApplicationName(processName);
0210         }
0211     }
0212 
0213     // If multiple identical notifications are sent in quick succession, refuse the request
0214     if (m_lastNotification.applicationName() == notification.applicationName() && m_lastNotification.summary() == notification.summary()
0215         && m_lastNotification.body() == notification.body() && m_lastNotification.desktopEntry() == notification.desktopEntry()
0216         && m_lastNotification.eventId() == notification.eventId() && m_lastNotification.actionNames() == notification.actionNames()
0217         && m_lastNotification.icon() == notification.icon()
0218         && m_lastNotification.urls() == notification.urls() && m_lastNotification.created().msecsTo(notification.created()) < 1000) {
0219         qCDebug(NOTIFICATIONMANAGER) << "Discarding excess notification creation request";
0220 
0221         sendErrorReply(QStringLiteral("org.freedesktop.Notifications.Error.ExcessNotificationGeneration"),
0222                        QStringLiteral("Created too many similar notifications in quick succession"));
0223         return 0;
0224     }
0225 
0226     m_lastNotification = notification;
0227 
0228     if (wasReplaced) {
0229         notification.resetUpdated();
0230         Q_EMIT static_cast<Server *>(parent())->notificationReplaced(replaces_id, notification);
0231     } else {
0232         Q_EMIT static_cast<Server *>(parent())->notificationAdded(notification);
0233     }
0234 
0235     // currently we dispatch all notification, this is ugly
0236     // TODO: come up with proper authentication/user selection
0237     for (const QString &service : m_notificationWatchers->watchedServices()) {
0238         QDBusMessage msg = QDBusMessage::createMethodCall(service,
0239                                                           QStringLiteral("/NotificationWatcher"),
0240                                                           QStringLiteral("org.kde.NotificationWatcher"),
0241                                                           QStringLiteral("Notify"));
0242         msg.setArguments({notificationId,
0243                           notification.applicationName(),
0244                           replaces_id,
0245                           notification.applicationIconName(),
0246                           notification.summary(),
0247                           // we pass raw body data since this data goes through another sanitization
0248                           // in WatchedNotificationsModel when notification object is created.
0249                           notification.rawBody(),
0250                           actions,
0251                           hints,
0252                           notification.timeout()});
0253         QDBusConnection::sessionBus().call(msg, QDBus::NoBlock);
0254     }
0255 
0256     return notificationId;
0257 }
0258 
0259 void ServerPrivate::CloseNotification(uint id)
0260 {
0261     for (const QString &service : m_notificationWatchers->watchedServices()) {
0262         QDBusMessage msg = QDBusMessage::createMethodCall(service,
0263                                                           QStringLiteral("/NotificationWatcher"),
0264                                                           QStringLiteral("org.kde.NotificationWatcher"),
0265                                                           QStringLiteral("CloseNotification"));
0266         msg.setArguments({id});
0267         QDBusConnection::sessionBus().call(msg, QDBus::NoBlock);
0268     }
0269     // spec says "If the notification no longer exists, an empty D-BUS Error message is sent back."
0270     static_cast<Server *>(parent())->closeNotification(id, Server::CloseReason::Revoked);
0271 }
0272 
0273 QStringList ServerPrivate::GetCapabilities() const
0274 {
0275     // should this be configurable somehow so the UI can tell what it implements?
0276     return QStringList{QStringLiteral("body"),
0277                        QStringLiteral("body-hyperlinks"),
0278                        QStringLiteral("body-markup"),
0279                        QStringLiteral("body-images"),
0280                        QStringLiteral("icon-static"),
0281                        QStringLiteral("actions"),
0282                        QStringLiteral("persistence"),
0283                        QStringLiteral("inline-reply"),
0284 
0285                        QStringLiteral("x-kde-urls"),
0286                        QStringLiteral("x-kde-origin-name"),
0287                        QStringLiteral("x-kde-display-appname"),
0288 
0289                        QStringLiteral("inhibitions")};
0290 }
0291 
0292 QString ServerPrivate::GetServerInformation(QString &vendor, QString &version, QString &specVersion) const
0293 {
0294     vendor = QStringLiteral("KDE");
0295     version = QLatin1String(PROJECT_VERSION);
0296     specVersion = QStringLiteral("1.2");
0297     return QStringLiteral("Plasma");
0298 }
0299 
0300 void ServerPrivate::onBroadcastNotification(const QMap<QString, QVariant> &properties)
0301 {
0302     qCDebug(NOTIFICATIONMANAGER) << "Received broadcast notification";
0303 
0304     const auto currentUserId = KUserId::currentEffectiveUserId().nativeId();
0305 
0306     // a QVariantList with ints arrives as QDBusArgument here, using a QStringList for simplicity
0307     const QStringList &userIds = properties.value(QStringLiteral("uids")).toStringList();
0308     if (!userIds.isEmpty()) {
0309         auto it = std::find_if(userIds.constBegin(), userIds.constEnd(), [currentUserId](const QVariant &id) {
0310             bool ok;
0311             auto uid = id.toString().toLongLong(&ok);
0312             return ok && uid == currentUserId;
0313         });
0314 
0315         if (it == userIds.constEnd()) {
0316             qCDebug(NOTIFICATIONMANAGER) << "It is not meant for us, ignoring";
0317             return;
0318         }
0319     }
0320 
0321     bool ok;
0322     int timeout = properties.value(QStringLiteral("timeout")).toInt(&ok);
0323     if (!ok) {
0324         timeout = -1; // -1 = server default, 0 would be "persistent"
0325     }
0326 
0327     Notify(properties.value(QStringLiteral("appName")).toString(),
0328            0, // replaces_id
0329            properties.value(QStringLiteral("appIcon")).toString(),
0330            properties.value(QStringLiteral("summary")).toString(),
0331            properties.value(QStringLiteral("body")).toString(),
0332            {}, // no actions
0333            properties.value(QStringLiteral("hints")).toMap(),
0334            timeout);
0335 }
0336 
0337 uint ServerPrivate::add(const Notification &notification)
0338 {
0339     // TODO check if notification with ID already exists and signal update instead
0340     if (notification.id() == 0) {
0341         ++m_highestNotificationId;
0342         notification.d->id = m_highestNotificationId;
0343 
0344         Q_EMIT static_cast<Server *>(parent())->notificationAdded(notification);
0345     } else {
0346         Q_EMIT static_cast<Server *>(parent())->notificationReplaced(notification.id(), notification);
0347     }
0348 
0349     return notification.id();
0350 }
0351 
0352 void ServerPrivate::sendReplyText(const QString &dbusService, uint notificationId, const QString &text, Notifications::InvokeBehavior behavior)
0353 {
0354     if (dbusService.isEmpty()) {
0355         qCWarning(NOTIFICATIONMANAGER) << "Sending notification reply text for notification" << notificationId << "untargeted";
0356     }
0357 
0358     QDBusMessage msg =
0359         QDBusMessage::createTargetedSignal(dbusService, notificationServicePath(), notificationServiceName(), QStringLiteral("NotificationReplied"));
0360     msg.setArguments({notificationId, text});
0361     QDBusConnection::sessionBus().send(msg);
0362 
0363     if (behavior & Notifications::Close) {
0364         Q_EMIT CloseNotification(notificationId);
0365     }
0366 }
0367 
0368 uint ServerPrivate::Inhibit(const QString &desktop_entry, const QString &reason, const QVariantMap &hints)
0369 {
0370     const QString dbusService = message().service();
0371 
0372     QString applicationName = desktop_entry;
0373 
0374     qCDebug(NOTIFICATIONMANAGER) << "Request inhibit from service" << dbusService << "which is" << desktop_entry << "with reason" << reason;
0375 
0376     // xdg-desktop-portal forwards appId only for sandboxed apps it can trust
0377     // Resolve it to process name here to at least have something, even if that means showing "xdg-desktop-portal-kde is currently..."
0378     if (desktop_entry.isEmpty()) {
0379         QDBusReply<uint> pidReply = connection().interface()->servicePid(message().service());
0380         if (pidReply.isValid()) {
0381             const auto pid = pidReply.value();
0382 
0383             const QString processName = Utils::processNameFromPid(pid);
0384             if (!processName.isEmpty()) {
0385                 qCDebug(NOTIFICATIONMANAGER) << "Resolved inhibition to be from process name" << processName;
0386                 applicationName = processName;
0387             }
0388         }
0389     } else {
0390         KService::Ptr service = KService::serviceByDesktopName(desktop_entry);
0391         if (service) {
0392             applicationName = service->name();
0393         }
0394     }
0395 
0396     if (applicationName.isEmpty()) {
0397         sendErrorReply(QDBusError::InvalidArgs, QStringLiteral("No meaningful desktop_entry provided"));
0398         return 0;
0399     }
0400 
0401     m_inhibitionWatcher->addWatchedService(dbusService);
0402 
0403     ++m_highestInhibitionCookie;
0404 
0405     const bool oldExternalInhibited = externalInhibited();
0406 
0407     m_externalInhibitions.insert(m_highestInhibitionCookie, {desktop_entry, applicationName, reason, hints});
0408 
0409     m_inhibitionServices.insert(m_highestInhibitionCookie, dbusService);
0410 
0411     if (externalInhibited() != oldExternalInhibited) {
0412         Q_EMIT externalInhibitedChanged();
0413     }
0414     Q_EMIT externalInhibitionsChanged();
0415 
0416     return m_highestInhibitionCookie;
0417 }
0418 
0419 void ServerPrivate::onServiceOwnershipLost(const QString &serviceName)
0420 {
0421     if (serviceName != notificationServiceName()) {
0422         return;
0423     }
0424 
0425     qCDebug(NOTIFICATIONMANAGER) << "Lost ownership of" << serviceName << "service";
0426 
0427     disconnect(QDBusConnection::sessionBus().interface(), &QDBusConnectionInterface::serviceUnregistered, this, &ServerPrivate::onServiceOwnershipLost);
0428     disconnect(this, &ServerPrivate::inhibitedChanged, this, &ServerPrivate::onInhibitedChanged);
0429 
0430     QDBusConnection::systemBus().disconnect({},
0431                                             {},
0432                                             QStringLiteral("org.kde.BroadcastNotifications"),
0433                                             QStringLiteral("Notify"),
0434                                             this,
0435                                             SLOT(onBroadcastNotification(QMap<QString, QVariant>)));
0436 
0437     m_valid = false;
0438 
0439     Q_EMIT validChanged();
0440     Q_EMIT serviceOwnershipLost();
0441 }
0442 
0443 void ServerPrivate::onInhibitionServiceUnregistered(const QString &serviceName)
0444 {
0445     qCDebug(NOTIFICATIONMANAGER) << "Inhibition service unregistered" << serviceName;
0446 
0447     const QList<uint> cookies = m_inhibitionServices.keys(serviceName);
0448     if (cookies.isEmpty()) {
0449         qCInfo(NOTIFICATIONMANAGER) << "Unknown inhibition service unregistered" << serviceName;
0450         return;
0451     }
0452 
0453     // We do lookups in there again...
0454     for (uint cookie : cookies) {
0455         UnInhibit(cookie);
0456     }
0457 }
0458 
0459 void ServerPrivate::onInhibitedChanged()
0460 {
0461     // Q_EMIT DBus change signal...
0462     QDBusMessage signal =
0463         QDBusMessage::createSignal(notificationServicePath(), QStringLiteral("org.freedesktop.DBus.Properties"), QStringLiteral("PropertiesChanged"));
0464 
0465     signal.setArguments({
0466         notificationServiceInterface(),
0467         QVariantMap{
0468             // updated
0469             {QStringLiteral("Inhibited"), inhibited()},
0470         },
0471         QStringList() // invalidated
0472     });
0473 
0474     QDBusConnection::sessionBus().send(signal);
0475 }
0476 
0477 void ServerPrivate::UnInhibit(uint cookie)
0478 {
0479     qCDebug(NOTIFICATIONMANAGER) << "Request release inhibition for cookie" << cookie;
0480 
0481     const QString service = m_inhibitionServices.value(cookie);
0482     if (service.isEmpty()) {
0483         qCInfo(NOTIFICATIONMANAGER) << "Requested to release inhibition with cookie" << cookie << "that doesn't exist";
0484         // TODO if called from dbus raise error
0485         return;
0486     }
0487 
0488     m_inhibitionWatcher->removeWatchedService(service);
0489     m_externalInhibitions.remove(cookie);
0490     m_inhibitionServices.remove(cookie);
0491 
0492     if (m_externalInhibitions.isEmpty()) {
0493         Q_EMIT externalInhibitedChanged();
0494     }
0495     Q_EMIT externalInhibitionsChanged();
0496 }
0497 
0498 QList<Inhibition> ServerPrivate::externalInhibitions() const
0499 {
0500     return m_externalInhibitions.values();
0501 }
0502 
0503 bool ServerPrivate::inhibited() const
0504 {
0505     return m_inhibited;
0506 }
0507 
0508 void ServerPrivate::setInhibited(bool inhibited)
0509 {
0510     if (m_inhibited != inhibited) {
0511         m_inhibited = inhibited;
0512         Q_EMIT inhibitedChanged();
0513     }
0514 }
0515 
0516 bool ServerPrivate::externalInhibited() const
0517 {
0518     return !m_externalInhibitions.isEmpty();
0519 }
0520 
0521 void ServerPrivate::clearExternalInhibitions()
0522 {
0523     if (m_externalInhibitions.isEmpty()) {
0524         return;
0525     }
0526 
0527     m_inhibitionWatcher->setWatchedServices(QStringList()); // remove all watches
0528     m_inhibitionServices.clear();
0529     m_externalInhibitions.clear();
0530 
0531     Q_EMIT externalInhibitedChanged();
0532     Q_EMIT externalInhibitionsChanged();
0533 }
0534 
0535 void ServerPrivate::RegisterWatcher()
0536 {
0537     m_notificationWatchers->addWatchedService(message().service());
0538 }
0539 
0540 void ServerPrivate::UnRegisterWatcher()
0541 {
0542     m_notificationWatchers->removeWatchedService(message().service());
0543 }
0544 
0545 void ServerPrivate::InvokeAction(uint id, const QString &actionKey)
0546 {
0547     Q_EMIT ActionInvoked(id, actionKey);
0548 }