1/*
2 SPDX-FileCopyrightText: 2017, 2018 David Edmundson <davidedmundson@kde.org>
3 SPDX-FileCopyrightText: 2020 Alexander Lohnau <alexander.lohnau@gmx.de>
4 SPDX-FileCopyrightText: 2020 Kai Uwe Broulik <kde@broulik.de>
5
6 SPDX-License-Identifier: LGPL-2.0-or-later
7*/
8
9#include "dbusrunner_p.h"
10
11#include <QDBusConnection>
12#include <QDBusConnectionInterface>
13#include <QDBusMessage>
14#include <QDBusMetaType>
15#include <QDBusPendingReply>
16#include <QGuiApplication>
17#include <QIcon>
18#include <set>
19
20#include <KWaylandExtras>
21#include <KWindowSystem>
22
23#include "dbusutils_p.h"
24#include "krunner_debug.h"
25
26namespace KRunner
27{
28DBusRunner::DBusRunner(QObject *parent, const KPluginMetaData &data)
29 : KRunner::AbstractRunner(parent, data)
30 , m_path(data.value(QStringLiteral("X-Plasma-DBusRunner-Path"), QStringLiteral("/runner")))
31 , m_hasUniqueResults(data.value(QStringLiteral("X-Plasma-Runner-Unique-Results"), defaultValue: false))
32 , m_requestActionsOnce(data.value(QStringLiteral("X-Plasma-Request-Actions-Once"), defaultValue: false))
33 , m_callLifecycleMethods(data.value(QStringLiteral("X-Plasma-API")) == QLatin1String("DBus2"))
34 , m_ifaceName(QStringLiteral("org.kde.krunner1"))
35{
36 qDBusRegisterMetaType<RemoteMatch>();
37 qDBusRegisterMetaType<RemoteMatches>();
38 qDBusRegisterMetaType<KRunner::Action>();
39 qDBusRegisterMetaType<KRunner::Actions>();
40 qDBusRegisterMetaType<RemoteImage>();
41
42 QString requestedServiceName = data.value(QStringLiteral("X-Plasma-DBusRunner-Service"));
43 if (requestedServiceName.isEmpty() || m_path.isEmpty()) {
44 qCWarning(KRUNNER) << "Invalid entry:" << data;
45 return;
46 }
47
48 if (requestedServiceName.endsWith(c: QLatin1Char('*'))) {
49 requestedServiceName.chop(n: 1);
50 // find existing matching names
51 auto namesReply = QDBusConnection::sessionBus().interface()->registeredServiceNames();
52 if (namesReply.isValid()) {
53 const auto names = namesReply.value();
54 for (const QString &serviceName : names) {
55 if (serviceName.startsWith(s: requestedServiceName)) {
56 m_matchingServices << serviceName;
57 }
58 }
59 }
60 // and watch for changes
61 connect(sender: QDBusConnection::sessionBus().interface(),
62 signal: &QDBusConnectionInterface::serviceOwnerChanged,
63 context: this,
64 slot: [this, requestedServiceName](const QString &serviceName, const QString &oldOwner, const QString &newOwner) {
65 if (!serviceName.startsWith(s: requestedServiceName)) {
66 return;
67 }
68 if (!oldOwner.isEmpty() && !newOwner.isEmpty()) {
69 // changed owner, but service still exists. Don't need to adjust anything
70 return;
71 }
72 if (!newOwner.isEmpty()) {
73 m_matchingServices.insert(value: serviceName);
74 }
75 if (!oldOwner.isEmpty()) {
76 m_matchingServices.remove(value: serviceName);
77 }
78 });
79 } else {
80 // don't check when not wildcarded, as it could be used with DBus-activation
81 m_matchingServices << requestedServiceName;
82 }
83
84 connect(sender: this, signal: &AbstractRunner::teardown, context: this, slot: [this]() {
85 if (m_matchWasCalled) {
86 for (const QString &service : std::as_const(t&: m_matchingServices)) {
87 auto method = QDBusMessage::createMethodCall(destination: service, path: m_path, interface: m_ifaceName, QStringLiteral("Teardown"));
88 QDBusConnection::sessionBus().asyncCall(message: method);
89 }
90 }
91 m_actionsForSessionRequested = false;
92 m_matchWasCalled = false;
93 });
94
95 // Load the runner syntaxes
96 const QStringList syntaxes = data.value(QStringLiteral("X-Plasma-Runner-Syntaxes"), defaultValue: QStringList());
97 const QStringList syntaxDescriptions = data.value(QStringLiteral("X-Plasma-Runner-Syntax-Descriptions"), defaultValue: QStringList());
98 const int descriptionCount = syntaxDescriptions.count();
99 for (int i = 0; i < syntaxes.count(); ++i) {
100 const QString &query = syntaxes.at(i);
101 const QString description = i < descriptionCount ? syntaxDescriptions.at(i) : QString();
102 addSyntax(exampleQuery: query, description);
103 }
104}
105
106void DBusRunner::reloadConfiguration()
107{
108 // If we have already loaded a config, but the runner is told to reload it's config
109 if (m_callLifecycleMethods) {
110 suspendMatching(suspend: true);
111 requestConfig();
112 }
113}
114
115void DBusRunner::requestActionsForService(const QString &service, const std::function<void()> &finishedCallback)
116{
117 if (m_actionsForSessionRequested) {
118 finishedCallback();
119 return; // only once per match session
120 }
121 if (m_requestActionsOnce) {
122 if (m_requestedActionServices.contains(value: service)) {
123 finishedCallback();
124 return;
125 } else {
126 m_requestedActionServices << service;
127 }
128 }
129
130 auto getActionsMethod = QDBusMessage::createMethodCall(destination: service, path: m_path, interface: m_ifaceName, QStringLiteral("Actions"));
131 QDBusPendingReply<QList<KRunner::Action>> reply = QDBusConnection::sessionBus().asyncCall(message: getActionsMethod);
132 connect(sender: new QDBusPendingCallWatcher(reply), signal: &QDBusPendingCallWatcher::finished, context: this, slot: [this, service, reply, finishedCallback](auto watcher) {
133 watcher->deleteLater();
134 if (!reply.isValid()) {
135 qCDebug(KRUNNER) << "Error requesting actions; calling" << service << " :" << reply.error().name() << reply.error().message();
136 } else {
137 m_actions[service] = reply.value();
138 }
139 finishedCallback();
140 });
141}
142
143void DBusRunner::requestConfig()
144{
145 const QString service = *m_matchingServices.constBegin();
146 auto getConfigMethod = QDBusMessage::createMethodCall(destination: service, path: m_path, interface: m_ifaceName, QStringLiteral("Config"));
147 QDBusPendingReply<QVariantMap> reply = QDBusConnection::sessionBus().asyncCall(message: getConfigMethod);
148
149 auto watcher = new QDBusPendingCallWatcher(reply);
150 connect(sender: watcher, signal: &QDBusPendingCallWatcher::finished, context: this, slot: [this, watcher, service]() {
151 watcher->deleteLater();
152 QDBusReply<QVariantMap> reply = *watcher;
153 if (!reply.isValid()) {
154 suspendMatching(suspend: false);
155 qCWarning(KRUNNER) << "Error requesting config; calling" << service << " :" << reply.error().name() << reply.error().message();
156 return;
157 }
158 const QVariantMap config = reply.value();
159 for (auto it = config.cbegin(), end = config.cend(); it != end; ++it) {
160 if (it.key() == QLatin1String("MatchRegex")) {
161 QRegularExpression regex(it.value().toString());
162 setMatchRegex(regex);
163 } else if (it.key() == QLatin1String("MinLetterCount")) {
164 setMinLetterCount(it.value().toInt());
165 } else if (it.key() == QLatin1String("TriggerWords")) {
166 setTriggerWords(it.value().toStringList());
167 } else if (it.key() == QLatin1String("Actions")) {
168 m_actions[service] = it.value().value<QList<KRunner::Action>>();
169 m_requestedActionServices << service;
170 }
171 }
172 suspendMatching(suspend: false);
173 });
174}
175
176QList<QueryMatch> DBusRunner::convertMatches(const QString &service, const RemoteMatches &remoteMatches)
177{
178 QList<KRunner::QueryMatch> matches;
179 for (const RemoteMatch &match : remoteMatches) {
180 KRunner::QueryMatch m(this);
181
182 m.setText(match.text);
183 m.setIconName(match.iconName);
184 m.setCategoryRelevance(match.categoryRelevance);
185 m.setRelevance(match.relevance);
186
187 // split is essential items are as native DBus types, optional extras are in the property map (which is obviously a lot slower to parse)
188 m.setUrls(QUrl::fromStringList(uris: match.properties.value(QStringLiteral("urls")).toStringList()));
189 m.setMatchCategory(match.properties.value(QStringLiteral("category")).toString());
190 m.setSubtext(match.properties.value(QStringLiteral("subtext")).toString());
191 m.setData(QVariantList({service}));
192 m.setId(match.id);
193 m.setMultiLine(match.properties.value(QStringLiteral("multiline")).toBool());
194
195 const auto actionsIt = match.properties.find(QStringLiteral("actions"));
196 const KRunner::Actions actionList = m_actions.value(key: service);
197 if (actionsIt == match.properties.cend()) {
198 m.setActions(actionList);
199 } else {
200 KRunner::Actions requestedActions;
201 const QStringList actionIds = actionsIt.value().toStringList();
202 for (const auto &action : actionList) {
203 if (actionIds.contains(str: action.id())) {
204 requestedActions << action;
205 }
206 }
207 m.setActions(requestedActions);
208 }
209
210 const QVariant iconData = match.properties.value(QStringLiteral("icon-data"));
211 if (iconData.isValid()) {
212 const auto iconDataArgument = iconData.value<QDBusArgument>();
213 if (iconDataArgument.currentType() == QDBusArgument::StructureType && iconDataArgument.currentSignature() == QLatin1String("(iiibiiay)")) {
214 const RemoteImage remoteImage = qdbus_cast<RemoteImage>(arg: iconDataArgument);
215 if (QImage decodedImage = decodeImage(remoteImage); !decodedImage.isNull()) {
216 const QPixmap pix = QPixmap::fromImage(image: std::move(decodedImage));
217 QIcon icon(pix);
218 m.setIcon(icon);
219 // iconName normally takes precedence
220 m.setIconName(QString());
221 }
222 } else {
223 qCWarning(KRUNNER) << "Invalid signature of icon-data property:" << iconDataArgument.currentSignature();
224 }
225 }
226 matches.append(t: m);
227 }
228 return matches;
229}
230void DBusRunner::matchInternal(KRunner::RunnerContext context)
231{
232 const QString jobId = context.runnerJobId(runner: this);
233 if (m_matchingServices.isEmpty()) {
234 Q_EMIT matchInternalFinished(jobId);
235 }
236 m_matchWasCalled = true;
237
238 // we scope watchers to make sure the lambda that captures context by reference definitely gets disconnected when this function ends
239 std::shared_ptr<std::set<QString>> pendingServices(new std::set<QString>);
240
241 for (const QString &service : std::as_const(t&: m_matchingServices)) {
242 pendingServices->insert(x: service);
243
244 const auto onActionsFinished = [=, this]() mutable {
245 auto matchMethod = QDBusMessage::createMethodCall(destination: service, path: m_path, interface: m_ifaceName, QStringLiteral("Match"));
246 matchMethod.setArguments(QList<QVariant>({context.query()}));
247 QDBusPendingReply<RemoteMatches> reply = QDBusConnection::sessionBus().asyncCall(message: matchMethod);
248
249 auto watcher = new QDBusPendingCallWatcher(reply);
250
251 connect(sender: watcher, signal: &QDBusPendingCallWatcher::finished, context: this, slot: [this, service, context, reply, jobId, pendingServices, watcher]() mutable {
252 watcher->deleteLater();
253 pendingServices->erase(x: service);
254 if (reply.isError()) {
255 qCWarning(KRUNNER) << "Error requesting matches; calling" << service << " :" << reply.error().name() << reply.error().message();
256 } else {
257 context.addMatches(matches: convertMatches(service, remoteMatches: reply.value()));
258 }
259 // We are finished when all watchers finished
260 if (pendingServices->size() == 0) {
261 Q_EMIT matchInternalFinished(jobId);
262 }
263 });
264 };
265 requestActionsForService(service, finishedCallback: onActionsFinished);
266 }
267 m_actionsForSessionRequested = true;
268}
269
270void DBusRunner::run(const KRunner::RunnerContext & /*context*/, const KRunner::QueryMatch &match)
271{
272 QString actionId;
273 QString matchId;
274 if (m_hasUniqueResults) {
275 matchId = match.id();
276 } else {
277 matchId = match.id().mid(position: id().length() + 1); // QueryMatch::setId mangles the match ID with runnerID + '_'. This unmangles it
278 }
279 const QString service = match.data().toList().constFirst().toString();
280
281 if (match.selectedAction()) {
282 actionId = match.selectedAction().id();
283 }
284
285 auto run = [this, service, matchId, actionId] {
286 auto runMethod = QDBusMessage::createMethodCall(destination: service, path: m_path, interface: m_ifaceName, QStringLiteral("Run"));
287 runMethod.setArguments(QList<QVariant>({matchId, actionId}));
288 QDBusConnection::sessionBus().call(message: runMethod, mode: QDBus::NoBlock);
289 };
290
291 if (KWindowSystem::isPlatformWayland() && qGuiApp->focusWindow()) {
292 const int launchedSerial = KWaylandExtras::lastInputSerial(qGuiApp->focusWindow());
293 connect(
294 sender: KWaylandExtras::self(),
295 signal: &KWaylandExtras::xdgActivationTokenArrived,
296 context: this,
297 slot: [this, launchedSerial, service, matchId, actionId, run](int tokenSerial, const QString &token) {
298 Q_UNUSED(tokenSerial);
299 if (!token.isEmpty()) {
300 auto activationTokenMethod = QDBusMessage::createMethodCall(destination: service, path: m_path, interface: m_ifaceName, QStringLiteral("SetActivationToken"));
301 activationTokenMethod.setArguments(QList<QVariant>{token});
302 QDBusConnection::sessionBus().call(message: activationTokenMethod, mode: QDBus::NoBlock);
303 }
304
305 run();
306 },
307 type: Qt::SingleShotConnection);
308 KWaylandExtras::requestXdgActivationToken(qGuiApp->focusWindow(), serial: launchedSerial, app_id: {});
309 } else {
310 run();
311 }
312}
313
314QImage DBusRunner::decodeImage(const RemoteImage &remoteImage)
315{
316 auto copyLineRGB32 = [](QRgb *dst, const char *src, int width) {
317 const char *end = src + width * 3;
318 for (; src != end; ++dst, src += 3) {
319 *dst = qRgb(r: src[0], g: src[1], b: src[2]);
320 }
321 };
322
323 auto copyLineARGB32 = [](QRgb *dst, const char *src, int width) {
324 const char *end = src + width * 4;
325 for (; src != end; ++dst, src += 4) {
326 *dst = qRgba(r: src[0], g: src[1], b: src[2], a: src[3]);
327 }
328 };
329
330 if (remoteImage.width <= 0 || remoteImage.width >= 2048 || remoteImage.height <= 0 || remoteImage.height >= 2048 || remoteImage.rowStride <= 0) {
331 qCWarning(KRUNNER) << "Invalid image metadata (width:" << remoteImage.width << "height:" << remoteImage.height << "rowStride:" << remoteImage.rowStride
332 << ")";
333 return QImage();
334 }
335
336 QImage::Format format = QImage::Format_Invalid;
337 void (*copyFn)(QRgb *, const char *, int) = nullptr;
338 if (remoteImage.bitsPerSample == 8) {
339 if (remoteImage.channels == 4) {
340 format = QImage::Format_ARGB32;
341 copyFn = copyLineARGB32;
342 } else if (remoteImage.channels == 3) {
343 format = QImage::Format_RGB32;
344 copyFn = copyLineRGB32;
345 }
346 }
347 if (format == QImage::Format_Invalid) {
348 qCWarning(KRUNNER) << "Unsupported image format (hasAlpha:" << remoteImage.hasAlpha << "bitsPerSample:" << remoteImage.bitsPerSample
349 << "channels:" << remoteImage.channels << ")";
350 return QImage();
351 }
352
353 QImage image(remoteImage.width, remoteImage.height, format);
354 const QByteArray pixels = remoteImage.data;
355 const char *ptr = pixels.data();
356 const char *end = ptr + pixels.length();
357 for (int y = 0; y < remoteImage.height; ++y, ptr += remoteImage.rowStride) {
358 if (Q_UNLIKELY(ptr + remoteImage.channels * remoteImage.width > end)) {
359 qCWarning(KRUNNER) << "Image data is incomplete. y:" << y << "height:" << remoteImage.height;
360 break;
361 }
362 copyFn(reinterpret_cast<QRgb *>(image.scanLine(y)), ptr, remoteImage.width);
363 }
364
365 return image;
366}
367}
368#include "moc_dbusrunner_p.cpp"
369

source code of krunner/src/dbusrunner.cpp