1/*
2 This file is part of the KDE libraries
3
4 SPDX-FileCopyrightText: 2005-2012 David Faure <faure@kde.org>
5 SPDX-FileCopyrightText: 2022-2023 Harald Sitter <sitter@kde.org>
6
7 SPDX-License-Identifier: LGPL-2.0-or-later
8*/
9
10#include "kurlmimedata.h"
11#include "config-kdirwatch.h"
12
13#if HAVE_QTDBUS // not used outside dbus/xdg-portal related code
14#include <fcntl.h>
15#include <sys/stat.h>
16#include <sys/types.h>
17#include <unistd.h>
18#endif
19
20#include <optional>
21
22#include <QMimeData>
23#include <QStringList>
24
25#include "kcoreaddons_debug.h"
26#if HAVE_QTDBUS
27#include "org.freedesktop.portal.FileTransfer.h"
28#include "org.kde.KIOFuse.VFS.h"
29#endif
30
31#include "kurlmimedata_p.h"
32
33static QString kdeUriListMime()
34{
35 return QStringLiteral("application/x-kde4-urilist");
36} // keep this name "kde4" for compat.
37
38static QByteArray uriListData(const QList<QUrl> &urls)
39{
40 // compatible with qmimedata.cpp encoding of QUrls
41 QByteArray result;
42 for (int i = 0; i < urls.size(); ++i) {
43 result += urls.at(i).toEncoded();
44 result += "\r\n";
45 }
46 return result;
47}
48
49void KUrlMimeData::setUrls(const QList<QUrl> &urls, const QList<QUrl> &mostLocalUrls, QMimeData *mimeData)
50{
51 // Export the most local urls as text/uri-list and plain text, for non KDE apps.
52 mimeData->setUrls(mostLocalUrls); // set text/uri-list and text/plain
53
54 // Export the real KIO urls as a kde-specific mimetype
55 mimeData->setData(mimetype: kdeUriListMime(), data: uriListData(urls));
56}
57
58void KUrlMimeData::setMetaData(const MetaDataMap &metaData, QMimeData *mimeData)
59{
60 QByteArray metaDataData; // :)
61 for (auto it = metaData.cbegin(); it != metaData.cend(); ++it) {
62 metaDataData += it.key().toUtf8();
63 metaDataData += "$@@$";
64 metaDataData += it.value().toUtf8();
65 metaDataData += "$@@$";
66 }
67 mimeData->setData(QStringLiteral("application/x-kio-metadata"), data: metaDataData);
68}
69
70QStringList KUrlMimeData::mimeDataTypes()
71{
72 return QStringList{kdeUriListMime(), QStringLiteral("text/uri-list")};
73}
74
75static QList<QUrl> extractKdeUriList(const QMimeData *mimeData)
76{
77 QList<QUrl> uris;
78 const QByteArray ba = mimeData->data(mimetype: kdeUriListMime());
79 // Code from qmimedata.cpp
80 QList<QByteArray> urls = ba.split(sep: '\n');
81 uris.reserve(asize: urls.size());
82 for (int i = 0; i < urls.size(); ++i) {
83 QByteArray data = urls.at(i).trimmed();
84 if (!data.isEmpty()) {
85 uris.append(t: QUrl::fromEncoded(url: data));
86 }
87 }
88 return uris;
89}
90
91#if HAVE_QTDBUS
92static QString kioFuseServiceName()
93{
94 return QStringLiteral("org.kde.KIOFuse");
95}
96
97static QString portalServiceName()
98{
99 return QStringLiteral("org.freedesktop.portal.Documents");
100}
101
102static bool isKIOFuseAvailable()
103{
104 static bool available = QDBusConnection::sessionBus().interface()
105 && QDBusConnection::sessionBus().interface()->activatableServiceNames().value().contains(str: kioFuseServiceName());
106 return available;
107}
108
109bool KUrlMimeData::isDocumentsPortalAvailable()
110{
111 static bool available =
112 QDBusConnection::sessionBus().interface() && QDBusConnection::sessionBus().interface()->activatableServiceNames().value().contains(str: portalServiceName());
113 return available;
114}
115
116static QString portalFormat()
117{
118 return QStringLiteral("application/vnd.portal.filetransfer");
119}
120
121static QList<QUrl> extractPortalUriList(const QMimeData *mimeData)
122{
123 Q_ASSERT(QCoreApplication::instance()->thread() == QThread::currentThread());
124 static std::pair<QByteArray, QList<QUrl>> cache;
125 const auto transferId = mimeData->data(mimetype: portalFormat());
126 qCDebug(KCOREADDONS_DEBUG) << "Picking up portal urls from transfer" << transferId;
127 if (std::get<QByteArray>(p&: cache) == transferId) {
128 const auto uris = std::get<QList<QUrl>>(p&: cache);
129 qCDebug(KCOREADDONS_DEBUG) << "Urls from portal cache" << uris;
130 return uris;
131 }
132 auto iface =
133 new OrgFreedesktopPortalFileTransferInterface(portalServiceName(), QStringLiteral("/org/freedesktop/portal/documents"), QDBusConnection::sessionBus());
134 const QStringList list = iface->RetrieveFiles(key: QString::fromUtf8(ba: transferId), options: {});
135 QList<QUrl> uris;
136 uris.reserve(asize: list.size());
137 for (const auto &path : list) {
138 uris.append(t: QUrl::fromLocalFile(localfile: path));
139 }
140 qCDebug(KCOREADDONS_DEBUG) << "Urls from portal" << uris;
141 cache = std::make_pair(x: transferId, y&: uris);
142 return uris;
143}
144
145static QString sourceIdMime()
146{
147 return QStringLiteral("application/x-kde-source-id");
148}
149
150static QString sourceId()
151{
152 return QDBusConnection::sessionBus().baseService();
153}
154
155void KUrlMimeData::setSourceId(QMimeData *mimeData)
156{
157 mimeData->setData(mimetype: sourceIdMime(), data: sourceId().toUtf8());
158}
159
160static bool hasSameSourceId(const QMimeData *mimeData)
161{
162 return mimeData->hasFormat(mimetype: sourceIdMime()) && mimeData->data(mimetype: sourceIdMime()) == sourceId().toUtf8();
163}
164
165#endif
166
167QList<QUrl> KUrlMimeData::urlsFromMimeData(const QMimeData *mimeData, DecodeOptions decodeOptions, MetaDataMap *metaData)
168{
169 QList<QUrl> uris;
170
171#if HAVE_QTDBUS
172 if (!hasSameSourceId(mimeData) && isDocumentsPortalAvailable() && mimeData->hasFormat(mimetype: portalFormat())) {
173 uris = extractPortalUriList(mimeData);
174 }
175#endif
176
177 if (uris.isEmpty()) {
178 if (decodeOptions.testFlag(flag: PreferLocalUrls)) {
179 // Extracting uris from text/uri-list, use the much faster QMimeData method urls()
180 uris = mimeData->urls();
181 if (uris.isEmpty()) {
182 uris = extractKdeUriList(mimeData);
183 }
184 } else {
185 uris = extractKdeUriList(mimeData);
186 if (uris.isEmpty()) {
187 uris = mimeData->urls();
188 }
189 }
190 }
191
192 if (metaData) {
193 const QByteArray metaDataPayload = mimeData->data(QStringLiteral("application/x-kio-metadata"));
194 if (!metaDataPayload.isEmpty()) {
195 QString str = QString::fromUtf8(utf8: metaDataPayload.constData());
196 Q_ASSERT(str.endsWith(QLatin1String("$@@$")));
197 str.chop(n: 4);
198 const QStringList lst = str.split(QStringLiteral("$@@$"));
199 bool readingKey = true; // true, then false, then true, etc.
200 QString key;
201 for (const QString &s : lst) {
202 if (readingKey) {
203 key = s;
204 } else {
205 metaData->insert(key, value: s);
206 }
207 readingKey = !readingKey;
208 }
209 Q_ASSERT(readingKey); // an odd number of items would be, well, odd ;-)
210 }
211 }
212 return uris;
213}
214
215#if HAVE_QTDBUS
216static QStringList urlListToStringList(const QList<QUrl> urls)
217{
218 QStringList list;
219 for (const auto &url : urls) {
220 list << url.toLocalFile();
221 }
222 return list;
223}
224
225static std::optional<QStringList> fuseRedirect(QList<QUrl> urls, bool onlyLocalFiles)
226{
227 qCDebug(KCOREADDONS_DEBUG) << "mounting urls with fuse" << urls;
228
229 // Fuse redirection only applies if the list contains non-local files.
230 if (onlyLocalFiles) {
231 return urlListToStringList(urls);
232 }
233
234 OrgKdeKIOFuseVFSInterface kiofuse_iface(kioFuseServiceName(), QStringLiteral("/org/kde/KIOFuse"), QDBusConnection::sessionBus());
235 struct MountRequest {
236 QDBusPendingReply<QString> reply;
237 int urlIndex;
238 QString basename;
239 };
240 QList<MountRequest> requests;
241 requests.reserve(asize: urls.count());
242 for (int i = 0; i < urls.count(); ++i) {
243 QUrl url = urls.at(i);
244 if (!url.isLocalFile()) {
245 const QString path(url.path());
246 const int slashes = path.count(c: QLatin1Char('/'));
247 QString basename;
248 if (slashes > 1) {
249 url.setPath(path: path.section(asep: QLatin1Char('/'), astart: 0, aend: slashes - 1));
250 basename = path.section(asep: QLatin1Char('/'), astart: slashes, aend: slashes);
251 }
252 requests.push_back(t: {.reply: kiofuse_iface.mountUrl(remoteUrl: url.toString()), .urlIndex: i, .basename: basename});
253 }
254 }
255
256 for (auto &request : requests) {
257 request.reply.waitForFinished();
258 if (request.reply.isError()) {
259 qWarning() << "FUSE request failed:" << request.reply.error();
260 return std::nullopt;
261 }
262
263 urls[request.urlIndex] = QUrl::fromLocalFile(localfile: request.reply.value() + QLatin1Char('/') + request.basename);
264 };
265
266 qCDebug(KCOREADDONS_DEBUG) << "mounted urls with fuse, maybe" << urls;
267
268 return urlListToStringList(urls);
269}
270#endif
271
272bool KUrlMimeData::exportUrlsToPortal(QMimeData *mimeData)
273{
274#if HAVE_QTDBUS
275 if (!isDocumentsPortalAvailable()) {
276 return false;
277 }
278 QList<QUrl> urls = mimeData->urls();
279
280 bool onlyLocalFiles = true;
281 for (const auto &url : urls) {
282 const auto isLocal = url.isLocalFile();
283 if (!isLocal) {
284 onlyLocalFiles = false;
285
286 // For the time being the fuse redirection is opt-in because we later need to open() the files
287 // and this is an insanely expensive operation involving a stat() for remote URLs that we can't
288 // really get rid of. We'll need a way to avoid the open().
289 // https://bugs.kde.org/show_bug.cgi?id=457529
290 // https://github.com/flatpak/xdg-desktop-portal/issues/961
291 static const auto fuseRedirect = qEnvironmentVariableIntValue(varName: "KCOREADDONS_FUSE_REDIRECT");
292 if (!fuseRedirect) {
293 return false;
294 }
295
296 // some remotes, fusing is enabled, but kio-fuse is unavailable -> cannot run this url list through the portal
297 if (!isKIOFuseAvailable()) {
298 qWarning() << "kio-fuse is missing";
299 return false;
300 }
301 } else {
302 const QFileInfo info(url.toLocalFile());
303 if (info.isDir()) {
304 // XDG Document Portal doesn't support directories and silently drops them.
305 return false;
306 }
307 if (info.isSymbolicLink()) {
308 // XDG Document Portal also doesn't support symlinks since it doesn't let us open the fd O_NOFOLLOW.
309 // https://github.com/flatpak/xdg-desktop-portal/issues/961#issuecomment-1573646299
310 return false;
311 }
312 }
313 }
314
315 auto iface =
316 new OrgFreedesktopPortalFileTransferInterface(portalServiceName(), QStringLiteral("/org/freedesktop/portal/documents"), QDBusConnection::sessionBus());
317
318 // Do not autostop, we'll stop once our mimedata disappears (i.e. the drag operation has finished);
319 // Otherwise not-wellbehaved clients that read the urls multiple times will trip the automatic-transfer-
320 // closing-upon-read inside the portal and have any reads, but the first, not properly resolve anymore.
321 const QString transferId = iface->StartTransfer(options: {{QStringLiteral("autostop"), QVariant::fromValue(value: false)}});
322 mimeData->setData(QStringLiteral("application/vnd.portal.filetransfer"), data: QFile::encodeName(fileName: transferId));
323 setSourceId(mimeData);
324
325 auto optionalPaths = fuseRedirect(urls, onlyLocalFiles);
326 if (!optionalPaths.has_value()) {
327 qCWarning(KCOREADDONS_DEBUG) << "Failed to mount with fuse!";
328 return false;
329 }
330
331 // Prevent running into "too many open files" errors.
332 // Because submission of calls happens on the qdbus thread we may be feeding
333 // it QDBusUnixFileDescriptors faster than it can submit them over the wire, this would eventually
334 // lead to running into the open file cap since the QDBusUnixFileDescriptor hold
335 // an open FD until their call has been made.
336 // To prevent this from happening we collect a submission batch, make the call and **wait** for
337 // the call to succeed.
338 FDList pendingFds;
339 static constexpr decltype(pendingFds.size()) maximumBatchSize = 16;
340 pendingFds.reserve(asize: maximumBatchSize);
341
342 const auto addFilesAndClear = [transferId, &iface, &pendingFds]() {
343 if (pendingFds.isEmpty()) {
344 return;
345 }
346 auto reply = iface->AddFiles(key: transferId, fds: pendingFds, options: {});
347 reply.waitForFinished();
348 if (reply.isError()) {
349 qCWarning(KCOREADDONS_DEBUG) << "Some files could not be exported. " << reply.error();
350 }
351 pendingFds.clear();
352 };
353
354 for (const auto &path : optionalPaths.value()) {
355 const int fd = open(file: QFile::encodeName(fileName: path).constData(), O_RDONLY | O_CLOEXEC | O_NONBLOCK);
356 if (fd == -1) {
357 const int error = errno;
358 qCWarning(KCOREADDONS_DEBUG) << "Failed to open" << path << strerror(errnum: error);
359 }
360 pendingFds << QDBusUnixFileDescriptor(fd);
361 close(fd: fd);
362
363 if (pendingFds.size() >= maximumBatchSize) {
364 addFilesAndClear();
365 }
366 }
367 addFilesAndClear();
368
369 QObject::connect(sender: mimeData, signal: &QObject::destroyed, context: iface, slot: [transferId, iface] {
370 iface->StopTransfer(key: transferId);
371 iface->deleteLater();
372 });
373 QObject::connect(sender: iface, signal: &OrgFreedesktopPortalFileTransferInterface::TransferClosed, context: mimeData, slot: [iface]() {
374 iface->deleteLater();
375 });
376
377 return true;
378#else
379 Q_UNUSED(mimeData);
380 return false;
381#endif
382}
383

source code of kcoreaddons/src/lib/io/kurlmimedata.cpp