File indexing completed on 2024-05-12 09:40:46
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, [this](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() && m_lastNotification.urls() == notification.urls() 0218 && 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 ¬ification) 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 }