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

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