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

source code of krunner/src/dbusrunner.cpp