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 | |
33 | static QString kdeUriListMime() |
34 | { |
35 | return QStringLiteral("application/x-kde4-urilist" ); |
36 | } // keep this name "kde4" for compat. |
37 | |
38 | static 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 | |
49 | void 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 | |
58 | void 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 | |
70 | QStringList KUrlMimeData::mimeDataTypes() |
71 | { |
72 | return QStringList{kdeUriListMime(), QStringLiteral("text/uri-list" )}; |
73 | } |
74 | |
75 | static QList<QUrl> (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 |
92 | static QString kioFuseServiceName() |
93 | { |
94 | return QStringLiteral("org.kde.KIOFuse" ); |
95 | } |
96 | |
97 | static QString portalServiceName() |
98 | { |
99 | return QStringLiteral("org.freedesktop.portal.Documents" ); |
100 | } |
101 | |
102 | static bool isKIOFuseAvailable() |
103 | { |
104 | static bool available = QDBusConnection::sessionBus().interface() |
105 | && QDBusConnection::sessionBus().interface()->activatableServiceNames().value().contains(str: kioFuseServiceName()); |
106 | return available; |
107 | } |
108 | |
109 | bool KUrlMimeData::isDocumentsPortalAvailable() |
110 | { |
111 | static bool available = |
112 | QDBusConnection::sessionBus().interface() && QDBusConnection::sessionBus().interface()->activatableServiceNames().value().contains(str: portalServiceName()); |
113 | return available; |
114 | } |
115 | |
116 | static QString portalFormat() |
117 | { |
118 | return QStringLiteral("application/vnd.portal.filetransfer" ); |
119 | } |
120 | |
121 | static QList<QUrl> (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 | |
145 | static QString sourceIdMime() |
146 | { |
147 | return QStringLiteral("application/x-kde-source-id" ); |
148 | } |
149 | |
150 | static QString sourceId() |
151 | { |
152 | return QDBusConnection::sessionBus().baseService(); |
153 | } |
154 | |
155 | void KUrlMimeData::setSourceId(QMimeData *mimeData) |
156 | { |
157 | mimeData->setData(mimetype: sourceIdMime(), data: sourceId().toUtf8()); |
158 | } |
159 | |
160 | static bool hasSameSourceId(const QMimeData *mimeData) |
161 | { |
162 | return mimeData->hasFormat(mimetype: sourceIdMime()) && mimeData->data(mimetype: sourceIdMime()) == sourceId().toUtf8(); |
163 | } |
164 | |
165 | #endif |
166 | |
167 | QList<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 |
216 | static 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 | |
225 | static 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 | |
272 | bool 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 | |