File indexing completed on 2024-04-21 03:56:47

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 <QDBusConnection>
0012 #include <QDBusConnectionInterface>
0013 #include <QDBusMessage>
0014 #include <QDBusMetaType>
0015 #include <QDBusPendingReply>
0016 #include <QIcon>
0017 #include <set>
0018 
0019 #include "dbusutils_p.h"
0020 #include "krunner_debug.h"
0021 
0022 namespace KRunner
0023 {
0024 DBusRunner::DBusRunner(QObject *parent, const KPluginMetaData &data)
0025     : KRunner::AbstractRunner(parent, data)
0026     , m_path(data.value(QStringLiteral("X-Plasma-DBusRunner-Path"), QStringLiteral("/runner")))
0027     , m_hasUniqueResults(data.value(QStringLiteral("X-Plasma-Runner-Unique-Results"), false))
0028     , m_requestActionsOnce(data.value(QStringLiteral("X-Plasma-Request-Actions-Once"), false))
0029     , m_callLifecycleMethods(data.value(QStringLiteral("X-Plasma-API")) == QLatin1String("DBus2"))
0030     , m_ifaceName(QStringLiteral("org.kde.krunner1"))
0031 {
0032     qDBusRegisterMetaType<RemoteMatch>();
0033     qDBusRegisterMetaType<RemoteMatches>();
0034     qDBusRegisterMetaType<KRunner::Action>();
0035     qDBusRegisterMetaType<KRunner::Actions>();
0036     qDBusRegisterMetaType<RemoteImage>();
0037 
0038     QString requestedServiceName = data.value(QStringLiteral("X-Plasma-DBusRunner-Service"));
0039     if (requestedServiceName.isEmpty() || m_path.isEmpty()) {
0040         qCWarning(KRUNNER) << "Invalid entry:" << data;
0041         return;
0042     }
0043 
0044     if (requestedServiceName.endsWith(QLatin1Char('*'))) {
0045         requestedServiceName.chop(1);
0046         // find existing matching names
0047         auto namesReply = QDBusConnection::sessionBus().interface()->registeredServiceNames();
0048         if (namesReply.isValid()) {
0049             const auto names = namesReply.value();
0050             for (const QString &serviceName : names) {
0051                 if (serviceName.startsWith(requestedServiceName)) {
0052                     m_matchingServices << serviceName;
0053                 }
0054             }
0055         }
0056         // and watch for changes
0057         connect(QDBusConnection::sessionBus().interface(),
0058                 &QDBusConnectionInterface::serviceOwnerChanged,
0059                 this,
0060                 [this, requestedServiceName](const QString &serviceName, const QString &oldOwner, const QString &newOwner) {
0061                     if (!serviceName.startsWith(requestedServiceName)) {
0062                         return;
0063                     }
0064                     if (!oldOwner.isEmpty() && !newOwner.isEmpty()) {
0065                         // changed owner, but service still exists. Don't need to adjust anything
0066                         return;
0067                     }
0068                     if (!newOwner.isEmpty()) {
0069                         m_matchingServices.insert(serviceName);
0070                     }
0071                     if (!oldOwner.isEmpty()) {
0072                         m_matchingServices.remove(serviceName);
0073                     }
0074                 });
0075     } else {
0076         // don't check when not wildcarded, as it could be used with DBus-activation
0077         m_matchingServices << requestedServiceName;
0078     }
0079 
0080     connect(this, &AbstractRunner::teardown, this, &DBusRunner::teardown);
0081 
0082     // Load the runner syntaxes
0083     const QStringList syntaxes = data.value(QStringLiteral("X-Plasma-Runner-Syntaxes"), QStringList());
0084     const QStringList syntaxDescriptions = data.value(QStringLiteral("X-Plasma-Runner-Syntax-Descriptions"), QStringList());
0085     const int descriptionCount = syntaxDescriptions.count();
0086     for (int i = 0; i < syntaxes.count(); ++i) {
0087         const QString &query = syntaxes.at(i);
0088         const QString description = i < descriptionCount ? syntaxDescriptions.at(i) : QString();
0089         addSyntax(query, description);
0090     }
0091 }
0092 
0093 void DBusRunner::reloadConfiguration()
0094 {
0095     // If we have already loaded a config, but the runner is told to reload it's config
0096     if (m_callLifecycleMethods) {
0097         suspendMatching(true);
0098         requestConfig();
0099     }
0100 }
0101 
0102 void DBusRunner::teardown()
0103 {
0104     if (m_matchWasCalled) {
0105         for (const QString &service : std::as_const(m_matchingServices)) {
0106             auto method = QDBusMessage::createMethodCall(service, m_path, m_ifaceName, QStringLiteral("Teardown"));
0107             QDBusConnection::sessionBus().asyncCall(method);
0108         }
0109     }
0110     m_actionsForSessionRequested = false;
0111     m_matchWasCalled = false;
0112 }
0113 
0114 void DBusRunner::requestActionsForService(const QString &service, const std::function<void()> &finishedCallback)
0115 {
0116     if (m_actionsForSessionRequested) {
0117         finishedCallback();
0118         return; // only once per match session
0119     }
0120     if (m_requestActionsOnce) {
0121         if (m_requestedActionServices.contains(service)) {
0122             finishedCallback();
0123             return;
0124         } else {
0125             m_requestedActionServices << service;
0126         }
0127     }
0128 
0129     auto getActionsMethod = QDBusMessage::createMethodCall(service, m_path, m_ifaceName, QStringLiteral("Actions"));
0130     QDBusPendingReply<QList<KRunner::Action>> reply = QDBusConnection::sessionBus().asyncCall(getActionsMethod);
0131     connect(new QDBusPendingCallWatcher(reply), &QDBusPendingCallWatcher::finished, this, [this, service, reply, finishedCallback](auto watcher) {
0132         watcher->deleteLater();
0133         if (!reply.isValid()) {
0134             qCDebug(KRUNNER) << "Error requesting actions; calling" << service << " :" << reply.error().name() << reply.error().message();
0135         } else {
0136             m_actions[service] = reply.value();
0137         }
0138         finishedCallback();
0139     });
0140 }
0141 
0142 void DBusRunner::requestConfig()
0143 {
0144     const QString service = *m_matchingServices.constBegin();
0145     auto getConfigMethod = QDBusMessage::createMethodCall(service, m_path, m_ifaceName, QStringLiteral("Config"));
0146     QDBusPendingReply<QVariantMap> reply = QDBusConnection::sessionBus().asyncCall(getConfigMethod);
0147 
0148     auto watcher = new QDBusPendingCallWatcher(reply);
0149     connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, watcher, service]() {
0150         watcher->deleteLater();
0151         QDBusReply<QVariantMap> reply = *watcher;
0152         if (!reply.isValid()) {
0153             suspendMatching(false);
0154             qCWarning(KRUNNER) << "Error requesting config; calling" << service << " :" << reply.error().name() << reply.error().message();
0155             return;
0156         }
0157         const QVariantMap config = reply.value();
0158         for (auto it = config.cbegin(), end = config.cend(); it != end; ++it) {
0159             if (it.key() == QLatin1String("MatchRegex")) {
0160                 QRegularExpression regex(it.value().toString());
0161                 setMatchRegex(regex);
0162             } else if (it.key() == QLatin1String("MinLetterCount")) {
0163                 setMinLetterCount(it.value().toInt());
0164             } else if (it.key() == QLatin1String("TriggerWords")) {
0165                 setTriggerWords(it.value().toStringList());
0166             } else if (it.key() == QLatin1String("Actions")) {
0167                 m_actions[service] = it.value().value<QList<KRunner::Action>>();
0168                 m_requestedActionServices << service;
0169             }
0170         }
0171         suspendMatching(false);
0172     });
0173 }
0174 
0175 QList<QueryMatch> DBusRunner::convertMatches(const QString &service, const RemoteMatches &remoteMatches)
0176 {
0177     QList<KRunner::QueryMatch> matches;
0178     for (const RemoteMatch &match : remoteMatches) {
0179         KRunner::QueryMatch m(this);
0180 
0181         m.setText(match.text);
0182         m.setIconName(match.iconName);
0183         m.setCategoryRelevance(match.categoryRelevance);
0184         m.setRelevance(match.relevance);
0185 
0186         // split is essential items are as native DBus types, optional extras are in the property map (which is obviously a lot slower to parse)
0187         m.setUrls(QUrl::fromStringList(match.properties.value(QStringLiteral("urls")).toStringList()));
0188         m.setMatchCategory(match.properties.value(QStringLiteral("category")).toString());
0189         m.setSubtext(match.properties.value(QStringLiteral("subtext")).toString());
0190         m.setData(QVariantList({service}));
0191         m.setId(match.id);
0192         m.setMultiLine(match.properties.value(QStringLiteral("multiline")).toBool());
0193 
0194         const auto actionsIt = match.properties.find(QStringLiteral("actions"));
0195         const KRunner::Actions actionList = m_actions.value(service);
0196         if (actionsIt == match.properties.cend()) {
0197             m.setActions(actionList);
0198         } else {
0199             KRunner::Actions requestedActions;
0200             const QStringList actionIds = actionsIt.value().toStringList();
0201             for (const auto &action : actionList) {
0202                 if (actionIds.contains(action.id())) {
0203                     requestedActions << action;
0204                 }
0205             }
0206             m.setActions(requestedActions);
0207         }
0208 
0209         const QVariant iconData = match.properties.value(QStringLiteral("icon-data"));
0210         if (iconData.isValid()) {
0211             const auto iconDataArgument = iconData.value<QDBusArgument>();
0212             if (iconDataArgument.currentType() == QDBusArgument::StructureType && iconDataArgument.currentSignature() == QLatin1String("(iiibiiay)")) {
0213                 const RemoteImage remoteImage = qdbus_cast<RemoteImage>(iconDataArgument);
0214                 if (QImage decodedImage = decodeImage(remoteImage); !decodedImage.isNull()) {
0215                     const QPixmap pix = QPixmap::fromImage(std::move(decodedImage));
0216                     QIcon icon(pix);
0217                     m.setIcon(icon);
0218                     // iconName normally takes precedence
0219                     m.setIconName(QString());
0220                 }
0221             } else {
0222                 qCWarning(KRUNNER) << "Invalid signature of icon-data property:" << iconDataArgument.currentSignature();
0223             }
0224         }
0225         matches.append(m);
0226     }
0227     return matches;
0228 }
0229 void DBusRunner::matchInternal(KRunner::RunnerContext context)
0230 {
0231     const QString jobId = context.runnerJobId(this);
0232     if (m_matchingServices.isEmpty()) {
0233         Q_EMIT matchInternalFinished(jobId);
0234     }
0235     m_matchWasCalled = true;
0236 
0237     // we scope watchers to make sure the lambda that captures context by reference definitely gets disconnected when this function ends
0238     std::shared_ptr<std::set<QString>> pendingServices(new std::set<QString>);
0239 
0240     for (const QString &service : std::as_const(m_matchingServices)) {
0241         pendingServices->insert(service);
0242 
0243         const auto onActionsFinished = [=, this]() mutable {
0244             auto matchMethod = QDBusMessage::createMethodCall(service, m_path, m_ifaceName, QStringLiteral("Match"));
0245             matchMethod.setArguments(QList<QVariant>({context.query()}));
0246             QDBusPendingReply<RemoteMatches> reply = QDBusConnection::sessionBus().asyncCall(matchMethod);
0247 
0248             auto watcher = new QDBusPendingCallWatcher(reply);
0249 
0250             connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, service, context, reply, jobId, pendingServices, watcher]() mutable {
0251                 watcher->deleteLater();
0252                 pendingServices->erase(service);
0253                 if (reply.isError()) {
0254                     qCWarning(KRUNNER) << "Error requesting matches; calling" << service << " :" << reply.error().name() << reply.error().message();
0255                     return;
0256                 }
0257                 context.addMatches(convertMatches(service, reply.value()));
0258                 // We are finished when all watchers finished
0259                 if (pendingServices->size() == 0) {
0260                     Q_EMIT matchInternalFinished(jobId);
0261                 }
0262             });
0263         };
0264         requestActionsForService(service, onActionsFinished);
0265     }
0266     m_actionsForSessionRequested = true;
0267 }
0268 
0269 void DBusRunner::run(const KRunner::RunnerContext & /*context*/, const KRunner::QueryMatch &match)
0270 {
0271     QString actionId;
0272     QString matchId;
0273     if (m_hasUniqueResults) {
0274         matchId = match.id();
0275     } else {
0276         matchId = match.id().mid(id().length() + 1); // QueryMatch::setId mangles the match ID with runnerID + '_'. This unmangles it
0277     }
0278     const QString service = match.data().toList().constFirst().toString();
0279 
0280     if (match.selectedAction()) {
0281         actionId = match.selectedAction().id();
0282     }
0283 
0284     auto runMethod = QDBusMessage::createMethodCall(service, m_path, m_ifaceName, QStringLiteral("Run"));
0285     runMethod.setArguments(QList<QVariant>({matchId, actionId}));
0286     QDBusConnection::sessionBus().call(runMethod, QDBus::NoBlock);
0287 }
0288 
0289 QImage DBusRunner::decodeImage(const RemoteImage &remoteImage)
0290 {
0291     auto copyLineRGB32 = [](QRgb *dst, const char *src, int width) {
0292         const char *end = src + width * 3;
0293         for (; src != end; ++dst, src += 3) {
0294             *dst = qRgb(src[0], src[1], src[2]);
0295         }
0296     };
0297 
0298     auto copyLineARGB32 = [](QRgb *dst, const char *src, int width) {
0299         const char *end = src + width * 4;
0300         for (; src != end; ++dst, src += 4) {
0301             *dst = qRgba(src[0], src[1], src[2], src[3]);
0302         }
0303     };
0304 
0305     if (remoteImage.width <= 0 || remoteImage.width >= 2048 || remoteImage.height <= 0 || remoteImage.height >= 2048 || remoteImage.rowStride <= 0) {
0306         qCWarning(KRUNNER) << "Invalid image metadata (width:" << remoteImage.width << "height:" << remoteImage.height << "rowStride:" << remoteImage.rowStride
0307                            << ")";
0308         return QImage();
0309     }
0310 
0311     QImage::Format format = QImage::Format_Invalid;
0312     void (*copyFn)(QRgb *, const char *, int) = nullptr;
0313     if (remoteImage.bitsPerSample == 8) {
0314         if (remoteImage.channels == 4) {
0315             format = QImage::Format_ARGB32;
0316             copyFn = copyLineARGB32;
0317         } else if (remoteImage.channels == 3) {
0318             format = QImage::Format_RGB32;
0319             copyFn = copyLineRGB32;
0320         }
0321     }
0322     if (format == QImage::Format_Invalid) {
0323         qCWarning(KRUNNER) << "Unsupported image format (hasAlpha:" << remoteImage.hasAlpha << "bitsPerSample:" << remoteImage.bitsPerSample
0324                            << "channels:" << remoteImage.channels << ")";
0325         return QImage();
0326     }
0327 
0328     QImage image(remoteImage.width, remoteImage.height, format);
0329     const QByteArray pixels = remoteImage.data;
0330     const char *ptr = pixels.data();
0331     const char *end = ptr + pixels.length();
0332     for (int y = 0; y < remoteImage.height; ++y, ptr += remoteImage.rowStride) {
0333         if (Q_UNLIKELY(ptr + remoteImage.channels * remoteImage.width > end)) {
0334             qCWarning(KRUNNER) << "Image data is incomplete. y:" << y << "height:" << remoteImage.height;
0335             break;
0336         }
0337         copyFn(reinterpret_cast<QRgb *>(image.scanLine(y)), ptr, remoteImage.width);
0338     }
0339 
0340     return image;
0341 }
0342 }
0343 #include "moc_dbusrunner_p.cpp"