1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3// Qt-Security score:critical reason:execute-external-code
4
5#include "qdesktopunixservices_p.h"
6#include <QtGui/private/qtguiglobal_p.h>
7#include "qguiapplication.h"
8#include "qwindow.h"
9#include <QtGui/qpa/qplatformwindow_p.h>
10#include <QtGui/qpa/qplatformwindow.h>
11#include <QtGui/qpa/qplatformnativeinterface.h>
12
13#include <QtCore/QDebug>
14#include <QtCore/QFile>
15#if QT_CONFIG(process)
16# include <QtCore/QProcess>
17#endif
18#if QT_CONFIG(settings)
19#include <QtCore/QSettings>
20#endif
21#include <QtCore/QStandardPaths>
22#include <QtCore/QUrl>
23
24#if QT_CONFIG(dbus)
25// These QtCore includes are needed for xdg-desktop-portal support
26#include <QtCore/private/qcore_unix_p.h>
27
28#include <QtCore/QFileInfo>
29#include <QtCore/QUrlQuery>
30
31#include <QtDBus/QDBusConnection>
32#include <QtDBus/QDBusMessage>
33#include <QtDBus/QDBusPendingCall>
34#include <QtDBus/QDBusPendingCallWatcher>
35#include <QtDBus/QDBusPendingReply>
36#include <QtDBus/QDBusUnixFileDescriptor>
37
38#include <fcntl.h>
39
40#endif // QT_CONFIG(dbus)
41
42#include <stdlib.h>
43
44QT_BEGIN_NAMESPACE
45
46using namespace Qt::StringLiterals;
47
48#if QT_CONFIG(multiprocess)
49
50static inline QByteArray detectDesktopEnvironment()
51{
52 const QByteArray xdgCurrentDesktop = qgetenv(varName: "XDG_CURRENT_DESKTOP");
53 if (!xdgCurrentDesktop.isEmpty())
54 return xdgCurrentDesktop.toUpper(); // KDE, GNOME, UNITY, LXDE, MATE, XFCE...
55
56 // Classic fallbacks
57 if (!qEnvironmentVariableIsEmpty(varName: "KDE_FULL_SESSION"))
58 return QByteArrayLiteral("KDE");
59 if (!qEnvironmentVariableIsEmpty(varName: "GNOME_DESKTOP_SESSION_ID"))
60 return QByteArrayLiteral("GNOME");
61
62 // Fallback to checking $DESKTOP_SESSION (unreliable)
63 QByteArray desktopSession = qgetenv(varName: "DESKTOP_SESSION");
64
65 // This can be a path in /usr/share/xsessions
66 int slash = desktopSession.lastIndexOf(ch: '/');
67 if (slash != -1) {
68#if QT_CONFIG(settings)
69 QSettings desktopFile(QFile::decodeName(localFileName: desktopSession + ".desktop"), QSettings::IniFormat);
70 desktopFile.beginGroup(QStringLiteral("Desktop Entry"));
71 QByteArray desktopName = desktopFile.value(QStringLiteral("DesktopNames")).toByteArray();
72 if (!desktopName.isEmpty())
73 return desktopName;
74#endif
75
76 // try decoding just the basename
77 desktopSession = desktopSession.mid(index: slash + 1);
78 }
79
80 if (desktopSession == "gnome")
81 return QByteArrayLiteral("GNOME");
82 else if (desktopSession == "xfce")
83 return QByteArrayLiteral("XFCE");
84 else if (desktopSession == "kde")
85 return QByteArrayLiteral("KDE");
86
87 return QByteArrayLiteral("UNKNOWN");
88}
89
90static inline bool checkExecutable(const QString &candidate, QString *result)
91{
92 *result = QStandardPaths::findExecutable(executableName: candidate);
93 return !result->isEmpty();
94}
95
96static inline bool detectWebBrowser(const QByteArray &desktop,
97 bool checkBrowserVariable,
98 QString *browser)
99{
100 const char *browsers[] = {"google-chrome", "firefox", "mozilla", "opera"};
101
102 browser->clear();
103 if (checkExecutable(QStringLiteral("xdg-open"), result: browser))
104 return true;
105
106 if (checkBrowserVariable) {
107 QString browserVariable = qEnvironmentVariable(varName: "DEFAULT_BROWSER");
108 if (browserVariable.isEmpty())
109 browserVariable = qEnvironmentVariable(varName: "BROWSER");
110 if (!browserVariable.isEmpty() && checkExecutable(candidate: browserVariable, result: browser))
111 return true;
112 }
113
114 if (desktop == QByteArray("KDE")) {
115 if (checkExecutable(QStringLiteral("kde-open5"), result: browser))
116 return true;
117 // Konqueror launcher
118 if (checkExecutable(QStringLiteral("kfmclient"), result: browser)) {
119 browser->append(s: " exec"_L1);
120 return true;
121 }
122 } else if (desktop == QByteArray("GNOME")) {
123 if (checkExecutable(QStringLiteral("gnome-open"), result: browser))
124 return true;
125 }
126
127 for (size_t i = 0; i < sizeof(browsers)/sizeof(char *); ++i)
128 if (checkExecutable(candidate: QLatin1StringView(browsers[i]), result: browser))
129 return true;
130 return false;
131}
132
133static inline bool launch(const QString &launcher, const QUrl &url,
134 const QString &xdgActivationToken)
135{
136
137 const QString command = launcher + u' ' + QLatin1StringView(url.toEncoded());
138 qCDebug(lcQpaServices, "Launching %s", qPrintable(command));
139#if !QT_CONFIG(process)
140 if (!xdgActivationToken.isEmpty())
141 qputenv("XDG_ACTIVATION_TOKEN", xdgActivationToken.toUtf8());
142 const bool ok = ::system(qPrintable(command + " &"_L1));
143 if (!xdgActivationToken.isEmpty())
144 qunsetenv("XDG_ACTIVATION_TOKEN");
145# else
146 QStringList args = QProcess::splitCommand(command);
147 bool ok = false;
148 if (!args.isEmpty()) {
149 QString program = args.takeFirst();
150 QProcess process;
151 process.setProgram(program);
152 process.setArguments(args);
153
154 if (!xdgActivationToken.isEmpty()) {
155 auto env = QProcessEnvironment::systemEnvironment();
156 env.insert(name: u"XDG_ACTIVATION_TOKEN"_s, value: xdgActivationToken);
157 process.setEnvironment(env.toStringList());
158 }
159 ok = process.startDetached(pid: nullptr);
160 }
161# endif
162 if (!ok)
163 qCWarning(lcQpaServices, "Launch failed (%s)", qPrintable(command));
164
165 return ok;
166}
167
168#if QT_CONFIG(dbus)
169static inline bool checkNeedPortalSupport()
170{
171 return QFileInfo::exists(file: "/.flatpak-info"_L1) || qEnvironmentVariableIsSet(varName: "SNAP");
172}
173
174static inline QDBusMessage xdgDesktopPortalOpenFile(const QUrl &url, const QString &parentWindow,
175 const QString &xdgActivationToken)
176{
177 // DBus signature:
178 // OpenFile (IN s parent_window,
179 // IN h fd,
180 // IN a{sv} options,
181 // OUT o handle)
182 // Options:
183 // handle_token (s) - A string that will be used as the last element of the @handle.
184 // writable (b) - Whether to allow the chosen application to write to the file.
185
186 const int fd = qt_safe_open(pathname: QFile::encodeName(fileName: url.toLocalFile()), O_RDONLY);
187 if (fd != -1) {
188 QDBusMessage message = QDBusMessage::createMethodCall(destination: "org.freedesktop.portal.Desktop"_L1,
189 path: "/org/freedesktop/portal/desktop"_L1,
190 interface: "org.freedesktop.portal.OpenURI"_L1,
191 method: "OpenFile"_L1);
192
193 QDBusUnixFileDescriptor descriptor;
194 descriptor.giveFileDescriptor(fileDescriptor: fd);
195
196 QVariantMap options = {};
197
198 if (!xdgActivationToken.isEmpty()) {
199 options.insert(key: "activation_token"_L1, value: xdgActivationToken);
200 }
201
202 message << parentWindow << QVariant::fromValue(value: descriptor) << options;
203
204 return QDBusConnection::sessionBus().call(message);
205 }
206
207 return QDBusMessage::createError(type: QDBusError::InternalError, msg: qt_error_string());
208}
209
210static inline QDBusMessage xdgDesktopPortalOpenUrl(const QUrl &url, const QString &parentWindow,
211 const QString &xdgActivationToken)
212{
213 // DBus signature:
214 // OpenURI (IN s parent_window,
215 // IN s uri,
216 // IN a{sv} options,
217 // OUT o handle)
218 // Options:
219 // handle_token (s) - A string that will be used as the last element of the @handle.
220 // writable (b) - Whether to allow the chosen application to write to the file.
221 // This key only takes effect the uri points to a local file that is exported in the document portal,
222 // and the chosen application is sandboxed itself.
223
224 QDBusMessage message = QDBusMessage::createMethodCall(destination: "org.freedesktop.portal.Desktop"_L1,
225 path: "/org/freedesktop/portal/desktop"_L1,
226 interface: "org.freedesktop.portal.OpenURI"_L1,
227 method: "OpenURI"_L1);
228 // FIXME parent_window_id and handle writable option
229 QVariantMap options;
230
231 if (!xdgActivationToken.isEmpty()) {
232 options.insert(key: "activation_token"_L1, value: xdgActivationToken);
233 }
234
235 message << parentWindow << url.toString() << options;
236
237 return QDBusConnection::sessionBus().call(message);
238}
239
240static inline QDBusMessage xdgDesktopPortalSendEmail(const QUrl &url, const QString &parentWindow,
241 const QString &xdgActivationToken)
242{
243 // DBus signature:
244 // ComposeEmail (IN s parent_window,
245 // IN a{sv} options,
246 // OUT o handle)
247 // Options:
248 // address (s) - The email address to send to.
249 // subject (s) - The subject for the email.
250 // body (s) - The body for the email.
251 // attachment_fds (ah) - File descriptors for files to attach.
252
253 QUrlQuery urlQuery(url);
254 QVariantMap options;
255 options.insert(key: "address"_L1, value: url.path());
256 options.insert(key: "subject"_L1, value: urlQuery.queryItemValue(key: "subject"_L1));
257 options.insert(key: "body"_L1, value: urlQuery.queryItemValue(key: "body"_L1));
258
259 // O_PATH seems to be present since Linux 2.6.39, which is not case of RHEL 6
260#ifdef O_PATH
261 QList<QDBusUnixFileDescriptor> attachments;
262 const QStringList attachmentUris = urlQuery.allQueryItemValues(key: "attachment"_L1);
263
264 for (const QString &attachmentUri : attachmentUris) {
265 const int fd = qt_safe_open(pathname: QFile::encodeName(fileName: attachmentUri), O_PATH);
266 if (fd != -1) {
267 QDBusUnixFileDescriptor descriptor(fd);
268 attachments << descriptor;
269 qt_safe_close(fd);
270 }
271 }
272
273 options.insert(key: "attachment_fds"_L1, value: QVariant::fromValue(value: attachments));
274#endif
275
276 if (!xdgActivationToken.isEmpty()) {
277 options.insert(key: "activation_token"_L1, value: xdgActivationToken);
278 }
279
280 QDBusMessage message = QDBusMessage::createMethodCall(destination: "org.freedesktop.portal.Desktop"_L1,
281 path: "/org/freedesktop/portal/desktop"_L1,
282 interface: "org.freedesktop.portal.Email"_L1,
283 method: "ComposeEmail"_L1);
284
285 message << parentWindow << options;
286
287 return QDBusConnection::sessionBus().call(message);
288}
289
290namespace {
291struct XDGDesktopColor
292{
293 double r = 0;
294 double g = 0;
295 double b = 0;
296
297 QColor toQColor() const
298 {
299 constexpr auto rgbMax = 255;
300 return { static_cast<int>(r * rgbMax), static_cast<int>(g * rgbMax),
301 static_cast<int>(b * rgbMax) };
302 }
303};
304
305const QDBusArgument &operator>>(const QDBusArgument &argument, XDGDesktopColor &myStruct)
306{
307 argument.beginStructure();
308 argument >> myStruct.r >> myStruct.g >> myStruct.b;
309 argument.endStructure();
310 return argument;
311}
312
313class XdgDesktopPortalColorPicker : public QPlatformServiceColorPicker
314{
315 Q_OBJECT
316public:
317 XdgDesktopPortalColorPicker(const QString &parentWindowId, QWindow *parent)
318 : QPlatformServiceColorPicker(parent), m_parentWindowId(parentWindowId)
319 {
320 }
321
322 void pickColor() override
323 {
324 // DBus signature:
325 // PickColor (IN s parent_window,
326 // IN a{sv} options
327 // OUT o handle)
328 // Options:
329 // handle_token (s) - A string that will be used as the last element of the @handle.
330
331 QDBusMessage message = QDBusMessage::createMethodCall(
332 destination: "org.freedesktop.portal.Desktop"_L1, path: "/org/freedesktop/portal/desktop"_L1,
333 interface: "org.freedesktop.portal.Screenshot"_L1, method: "PickColor"_L1);
334 message << m_parentWindowId << QVariantMap();
335
336 QDBusPendingCall pendingCall = QDBusConnection::sessionBus().asyncCall(message);
337 auto watcher = new QDBusPendingCallWatcher(pendingCall, this);
338 connect(sender: watcher, signal: &QDBusPendingCallWatcher::finished, context: this,
339 slot: [this](QDBusPendingCallWatcher *watcher) {
340 watcher->deleteLater();
341 QDBusPendingReply<QDBusObjectPath> reply = *watcher;
342 if (reply.isError()) {
343 qWarning(msg: "DBus call to pick color failed: %s",
344 qPrintable(reply.error().message()));
345 Q_EMIT colorPicked(color: {});
346 } else {
347 QDBusConnection::sessionBus().connect(
348 service: "org.freedesktop.portal.Desktop"_L1, path: reply.value().path(),
349 interface: "org.freedesktop.portal.Request"_L1, name: "Response"_L1, receiver: this,
350 // clang-format off
351 SLOT(gotColorResponse(uint,QVariantMap))
352 // clang-format on
353 );
354 }
355 });
356 }
357
358private Q_SLOTS:
359 void gotColorResponse(uint result, const QVariantMap &map)
360 {
361 if (result != 0)
362 return;
363 if (map.contains(key: u"color"_s)) {
364 XDGDesktopColor color{};
365 map.value(key: u"color"_s).value<QDBusArgument>() >> color;
366 Q_EMIT colorPicked(color: color.toQColor());
367 } else {
368 Q_EMIT colorPicked(color: {});
369 }
370 deleteLater();
371 }
372
373private:
374 const QString m_parentWindowId;
375};
376} // namespace
377
378#endif // QT_CONFIG(dbus)
379
380QDesktopUnixServices::QDesktopUnixServices()
381{
382 if (detectDesktopEnvironment() == QByteArrayLiteral("UNKNOWN"))
383 return;
384
385#if QT_CONFIG(dbus)
386 if (qEnvironmentVariableIntValue(varName: "QT_NO_XDG_DESKTOP_PORTAL") > 0) {
387 return;
388 }
389 QDBusMessage message = QDBusMessage::createMethodCall(
390 destination: "org.freedesktop.portal.Desktop"_L1, path: "/org/freedesktop/portal/desktop"_L1,
391 interface: "org.freedesktop.DBus.Properties"_L1, method: "Get"_L1);
392 message << "org.freedesktop.portal.Screenshot"_L1
393 << "version"_L1;
394
395 QDBusPendingCall pendingCall = QDBusConnection::sessionBus().asyncCall(message);
396 auto watcher = new QDBusPendingCallWatcher(pendingCall);
397 m_watcher = watcher;
398 QObject::connect(sender: watcher, signal: &QDBusPendingCallWatcher::finished, context: watcher,
399 slot: [this](QDBusPendingCallWatcher *watcher) {
400 watcher->deleteLater();
401 QDBusPendingReply<QVariant> reply = *watcher;
402 if (!reply.isError() && reply.value().toUInt() >= 2)
403 m_hasScreenshotPortalWithColorPicking = true;
404 });
405
406#endif
407}
408
409QDesktopUnixServices::~QDesktopUnixServices()
410{
411#if QT_CONFIG(dbus)
412 delete m_watcher;
413#endif
414}
415
416QPlatformServiceColorPicker *QDesktopUnixServices::colorPicker(QWindow *parent)
417{
418#if QT_CONFIG(dbus)
419 // Make double sure that we are in a wayland environment. In particular check
420 // WAYLAND_DISPLAY so also XWayland apps benefit from portal-based color picking.
421 // Outside wayland we'll rather rely on other means than the XDG desktop portal.
422 if (!qEnvironmentVariableIsEmpty(varName: "WAYLAND_DISPLAY")
423 || QGuiApplication::platformName().startsWith(s: "wayland"_L1)) {
424 return new XdgDesktopPortalColorPicker(portalWindowIdentifier(window: parent), parent);
425 }
426 return nullptr;
427#else
428 Q_UNUSED(parent);
429 return nullptr;
430#endif
431}
432
433QByteArray QDesktopUnixServices::desktopEnvironment() const
434{
435 static const QByteArray result = detectDesktopEnvironment();
436 return result;
437}
438
439template<typename F>
440void runWithXdgActivationToken(F &&functionToCall)
441{
442#if QT_CONFIG(wayland)
443 QWindow *window = qGuiApp->focusWindow();
444
445 if (!window) {
446 functionToCall({});
447 return;
448 }
449
450 auto waylandApp = dynamic_cast<QNativeInterface::QWaylandApplication *>(
451 qGuiApp->platformNativeInterface());
452 auto waylandWindow =
453 dynamic_cast<QNativeInterface::Private::QWaylandWindow *>(window->handle());
454
455 if (!waylandWindow || !waylandApp) {
456 functionToCall({});
457 return;
458 }
459
460 QObject::connect(waylandWindow,
461 &QNativeInterface::Private::QWaylandWindow::xdgActivationTokenCreated,
462 waylandWindow, functionToCall, Qt::SingleShotConnection);
463 waylandWindow->requestXdgActivationToken(serial: waylandApp->lastInputSerial());
464#else
465 functionToCall({});
466#endif
467}
468
469bool QDesktopUnixServices::openUrl(const QUrl &url)
470{
471 auto openUrlInternal = [this](const QUrl &url, const QString &xdgActivationToken) {
472 if (url.scheme() == "mailto"_L1) {
473# if QT_CONFIG(dbus)
474 if (checkNeedPortalSupport()) {
475 const QString parentWindow = QGuiApplication::focusWindow()
476 ? portalWindowIdentifier(window: QGuiApplication::focusWindow())
477 : QString();
478 QDBusError error = xdgDesktopPortalSendEmail(url, parentWindow, xdgActivationToken);
479 if (!error.isValid())
480 return true;
481
482 // service not running, fall back
483 }
484# endif
485 return openDocument(url);
486 }
487
488# if QT_CONFIG(dbus)
489 if (checkNeedPortalSupport()) {
490 const QString parentWindow = QGuiApplication::focusWindow()
491 ? portalWindowIdentifier(window: QGuiApplication::focusWindow())
492 : QString();
493 QDBusError error = xdgDesktopPortalOpenUrl(url, parentWindow, xdgActivationToken);
494 if (!error.isValid())
495 return true;
496 }
497# endif
498
499 if (m_webBrowser.isEmpty()
500 && !detectWebBrowser(desktop: desktopEnvironment(), checkBrowserVariable: true, browser: &m_webBrowser)) {
501 qCWarning(lcQpaServices, "Unable to detect a web browser to launch '%s'", qPrintable(url.toString()));
502 return false;
503 }
504 return launch(launcher: m_webBrowser, url, xdgActivationToken);
505 };
506
507 if (QGuiApplication::platformName().startsWith(s: "wayland"_L1)) {
508 runWithXdgActivationToken(
509 functionToCall: [openUrlInternal, url](const QString &token) { openUrlInternal(url, token); });
510
511 return true;
512
513 } else {
514 return openUrlInternal(url, QString());
515 }
516}
517
518bool QDesktopUnixServices::openDocument(const QUrl &url)
519{
520 auto openDocumentInternal = [this](const QUrl &url, const QString &xdgActivationToken) {
521
522# if QT_CONFIG(dbus)
523 if (checkNeedPortalSupport()) {
524 const QString parentWindow = QGuiApplication::focusWindow()
525 ? portalWindowIdentifier(window: QGuiApplication::focusWindow())
526 : QString();
527 QDBusError error = xdgDesktopPortalOpenFile(url, parentWindow, xdgActivationToken);
528 if (!error.isValid())
529 return true;
530 }
531# endif
532
533 if (m_documentLauncher.isEmpty()
534 && !detectWebBrowser(desktop: desktopEnvironment(), checkBrowserVariable: false, browser: &m_documentLauncher)) {
535 qCWarning(lcQpaServices, "Unable to detect a launcher for '%s'", qPrintable(url.toString()));
536 return false;
537 }
538 return launch(launcher: m_documentLauncher, url, xdgActivationToken);
539 };
540
541 if (QGuiApplication::platformName().startsWith(s: "wayland"_L1)) {
542 runWithXdgActivationToken(functionToCall: [openDocumentInternal, url](const QString &token) {
543 openDocumentInternal(url, token);
544 });
545
546 return true;
547 } else {
548 return openDocumentInternal(url, QString());
549 }
550}
551
552#else
553QDesktopUnixServices::QDesktopUnixServices() = default;
554QDesktopUnixServices::~QDesktopUnixServices() = default;
555
556QByteArray QDesktopUnixServices::desktopEnvironment() const
557{
558 return QByteArrayLiteral("UNKNOWN");
559}
560
561bool QDesktopUnixServices::openUrl(const QUrl &url)
562{
563 Q_UNUSED(url);
564 qWarning("openUrl() not supported on this platform");
565 return false;
566}
567
568bool QDesktopUnixServices::openDocument(const QUrl &url)
569{
570 Q_UNUSED(url);
571 qWarning("openDocument() not supported on this platform");
572 return false;
573}
574
575QPlatformServiceColorPicker *QDesktopUnixServices::colorPicker(QWindow *parent)
576{
577 Q_UNUSED(parent);
578 return nullptr;
579}
580
581#endif // QT_CONFIG(multiprocess)
582
583QString QDesktopUnixServices::portalWindowIdentifier(QWindow *window)
584{
585 Q_UNUSED(window);
586 return QString();
587}
588
589
590void QDesktopUnixServices::registerDBusMenuForWindow(QWindow *window, const QString &service, const QString &path)
591{
592 Q_UNUSED(window);
593 Q_UNUSED(service);
594 Q_UNUSED(path);
595}
596
597void QDesktopUnixServices::unregisterDBusMenuForWindow(QWindow *window)
598{
599 Q_UNUSED(window);
600}
601
602
603bool QDesktopUnixServices::hasCapability(Capability capability) const
604{
605 switch (capability) {
606 case Capability::ColorPicking:
607 return m_hasScreenshotPortalWithColorPicking;
608 }
609 return false;
610}
611
612void QDesktopUnixServices::setApplicationBadge(qint64 number)
613{
614#if QT_CONFIG(dbus)
615 if (qGuiApp->desktopFileName().isEmpty()) {
616 qCWarning(lcQpaServices, "Cannot set badge number - QGuiApplication::desktopFileName() is empty");
617 return;
618 }
619
620
621 const QString launcherUrl = QStringLiteral("application://") + qGuiApp->desktopFileName() + QStringLiteral(".desktop");
622 const qint64 count = qBound(min: 0, val: number, max: 9999);
623 QVariantMap dbusUnityProperties;
624
625 if (count > 0) {
626 dbusUnityProperties[QStringLiteral("count")] = count;
627 dbusUnityProperties[QStringLiteral("count-visible")] = true;
628 } else {
629 dbusUnityProperties[QStringLiteral("count-visible")] = false;
630 }
631
632 auto signal = QDBusMessage::createSignal(QStringLiteral("/com/canonical/unity/launcherentry/")
633 + qGuiApp->applicationName(), QStringLiteral("com.canonical.Unity.LauncherEntry"), QStringLiteral("Update"));
634
635 signal.setArguments({launcherUrl, dbusUnityProperties});
636
637 QDBusConnection::sessionBus().send(message: signal);
638#else
639 Q_UNUSED(number)
640#endif
641}
642
643QT_END_NAMESPACE
644
645#include "qdesktopunixservices.moc"
646

source code of qtbase/src/gui/platform/unix/qdesktopunixservices.cpp