1 | /**************************************************************************** |
2 | ** |
3 | ** Copyright (C) 2016 The Qt Company Ltd. |
4 | ** Contact: https://www.qt.io/licensing/ |
5 | ** |
6 | ** This file is part of the plugins of the Qt Toolkit. |
7 | ** |
8 | ** $QT_BEGIN_LICENSE:LGPL$ |
9 | ** Commercial License Usage |
10 | ** Licensees holding valid commercial Qt licenses may use this file in |
11 | ** accordance with the commercial license agreement provided with the |
12 | ** Software or, alternatively, in accordance with the terms contained in |
13 | ** a written agreement between you and The Qt Company. For licensing terms |
14 | ** and conditions see https://www.qt.io/terms-conditions. For further |
15 | ** information use the contact form at https://www.qt.io/contact-us. |
16 | ** |
17 | ** GNU Lesser General Public License Usage |
18 | ** Alternatively, this file may be used under the terms of the GNU Lesser |
19 | ** General Public License version 3 as published by the Free Software |
20 | ** Foundation and appearing in the file LICENSE.LGPL3 included in the |
21 | ** packaging of this file. Please review the following information to |
22 | ** ensure the GNU Lesser General Public License version 3 requirements |
23 | ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. |
24 | ** |
25 | ** GNU General Public License Usage |
26 | ** Alternatively, this file may be used under the terms of the GNU |
27 | ** General Public License version 2.0 or (at your option) the GNU General |
28 | ** Public license version 3 or any later version approved by the KDE Free |
29 | ** Qt Foundation. The licenses are as published by the Free Software |
30 | ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 |
31 | ** included in the packaging of this file. Please review the following |
32 | ** information to ensure the GNU General Public License requirements will |
33 | ** be met: https://www.gnu.org/licenses/gpl-2.0.html and |
34 | ** https://www.gnu.org/licenses/gpl-3.0.html. |
35 | ** |
36 | ** $QT_END_LICENSE$ |
37 | ** |
38 | ****************************************************************************/ |
39 | |
40 | #include "qgenericunixservices_p.h" |
41 | #include <QtGui/private/qtguiglobal_p.h> |
42 | |
43 | #include <QtCore/QDebug> |
44 | #include <QtCore/QFile> |
45 | #if QT_CONFIG(process) |
46 | # include <QtCore/QProcess> |
47 | #endif |
48 | #if QT_CONFIG(settings) |
49 | #include <QtCore/QSettings> |
50 | #endif |
51 | #include <QtCore/QStandardPaths> |
52 | #include <QtCore/QUrl> |
53 | |
54 | #if QT_CONFIG(dbus) |
55 | // These QtCore includes are needed for xdg-desktop-portal support |
56 | #include <QtCore/private/qcore_unix_p.h> |
57 | |
58 | #include <QtCore/QFileInfo> |
59 | #include <QtCore/QUrlQuery> |
60 | |
61 | #include <QtDBus/QDBusConnection> |
62 | #include <QtDBus/QDBusMessage> |
63 | #include <QtDBus/QDBusPendingCall> |
64 | #include <QtDBus/QDBusPendingCallWatcher> |
65 | #include <QtDBus/QDBusPendingReply> |
66 | #include <QtDBus/QDBusUnixFileDescriptor> |
67 | |
68 | #include <fcntl.h> |
69 | |
70 | #endif // QT_CONFIG(dbus) |
71 | |
72 | #include <stdlib.h> |
73 | |
74 | QT_BEGIN_NAMESPACE |
75 | |
76 | #if QT_CONFIG(multiprocess) |
77 | |
78 | enum { debug = 0 }; |
79 | |
80 | static inline QByteArray detectDesktopEnvironment() |
81 | { |
82 | const QByteArray xdgCurrentDesktop = qgetenv("XDG_CURRENT_DESKTOP" ); |
83 | if (!xdgCurrentDesktop.isEmpty()) |
84 | return xdgCurrentDesktop.toUpper(); // KDE, GNOME, UNITY, LXDE, MATE, XFCE... |
85 | |
86 | // Classic fallbacks |
87 | if (!qEnvironmentVariableIsEmpty("KDE_FULL_SESSION" )) |
88 | return QByteArrayLiteral("KDE" ); |
89 | if (!qEnvironmentVariableIsEmpty("GNOME_DESKTOP_SESSION_ID" )) |
90 | return QByteArrayLiteral("GNOME" ); |
91 | |
92 | // Fallback to checking $DESKTOP_SESSION (unreliable) |
93 | QByteArray desktopSession = qgetenv("DESKTOP_SESSION" ); |
94 | |
95 | // This can be a path in /usr/share/xsessions |
96 | int slash = desktopSession.lastIndexOf('/'); |
97 | if (slash != -1) { |
98 | #if QT_CONFIG(settings) |
99 | QSettings desktopFile(QFile::decodeName(desktopSession + ".desktop" ), QSettings::IniFormat); |
100 | desktopFile.beginGroup(QStringLiteral("Desktop Entry" )); |
101 | QByteArray desktopName = desktopFile.value(QStringLiteral("DesktopNames" )).toByteArray(); |
102 | if (!desktopName.isEmpty()) |
103 | return desktopName; |
104 | #endif |
105 | |
106 | // try decoding just the basename |
107 | desktopSession = desktopSession.mid(slash + 1); |
108 | } |
109 | |
110 | if (desktopSession == "gnome" ) |
111 | return QByteArrayLiteral("GNOME" ); |
112 | else if (desktopSession == "xfce" ) |
113 | return QByteArrayLiteral("XFCE" ); |
114 | else if (desktopSession == "kde" ) |
115 | return QByteArrayLiteral("KDE" ); |
116 | |
117 | return QByteArrayLiteral("UNKNOWN" ); |
118 | } |
119 | |
120 | static inline bool checkExecutable(const QString &candidate, QString *result) |
121 | { |
122 | *result = QStandardPaths::findExecutable(candidate); |
123 | return !result->isEmpty(); |
124 | } |
125 | |
126 | static inline bool detectWebBrowser(const QByteArray &desktop, |
127 | bool checkBrowserVariable, |
128 | QString *browser) |
129 | { |
130 | const char *browsers[] = {"google-chrome" , "firefox" , "mozilla" , "opera" }; |
131 | |
132 | browser->clear(); |
133 | if (checkExecutable(QStringLiteral("xdg-open" ), browser)) |
134 | return true; |
135 | |
136 | if (checkBrowserVariable) { |
137 | QByteArray browserVariable = qgetenv("DEFAULT_BROWSER" ); |
138 | if (browserVariable.isEmpty()) |
139 | browserVariable = qgetenv("BROWSER" ); |
140 | if (!browserVariable.isEmpty() && checkExecutable(QString::fromLocal8Bit(browserVariable), browser)) |
141 | return true; |
142 | } |
143 | |
144 | if (desktop == QByteArray("KDE" )) { |
145 | // Konqueror launcher |
146 | if (checkExecutable(QStringLiteral("kfmclient" ), browser)) { |
147 | browser->append(QLatin1String(" exec" )); |
148 | return true; |
149 | } |
150 | } else if (desktop == QByteArray("GNOME" )) { |
151 | if (checkExecutable(QStringLiteral("gnome-open" ), browser)) |
152 | return true; |
153 | } |
154 | |
155 | for (size_t i = 0; i < sizeof(browsers)/sizeof(char *); ++i) |
156 | if (checkExecutable(QLatin1String(browsers[i]), browser)) |
157 | return true; |
158 | return false; |
159 | } |
160 | |
161 | static inline bool launch(const QString &launcher, const QUrl &url) |
162 | { |
163 | const QString command = launcher + QLatin1Char(' ') + QLatin1String(url.toEncoded()); |
164 | if (debug) |
165 | qDebug("Launching %s" , qPrintable(command)); |
166 | #if !QT_CONFIG(process) |
167 | const bool ok = ::system(qPrintable(command + QLatin1String(" &" ))); |
168 | #else |
169 | QStringList args = QProcess::splitCommand(command); |
170 | bool ok = false; |
171 | if (!args.isEmpty()) { |
172 | QString program = args.takeFirst(); |
173 | ok = QProcess::startDetached(program, args); |
174 | } |
175 | #endif |
176 | if (!ok) |
177 | qWarning("Launch failed (%s)" , qPrintable(command)); |
178 | return ok; |
179 | } |
180 | |
181 | #if QT_CONFIG(dbus) |
182 | static inline bool checkNeedPortalSupport() |
183 | { |
184 | return !QStandardPaths::locate(QStandardPaths::RuntimeLocation, QLatin1String("flatpak-info" )).isEmpty() || qEnvironmentVariableIsSet("SNAP" ); |
185 | } |
186 | |
187 | static inline bool isPortalReturnPermanent(const QDBusError &error) |
188 | { |
189 | // A service unknown error isn't permanent, it just indicates that we |
190 | // should fall back to the regular way. This check includes |
191 | // QDBusError::NoError. |
192 | return error.type() != QDBusError::ServiceUnknown && error.type() != QDBusError::AccessDenied; |
193 | } |
194 | |
195 | static inline QDBusMessage xdgDesktopPortalOpenFile(const QUrl &url) |
196 | { |
197 | // DBus signature: |
198 | // OpenFile (IN s parent_window, |
199 | // IN h fd, |
200 | // IN a{sv} options, |
201 | // OUT o handle) |
202 | // Options: |
203 | // handle_token (s) - A string that will be used as the last element of the @handle. |
204 | // writable (b) - Whether to allow the chosen application to write to the file. |
205 | |
206 | #ifdef O_PATH |
207 | const int fd = qt_safe_open(QFile::encodeName(url.toLocalFile()), O_PATH); |
208 | if (fd != -1) { |
209 | QDBusMessage message = QDBusMessage::createMethodCall(QLatin1String("org.freedesktop.portal.Desktop" ), |
210 | QLatin1String("/org/freedesktop/portal/desktop" ), |
211 | QLatin1String("org.freedesktop.portal.OpenURI" ), |
212 | QLatin1String("OpenFile" )); |
213 | |
214 | QDBusUnixFileDescriptor descriptor; |
215 | descriptor.giveFileDescriptor(fd); |
216 | |
217 | const QVariantMap options = {{QLatin1String("writable" ), true}}; |
218 | |
219 | // FIXME parent_window_id |
220 | message << QString() << QVariant::fromValue(descriptor) << options; |
221 | |
222 | return QDBusConnection::sessionBus().call(message); |
223 | } |
224 | #else |
225 | Q_UNUSED(url); |
226 | #endif |
227 | |
228 | return QDBusMessage::createError(QDBusError::InternalError, qt_error_string()); |
229 | } |
230 | |
231 | static inline QDBusMessage xdgDesktopPortalOpenUrl(const QUrl &url) |
232 | { |
233 | // DBus signature: |
234 | // OpenURI (IN s parent_window, |
235 | // IN s uri, |
236 | // IN a{sv} options, |
237 | // OUT o handle) |
238 | // Options: |
239 | // handle_token (s) - A string that will be used as the last element of the @handle. |
240 | // writable (b) - Whether to allow the chosen application to write to the file. |
241 | // This key only takes effect the uri points to a local file that is exported in the document portal, |
242 | // and the chosen application is sandboxed itself. |
243 | |
244 | QDBusMessage message = QDBusMessage::createMethodCall(QLatin1String("org.freedesktop.portal.Desktop" ), |
245 | QLatin1String("/org/freedesktop/portal/desktop" ), |
246 | QLatin1String("org.freedesktop.portal.OpenURI" ), |
247 | QLatin1String("OpenURI" )); |
248 | // FIXME parent_window_id and handle writable option |
249 | message << QString() << url.toString() << QVariantMap(); |
250 | |
251 | return QDBusConnection::sessionBus().call(message); |
252 | } |
253 | |
254 | static inline QDBusMessage xdgDesktopPortalSendEmail(const QUrl &url) |
255 | { |
256 | // DBus signature: |
257 | // ComposeEmail (IN s parent_window, |
258 | // IN a{sv} options, |
259 | // OUT o handle) |
260 | // Options: |
261 | // address (s) - The email address to send to. |
262 | // subject (s) - The subject for the email. |
263 | // body (s) - The body for the email. |
264 | // attachment_fds (ah) - File descriptors for files to attach. |
265 | |
266 | QUrlQuery urlQuery(url); |
267 | QVariantMap options; |
268 | options.insert(QLatin1String("address" ), url.path()); |
269 | options.insert(QLatin1String("subject" ), urlQuery.queryItemValue(QLatin1String("subject" ))); |
270 | options.insert(QLatin1String("body" ), urlQuery.queryItemValue(QLatin1String("body" ))); |
271 | |
272 | // O_PATH seems to be present since Linux 2.6.39, which is not case of RHEL 6 |
273 | #ifdef O_PATH |
274 | QList<QDBusUnixFileDescriptor> attachments; |
275 | const QStringList attachmentUris = urlQuery.allQueryItemValues(QLatin1String("attachment" )); |
276 | |
277 | for (const QString &attachmentUri : attachmentUris) { |
278 | const int fd = qt_safe_open(QFile::encodeName(attachmentUri), O_PATH); |
279 | if (fd != -1) { |
280 | QDBusUnixFileDescriptor descriptor(fd); |
281 | attachments << descriptor; |
282 | qt_safe_close(fd); |
283 | } |
284 | } |
285 | |
286 | options.insert(QLatin1String("attachment_fds" ), QVariant::fromValue(attachments)); |
287 | #endif |
288 | |
289 | QDBusMessage message = QDBusMessage::createMethodCall(QLatin1String("org.freedesktop.portal.Desktop" ), |
290 | QLatin1String("/org/freedesktop/portal/desktop" ), |
291 | QLatin1String("org.freedesktop.portal.Email" ), |
292 | QLatin1String("ComposeEmail" )); |
293 | |
294 | // FIXME parent_window_id |
295 | message << QString() << options; |
296 | |
297 | return QDBusConnection::sessionBus().call(message); |
298 | } |
299 | #endif // QT_CONFIG(dbus) |
300 | |
301 | QByteArray QGenericUnixServices::desktopEnvironment() const |
302 | { |
303 | static const QByteArray result = detectDesktopEnvironment(); |
304 | return result; |
305 | } |
306 | |
307 | bool QGenericUnixServices::openUrl(const QUrl &url) |
308 | { |
309 | if (url.scheme() == QLatin1String("mailto" )) { |
310 | #if QT_CONFIG(dbus) |
311 | if (checkNeedPortalSupport()) { |
312 | QDBusError error = xdgDesktopPortalSendEmail(url); |
313 | if (isPortalReturnPermanent(error)) |
314 | return !error.isValid(); |
315 | |
316 | // service not running, fall back |
317 | } |
318 | #endif |
319 | return openDocument(url); |
320 | } |
321 | |
322 | #if QT_CONFIG(dbus) |
323 | if (checkNeedPortalSupport()) { |
324 | QDBusError error = xdgDesktopPortalOpenUrl(url); |
325 | if (isPortalReturnPermanent(error)) |
326 | return !error.isValid(); |
327 | } |
328 | #endif |
329 | |
330 | if (m_webBrowser.isEmpty() && !detectWebBrowser(desktopEnvironment(), true, &m_webBrowser)) { |
331 | qWarning("Unable to detect a web browser to launch '%s'" , qPrintable(url.toString())); |
332 | return false; |
333 | } |
334 | return launch(m_webBrowser, url); |
335 | } |
336 | |
337 | bool QGenericUnixServices::openDocument(const QUrl &url) |
338 | { |
339 | #if QT_CONFIG(dbus) |
340 | if (checkNeedPortalSupport()) { |
341 | QDBusError error = xdgDesktopPortalOpenFile(url); |
342 | if (isPortalReturnPermanent(error)) |
343 | return !error.isValid(); |
344 | } |
345 | #endif |
346 | |
347 | if (m_documentLauncher.isEmpty() && !detectWebBrowser(desktopEnvironment(), false, &m_documentLauncher)) { |
348 | qWarning("Unable to detect a launcher for '%s'" , qPrintable(url.toString())); |
349 | return false; |
350 | } |
351 | return launch(m_documentLauncher, url); |
352 | } |
353 | |
354 | #else |
355 | QByteArray QGenericUnixServices::desktopEnvironment() const |
356 | { |
357 | return QByteArrayLiteral("UNKNOWN" ); |
358 | } |
359 | |
360 | bool QGenericUnixServices::openUrl(const QUrl &url) |
361 | { |
362 | Q_UNUSED(url); |
363 | qWarning("openUrl() not supported on this platform" ); |
364 | return false; |
365 | } |
366 | |
367 | bool QGenericUnixServices::openDocument(const QUrl &url) |
368 | { |
369 | Q_UNUSED(url); |
370 | qWarning("openDocument() not supported on this platform" ); |
371 | return false; |
372 | } |
373 | |
374 | #endif // QT_NO_MULTIPROCESS |
375 | |
376 | QT_END_NAMESPACE |
377 | |