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 QStringList list = iface->RetrieveFiles(QString::fromUtf8(transferId), {});
135 QList<QUrl> uris;
136 uris.reserve(list.size());
137 for (const auto &path : list) {
138 uris.append(QUrl::fromLocalFile(path));
139 }
140 qCDebug(KCOREADDONS_DEBUG) << "Urls from portal" << uris;
141 cache = std::make_pair(transferId, uris);
142 return uris;
143}
144
145static QString sourceIdMime()
146{
147 return QStringLiteral("application/x-kde-source-id");
148}
149
150static QString sourceId()
151{
153}
154
155void KUrlMimeData::setSourceId(QMimeData *mimeData)
156{
157 mimeData->setData(sourceIdMime(), sourceId().toUtf8());
158}
159
160static bool hasSameSourceId(const QMimeData *mimeData)
161{
162 return mimeData->hasFormat(sourceIdMime()) && mimeData->data(sourceIdMime()) == sourceId().toUtf8();
163}
164
165#endif
166
168{
169 QList<QUrl> uris;
170
171#if HAVE_QTDBUS
172 if (!hasSameSourceId(mimeData) && isDocumentsPortalAvailable() && mimeData->hasFormat(portalFormat())) {
173 uris = extractPortalUriList(mimeData);
174 }
175#endif
176
177 if (uris.isEmpty()) {
178 if (decodeOptions.testFlag(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(metaDataPayload.constData());
196 Q_ASSERT(str.endsWith(QLatin1String("$@@$")));
197 str.chop(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, 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 {
237 int urlIndex;
238 QString basename;
239 };
240 QList<MountRequest> requests;
241 requests.reserve(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(QLatin1Char('/'));
247 QString basename;
248 if (slashes > 1) {
249 url.setPath(path.section(QLatin1Char('/'), 0, slashes - 1));
250 basename = path.section(QLatin1Char('/'), slashes, slashes);
251 }
252 requests.push_back({kiofuse_iface.mountUrl(url.toString()), i, 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(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
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("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({{QStringLiteral("autostop"), QVariant::fromValue(false)}});
322 mimeData->setData(QStringLiteral("application/vnd.portal.filetransfer"), QFile::encodeName(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(maximumBatchSize);
341
342 const auto addFilesAndClear = [transferId, &iface, &pendingFds]() {
343 if (pendingFds.isEmpty()) {
344 return;
345 }
346 auto reply = iface->AddFiles(transferId, pendingFds, {});
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(QFile::encodeName(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(error);
359 }
360 pendingFds << QDBusUnixFileDescriptor(fd);
361 close(fd);
362
363 if (pendingFds.size() >= maximumBatchSize) {
364 addFilesAndClear();
365 }
366 }
367 addFilesAndClear();
368
369 QObject::connect(mimeData, &QObject::destroyed, iface, [transferId, iface] {
370 iface->StopTransfer(transferId);
371 iface->deleteLater();
372 });
373 QObject::connect(iface, &OrgFreedesktopPortalFileTransferInterface::TransferClosed, mimeData, [iface]() {
374 iface->deleteLater();
375 });
376
377 return true;
378#else
379 Q_UNUSED(mimeData);
380 return false;
381#endif
382}
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()
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 Jun 7 2024 12:01:00 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.