forked from Qortal/Brooklyn
548 lines
20 KiB
C++
548 lines
20 KiB
C++
/*
|
|
SPDX-FileCopyrightText: 2018-2019 Kai Uwe Broulik <kde@privat.broulik.de>
|
|
|
|
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
|
|
*/
|
|
|
|
#include "server_p.h"
|
|
|
|
#include "debug.h"
|
|
|
|
#include "notificationmanageradaptor.h"
|
|
#include "notificationsadaptor.h"
|
|
|
|
#include "notification_p.h"
|
|
|
|
#include "server.h"
|
|
#include "serverinfo.h"
|
|
|
|
#include "utils_p.h"
|
|
|
|
#include <QDBusConnection>
|
|
#include <QDBusServiceWatcher>
|
|
|
|
#include <KConfigGroup>
|
|
#include <KService>
|
|
#include <KSharedConfig>
|
|
#include <KUser>
|
|
|
|
using namespace NotificationManager;
|
|
|
|
ServerPrivate::ServerPrivate(QObject *parent)
|
|
: QObject(parent)
|
|
, m_inhibitionWatcher(new QDBusServiceWatcher(this))
|
|
, m_notificationWatchers(new QDBusServiceWatcher(this))
|
|
{
|
|
m_inhibitionWatcher->setConnection(QDBusConnection::sessionBus());
|
|
m_inhibitionWatcher->setWatchMode(QDBusServiceWatcher::WatchForUnregistration);
|
|
connect(m_inhibitionWatcher, &QDBusServiceWatcher::serviceUnregistered, this, &ServerPrivate::onInhibitionServiceUnregistered);
|
|
|
|
m_notificationWatchers->setConnection(QDBusConnection::sessionBus());
|
|
m_notificationWatchers->setWatchMode(QDBusServiceWatcher::WatchForUnregistration);
|
|
connect(m_notificationWatchers, &QDBusServiceWatcher::serviceUnregistered, [=](const QString &service) {
|
|
m_notificationWatchers->removeWatchedService(service);
|
|
});
|
|
}
|
|
|
|
ServerPrivate::~ServerPrivate() = default;
|
|
|
|
QString ServerPrivate::notificationServiceName()
|
|
{
|
|
return QStringLiteral("org.freedesktop.Notifications");
|
|
}
|
|
|
|
QString ServerPrivate::notificationServicePath()
|
|
{
|
|
return QStringLiteral("/org/freedesktop/Notifications");
|
|
}
|
|
|
|
QString ServerPrivate::notificationServiceInterface()
|
|
{
|
|
return notificationServiceName();
|
|
}
|
|
|
|
ServerInfo *ServerPrivate::currentOwner() const
|
|
{
|
|
if (!m_currentOwner) {
|
|
m_currentOwner.reset(new ServerInfo());
|
|
}
|
|
|
|
return m_currentOwner.data();
|
|
}
|
|
|
|
bool ServerPrivate::init()
|
|
{
|
|
if (m_valid) {
|
|
return true;
|
|
}
|
|
|
|
new NotificationsAdaptor(this);
|
|
new NotificationManagerAdaptor(this);
|
|
|
|
if (!m_dbusObjectValid) { // if already registered, don't fail here
|
|
m_dbusObjectValid = QDBusConnection::sessionBus().registerObject(notificationServicePath(), this);
|
|
}
|
|
|
|
if (!m_dbusObjectValid) {
|
|
qCWarning(NOTIFICATIONMANAGER) << "Failed to register Notification DBus object";
|
|
return false;
|
|
}
|
|
|
|
// Only the "dbus master" (effectively plasmashell) should be the true owner of notifications
|
|
const bool master = Utils::isDBusMaster();
|
|
|
|
QDBusConnectionInterface *dbusIface = QDBusConnection::sessionBus().interface();
|
|
|
|
if (!master) {
|
|
// NOTE this connects to whether the application lost ownership of given service
|
|
// This is not a wildcard listener for all unregistered services on the bus!
|
|
connect(dbusIface, &QDBusConnectionInterface::serviceUnregistered, this, &ServerPrivate::onServiceOwnershipLost, Qt::UniqueConnection);
|
|
}
|
|
|
|
auto registration = dbusIface->registerService(notificationServiceName(),
|
|
master ? QDBusConnectionInterface::ReplaceExistingService : QDBusConnectionInterface::DontQueueService,
|
|
master ? QDBusConnectionInterface::DontAllowReplacement : QDBusConnectionInterface::AllowReplacement);
|
|
if (registration.value() != QDBusConnectionInterface::ServiceRegistered) {
|
|
qCWarning(NOTIFICATIONMANAGER) << "Failed to register Notification service on DBus";
|
|
return false;
|
|
}
|
|
|
|
connect(this, &ServerPrivate::inhibitedChanged, this, &ServerPrivate::onInhibitedChanged, Qt::UniqueConnection);
|
|
|
|
qCDebug(NOTIFICATIONMANAGER) << "Registered Notification service on DBus";
|
|
|
|
KConfigGroup config(KSharedConfig::openConfig(), QStringLiteral("Notifications"));
|
|
const bool broadcastsEnabled = config.readEntry("ListenForBroadcasts", false);
|
|
|
|
if (broadcastsEnabled) {
|
|
qCDebug(NOTIFICATIONMANAGER) << "Notification server is configured to listen for broadcasts";
|
|
// NOTE Keep disconnect() call in onServiceOwnershipLost in sync if you change this!
|
|
QDBusConnection::systemBus().connect({},
|
|
{},
|
|
QStringLiteral("org.kde.BroadcastNotifications"),
|
|
QStringLiteral("Notify"),
|
|
this,
|
|
SLOT(onBroadcastNotification(QMap<QString, QVariant>)));
|
|
}
|
|
|
|
m_valid = true;
|
|
Q_EMIT validChanged();
|
|
|
|
return true;
|
|
}
|
|
|
|
uint ServerPrivate::Notify(const QString &app_name,
|
|
uint replaces_id,
|
|
const QString &app_icon,
|
|
const QString &summary,
|
|
const QString &body,
|
|
const QStringList &actions,
|
|
const QVariantMap &hints,
|
|
int timeout)
|
|
{
|
|
const bool wasReplaced = replaces_id > 0;
|
|
uint notificationId = 0;
|
|
if (wasReplaced) {
|
|
notificationId = replaces_id;
|
|
} else {
|
|
// Avoid wrapping around to 0 in case of overflow
|
|
if (!m_highestNotificationId) {
|
|
++m_highestNotificationId;
|
|
}
|
|
notificationId = m_highestNotificationId;
|
|
++m_highestNotificationId;
|
|
}
|
|
|
|
Notification notification(notificationId);
|
|
notification.setDBusService(message().service());
|
|
notification.setSummary(summary);
|
|
notification.setBody(body);
|
|
notification.setApplicationName(app_name);
|
|
|
|
notification.setActions(actions);
|
|
|
|
notification.setTimeout(timeout);
|
|
|
|
// might override some of the things we set above (like application name)
|
|
notification.d->processHints(hints);
|
|
|
|
// If we didn't get a pixmap, load the app_icon instead
|
|
if (notification.d->image.isNull()) {
|
|
notification.setIcon(app_icon);
|
|
}
|
|
|
|
uint pid = 0;
|
|
if (notification.desktopEntry().isEmpty() || notification.applicationName().isEmpty()) {
|
|
if (notification.desktopEntry().isEmpty() && notification.applicationName().isEmpty()) {
|
|
qCInfo(NOTIFICATIONMANAGER) << "Notification from service" << message().service()
|
|
<< "didn't contain any identification information, this is an application bug!";
|
|
}
|
|
QDBusReply<uint> pidReply = connection().interface()->servicePid(message().service());
|
|
if (pidReply.isValid()) {
|
|
pid = pidReply.value();
|
|
}
|
|
}
|
|
|
|
// No desktop entry? Try to read the BAMF_DESKTOP_FILE_HINT in the environment of snaps
|
|
if (notification.desktopEntry().isEmpty() && pid > 0) {
|
|
const QString desktopEntry = Utils::desktopEntryFromPid(pid);
|
|
if (!desktopEntry.isEmpty()) {
|
|
qCDebug(NOTIFICATIONMANAGER) << "Resolved notification to be from desktop entry" << desktopEntry;
|
|
notification.setDesktopEntry(desktopEntry);
|
|
|
|
// No application name? Set it to the service name, which is nicer than the process name fallback below
|
|
// Also if the title looks like it's just the desktop entry, use the nicer service name
|
|
if (notification.applicationName().isEmpty() || notification.applicationName() == desktopEntry) {
|
|
KService::Ptr service = KService::serviceByDesktopName(desktopEntry);
|
|
if (service) {
|
|
notification.setApplicationName(service->name());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// No application name? Try to figure out the process name using the sender's PID
|
|
if (notification.applicationName().isEmpty() && pid > 0) {
|
|
const QString processName = Utils::processNameFromPid(pid);
|
|
if (!processName.isEmpty()) {
|
|
qCDebug(NOTIFICATIONMANAGER) << "Resolved notification to be from process name" << processName;
|
|
notification.setApplicationName(processName);
|
|
}
|
|
}
|
|
|
|
// If multiple identical notifications are sent in quick succession, refuse the request
|
|
if (m_lastNotification.applicationName() == notification.applicationName() && m_lastNotification.summary() == notification.summary()
|
|
&& m_lastNotification.body() == notification.body() && m_lastNotification.desktopEntry() == notification.desktopEntry()
|
|
&& m_lastNotification.eventId() == notification.eventId() && m_lastNotification.actionNames() == notification.actionNames()
|
|
&& m_lastNotification.urls() == notification.urls() && m_lastNotification.created().msecsTo(notification.created()) < 1000) {
|
|
qCDebug(NOTIFICATIONMANAGER) << "Discarding excess notification creation request";
|
|
|
|
sendErrorReply(QStringLiteral("org.freedesktop.Notifications.Error.ExcessNotificationGeneration"),
|
|
QStringLiteral("Created too many similar notifications in quick succession"));
|
|
return 0;
|
|
}
|
|
|
|
m_lastNotification = notification;
|
|
|
|
if (wasReplaced) {
|
|
notification.resetUpdated();
|
|
Q_EMIT static_cast<Server *>(parent())->notificationReplaced(replaces_id, notification);
|
|
} else {
|
|
Q_EMIT static_cast<Server *>(parent())->notificationAdded(notification);
|
|
}
|
|
|
|
// currently we dispatch all notification, this is ugly
|
|
// TODO: come up with proper authentication/user selection
|
|
for (const QString &service : m_notificationWatchers->watchedServices()) {
|
|
QDBusMessage msg = QDBusMessage::createMethodCall(service,
|
|
QStringLiteral("/NotificationWatcher"),
|
|
QStringLiteral("org.kde.NotificationWatcher"),
|
|
QStringLiteral("Notify"));
|
|
msg.setArguments({notificationId,
|
|
notification.applicationName(),
|
|
replaces_id,
|
|
notification.applicationIconName(),
|
|
notification.summary(),
|
|
// we pass raw body data since this data goes through another sanitization
|
|
// in WatchedNotificationsModel when notification object is created.
|
|
notification.rawBody(),
|
|
actions,
|
|
hints,
|
|
notification.timeout()});
|
|
QDBusConnection::sessionBus().call(msg, QDBus::NoBlock);
|
|
}
|
|
|
|
return notificationId;
|
|
}
|
|
|
|
void ServerPrivate::CloseNotification(uint id)
|
|
{
|
|
for (const QString &service : m_notificationWatchers->watchedServices()) {
|
|
QDBusMessage msg = QDBusMessage::createMethodCall(service,
|
|
QStringLiteral("/NotificationWatcher"),
|
|
QStringLiteral("org.kde.NotificationWatcher"),
|
|
QStringLiteral("CloseNotification"));
|
|
msg.setArguments({id});
|
|
QDBusConnection::sessionBus().call(msg, QDBus::NoBlock);
|
|
}
|
|
// spec says "If the notification no longer exists, an empty D-BUS Error message is sent back."
|
|
static_cast<Server *>(parent())->closeNotification(id, Server::CloseReason::Revoked);
|
|
}
|
|
|
|
QStringList ServerPrivate::GetCapabilities() const
|
|
{
|
|
// should this be configurable somehow so the UI can tell what it implements?
|
|
return QStringList{QStringLiteral("body"),
|
|
QStringLiteral("body-hyperlinks"),
|
|
QStringLiteral("body-markup"),
|
|
QStringLiteral("body-images"),
|
|
QStringLiteral("icon-static"),
|
|
QStringLiteral("actions"),
|
|
QStringLiteral("persistence"),
|
|
QStringLiteral("inline-reply"),
|
|
|
|
QStringLiteral("x-kde-urls"),
|
|
QStringLiteral("x-kde-origin-name"),
|
|
QStringLiteral("x-kde-display-appname"),
|
|
|
|
QStringLiteral("inhibitions")};
|
|
}
|
|
|
|
QString ServerPrivate::GetServerInformation(QString &vendor, QString &version, QString &specVersion) const
|
|
{
|
|
vendor = QStringLiteral("KDE");
|
|
version = QLatin1String(PROJECT_VERSION);
|
|
specVersion = QStringLiteral("1.2");
|
|
return QStringLiteral("Plasma");
|
|
}
|
|
|
|
void ServerPrivate::onBroadcastNotification(const QMap<QString, QVariant> &properties)
|
|
{
|
|
qCDebug(NOTIFICATIONMANAGER) << "Received broadcast notification";
|
|
|
|
const auto currentUserId = KUserId::currentEffectiveUserId().nativeId();
|
|
|
|
// a QVariantList with ints arrives as QDBusArgument here, using a QStringList for simplicity
|
|
const QStringList &userIds = properties.value(QStringLiteral("uids")).toStringList();
|
|
if (!userIds.isEmpty()) {
|
|
auto it = std::find_if(userIds.constBegin(), userIds.constEnd(), [currentUserId](const QVariant &id) {
|
|
bool ok;
|
|
auto uid = id.toString().toLongLong(&ok);
|
|
return ok && uid == currentUserId;
|
|
});
|
|
|
|
if (it == userIds.constEnd()) {
|
|
qCDebug(NOTIFICATIONMANAGER) << "It is not meant for us, ignoring";
|
|
return;
|
|
}
|
|
}
|
|
|
|
bool ok;
|
|
int timeout = properties.value(QStringLiteral("timeout")).toInt(&ok);
|
|
if (!ok) {
|
|
timeout = -1; // -1 = server default, 0 would be "persistent"
|
|
}
|
|
|
|
Notify(properties.value(QStringLiteral("appName")).toString(),
|
|
0, // replaces_id
|
|
properties.value(QStringLiteral("appIcon")).toString(),
|
|
properties.value(QStringLiteral("summary")).toString(),
|
|
properties.value(QStringLiteral("body")).toString(),
|
|
{}, // no actions
|
|
properties.value(QStringLiteral("hints")).toMap(),
|
|
timeout);
|
|
}
|
|
|
|
uint ServerPrivate::add(const Notification ¬ification)
|
|
{
|
|
// TODO check if notification with ID already exists and signal update instead
|
|
if (notification.id() == 0) {
|
|
++m_highestNotificationId;
|
|
notification.d->id = m_highestNotificationId;
|
|
|
|
Q_EMIT static_cast<Server *>(parent())->notificationAdded(notification);
|
|
} else {
|
|
Q_EMIT static_cast<Server *>(parent())->notificationReplaced(notification.id(), notification);
|
|
}
|
|
|
|
return notification.id();
|
|
}
|
|
|
|
void ServerPrivate::sendReplyText(const QString &dbusService, uint notificationId, const QString &text, Notifications::InvokeBehavior behavior)
|
|
{
|
|
if (dbusService.isEmpty()) {
|
|
qCWarning(NOTIFICATIONMANAGER) << "Sending notification reply text for notification" << notificationId << "untargeted";
|
|
}
|
|
|
|
QDBusMessage msg =
|
|
QDBusMessage::createTargetedSignal(dbusService, notificationServicePath(), notificationServiceName(), QStringLiteral("NotificationReplied"));
|
|
msg.setArguments({notificationId, text});
|
|
QDBusConnection::sessionBus().send(msg);
|
|
|
|
if (behavior & Notifications::Close) {
|
|
Q_EMIT CloseNotification(notificationId);
|
|
}
|
|
}
|
|
|
|
uint ServerPrivate::Inhibit(const QString &desktop_entry, const QString &reason, const QVariantMap &hints)
|
|
{
|
|
const QString dbusService = message().service();
|
|
|
|
QString applicationName = desktop_entry;
|
|
|
|
qCDebug(NOTIFICATIONMANAGER) << "Request inhibit from service" << dbusService << "which is" << desktop_entry << "with reason" << reason;
|
|
|
|
// xdg-desktop-portal forwards appId only for sandboxed apps it can trust
|
|
// Resolve it to process name here to at least have something, even if that means showing "xdg-desktop-portal-kde is currently..."
|
|
if (desktop_entry.isEmpty()) {
|
|
QDBusReply<uint> pidReply = connection().interface()->servicePid(message().service());
|
|
if (pidReply.isValid()) {
|
|
const auto pid = pidReply.value();
|
|
|
|
const QString processName = Utils::processNameFromPid(pid);
|
|
if (!processName.isEmpty()) {
|
|
qCDebug(NOTIFICATIONMANAGER) << "Resolved inhibition to be from process name" << processName;
|
|
applicationName = processName;
|
|
}
|
|
}
|
|
} else {
|
|
KService::Ptr service = KService::serviceByDesktopName(desktop_entry);
|
|
if (service) {
|
|
applicationName = service->name();
|
|
}
|
|
}
|
|
|
|
if (applicationName.isEmpty()) {
|
|
sendErrorReply(QDBusError::InvalidArgs, QStringLiteral("No meaningful desktop_entry provided"));
|
|
return 0;
|
|
}
|
|
|
|
m_inhibitionWatcher->addWatchedService(dbusService);
|
|
|
|
++m_highestInhibitionCookie;
|
|
|
|
const bool oldExternalInhibited = externalInhibited();
|
|
|
|
m_externalInhibitions.insert(m_highestInhibitionCookie, {desktop_entry, applicationName, reason, hints});
|
|
|
|
m_inhibitionServices.insert(m_highestInhibitionCookie, dbusService);
|
|
|
|
if (externalInhibited() != oldExternalInhibited) {
|
|
Q_EMIT externalInhibitedChanged();
|
|
}
|
|
Q_EMIT externalInhibitionsChanged();
|
|
|
|
return m_highestInhibitionCookie;
|
|
}
|
|
|
|
void ServerPrivate::onServiceOwnershipLost(const QString &serviceName)
|
|
{
|
|
if (serviceName != notificationServiceName()) {
|
|
return;
|
|
}
|
|
|
|
qCDebug(NOTIFICATIONMANAGER) << "Lost ownership of" << serviceName << "service";
|
|
|
|
disconnect(QDBusConnection::sessionBus().interface(), &QDBusConnectionInterface::serviceUnregistered, this, &ServerPrivate::onServiceOwnershipLost);
|
|
disconnect(this, &ServerPrivate::inhibitedChanged, this, &ServerPrivate::onInhibitedChanged);
|
|
|
|
QDBusConnection::systemBus().disconnect({},
|
|
{},
|
|
QStringLiteral("org.kde.BroadcastNotifications"),
|
|
QStringLiteral("Notify"),
|
|
this,
|
|
SLOT(onBroadcastNotification(QMap<QString, QVariant>)));
|
|
|
|
m_valid = false;
|
|
|
|
Q_EMIT validChanged();
|
|
Q_EMIT serviceOwnershipLost();
|
|
}
|
|
|
|
void ServerPrivate::onInhibitionServiceUnregistered(const QString &serviceName)
|
|
{
|
|
qCDebug(NOTIFICATIONMANAGER) << "Inhibition service unregistered" << serviceName;
|
|
|
|
const QList<uint> cookies = m_inhibitionServices.keys(serviceName);
|
|
if (cookies.isEmpty()) {
|
|
qCInfo(NOTIFICATIONMANAGER) << "Unknown inhibition service unregistered" << serviceName;
|
|
return;
|
|
}
|
|
|
|
// We do lookups in there again...
|
|
for (uint cookie : cookies) {
|
|
UnInhibit(cookie);
|
|
}
|
|
}
|
|
|
|
void ServerPrivate::onInhibitedChanged()
|
|
{
|
|
// Q_EMIT DBus change signal...
|
|
QDBusMessage signal =
|
|
QDBusMessage::createSignal(notificationServicePath(), QStringLiteral("org.freedesktop.DBus.Properties"), QStringLiteral("PropertiesChanged"));
|
|
|
|
signal.setArguments({
|
|
notificationServiceInterface(),
|
|
QVariantMap{
|
|
// updated
|
|
{QStringLiteral("Inhibited"), inhibited()},
|
|
},
|
|
QStringList() // invalidated
|
|
});
|
|
|
|
QDBusConnection::sessionBus().send(signal);
|
|
}
|
|
|
|
void ServerPrivate::UnInhibit(uint cookie)
|
|
{
|
|
qCDebug(NOTIFICATIONMANAGER) << "Request release inhibition for cookie" << cookie;
|
|
|
|
const QString service = m_inhibitionServices.value(cookie);
|
|
if (service.isEmpty()) {
|
|
qCInfo(NOTIFICATIONMANAGER) << "Requested to release inhibition with cookie" << cookie << "that doesn't exist";
|
|
// TODO if called from dbus raise error
|
|
return;
|
|
}
|
|
|
|
m_inhibitionWatcher->removeWatchedService(service);
|
|
m_externalInhibitions.remove(cookie);
|
|
m_inhibitionServices.remove(cookie);
|
|
|
|
if (m_externalInhibitions.isEmpty()) {
|
|
Q_EMIT externalInhibitedChanged();
|
|
}
|
|
Q_EMIT externalInhibitionsChanged();
|
|
}
|
|
|
|
QList<Inhibition> ServerPrivate::externalInhibitions() const
|
|
{
|
|
return m_externalInhibitions.values();
|
|
}
|
|
|
|
bool ServerPrivate::inhibited() const
|
|
{
|
|
return m_inhibited;
|
|
}
|
|
|
|
void ServerPrivate::setInhibited(bool inhibited)
|
|
{
|
|
if (m_inhibited != inhibited) {
|
|
m_inhibited = inhibited;
|
|
Q_EMIT inhibitedChanged();
|
|
}
|
|
}
|
|
|
|
bool ServerPrivate::externalInhibited() const
|
|
{
|
|
return !m_externalInhibitions.isEmpty();
|
|
}
|
|
|
|
void ServerPrivate::clearExternalInhibitions()
|
|
{
|
|
if (m_externalInhibitions.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
m_inhibitionWatcher->setWatchedServices(QStringList()); // remove all watches
|
|
m_inhibitionServices.clear();
|
|
m_externalInhibitions.clear();
|
|
|
|
Q_EMIT externalInhibitedChanged();
|
|
Q_EMIT externalInhibitionsChanged();
|
|
}
|
|
|
|
void ServerPrivate::RegisterWatcher()
|
|
{
|
|
m_notificationWatchers->addWatchedService(message().service());
|
|
}
|
|
|
|
void ServerPrivate::UnRegisterWatcher()
|
|
{
|
|
m_notificationWatchers->removeWatchedService(message().service());
|
|
}
|
|
|
|
void ServerPrivate::InvokeAction(uint id, const QString &actionKey)
|
|
{
|
|
Q_EMIT ActionInvoked(id, actionKey);
|
|
}
|