KCoreAddons

kurlmimedata.cpp
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(kdeUriListMime(), 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"), metaDataData);
68}
69
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(kdeUriListMime());
79 // Code from qmimedata.cpp
80 QList<QByteArray> urls = ba.split('\n');
81 uris.reserve(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(QUrl::fromEncoded(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(kioFuseServiceName());
106 return available;
107}
108
109bool KUrlMimeData::isDocumentsPortalAvailable()
110{
111 static bool available =
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(portalFormat());
126 qCDebug(KCOREADDONS_DEBUG) << "Picking up portal urls from transfer" << transferId;
127 if (std::get<QByteArray>(cache) == transferId) {
128 const auto uris = std::get<QList<QUrl>>(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 QDBusReply<QStringList> reply = iface->RetrieveFiles(QString::fromUtf8(transferId), {});
135 if (!reply.isValid()) {
136 qCWarning(KCOREADDONS_DEBUG) << "Failed to retrieve files from portal:" << reply.error();
137 return {};
138 }
139 const QStringList list = reply.value();
140 QList<QUrl> uris;
141 uris.reserve(list.size());
142 for (const auto &path : list) {
143 uris.append(QUrl::fromLocalFile(path));
144 }
145 qCDebug(KCOREADDONS_DEBUG) << "Urls from portal" << uris;
146 cache = std::make_pair(transferId, uris);
147 return uris;
148}
149
150static QString sourceIdMime()
151{
152 return QStringLiteral("application/x-kde-source-id");
153}
154
155static QString sourceId()
156{
158}
159
160void KUrlMimeData::setSourceId(QMimeData *mimeData)
161{
162 mimeData->setData(sourceIdMime(), sourceId().toUtf8());
163}
164
165static bool hasSameSourceId(const QMimeData *mimeData)
166{
167 return mimeData->hasFormat(sourceIdMime()) && mimeData->data(sourceIdMime()) == sourceId().toUtf8();
168}
169
170#endif
171
173{
174 QList<QUrl> uris;
175
176#if HAVE_QTDBUS
177 if (!hasSameSourceId(mimeData) && isDocumentsPortalAvailable() && mimeData->hasFormat(portalFormat())) {
178 uris = extractPortalUriList(mimeData);
179 if (static const auto force = qEnvironmentVariableIntValue("KCOREADDONS_FORCE_DOCUMENTS_PORTAL"); force == 1) {
180 // The environment variable is FOR TESTING ONLY!
181 // It is used to prevent the fallback logic from running.
182 return uris;
183 }
184 }
185#endif
186
187 if (uris.isEmpty()) {
188 if (decodeOptions.testFlag(PreferLocalUrls)) {
189 // Extracting uris from text/uri-list, use the much faster QMimeData method urls()
190 uris = mimeData->urls();
191 if (uris.isEmpty()) {
192 uris = extractKdeUriList(mimeData);
193 }
194 } else {
195 uris = extractKdeUriList(mimeData);
196 if (uris.isEmpty()) {
197 uris = mimeData->urls();
198 }
199 }
200 }
201
202 if (metaData) {
203 const QByteArray metaDataPayload = mimeData->data(QStringLiteral("application/x-kio-metadata"));
204 if (!metaDataPayload.isEmpty()) {
205 QString str = QString::fromUtf8(metaDataPayload.constData());
206 Q_ASSERT(str.endsWith(QLatin1String("$@@$")));
207 str.chop(4);
208 const QStringList lst = str.split(QStringLiteral("$@@$"));
209 bool readingKey = true; // true, then false, then true, etc.
210 QString key;
211 for (const QString &s : lst) {
212 if (readingKey) {
213 key = s;
214 } else {
215 metaData->insert(key, s);
216 }
217 readingKey = !readingKey;
218 }
219 Q_ASSERT(readingKey); // an odd number of items would be, well, odd ;-)
220 }
221 }
222 return uris;
223}
224
225#if HAVE_QTDBUS
226static QStringList urlListToStringList(const QList<QUrl> urls)
227{
228 QStringList list;
229 for (const auto &url : urls) {
230 list << url.toLocalFile();
231 }
232 return list;
233}
234
235static std::optional<QStringList> fuseRedirect(QList<QUrl> urls, bool onlyLocalFiles)
236{
237 qCDebug(KCOREADDONS_DEBUG) << "mounting urls with fuse" << urls;
238
239 // Fuse redirection only applies if the list contains non-local files.
240 if (onlyLocalFiles) {
241 return urlListToStringList(urls);
242 }
243
244 OrgKdeKIOFuseVFSInterface kiofuse_iface(kioFuseServiceName(), QStringLiteral("/org/kde/KIOFuse"), QDBusConnection::sessionBus());
245 struct MountRequest {
247 int urlIndex;
248 QString basename;
249 };
250 QList<MountRequest> requests;
251 requests.reserve(urls.count());
252 for (int i = 0; i < urls.count(); ++i) {
253 QUrl url = urls.at(i);
254 if (!url.isLocalFile()) {
255 const QString path(url.path());
256 const int slashes = path.count(QLatin1Char('/'));
257 QString basename;
258 if (slashes > 1) {
259 url.setPath(path.section(QLatin1Char('/'), 0, slashes - 1));
260 basename = path.section(QLatin1Char('/'), slashes, slashes);
261 }
262 requests.push_back({kiofuse_iface.mountUrl(url.toString()), i, basename});
263 }
264 }
265
266 for (auto &request : requests) {
267 request.reply.waitForFinished();
268 if (request.reply.isError()) {
269 qWarning() << "FUSE request failed:" << request.reply.error();
270 return std::nullopt;
271 }
272
273 urls[request.urlIndex] = QUrl::fromLocalFile(request.reply.value() + QLatin1Char('/') + request.basename);
274 };
275
276 qCDebug(KCOREADDONS_DEBUG) << "mounted urls with fuse, maybe" << urls;
277
278 return urlListToStringList(urls);
279}
280#endif
281
283{
284#if HAVE_QTDBUS
285 if (!isDocumentsPortalAvailable()) {
286 return false;
287 }
288 QList<QUrl> urls = mimeData->urls();
289
290 bool onlyLocalFiles = true;
291 for (const auto &url : urls) {
292 const auto isLocal = url.isLocalFile();
293 if (!isLocal) {
294 onlyLocalFiles = false;
295
296 // For the time being the fuse redirection is opt-in because we later need to open() the files
297 // and this is an insanely expensive operation involving a stat() for remote URLs that we can't
298 // really get rid of. We'll need a way to avoid the open().
299 // https://bugs.kde.org/show_bug.cgi?id=457529
300 // https://github.com/flatpak/xdg-desktop-portal/issues/961
301 static const auto fuseRedirect = qEnvironmentVariableIntValue("KCOREADDONS_FUSE_REDIRECT");
302 if (!fuseRedirect) {
303 return false;
304 }
305
306 // some remotes, fusing is enabled, but kio-fuse is unavailable -> cannot run this url list through the portal
307 if (!isKIOFuseAvailable()) {
308 qWarning() << "kio-fuse is missing";
309 return false;
310 }
311 } else {
312 const QFileInfo info(url.toLocalFile());
313 if (info.isDir()) {
314 // XDG Document Portal doesn't support directories and silently drops them.
315 return false;
316 }
317 if (info.isSymbolicLink()) {
318 // XDG Document Portal also doesn't support symlinks since it doesn't let us open the fd O_NOFOLLOW.
319 // https://github.com/flatpak/xdg-desktop-portal/issues/961#issuecomment-1573646299
320 return false;
321 }
322 }
323 }
324
325 auto iface =
326 new OrgFreedesktopPortalFileTransferInterface(portalServiceName(), QStringLiteral("/org/freedesktop/portal/documents"), QDBusConnection::sessionBus());
327
328 // Do not autostop, we'll stop once our mimedata disappears (i.e. the drag operation has finished);
329 // Otherwise not-wellbehaved clients that read the urls multiple times will trip the automatic-transfer-
330 // closing-upon-read inside the portal and have any reads, but the first, not properly resolve anymore.
331 const QString transferId = iface->StartTransfer({{QStringLiteral("autostop"), QVariant::fromValue(false)}});
332 mimeData->setData(QStringLiteral("application/vnd.portal.filetransfer"), QFile::encodeName(transferId));
333 setSourceId(mimeData);
334
335 auto optionalPaths = fuseRedirect(urls, onlyLocalFiles);
336 if (!optionalPaths.has_value()) {
337 qCWarning(KCOREADDONS_DEBUG) << "Failed to mount with fuse!";
338 return false;
339 }
340
341 // Prevent running into "too many open files" errors.
342 // Because submission of calls happens on the qdbus thread we may be feeding
343 // it QDBusUnixFileDescriptors faster than it can submit them over the wire, this would eventually
344 // lead to running into the open file cap since the QDBusUnixFileDescriptor hold
345 // an open FD until their call has been made.
346 // To prevent this from happening we collect a submission batch, make the call and **wait** for
347 // the call to succeed.
348 FDList pendingFds;
349 static constexpr decltype(pendingFds.size()) maximumBatchSize = 16;
350 pendingFds.reserve(maximumBatchSize);
351
352 const auto addFilesAndClear = [transferId, &iface, &pendingFds]() {
353 if (pendingFds.isEmpty()) {
354 return;
355 }
356 auto reply = iface->AddFiles(transferId, pendingFds, {});
357 reply.waitForFinished();
358 if (reply.isError()) {
359 qCWarning(KCOREADDONS_DEBUG) << "Some files could not be exported. " << reply.error();
360 }
361 pendingFds.clear();
362 };
363
364 for (const auto &path : optionalPaths.value()) {
365 const int fd = open(QFile::encodeName(path).constData(), O_RDONLY | O_CLOEXEC | O_NONBLOCK);
366 if (fd == -1) {
367 const int error = errno;
368 qCWarning(KCOREADDONS_DEBUG) << "Failed to open" << path << strerror(error);
369 }
370 pendingFds << QDBusUnixFileDescriptor(fd);
371 close(fd);
372
373 if (pendingFds.size() >= maximumBatchSize) {
374 addFilesAndClear();
375 }
376 }
377 addFilesAndClear();
378
379 QObject::connect(mimeData, &QObject::destroyed, iface, [transferId, iface] {
380 iface->StopTransfer(transferId);
381 iface->deleteLater();
382 });
383 QObject::connect(iface, &OrgFreedesktopPortalFileTransferInterface::TransferClosed, mimeData, [iface]() {
384 iface->deleteLater();
385 });
386
387 return true;
388#else
389 Q_UNUSED(mimeData);
390 return false;
391#endif
392}
QString path(const QString &relativePath)
Absolute libexec path resolved in relative relation to the current shared object.
Definition klibexec.h:48
KIOCORE_EXPORT QStringList list(const QString &fileClass)
KCOREADDONS_EXPORT void setUrls(const QList< QUrl > &urls, const QList< QUrl > &mostLocalUrls, QMimeData *mimeData)
Adds URLs and KIO metadata into the given QMimeData.
KCOREADDONS_EXPORT void setMetaData(const MetaDataMap &metaData, QMimeData *mimeData)
KCOREADDONS_EXPORT QStringList mimeDataTypes()
Return the list of mimeTypes that can be decoded by urlsFromMimeData.
KCOREADDONS_EXPORT bool exportUrlsToPortal(QMimeData *mimeData)
Export URLs through the XDG Documents Portal to allow interaction from/with sandbox code.
@ PreferLocalUrls
When the mimedata contains both KDE-style URLs (eg: desktop:/foo) and the "most local" version of the...
KCOREADDONS_EXPORT QList< QUrl > urlsFromMimeData(const QMimeData *mimeData, DecodeOptions decodeOptions=PreferKdeUrls, MetaDataMap *metaData=nullptr)
Extract a list of urls from the contents of mimeData.
const char * constData() const const
bool isEmpty() const const
QList< QByteArray > split(char sep) const const
QCoreApplication * instance()
QString baseService() const const
QDBusConnectionInterface * interface() const const
QDBusConnection sessionBus()
const QDBusError & error()
bool isValid() const const
QByteArray encodeName(const QString &fileName)
bool isDir() const const
bool testFlag(Enum flag) const const
void append(QList< T > &&value)
const_reference at(qsizetype i) const const
qsizetype count() const const
bool isEmpty() const const
void push_back(parameter_type value)
void reserve(qsizetype size)
qsizetype size() const const
const_iterator cbegin() const const
const_iterator cend() const const
iterator insert(const Key &key, const T &value)
QByteArray data(const QString &mimeType) const const
virtual bool hasFormat(const QString &mimeType) const const
void setData(const QString &mimeType, const QByteArray &data)
void setUrls(const QList< QUrl > &urls)
QList< QUrl > urls() const const
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
void deleteLater()
void destroyed(QObject *obj)
qsizetype count() const const
void chop(qsizetype n)
bool endsWith(QChar c, Qt::CaseSensitivity cs) const const
QString fromUtf8(QByteArrayView str)
QString section(QChar sep, qsizetype start, qsizetype end, SectionFlags flags) const const
QStringList split(QChar sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
QThread * currentThread()
QUrl fromEncoded(const QByteArray &input, ParsingMode parsingMode)
QUrl fromLocalFile(const QString &localFile)
bool isLocalFile() const const
QString path(ComponentFormattingOptions options) const const
void setPath(const QString &path, ParsingMode mode)
QString toLocalFile() const const
QString toString(FormattingOptions options) const const
QVariant fromValue(T &&value)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Jul 26 2024 11:56:13 by doxygen 1.11.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.