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.isSymbolicLink()) {
314 // XDG Document Portal also doesn't support symlinks since it doesn't let us open the fd O_NOFOLLOW.
315 // https://github.com/flatpak/xdg-desktop-portal/issues/961#issuecomment-1573646299
316 return false;
317 }
318 }
319 }
320
321 auto iface =
322 new OrgFreedesktopPortalFileTransferInterface(portalServiceName(), QStringLiteral("/org/freedesktop/portal/documents"), QDBusConnection::sessionBus());
323
324 // Do not autostop, we'll stop once our mimedata disappears (i.e. the drag operation has finished);
325 // Otherwise not-wellbehaved clients that read the urls multiple times will trip the automatic-transfer-
326 // closing-upon-read inside the portal and have any reads, but the first, not properly resolve anymore.
327 const QString transferId = iface->StartTransfer({{QStringLiteral("autostop"), QVariant::fromValue(false)}});
328 auto cleanup = qScopeGuard([transferId, iface] {
329 iface->StopTransfer(transferId);
330 iface->deleteLater();
331 });
332
333 auto optionalPaths = fuseRedirect(urls, onlyLocalFiles);
334 if (!optionalPaths.has_value()) {
335 qCWarning(KCOREADDONS_DEBUG) << "Failed to mount with fuse!";
336 return false;
337 }
338
339 // Prevent running into "too many open files" errors.
340 // Because submission of calls happens on the qdbus thread we may be feeding
341 // it QDBusUnixFileDescriptors faster than it can submit them over the wire, this would eventually
342 // lead to running into the open file cap since the QDBusUnixFileDescriptor hold
343 // an open FD until their call has been made.
344 // To prevent this from happening we collect a submission batch, make the call and **wait** for
345 // the call to succeed.
346 FDList pendingFds;
347 static constexpr decltype(pendingFds.size()) maximumBatchSize = 16;
348 pendingFds.reserve(maximumBatchSize);
349
350 const auto addFilesAndClear = [transferId, &iface, &pendingFds]() {
351 if (pendingFds.isEmpty()) {
352 return true;
353 }
354 auto reply = iface->AddFiles(transferId, pendingFds, {});
355 reply.waitForFinished();
356 if (reply.isError()) {
357 qCWarning(KCOREADDONS_DEBUG) << "Some files could not be exported. " << reply.error();
358 return false;
359 }
360 pendingFds.clear();
361 return true;
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 return false;
370 }
371 pendingFds << QDBusUnixFileDescriptor(fd);
372 close(fd);
373
374 if (pendingFds.size() >= maximumBatchSize) {
375 if (!addFilesAndClear()) {
376 return false;
377 }
378 }
379 }
380
381 if (!addFilesAndClear()) {
382 return false;
383 }
384
385 cleanup.dismiss();
386 QObject::connect(mimeData, &QObject::destroyed, iface, [transferId, iface] {
387 iface->StopTransfer(transferId);
388 iface->deleteLater();
389 });
390 QObject::connect(iface, &OrgFreedesktopPortalFileTransferInterface::TransferClosed, mimeData, [iface]() {
391 iface->deleteLater();
392 });
393
394 mimeData->setData(QStringLiteral("application/vnd.portal.filetransfer"), QFile::encodeName(transferId));
395 setSourceId(mimeData);
396 return true;
397#else
398 Q_UNUSED(mimeData);
399 return false;
400#endif
401}
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 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 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 Mon Nov 18 2024 12:08:22 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.