qtbase/src/gui/platform/unix/qgenericunixservices.cpp
Christian Ehrlicher de609d84b9 QGenericUnixServices: properly disconnect signals in dtor
Since QGenericUnixServices does not derive from QObject, the connection
to QDBusPendingCallWatcher made in the ctor might outlive the lifetime
of QGenericUnixServices.
Fix it by explicitly disconnecting it in the dtor.

Fixes: QTBUG-125239
Pick-to: 6.8 6.7 6.5
Change-Id: I5fac4fd5831b2dde16b3d7b479a8ee616bfb7e3a
Reviewed-by: Thiago Macieira <thiago.macieira@intel.com>
2024-08-30 23:56:26 +02:00

623 lines
21 KiB
C++

// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#include "qgenericunixservices_p.h"
#include <QtGui/private/qtguiglobal_p.h>
#include "qguiapplication.h"
#include "qwindow.h"
#include <QtGui/qpa/qplatformwindow_p.h>
#include <QtGui/qpa/qplatformwindow.h>
#include <QtGui/qpa/qplatformnativeinterface.h>
#include <QtCore/QDebug>
#include <QtCore/QFile>
#if QT_CONFIG(process)
# include <QtCore/QProcess>
#endif
#if QT_CONFIG(settings)
#include <QtCore/QSettings>
#endif
#include <QtCore/QStandardPaths>
#include <QtCore/QUrl>
#if QT_CONFIG(dbus)
// These QtCore includes are needed for xdg-desktop-portal support
#include <QtCore/private/qcore_unix_p.h>
#include <QtCore/QFileInfo>
#include <QtCore/QUrlQuery>
#include <QtDBus/QDBusConnection>
#include <QtDBus/QDBusMessage>
#include <QtDBus/QDBusPendingCall>
#include <QtDBus/QDBusPendingCallWatcher>
#include <QtDBus/QDBusPendingReply>
#include <QtDBus/QDBusUnixFileDescriptor>
#include <fcntl.h>
#endif // QT_CONFIG(dbus)
#include <stdlib.h>
QT_BEGIN_NAMESPACE
using namespace Qt::StringLiterals;
#if QT_CONFIG(multiprocess)
enum { debug = 0 };
static inline QByteArray detectDesktopEnvironment()
{
const QByteArray xdgCurrentDesktop = qgetenv("XDG_CURRENT_DESKTOP");
if (!xdgCurrentDesktop.isEmpty())
return xdgCurrentDesktop.toUpper(); // KDE, GNOME, UNITY, LXDE, MATE, XFCE...
// Classic fallbacks
if (!qEnvironmentVariableIsEmpty("KDE_FULL_SESSION"))
return QByteArrayLiteral("KDE");
if (!qEnvironmentVariableIsEmpty("GNOME_DESKTOP_SESSION_ID"))
return QByteArrayLiteral("GNOME");
// Fallback to checking $DESKTOP_SESSION (unreliable)
QByteArray desktopSession = qgetenv("DESKTOP_SESSION");
// This can be a path in /usr/share/xsessions
int slash = desktopSession.lastIndexOf('/');
if (slash != -1) {
#if QT_CONFIG(settings)
QSettings desktopFile(QFile::decodeName(desktopSession + ".desktop"), QSettings::IniFormat);
desktopFile.beginGroup(QStringLiteral("Desktop Entry"));
QByteArray desktopName = desktopFile.value(QStringLiteral("DesktopNames")).toByteArray();
if (!desktopName.isEmpty())
return desktopName;
#endif
// try decoding just the basename
desktopSession = desktopSession.mid(slash + 1);
}
if (desktopSession == "gnome")
return QByteArrayLiteral("GNOME");
else if (desktopSession == "xfce")
return QByteArrayLiteral("XFCE");
else if (desktopSession == "kde")
return QByteArrayLiteral("KDE");
return QByteArrayLiteral("UNKNOWN");
}
static inline bool checkExecutable(const QString &candidate, QString *result)
{
*result = QStandardPaths::findExecutable(candidate);
return !result->isEmpty();
}
static inline bool detectWebBrowser(const QByteArray &desktop,
bool checkBrowserVariable,
QString *browser)
{
const char *browsers[] = {"google-chrome", "firefox", "mozilla", "opera"};
browser->clear();
if (checkExecutable(QStringLiteral("xdg-open"), browser))
return true;
if (checkBrowserVariable) {
QByteArray browserVariable = qgetenv("DEFAULT_BROWSER");
if (browserVariable.isEmpty())
browserVariable = qgetenv("BROWSER");
if (!browserVariable.isEmpty() && checkExecutable(QString::fromLocal8Bit(browserVariable), browser))
return true;
}
if (desktop == QByteArray("KDE")) {
if (checkExecutable(QStringLiteral("kde-open5"), browser))
return true;
// Konqueror launcher
if (checkExecutable(QStringLiteral("kfmclient"), browser)) {
browser->append(" exec"_L1);
return true;
}
} else if (desktop == QByteArray("GNOME")) {
if (checkExecutable(QStringLiteral("gnome-open"), browser))
return true;
}
for (size_t i = 0; i < sizeof(browsers)/sizeof(char *); ++i)
if (checkExecutable(QLatin1StringView(browsers[i]), browser))
return true;
return false;
}
static inline bool launch(const QString &launcher, const QUrl &url,
const QString &xdgActivationToken)
{
if (!xdgActivationToken.isEmpty()) {
qputenv("XDG_ACTIVATION_TOKEN", xdgActivationToken.toUtf8());
}
const QString command = launcher + u' ' + QLatin1StringView(url.toEncoded());
if (debug)
qDebug("Launching %s", qPrintable(command));
#if !QT_CONFIG(process)
const bool ok = ::system(qPrintable(command + " &"_L1));
#else
QStringList args = QProcess::splitCommand(command);
bool ok = false;
if (!args.isEmpty()) {
QString program = args.takeFirst();
ok = QProcess::startDetached(program, args);
}
#endif
if (!ok)
qWarning("Launch failed (%s)", qPrintable(command));
qunsetenv("XDG_ACTIVATION_TOKEN");
return ok;
}
#if QT_CONFIG(dbus)
static inline bool checkNeedPortalSupport()
{
return QFileInfo::exists("/.flatpak-info"_L1) || qEnvironmentVariableIsSet("SNAP");
}
static inline QDBusMessage xdgDesktopPortalOpenFile(const QUrl &url, const QString &parentWindow,
const QString &xdgActivationToken)
{
// DBus signature:
// OpenFile (IN s parent_window,
// IN h fd,
// IN a{sv} options,
// OUT o handle)
// Options:
// handle_token (s) - A string that will be used as the last element of the @handle.
// writable (b) - Whether to allow the chosen application to write to the file.
const int fd = qt_safe_open(QFile::encodeName(url.toLocalFile()), O_RDONLY);
if (fd != -1) {
QDBusMessage message = QDBusMessage::createMethodCall("org.freedesktop.portal.Desktop"_L1,
"/org/freedesktop/portal/desktop"_L1,
"org.freedesktop.portal.OpenURI"_L1,
"OpenFile"_L1);
QDBusUnixFileDescriptor descriptor;
descriptor.giveFileDescriptor(fd);
QVariantMap options = {};
if (!xdgActivationToken.isEmpty()) {
options.insert("activation_token"_L1, xdgActivationToken);
}
message << parentWindow << QVariant::fromValue(descriptor) << options;
return QDBusConnection::sessionBus().call(message);
}
return QDBusMessage::createError(QDBusError::InternalError, qt_error_string());
}
static inline QDBusMessage xdgDesktopPortalOpenUrl(const QUrl &url, const QString &parentWindow,
const QString &xdgActivationToken)
{
// DBus signature:
// OpenURI (IN s parent_window,
// IN s uri,
// IN a{sv} options,
// OUT o handle)
// Options:
// handle_token (s) - A string that will be used as the last element of the @handle.
// writable (b) - Whether to allow the chosen application to write to the file.
// This key only takes effect the uri points to a local file that is exported in the document portal,
// and the chosen application is sandboxed itself.
QDBusMessage message = QDBusMessage::createMethodCall("org.freedesktop.portal.Desktop"_L1,
"/org/freedesktop/portal/desktop"_L1,
"org.freedesktop.portal.OpenURI"_L1,
"OpenURI"_L1);
// FIXME parent_window_id and handle writable option
QVariantMap options;
if (!xdgActivationToken.isEmpty()) {
options.insert("activation_token"_L1, xdgActivationToken);
}
message << parentWindow << url.toString() << options;
return QDBusConnection::sessionBus().call(message);
}
static inline QDBusMessage xdgDesktopPortalSendEmail(const QUrl &url, const QString &parentWindow,
const QString &xdgActivationToken)
{
// DBus signature:
// ComposeEmail (IN s parent_window,
// IN a{sv} options,
// OUT o handle)
// Options:
// address (s) - The email address to send to.
// subject (s) - The subject for the email.
// body (s) - The body for the email.
// attachment_fds (ah) - File descriptors for files to attach.
QUrlQuery urlQuery(url);
QVariantMap options;
options.insert("address"_L1, url.path());
options.insert("subject"_L1, urlQuery.queryItemValue("subject"_L1));
options.insert("body"_L1, urlQuery.queryItemValue("body"_L1));
// O_PATH seems to be present since Linux 2.6.39, which is not case of RHEL 6
#ifdef O_PATH
QList<QDBusUnixFileDescriptor> attachments;
const QStringList attachmentUris = urlQuery.allQueryItemValues("attachment"_L1);
for (const QString &attachmentUri : attachmentUris) {
const int fd = qt_safe_open(QFile::encodeName(attachmentUri), O_PATH);
if (fd != -1) {
QDBusUnixFileDescriptor descriptor(fd);
attachments << descriptor;
qt_safe_close(fd);
}
}
options.insert("attachment_fds"_L1, QVariant::fromValue(attachments));
#endif
if (!xdgActivationToken.isEmpty()) {
options.insert("activation_token"_L1, xdgActivationToken);
}
QDBusMessage message = QDBusMessage::createMethodCall("org.freedesktop.portal.Desktop"_L1,
"/org/freedesktop/portal/desktop"_L1,
"org.freedesktop.portal.Email"_L1,
"ComposeEmail"_L1);
message << parentWindow << options;
return QDBusConnection::sessionBus().call(message);
}
namespace {
struct XDGDesktopColor
{
double r = 0;
double g = 0;
double b = 0;
QColor toQColor() const
{
constexpr auto rgbMax = 255;
return { static_cast<int>(r * rgbMax), static_cast<int>(g * rgbMax),
static_cast<int>(b * rgbMax) };
}
};
const QDBusArgument &operator>>(const QDBusArgument &argument, XDGDesktopColor &myStruct)
{
argument.beginStructure();
argument >> myStruct.r >> myStruct.g >> myStruct.b;
argument.endStructure();
return argument;
}
class XdgDesktopPortalColorPicker : public QPlatformServiceColorPicker
{
Q_OBJECT
public:
XdgDesktopPortalColorPicker(const QString &parentWindowId, QWindow *parent)
: QPlatformServiceColorPicker(parent), m_parentWindowId(parentWindowId)
{
}
void pickColor() override
{
// DBus signature:
// PickColor (IN s parent_window,
// IN a{sv} options
// OUT o handle)
// Options:
// handle_token (s) - A string that will be used as the last element of the @handle.
QDBusMessage message = QDBusMessage::createMethodCall(
"org.freedesktop.portal.Desktop"_L1, "/org/freedesktop/portal/desktop"_L1,
"org.freedesktop.portal.Screenshot"_L1, "PickColor"_L1);
message << m_parentWindowId << QVariantMap();
QDBusPendingCall pendingCall = QDBusConnection::sessionBus().asyncCall(message);
auto watcher = new QDBusPendingCallWatcher(pendingCall, this);
connect(watcher, &QDBusPendingCallWatcher::finished, this,
[this](QDBusPendingCallWatcher *watcher) {
watcher->deleteLater();
QDBusPendingReply<QDBusObjectPath> reply = *watcher;
if (reply.isError()) {
qWarning("DBus call to pick color failed: %s",
qPrintable(reply.error().message()));
Q_EMIT colorPicked({});
} else {
QDBusConnection::sessionBus().connect(
"org.freedesktop.portal.Desktop"_L1, reply.value().path(),
"org.freedesktop.portal.Request"_L1, "Response"_L1, this,
// clang-format off
SLOT(gotColorResponse(uint,QVariantMap))
// clang-format on
);
}
});
}
private Q_SLOTS:
void gotColorResponse(uint result, const QVariantMap &map)
{
if (result != 0)
return;
if (map.contains(u"color"_s)) {
XDGDesktopColor color{};
map.value(u"color"_s).value<QDBusArgument>() >> color;
Q_EMIT colorPicked(color.toQColor());
} else {
Q_EMIT colorPicked({});
}
deleteLater();
}
private:
const QString m_parentWindowId;
};
} // namespace
#endif // QT_CONFIG(dbus)
QGenericUnixServices::QGenericUnixServices()
{
#if QT_CONFIG(dbus)
if (qEnvironmentVariableIntValue("QT_NO_XDG_DESKTOP_PORTAL") > 0) {
return;
}
QDBusMessage message = QDBusMessage::createMethodCall(
"org.freedesktop.portal.Desktop"_L1, "/org/freedesktop/portal/desktop"_L1,
"org.freedesktop.DBus.Properties"_L1, "Get"_L1);
message << "org.freedesktop.portal.Screenshot"_L1
<< "version"_L1;
QDBusPendingCall pendingCall = QDBusConnection::sessionBus().asyncCall(message);
auto watcher = new QDBusPendingCallWatcher(pendingCall);
m_watcherConnection =
QObject::connect(watcher, &QDBusPendingCallWatcher::finished, watcher,
[this](QDBusPendingCallWatcher *watcher) {
watcher->deleteLater();
QDBusPendingReply<QVariant> reply = *watcher;
if (!reply.isError() && reply.value().toUInt() >= 2)
m_hasScreenshotPortalWithColorPicking = true;
});
#endif
}
QGenericUnixServices::~QGenericUnixServices()
{
#if QT_CONFIG(dbus)
QObject::disconnect(m_watcherConnection);
#endif
}
QPlatformServiceColorPicker *QGenericUnixServices::colorPicker(QWindow *parent)
{
#if QT_CONFIG(dbus)
// Make double sure that we are in a wayland environment. In particular check
// WAYLAND_DISPLAY so also XWayland apps benefit from portal-based color picking.
// Outside wayland we'll rather rely on other means than the XDG desktop portal.
if (!qEnvironmentVariableIsEmpty("WAYLAND_DISPLAY")
|| QGuiApplication::platformName().startsWith("wayland"_L1)) {
return new XdgDesktopPortalColorPicker(portalWindowIdentifier(parent), parent);
}
return nullptr;
#else
Q_UNUSED(parent);
return nullptr;
#endif
}
QByteArray QGenericUnixServices::desktopEnvironment() const
{
static const QByteArray result = detectDesktopEnvironment();
return result;
}
template<typename F>
void runWithXdgActivationToken(F &&functionToCall)
{
#if QT_CONFIG(wayland)
QWindow *window = qGuiApp->focusWindow();
if (!window) {
functionToCall({});
return;
}
auto waylandApp = dynamic_cast<QNativeInterface::QWaylandApplication *>(
qGuiApp->platformNativeInterface());
auto waylandWindow =
dynamic_cast<QNativeInterface::Private::QWaylandWindow *>(window->handle());
if (!waylandWindow || !waylandApp) {
functionToCall({});
return;
}
QObject::connect(waylandWindow,
&QNativeInterface::Private::QWaylandWindow::xdgActivationTokenCreated,
waylandWindow, functionToCall, Qt::SingleShotConnection);
waylandWindow->requestXdgActivationToken(waylandApp->lastInputSerial());
#else
functionToCall({});
#endif
}
bool QGenericUnixServices::openUrl(const QUrl &url)
{
auto openUrlInternal = [this](const QUrl &url, const QString &xdgActivationToken) {
if (url.scheme() == "mailto"_L1) {
# if QT_CONFIG(dbus)
if (checkNeedPortalSupport()) {
const QString parentWindow = QGuiApplication::focusWindow()
? portalWindowIdentifier(QGuiApplication::focusWindow())
: QString();
QDBusError error = xdgDesktopPortalSendEmail(url, parentWindow, xdgActivationToken);
if (!error.isValid())
return true;
// service not running, fall back
}
# endif
return openDocument(url);
}
# if QT_CONFIG(dbus)
if (checkNeedPortalSupport()) {
const QString parentWindow = QGuiApplication::focusWindow()
? portalWindowIdentifier(QGuiApplication::focusWindow())
: QString();
QDBusError error = xdgDesktopPortalOpenUrl(url, parentWindow, xdgActivationToken);
if (!error.isValid())
return true;
}
# endif
if (m_webBrowser.isEmpty()
&& !detectWebBrowser(desktopEnvironment(), true, &m_webBrowser)) {
qWarning("Unable to detect a web browser to launch '%s'", qPrintable(url.toString()));
return false;
}
return launch(m_webBrowser, url, xdgActivationToken);
};
if (QGuiApplication::platformName().startsWith("wayland"_L1)) {
runWithXdgActivationToken(
[openUrlInternal, url](const QString &token) { openUrlInternal(url, token); });
return true;
} else {
return openUrlInternal(url, QString());
}
}
bool QGenericUnixServices::openDocument(const QUrl &url)
{
auto openDocumentInternal = [this](const QUrl &url, const QString &xdgActivationToken) {
# if QT_CONFIG(dbus)
if (checkNeedPortalSupport()) {
const QString parentWindow = QGuiApplication::focusWindow()
? portalWindowIdentifier(QGuiApplication::focusWindow())
: QString();
QDBusError error = xdgDesktopPortalOpenFile(url, parentWindow, xdgActivationToken);
if (!error.isValid())
return true;
}
# endif
if (m_documentLauncher.isEmpty()
&& !detectWebBrowser(desktopEnvironment(), false, &m_documentLauncher)) {
qWarning("Unable to detect a launcher for '%s'", qPrintable(url.toString()));
return false;
}
return launch(m_documentLauncher, url, xdgActivationToken);
};
if (QGuiApplication::platformName().startsWith("wayland"_L1)) {
runWithXdgActivationToken([openDocumentInternal, url](const QString &token) {
openDocumentInternal(url, token);
});
return true;
} else {
return openDocumentInternal(url, QString());
}
}
#else
QGenericUnixServices::QGenericUnixServices() = default;
QGenericUnixServices::~QGenericUnixServices() = default;
QByteArray QGenericUnixServices::desktopEnvironment() const
{
return QByteArrayLiteral("UNKNOWN");
}
bool QGenericUnixServices::openUrl(const QUrl &url)
{
Q_UNUSED(url);
qWarning("openUrl() not supported on this platform");
return false;
}
bool QGenericUnixServices::openDocument(const QUrl &url)
{
Q_UNUSED(url);
qWarning("openDocument() not supported on this platform");
return false;
}
QPlatformServiceColorPicker *QGenericUnixServices::colorPicker(QWindow *parent)
{
Q_UNUSED(parent);
return nullptr;
}
#endif // QT_NO_MULTIPROCESS
QString QGenericUnixServices::portalWindowIdentifier(QWindow *window)
{
Q_UNUSED(window);
return QString();
}
bool QGenericUnixServices::hasCapability(Capability capability) const
{
switch (capability) {
case Capability::ColorPicking:
return m_hasScreenshotPortalWithColorPicking;
}
return false;
}
void QGenericUnixServices::setApplicationBadge(qint64 number)
{
#if QT_CONFIG(dbus)
if (qGuiApp->desktopFileName().isEmpty()) {
qWarning("QGuiApplication::desktopFileName() is empty");
return;
}
const QString launcherUrl = QStringLiteral("application://") + qGuiApp->desktopFileName() + QStringLiteral(".desktop");
const qint64 count = qBound(0, number, 9999);
QVariantMap dbusUnityProperties;
if (count > 0) {
dbusUnityProperties[QStringLiteral("count")] = count;
dbusUnityProperties[QStringLiteral("count-visible")] = true;
} else {
dbusUnityProperties[QStringLiteral("count-visible")] = false;
}
auto signal = QDBusMessage::createSignal(QStringLiteral("/com/canonical/unity/launcherentry/")
+ qGuiApp->applicationName(), QStringLiteral("com.canonical.Unity.LauncherEntry"), QStringLiteral("Update"));
signal.setArguments({launcherUrl, dbusUnityProperties});
QDBusConnection::sessionBus().send(signal);
#else
Q_UNUSED(number)
#endif
}
QT_END_NAMESPACE
#include "qgenericunixservices.moc"