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 if (!xdgActivationToken.isEmpty()) {
138 qputenv(varName: "XDG_ACTIVATION_TOKEN", value: xdgActivationToken.toUtf8());
139 }
140
141 const QString command = launcher + u' ' + QLatin1StringView(url.toEncoded());
142 if (debug)
143 qDebug(msg: "Launching %s", qPrintable(command));
144#if !QT_CONFIG(process)
145 const bool ok = ::system(qPrintable(command + " &"_L1));
146#else
147 QStringList args = QProcess::splitCommand(command);
148 bool ok = false;
149 if (!args.isEmpty()) {
150 QString program = args.takeFirst();
151 ok = QProcess::startDetached(program, arguments: args);
152 }
153#endif
154 if (!ok)
155 qWarning(msg: "Launch failed (%s)", qPrintable(command));
156
157 qunsetenv(varName: "XDG_ACTIVATION_TOKEN");
158
159 return ok;
160}
161
162#if QT_CONFIG(dbus)
163static inline bool checkNeedPortalSupport()
164{
165 return QFileInfo::exists(file: "/.flatpak-info"_L1) || qEnvironmentVariableIsSet(varName: "SNAP");
166}
167
168static inline QDBusMessage xdgDesktopPortalOpenFile(const QUrl &url, const QString &parentWindow,
169 const QString &xdgActivationToken)
170{
171 // DBus signature:
172 // OpenFile (IN s parent_window,
173 // IN h fd,
174 // IN a{sv} options,
175 // OUT o handle)
176 // Options:
177 // handle_token (s) - A string that will be used as the last element of the @handle.
178 // writable (b) - Whether to allow the chosen application to write to the file.
179
180 const int fd = qt_safe_open(pathname: QFile::encodeName(fileName: url.toLocalFile()), O_RDONLY);
181 if (fd != -1) {
182 QDBusMessage message = QDBusMessage::createMethodCall(destination: "org.freedesktop.portal.Desktop"_L1,
183 path: "/org/freedesktop/portal/desktop"_L1,
184 interface: "org.freedesktop.portal.OpenURI"_L1,
185 method: "OpenFile"_L1);
186
187 QDBusUnixFileDescriptor descriptor;
188 descriptor.giveFileDescriptor(fileDescriptor: fd);
189
190 QVariantMap options = {};
191
192 if (!xdgActivationToken.isEmpty()) {
193 options.insert(key: "activation_token"_L1, value: xdgActivationToken);
194 }
195
196 message << parentWindow << QVariant::fromValue(value: descriptor) << options;
197
198 return QDBusConnection::sessionBus().call(message);
199 }
200
201 return QDBusMessage::createError(type: QDBusError::InternalError, msg: qt_error_string());
202}
203
204static inline QDBusMessage xdgDesktopPortalOpenUrl(const QUrl &url, const QString &parentWindow,
205 const QString &xdgActivationToken)
206{
207 // DBus signature:
208 // OpenURI (IN s parent_window,
209 // IN s uri,
210 // IN a{sv} options,
211 // OUT o handle)
212 // Options:
213 // handle_token (s) - A string that will be used as the last element of the @handle.
214 // writable (b) - Whether to allow the chosen application to write to the file.
215 // This key only takes effect the uri points to a local file that is exported in the document portal,
216 // and the chosen application is sandboxed itself.
217
218 QDBusMessage message = QDBusMessage::createMethodCall(destination: "org.freedesktop.portal.Desktop"_L1,
219 path: "/org/freedesktop/portal/desktop"_L1,
220 interface: "org.freedesktop.portal.OpenURI"_L1,
221 method: "OpenURI"_L1);
222 // FIXME parent_window_id and handle writable option
223 QVariantMap options;
224
225 if (!xdgActivationToken.isEmpty()) {
226 options.insert(key: "activation_token"_L1, value: xdgActivationToken);
227 }
228
229 message << parentWindow << url.toString() << options;
230
231 return QDBusConnection::sessionBus().call(message);
232}
233
234static inline QDBusMessage xdgDesktopPortalSendEmail(const QUrl &url, const QString &parentWindow,
235 const QString &xdgActivationToken)
236{
237 // DBus signature:
238 // ComposeEmail (IN s parent_window,
239 // IN a{sv} options,
240 // OUT o handle)
241 // Options:
242 // address (s) - The email address to send to.
243 // subject (s) - The subject for the email.
244 // body (s) - The body for the email.
245 // attachment_fds (ah) - File descriptors for files to attach.
246
247 QUrlQuery urlQuery(url);
248 QVariantMap options;
249 options.insert(key: "address"_L1, value: url.path());
250 options.insert(key: "subject"_L1, value: urlQuery.queryItemValue(key: "subject"_L1));
251 options.insert(key: "body"_L1, value: urlQuery.queryItemValue(key: "body"_L1));
252
253 // O_PATH seems to be present since Linux 2.6.39, which is not case of RHEL 6
254#ifdef O_PATH
255 QList<QDBusUnixFileDescriptor> attachments;
256 const QStringList attachmentUris = urlQuery.allQueryItemValues(key: "attachment"_L1);
257
258 for (const QString &attachmentUri : attachmentUris) {
259 const int fd = qt_safe_open(pathname: QFile::encodeName(fileName: attachmentUri), O_PATH);
260 if (fd != -1) {
261 QDBusUnixFileDescriptor descriptor(fd);
262 attachments << descriptor;
263 qt_safe_close(fd);
264 }
265 }
266
267 options.insert(key: "attachment_fds"_L1, value: QVariant::fromValue(value: attachments));
268#endif
269
270 if (!xdgActivationToken.isEmpty()) {
271 options.insert(key: "activation_token"_L1, value: xdgActivationToken);
272 }
273
274 QDBusMessage message = QDBusMessage::createMethodCall(destination: "org.freedesktop.portal.Desktop"_L1,
275 path: "/org/freedesktop/portal/desktop"_L1,
276 interface: "org.freedesktop.portal.Email"_L1,
277 method: "ComposeEmail"_L1);
278
279 message << parentWindow << options;
280
281 return QDBusConnection::sessionBus().call(message);
282}
283
284namespace {
285struct XDGDesktopColor
286{
287 double r = 0;
288 double g = 0;
289 double b = 0;
290
291 QColor toQColor() const
292 {
293 constexpr auto rgbMax = 255;
294 return { static_cast<int>(r * rgbMax), static_cast<int>(g * rgbMax),
295 static_cast<int>(b * rgbMax) };
296 }
297};
298
299const QDBusArgument &operator>>(const QDBusArgument &argument, XDGDesktopColor &myStruct)
300{
301 argument.beginStructure();
302 argument >> myStruct.r >> myStruct.g >> myStruct.b;
303 argument.endStructure();
304 return argument;
305}
306
307class XdgDesktopPortalColorPicker : public QPlatformServiceColorPicker
308{
309 Q_OBJECT
310public:
311 XdgDesktopPortalColorPicker(const QString &parentWindowId, QWindow *parent)
312 : QPlatformServiceColorPicker(parent), m_parentWindowId(parentWindowId)
313 {
314 }
315
316 void pickColor() override
317 {
318 // DBus signature:
319 // PickColor (IN s parent_window,
320 // IN a{sv} options
321 // OUT o handle)
322 // Options:
323 // handle_token (s) - A string that will be used as the last element of the @handle.
324
325 QDBusMessage message = QDBusMessage::createMethodCall(
326 destination: "org.freedesktop.portal.Desktop"_L1, path: "/org/freedesktop/portal/desktop"_L1,
327 interface: "org.freedesktop.portal.Screenshot"_L1, method: "PickColor"_L1);
328 message << m_parentWindowId << QVariantMap();
329
330 QDBusPendingCall pendingCall = QDBusConnection::sessionBus().asyncCall(message);
331 auto watcher = new QDBusPendingCallWatcher(pendingCall, this);
332 connect(sender: watcher, signal: &QDBusPendingCallWatcher::finished, context: this,
333 slot: [this](QDBusPendingCallWatcher *watcher) {
334 watcher->deleteLater();
335 QDBusPendingReply<QDBusObjectPath> reply = *watcher;
336 if (reply.isError()) {
337 qWarning(msg: "DBus call to pick color failed: %s",
338 qPrintable(reply.error().message()));
339 Q_EMIT colorPicked(color: {});
340 } else {
341 QDBusConnection::sessionBus().connect(
342 service: "org.freedesktop.portal.Desktop"_L1, path: reply.value().path(),
343 interface: "org.freedesktop.portal.Request"_L1, name: "Response"_L1, receiver: this,
344 // clang-format off
345 SLOT(gotColorResponse(uint,QVariantMap))
346 // clang-format on
347 );
348 }
349 });
350 }
351
352private Q_SLOTS:
353 void gotColorResponse(uint result, const QVariantMap &map)
354 {
355 if (result != 0)
356 return;
357 if (map.contains(key: u"color"_s)) {
358 XDGDesktopColor color{};
359 map.value(key: u"color"_s).value<QDBusArgument>() >> color;
360 Q_EMIT colorPicked(color: color.toQColor());
361 } else {
362 Q_EMIT colorPicked(color: {});
363 }
364 deleteLater();
365 }
366
367private:
368 const QString m_parentWindowId;
369};
370} // namespace
371
372#endif // QT_CONFIG(dbus)
373
374QGenericUnixServices::QGenericUnixServices()
375{
376#if QT_CONFIG(dbus)
377 if (qEnvironmentVariableIntValue(varName: "QT_NO_XDG_DESKTOP_PORTAL") > 0) {
378 return;
379 }
380 QDBusMessage message = QDBusMessage::createMethodCall(
381 destination: "org.freedesktop.portal.Desktop"_L1, path: "/org/freedesktop/portal/desktop"_L1,
382 interface: "org.freedesktop.DBus.Properties"_L1, method: "Get"_L1);
383 message << "org.freedesktop.portal.Screenshot"_L1
384 << "version"_L1;
385
386 QDBusPendingCall pendingCall = QDBusConnection::sessionBus().asyncCall(message);
387 auto watcher = new QDBusPendingCallWatcher(pendingCall);
388 m_watcherConnection =
389 QObject::connect(sender: watcher, signal: &QDBusPendingCallWatcher::finished, context: watcher,
390 slot: [this](QDBusPendingCallWatcher *watcher) {
391 watcher->deleteLater();
392 QDBusPendingReply<QVariant> reply = *watcher;
393 if (!reply.isError() && reply.value().toUInt() >= 2)
394 m_hasScreenshotPortalWithColorPicking = true;
395 });
396
397#endif
398}
399
400QGenericUnixServices::~QGenericUnixServices()
401{
402#if QT_CONFIG(dbus)
403 QObject::disconnect(m_watcherConnection);
404#endif
405}
406
407QPlatformServiceColorPicker *QGenericUnixServices::colorPicker(QWindow *parent)
408{
409#if QT_CONFIG(dbus)
410 // Make double sure that we are in a wayland environment. In particular check
411 // WAYLAND_DISPLAY so also XWayland apps benefit from portal-based color picking.
412 // Outside wayland we'll rather rely on other means than the XDG desktop portal.
413 if (!qEnvironmentVariableIsEmpty(varName: "WAYLAND_DISPLAY")
414 || QGuiApplication::platformName().startsWith(s: "wayland"_L1)) {
415 return new XdgDesktopPortalColorPicker(portalWindowIdentifier(window: parent), parent);
416 }
417 return nullptr;
418#else
419 Q_UNUSED(parent);
420 return nullptr;
421#endif
422}
423
424QByteArray QGenericUnixServices::desktopEnvironment() const
425{
426 static const QByteArray result = detectDesktopEnvironment();
427 return result;
428}
429
430template<typename F>
431void runWithXdgActivationToken(F &&functionToCall)
432{
433#if QT_CONFIG(wayland)
434 QWindow *window = qGuiApp->focusWindow();
435
436 if (!window) {
437 functionToCall({});
438 return;
439 }
440
441 auto waylandApp = dynamic_cast<QNativeInterface::QWaylandApplication *>(
442 qGuiApp->platformNativeInterface());
443 auto waylandWindow =
444 dynamic_cast<QNativeInterface::Private::QWaylandWindow *>(window->handle());
445
446 if (!waylandWindow || !waylandApp) {
447 functionToCall({});
448 return;
449 }
450
451 QObject::connect(waylandWindow,
452 &QNativeInterface::Private::QWaylandWindow::xdgActivationTokenCreated,
453 waylandWindow, functionToCall, Qt::SingleShotConnection);
454 waylandWindow->requestXdgActivationToken(serial: waylandApp->lastInputSerial());
455#else
456 functionToCall({});
457#endif
458}
459
460bool QGenericUnixServices::openUrl(const QUrl &url)
461{
462 auto openUrlInternal = [this](const QUrl &url, const QString &xdgActivationToken) {
463 if (url.scheme() == "mailto"_L1) {
464# if QT_CONFIG(dbus)
465 if (checkNeedPortalSupport()) {
466 const QString parentWindow = QGuiApplication::focusWindow()
467 ? portalWindowIdentifier(window: QGuiApplication::focusWindow())
468 : QString();
469 QDBusError error = xdgDesktopPortalSendEmail(url, parentWindow, xdgActivationToken);
470 if (!error.isValid())
471 return true;
472
473 // service not running, fall back
474 }
475# endif
476 return openDocument(url);
477 }
478
479# if QT_CONFIG(dbus)
480 if (checkNeedPortalSupport()) {
481 const QString parentWindow = QGuiApplication::focusWindow()
482 ? portalWindowIdentifier(window: QGuiApplication::focusWindow())
483 : QString();
484 QDBusError error = xdgDesktopPortalOpenUrl(url, parentWindow, xdgActivationToken);
485 if (!error.isValid())
486 return true;
487 }
488# endif
489
490 if (m_webBrowser.isEmpty()
491 && !detectWebBrowser(desktop: desktopEnvironment(), checkBrowserVariable: true, browser: &m_webBrowser)) {
492 qWarning(msg: "Unable to detect a web browser to launch '%s'", qPrintable(url.toString()));
493 return false;
494 }
495 return launch(launcher: m_webBrowser, url, xdgActivationToken);
496 };
497
498 if (QGuiApplication::platformName().startsWith(s: "wayland"_L1)) {
499 runWithXdgActivationToken(
500 functionToCall: [openUrlInternal, url](const QString &token) { openUrlInternal(url, token); });
501
502 return true;
503
504 } else {
505 return openUrlInternal(url, QString());
506 }
507}
508
509bool QGenericUnixServices::openDocument(const QUrl &url)
510{
511 auto openDocumentInternal = [this](const QUrl &url, const QString &xdgActivationToken) {
512
513# if QT_CONFIG(dbus)
514 if (checkNeedPortalSupport()) {
515 const QString parentWindow = QGuiApplication::focusWindow()
516 ? portalWindowIdentifier(window: QGuiApplication::focusWindow())
517 : QString();
518 QDBusError error = xdgDesktopPortalOpenFile(url, parentWindow, xdgActivationToken);
519 if (!error.isValid())
520 return true;
521 }
522# endif
523
524 if (m_documentLauncher.isEmpty()
525 && !detectWebBrowser(desktop: desktopEnvironment(), checkBrowserVariable: false, browser: &m_documentLauncher)) {
526 qWarning(msg: "Unable to detect a launcher for '%s'", qPrintable(url.toString()));
527 return false;
528 }
529 return launch(launcher: m_documentLauncher, url, xdgActivationToken);
530 };
531
532 if (QGuiApplication::platformName().startsWith(s: "wayland"_L1)) {
533 runWithXdgActivationToken(functionToCall: [openDocumentInternal, url](const QString &token) {
534 openDocumentInternal(url, token);
535 });
536
537 return true;
538 } else {
539 return openDocumentInternal(url, QString());
540 }
541}
542
543#else
544QGenericUnixServices::QGenericUnixServices() = default;
545QGenericUnixServices::~QGenericUnixServices() = default;
546
547QByteArray QGenericUnixServices::desktopEnvironment() const
548{
549 return QByteArrayLiteral("UNKNOWN");
550}
551
552bool QGenericUnixServices::openUrl(const QUrl &url)
553{
554 Q_UNUSED(url);
555 qWarning("openUrl() not supported on this platform");
556 return false;
557}
558
559bool QGenericUnixServices::openDocument(const QUrl &url)
560{
561 Q_UNUSED(url);
562 qWarning("openDocument() not supported on this platform");
563 return false;
564}
565
566QPlatformServiceColorPicker *QGenericUnixServices::colorPicker(QWindow *parent)
567{
568 Q_UNUSED(parent);
569 return nullptr;
570}
571
572#endif // QT_NO_MULTIPROCESS
573
574QString QGenericUnixServices::portalWindowIdentifier(QWindow *window)
575{
576 Q_UNUSED(window);
577 return QString();
578}
579
580bool QGenericUnixServices::hasCapability(Capability capability) const
581{
582 switch (capability) {
583 case Capability::ColorPicking:
584 return m_hasScreenshotPortalWithColorPicking;
585 }
586 return false;
587}
588
589void QGenericUnixServices::setApplicationBadge(qint64 number)
590{
591#if QT_CONFIG(dbus)
592 if (qGuiApp->desktopFileName().isEmpty()) {
593 qWarning(msg: "QGuiApplication::desktopFileName() is empty");
594 return;
595 }
596
597
598 const QString launcherUrl = QStringLiteral("application://") + qGuiApp->desktopFileName() + QStringLiteral(".desktop");
599 const qint64 count = qBound(min: 0, val: number, max: 9999);
600 QVariantMap dbusUnityProperties;
601
602 if (count > 0) {
603 dbusUnityProperties[QStringLiteral("count")] = count;
604 dbusUnityProperties[QStringLiteral("count-visible")] = true;
605 } else {
606 dbusUnityProperties[QStringLiteral("count-visible")] = false;
607 }
608
609 auto signal = QDBusMessage::createSignal(QStringLiteral("/com/canonical/unity/launcherentry/")
610 + qGuiApp->applicationName(), QStringLiteral("com.canonical.Unity.LauncherEntry"), QStringLiteral("Update"));
611
612 signal.setArguments({launcherUrl, dbusUnityProperties});
613
614 QDBusConnection::sessionBus().send(message: signal);
615#else
616 Q_UNUSED(number)
617#endif
618}
619
620QT_END_NAMESPACE
621
622#include "qgenericunixservices.moc"
623

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