File indexing completed on 2024-04-28 15:29:43

0001 /*
0002     SPDX-FileCopyrightText: 2017, 2018 David Edmundson <davidedmundson@kde.org>
0003     SPDX-FileCopyrightText: 2020 Alexander Lohnau <alexander.lohnau@gmx.de>
0004     SPDX-FileCopyrightText: 2020 Kai Uwe Broulik <kde@broulik.de>
0005 
0006     SPDX-License-Identifier: LGPL-2.0-or-later
0007 */
0008 
0009 #include "dbusrunner_p.h"
0010 
0011 #include <QAction>
0012 #include <QDBusConnection>
0013 #include <QDBusConnectionInterface>
0014 #include <QDBusMessage>
0015 #include <QDBusMetaType>
0016 #include <QDBusPendingReply>
0017 #include <QIcon>
0018 #include <QMutexLocker>
0019 #include <qobjectdefs.h>
0020 
0021 #include "dbusutils_p.h"
0022 #include "krunner_debug.h"
0023 
0024 #define IFACE_NAME "org.kde.krunner1"
0025 
0026 DBusRunner::DBusRunner(QObject *parent, const KPluginMetaData &pluginMetaData, const QVariantList &args)
0027     : Plasma::AbstractRunner(parent, pluginMetaData, args)
0028 {
0029     qDBusRegisterMetaType<RemoteMatch>();
0030     qDBusRegisterMetaType<RemoteMatches>();
0031     qDBusRegisterMetaType<RemoteAction>();
0032     qDBusRegisterMetaType<RemoteActions>();
0033     qDBusRegisterMetaType<RemoteImage>();
0034     qRegisterMetaType<QMap<QString, RemoteActions>>("QMap<QString, RemoteActions>");
0035 
0036     QString requestedServiceName = pluginMetaData.value(QStringLiteral("X-Plasma-DBusRunner-Service"));
0037     m_path = pluginMetaData.value(QStringLiteral("X-Plasma-DBusRunner-Path"), QStringLiteral("/runner"));
0038     m_hasUniqueResults = pluginMetaData.value(QStringLiteral("X-Plasma-Runner-Unique-Results"), false);
0039     m_callLifecycleMethods = pluginMetaData.value(QStringLiteral("X-Plasma-API")) == QLatin1String("DBus2");
0040 
0041     if (requestedServiceName.isEmpty() || m_path.isEmpty()) {
0042         qCWarning(KRUNNER) << "Invalid entry:" << pluginMetaData.name();
0043         return;
0044     }
0045 
0046     if (requestedServiceName.endsWith(QLatin1Char('*'))) {
0047         requestedServiceName.chop(1);
0048         // find existing matching names
0049         auto namesReply = QDBusConnection::sessionBus().interface()->registeredServiceNames();
0050         if (namesReply.isValid()) {
0051             const auto names = namesReply.value();
0052             for (const QString &serviceName : names) {
0053                 if (serviceName.startsWith(requestedServiceName)) {
0054                     m_matchingServices << serviceName;
0055                 }
0056             }
0057         }
0058         // and watch for changes
0059         connect(QDBusConnection::sessionBus().interface(),
0060                 &QDBusConnectionInterface::serviceOwnerChanged,
0061                 this,
0062                 [this, requestedServiceName](const QString &serviceName, const QString &oldOwner, const QString &newOwner) {
0063                     if (!serviceName.startsWith(requestedServiceName)) {
0064                         return;
0065                     }
0066                     if (!oldOwner.isEmpty() && !newOwner.isEmpty()) {
0067                         // changed owner, but service still exists. Don't need to adjust anything
0068                         return;
0069                     }
0070                     QMutexLocker lock(&m_mutex);
0071                     if (!newOwner.isEmpty()) {
0072                         m_matchingServices.insert(serviceName);
0073                     }
0074                     if (!oldOwner.isEmpty()) {
0075                         m_matchingServices.remove(serviceName);
0076                     }
0077                 });
0078     } else {
0079         // don't check when not wildcarded, as it could be used with DBus-activation
0080         m_matchingServices << requestedServiceName;
0081     }
0082 
0083     m_requestActionsOnce = pluginMetaData.value(QStringLiteral("X-Plasma-Request-Actions-Once"), false);
0084     connect(this, &AbstractRunner::teardown, this, &DBusRunner::teardown);
0085 
0086     // Load the runner syntaxes
0087     const QJsonValue syntaxesJson = pluginMetaData.rawData().value(QStringLiteral("X-Plasma-Runner-Syntaxes"));
0088     const QStringList syntaxes = syntaxesJson.isArray() ? syntaxesJson.toVariant().toStringList() : syntaxesJson.toString().split(QLatin1Char(','), Qt::SkipEmptyParts);
0089     const QJsonValue syntaxDescriptionsJson = pluginMetaData.rawData().value(QStringLiteral("X-Plasma-Runner-Syntax-Descriptions"));
0090     const QStringList syntaxDescriptions =
0091         syntaxDescriptionsJson.isArray() ? syntaxDescriptionsJson.toVariant().toStringList() : syntaxDescriptionsJson.toString().split(QLatin1Char(','), Qt::SkipEmptyParts);
0092     const int descriptionCount = syntaxDescriptions.count();
0093     for (int i = 0; i < syntaxes.count(); ++i) {
0094         const QString &query = syntaxes.at(i);
0095         const QString description = i < descriptionCount ? syntaxDescriptions.at(i) : QString();
0096         addSyntax(query, description);
0097     }
0098 }
0099 
0100 DBusRunner::~DBusRunner() = default;
0101 
0102 void DBusRunner::reloadConfiguration()
0103 {
0104     // If we have already loaded a config, but the runner is told to reload it's config
0105     if (m_callLifecycleMethods) {
0106         suspendMatching(true);
0107         requestConfig();
0108     }
0109 }
0110 
0111 void DBusRunner::createQActionsFromRemoteActions(const QMap<QString, RemoteActions> &remoteActions)
0112 {
0113     for (auto it = remoteActions.begin(), end = remoteActions.end(); it != end; it++) {
0114         const QString service = it.key();
0115         const RemoteActions actions = it.value();
0116         auto &serviceActions = m_actions[service];
0117         qDeleteAll(serviceActions);
0118         serviceActions.clear();
0119         for (const RemoteAction &action : actions) {
0120             auto a = new QAction(QIcon::fromTheme(action.iconName), action.text, this);
0121             a->setData(action.id);
0122             serviceActions.append(a);
0123         }
0124     }
0125 }
0126 
0127 void DBusRunner::teardown()
0128 {
0129     if (m_matchWasCalled) {
0130         for (const QString &service : std::as_const(m_matchingServices)) {
0131             auto method = QDBusMessage::createMethodCall(service, m_path, QStringLiteral(IFACE_NAME), QStringLiteral("Teardown"));
0132             QDBusConnection::sessionBus().asyncCall(method);
0133         }
0134     }
0135     m_actionsForSessionRequested = false;
0136     m_matchWasCalled = false;
0137 }
0138 
0139 QMap<QString, RemoteActions> DBusRunner::requestActions()
0140 {
0141     // in the multi-services case, register separate actions from each plugin in case they happen to be somehow different
0142     // then match together in matchForAction()
0143     QMap<QString, RemoteActions> returnedActions;
0144     for (const QString &service : std::as_const(m_matchingServices)) {
0145         // if we only want to request the actions once and have done so we want to skip the service
0146         // but in case it got newly loaded we need to request the actions, BUG: 435350
0147         if (m_requestActionsOnce) {
0148             if (m_requestedActionServices.contains(service)) {
0149                 continue;
0150             } else {
0151                 m_requestedActionServices << service;
0152             }
0153         }
0154 
0155         auto getActionsMethod = QDBusMessage::createMethodCall(service, m_path, QStringLiteral(IFACE_NAME), QStringLiteral("Actions"));
0156         QDBusPendingReply<RemoteActions> reply = QDBusConnection::sessionBus().call(getActionsMethod);
0157         if (!reply.isValid()) {
0158             qCDebug(KRUNNER) << "Error requesting actions; calling" << service << " :" << reply.error().name() << reply.error().message();
0159         } else {
0160             returnedActions.insert(service, reply.value());
0161         }
0162     }
0163     return returnedActions;
0164 }
0165 
0166 void DBusRunner::requestConfig()
0167 {
0168     const QString service = *m_matchingServices.constBegin();
0169     auto getConfigMethod = QDBusMessage::createMethodCall(service, m_path, QStringLiteral(IFACE_NAME), QStringLiteral("Config"));
0170     QDBusPendingReply<QVariantMap> reply = QDBusConnection::sessionBus().asyncCall(getConfigMethod);
0171 
0172     auto watcher = new QDBusPendingCallWatcher(reply);
0173     connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, watcher, service]() {
0174         watcher->deleteLater();
0175         QDBusReply<QVariantMap> reply = *watcher;
0176         if (!reply.isValid()) {
0177             suspendMatching(false);
0178             qCDebug(KRUNNER) << "Error requesting config; calling" << service << " :" << reply.error().name() << reply.error().message();
0179             return;
0180         }
0181         const QVariantMap config = reply.value();
0182         for (auto it = config.cbegin(), end = config.cend(); it != end; ++it) {
0183             if (it.key() == QLatin1String("MatchRegex")) {
0184                 QRegularExpression regex(it.value().toString());
0185                 setMatchRegex(regex);
0186             } else if (it.key() == QLatin1String("MinLetterCount")) {
0187                 setMinLetterCount(it.value().toInt());
0188             } else if (it.key() == QLatin1String("TriggerWords")) {
0189                 setTriggerWords(it.value().toStringList());
0190             } else if (it.key() == QLatin1String("Actions")) {
0191                 const auto remoteActions = it.value().value<RemoteActions>();
0192                 createQActionsFromRemoteActions(QMap<QString, RemoteActions>{{service, remoteActions}});
0193                 m_actionsOnceRequested = true;
0194                 m_actionsForSessionRequested = true;
0195             }
0196         }
0197         suspendMatching(false);
0198     });
0199 }
0200 
0201 void DBusRunner::match(Plasma::RunnerContext &context)
0202 {
0203     QSet<QString> services;
0204     {
0205         QMutexLocker lock(&m_mutex);
0206         services = m_matchingServices;
0207         m_matchWasCalled = true;
0208 
0209         // Request the actions
0210         if ((m_requestActionsOnce && !m_actionsOnceRequested) // We only want to fetch the actions once but haven't done so yet
0211             || (!m_actionsForSessionRequested)) { // We want to fetch the actions for each match session
0212             m_actionsOnceRequested = true;
0213             m_actionsForSessionRequested = true;
0214             auto actions = requestActions();
0215             QMetaObject::invokeMethod(this, "createQActionsFromRemoteActions", QArgument("QMap<QString, RemoteActions>", actions));
0216         }
0217     }
0218     // we scope watchers to make sure the lambda that captures context by reference definitely gets disconnected when this function ends
0219     std::vector<std::unique_ptr<QDBusPendingCallWatcher>> watchers;
0220 
0221     for (const QString &service : std::as_const(services)) {
0222         auto matchMethod = QDBusMessage::createMethodCall(service, m_path, QStringLiteral(IFACE_NAME), QStringLiteral("Match"));
0223         matchMethod.setArguments(QList<QVariant>({context.query()}));
0224         QDBusPendingReply<RemoteMatches> reply = QDBusConnection::sessionBus().asyncCall(matchMethod);
0225 
0226         watchers.push_back(std::make_unique<QDBusPendingCallWatcher>(reply));
0227         connect(
0228             watchers.back().get(),
0229             &QDBusPendingCallWatcher::finished,
0230             this,
0231             [this, service, &context, reply]() {
0232                 if (reply.isError()) {
0233                     qCDebug(KRUNNER) << "Error requesting matches; calling" << service << " :" << reply.error().name() << reply.error().message();
0234                     return;
0235                 }
0236                 const auto matches = reply.value();
0237                 for (const RemoteMatch &match : matches) {
0238                     Plasma::QueryMatch m(this);
0239 
0240                     m.setText(match.text);
0241                     m.setIconName(match.iconName);
0242                     m.setType(match.type);
0243                     m.setRelevance(match.relevance);
0244 
0245                     // split is essential items are as native DBus types, optional extras are in the property map (which is obviously a lot slower to parse)
0246                     m.setUrls(QUrl::fromStringList(match.properties.value(QStringLiteral("urls")).toStringList()));
0247                     m.setMatchCategory(match.properties.value(QStringLiteral("category")).toString());
0248                     m.setSubtext(match.properties.value(QStringLiteral("subtext")).toString());
0249                     const auto actionsIt = match.properties.find(QStringLiteral("actions"));
0250                     if (actionsIt == match.properties.cend()) {
0251                         m.setData(QVariantList({service}));
0252                     } else {
0253                         m.setData(QVariantList({service, actionsIt.value().toStringList()}));
0254                     }
0255                     m.setId(match.id);
0256                     m.setMultiLine(match.properties.value(QStringLiteral("multiline")).toBool());
0257 
0258                     const QVariant iconData = match.properties.value(QStringLiteral("icon-data"));
0259                     if (iconData.isValid()) {
0260                         const auto iconDataArgument = iconData.value<QDBusArgument>();
0261                         if (iconDataArgument.currentType() == QDBusArgument::StructureType
0262                             && iconDataArgument.currentSignature() == QLatin1String("(iiibiiay)")) {
0263                             const RemoteImage remoteImage = qdbus_cast<RemoteImage>(iconDataArgument);
0264                             const QImage decodedImage = decodeImage(remoteImage);
0265                             if (!decodedImage.isNull()) {
0266                                 const QPixmap pix = QPixmap::fromImage(decodedImage);
0267                                 QIcon icon(pix);
0268                                 m.setIcon(icon);
0269                                 // iconName normally takes precedence
0270                                 m.setIconName(QString());
0271                             }
0272                         } else {
0273                             qCWarning(KRUNNER) << "Invalid signature of icon-data property:" << iconDataArgument.currentSignature();
0274                         }
0275                     }
0276 
0277                     context.addMatch(m);
0278                 };
0279             },
0280             Qt::DirectConnection); // process reply in the watcher's thread (aka the one running ::match  not the one owning the runner)
0281     }
0282     // we're done matching when every service replies
0283     for (auto &w : watchers) {
0284         w->waitForFinished();
0285     }
0286 }
0287 
0288 QList<QAction *> DBusRunner::actionsForMatch(const Plasma::QueryMatch &match)
0289 {
0290     const QVariantList data = match.data().toList();
0291     if (data.count() > 1) {
0292         const QStringList actionIds = data.at(1).toStringList();
0293         const QList<QAction *> actionList = m_actions.value(data.constFirst().toString());
0294         QList<QAction *> requestedActions;
0295         for (QAction *action : actionList) {
0296             if (actionIds.contains(action->data().toString())) {
0297                 requestedActions << action;
0298             }
0299         }
0300         return requestedActions;
0301     } else {
0302         return m_actions.value(data.constFirst().toString());
0303     }
0304 }
0305 
0306 void DBusRunner::run(const Plasma::RunnerContext &context, const Plasma::QueryMatch &match)
0307 {
0308     Q_UNUSED(context);
0309 
0310     QString actionId;
0311     QString matchId;
0312     if (m_hasUniqueResults) {
0313         matchId = match.id();
0314     } else {
0315         matchId = match.id().mid(id().length() + 1); // QueryMatch::setId mangles the match ID with runnerID + '_'. This unmangles it
0316     }
0317     const QString service = match.data().toList().constFirst().toString();
0318 
0319     if (match.selectedAction()) {
0320         actionId = match.selectedAction()->data().toString();
0321     }
0322 
0323     auto runMethod = QDBusMessage::createMethodCall(service, m_path, QStringLiteral(IFACE_NAME), QStringLiteral("Run"));
0324     runMethod.setArguments(QList<QVariant>({matchId, actionId}));
0325     QDBusConnection::sessionBus().call(runMethod, QDBus::NoBlock);
0326 }
0327 
0328 QImage DBusRunner::decodeImage(const RemoteImage &remoteImage)
0329 {
0330     auto copyLineRGB32 = [](QRgb *dst, const char *src, int width) {
0331         const char *end = src + width * 3;
0332         for (; src != end; ++dst, src += 3) {
0333             *dst = qRgb(src[0], src[1], src[2]);
0334         }
0335     };
0336 
0337     auto copyLineARGB32 = [](QRgb *dst, const char *src, int width) {
0338         const char *end = src + width * 4;
0339         for (; src != end; ++dst, src += 4) {
0340             *dst = qRgba(src[0], src[1], src[2], src[3]);
0341         }
0342     };
0343 
0344     if (remoteImage.width <= 0 || remoteImage.width >= 2048 || remoteImage.height <= 0 || remoteImage.height >= 2048 || remoteImage.rowStride <= 0) {
0345         qCWarning(KRUNNER) << "Invalid image metadata (width:" << remoteImage.width << "height:" << remoteImage.height << "rowStride:" << remoteImage.rowStride
0346                            << ")";
0347         return QImage();
0348     }
0349 
0350     QImage::Format format = QImage::Format_Invalid;
0351     void (*copyFn)(QRgb *, const char *, int) = nullptr;
0352     if (remoteImage.bitsPerSample == 8) {
0353         if (remoteImage.channels == 4) {
0354             format = QImage::Format_ARGB32;
0355             copyFn = copyLineARGB32;
0356         } else if (remoteImage.channels == 3) {
0357             format = QImage::Format_RGB32;
0358             copyFn = copyLineRGB32;
0359         }
0360     }
0361     if (format == QImage::Format_Invalid) {
0362         qCWarning(KRUNNER) << "Unsupported image format (hasAlpha:" << remoteImage.hasAlpha << "bitsPerSample:" << remoteImage.bitsPerSample
0363                            << "channels:" << remoteImage.channels << ")";
0364         return QImage();
0365     }
0366 
0367     QImage image(remoteImage.width, remoteImage.height, format);
0368     const QByteArray pixels = remoteImage.data;
0369     const char *ptr = pixels.data();
0370     const char *end = ptr + pixels.length();
0371     for (int y = 0; y < remoteImage.height; ++y, ptr += remoteImage.rowStride) {
0372         if (Q_UNLIKELY(ptr + remoteImage.channels * remoteImage.width > end)) {
0373             qCWarning(KRUNNER) << "Image data is incomplete. y:" << y << "height:" << remoteImage.height;
0374             break;
0375         }
0376         copyFn(reinterpret_cast<QRgb *>(image.scanLine(y)), ptr, remoteImage.width);
0377     }
0378 
0379     return image;
0380 }
0381 
0382 #include "moc_dbusrunner_p.cpp"