File indexing completed on 2024-05-19 03:56:21

0001 /*
0002     This file is part of the KDE libraries
0003 
0004     SPDX-FileCopyrightText: 2005-2012 David Faure <faure@kde.org>
0005     SPDX-FileCopyrightText: 2022-2023 Harald Sitter <sitter@kde.org>
0006 
0007     SPDX-License-Identifier: LGPL-2.0-or-later
0008 */
0009 
0010 #include "kurlmimedata.h"
0011 #include "config-kdirwatch.h"
0012 
0013 #if HAVE_QTDBUS // not used outside dbus/xdg-portal related code
0014 #include <fcntl.h>
0015 #include <sys/stat.h>
0016 #include <sys/types.h>
0017 #include <unistd.h>
0018 #endif
0019 
0020 #include <optional>
0021 
0022 #include <QMimeData>
0023 #include <QStringList>
0024 
0025 #include "kcoreaddons_debug.h"
0026 #if HAVE_QTDBUS
0027 #include "org.freedesktop.portal.FileTransfer.h"
0028 #include "org.kde.KIOFuse.VFS.h"
0029 #endif
0030 
0031 #include "kurlmimedata_p.h"
0032 
0033 static QString kdeUriListMime()
0034 {
0035     return QStringLiteral("application/x-kde4-urilist");
0036 } // keep this name "kde4" for compat.
0037 
0038 static QByteArray uriListData(const QList<QUrl> &urls)
0039 {
0040     // compatible with qmimedata.cpp encoding of QUrls
0041     QByteArray result;
0042     for (int i = 0; i < urls.size(); ++i) {
0043         result += urls.at(i).toEncoded();
0044         result += "\r\n";
0045     }
0046     return result;
0047 }
0048 
0049 void KUrlMimeData::setUrls(const QList<QUrl> &urls, const QList<QUrl> &mostLocalUrls, QMimeData *mimeData)
0050 {
0051     // Export the most local urls as text/uri-list and plain text, for non KDE apps.
0052     mimeData->setUrls(mostLocalUrls); // set text/uri-list and text/plain
0053 
0054     // Export the real KIO urls as a kde-specific mimetype
0055     mimeData->setData(kdeUriListMime(), uriListData(urls));
0056 }
0057 
0058 void KUrlMimeData::setMetaData(const MetaDataMap &metaData, QMimeData *mimeData)
0059 {
0060     QByteArray metaDataData; // :)
0061     for (auto it = metaData.cbegin(); it != metaData.cend(); ++it) {
0062         metaDataData += it.key().toUtf8();
0063         metaDataData += "$@@$";
0064         metaDataData += it.value().toUtf8();
0065         metaDataData += "$@@$";
0066     }
0067     mimeData->setData(QStringLiteral("application/x-kio-metadata"), metaDataData);
0068 }
0069 
0070 QStringList KUrlMimeData::mimeDataTypes()
0071 {
0072     return QStringList{kdeUriListMime(), QStringLiteral("text/uri-list")};
0073 }
0074 
0075 static QList<QUrl> extractKdeUriList(const QMimeData *mimeData)
0076 {
0077     QList<QUrl> uris;
0078     const QByteArray ba = mimeData->data(kdeUriListMime());
0079     // Code from qmimedata.cpp
0080     QList<QByteArray> urls = ba.split('\n');
0081     uris.reserve(urls.size());
0082     for (int i = 0; i < urls.size(); ++i) {
0083         QByteArray data = urls.at(i).trimmed();
0084         if (!data.isEmpty()) {
0085             uris.append(QUrl::fromEncoded(data));
0086         }
0087     }
0088     return uris;
0089 }
0090 
0091 #if HAVE_QTDBUS
0092 static QString kioFuseServiceName()
0093 {
0094     return QStringLiteral("org.kde.KIOFuse");
0095 }
0096 
0097 static QString portalServiceName()
0098 {
0099     return QStringLiteral("org.freedesktop.portal.Documents");
0100 }
0101 
0102 static bool isKIOFuseAvailable()
0103 {
0104     static bool available = QDBusConnection::sessionBus().interface()
0105         && QDBusConnection::sessionBus().interface()->activatableServiceNames().value().contains(kioFuseServiceName());
0106     return available;
0107 }
0108 
0109 bool KUrlMimeData::isDocumentsPortalAvailable()
0110 {
0111     static bool available =
0112         QDBusConnection::sessionBus().interface() && QDBusConnection::sessionBus().interface()->activatableServiceNames().value().contains(portalServiceName());
0113     return available;
0114 }
0115 
0116 static QString portalFormat()
0117 {
0118     return QStringLiteral("application/vnd.portal.filetransfer");
0119 }
0120 
0121 static QList<QUrl> extractPortalUriList(const QMimeData *mimeData)
0122 {
0123     Q_ASSERT(QCoreApplication::instance()->thread() == QThread::currentThread());
0124     static std::pair<QByteArray, QList<QUrl>> cache;
0125     const auto transferId = mimeData->data(portalFormat());
0126     qCDebug(KCOREADDONS_DEBUG) << "Picking up portal urls from transfer" << transferId;
0127     if (std::get<QByteArray>(cache) == transferId) {
0128         const auto uris = std::get<QList<QUrl>>(cache);
0129         qCDebug(KCOREADDONS_DEBUG) << "Urls from portal cache" << uris;
0130         return uris;
0131     }
0132     auto iface =
0133         new OrgFreedesktopPortalFileTransferInterface(portalServiceName(), QStringLiteral("/org/freedesktop/portal/documents"), QDBusConnection::sessionBus());
0134     const QStringList list = iface->RetrieveFiles(QString::fromUtf8(transferId), {});
0135     QList<QUrl> uris;
0136     uris.reserve(list.size());
0137     for (const auto &path : list) {
0138         uris.append(QUrl::fromLocalFile(path));
0139     }
0140     qCDebug(KCOREADDONS_DEBUG) << "Urls from portal" << uris;
0141     cache = std::make_pair(transferId, uris);
0142     return uris;
0143 }
0144 
0145 static QString sourceIdMime()
0146 {
0147     return QStringLiteral("application/x-kde-source-id");
0148 }
0149 
0150 static QString sourceId()
0151 {
0152     return QDBusConnection::sessionBus().baseService();
0153 }
0154 
0155 void KUrlMimeData::setSourceId(QMimeData *mimeData)
0156 {
0157     mimeData->setData(sourceIdMime(), sourceId().toUtf8());
0158 }
0159 
0160 static bool hasSameSourceId(const QMimeData *mimeData)
0161 {
0162     return mimeData->hasFormat(sourceIdMime()) && mimeData->data(sourceIdMime()) == sourceId().toUtf8();
0163 }
0164 
0165 #endif
0166 
0167 QList<QUrl> KUrlMimeData::urlsFromMimeData(const QMimeData *mimeData, DecodeOptions decodeOptions, MetaDataMap *metaData)
0168 {
0169     QList<QUrl> uris;
0170 
0171 #if HAVE_QTDBUS
0172     if (!hasSameSourceId(mimeData) && isDocumentsPortalAvailable() && mimeData->hasFormat(portalFormat())) {
0173         uris = extractPortalUriList(mimeData);
0174     }
0175 #endif
0176 
0177     if (uris.isEmpty()) {
0178         if (decodeOptions.testFlag(PreferLocalUrls)) {
0179             // Extracting uris from text/uri-list, use the much faster QMimeData method urls()
0180             uris = mimeData->urls();
0181             if (uris.isEmpty()) {
0182                 uris = extractKdeUriList(mimeData);
0183             }
0184         } else {
0185             uris = extractKdeUriList(mimeData);
0186             if (uris.isEmpty()) {
0187                 uris = mimeData->urls();
0188             }
0189         }
0190     }
0191 
0192     if (metaData) {
0193         const QByteArray metaDataPayload = mimeData->data(QStringLiteral("application/x-kio-metadata"));
0194         if (!metaDataPayload.isEmpty()) {
0195             QString str = QString::fromUtf8(metaDataPayload.constData());
0196             Q_ASSERT(str.endsWith(QLatin1String("$@@$")));
0197             str.chop(4);
0198             const QStringList lst = str.split(QStringLiteral("$@@$"));
0199             bool readingKey = true; // true, then false, then true, etc.
0200             QString key;
0201             for (const QString &s : lst) {
0202                 if (readingKey) {
0203                     key = s;
0204                 } else {
0205                     metaData->insert(key, s);
0206                 }
0207                 readingKey = !readingKey;
0208             }
0209             Q_ASSERT(readingKey); // an odd number of items would be, well, odd ;-)
0210         }
0211     }
0212     return uris;
0213 }
0214 
0215 #if HAVE_QTDBUS
0216 static QStringList urlListToStringList(const QList<QUrl> urls)
0217 {
0218     QStringList list;
0219     for (const auto &url : urls) {
0220         list << url.toLocalFile();
0221     }
0222     return list;
0223 }
0224 
0225 static std::optional<QStringList> fuseRedirect(QList<QUrl> urls, bool onlyLocalFiles)
0226 {
0227     qCDebug(KCOREADDONS_DEBUG) << "mounting urls with fuse" << urls;
0228 
0229     // Fuse redirection only applies if the list contains non-local files.
0230     if (onlyLocalFiles) {
0231         return urlListToStringList(urls);
0232     }
0233 
0234     OrgKdeKIOFuseVFSInterface kiofuse_iface(kioFuseServiceName(), QStringLiteral("/org/kde/KIOFuse"), QDBusConnection::sessionBus());
0235     struct MountRequest {
0236         QDBusPendingReply<QString> reply;
0237         int urlIndex;
0238         QString basename;
0239     };
0240     QList<MountRequest> requests;
0241     requests.reserve(urls.count());
0242     for (int i = 0; i < urls.count(); ++i) {
0243         QUrl url = urls.at(i);
0244         if (!url.isLocalFile()) {
0245             const QString path(url.path());
0246             const int slashes = path.count(QLatin1Char('/'));
0247             QString basename;
0248             if (slashes > 1) {
0249                 url.setPath(path.section(QLatin1Char('/'), 0, slashes - 1));
0250                 basename = path.section(QLatin1Char('/'), slashes, slashes);
0251             }
0252             requests.push_back({kiofuse_iface.mountUrl(url.toString()), i, basename});
0253         }
0254     }
0255 
0256     for (auto &request : requests) {
0257         request.reply.waitForFinished();
0258         if (request.reply.isError()) {
0259             qWarning() << "FUSE request failed:" << request.reply.error();
0260             return std::nullopt;
0261         }
0262 
0263         urls[request.urlIndex] = QUrl::fromLocalFile(request.reply.value() + QLatin1Char('/') + request.basename);
0264     };
0265 
0266     qCDebug(KCOREADDONS_DEBUG) << "mounted urls with fuse, maybe" << urls;
0267 
0268     return urlListToStringList(urls);
0269 }
0270 #endif
0271 
0272 bool KUrlMimeData::exportUrlsToPortal(QMimeData *mimeData)
0273 {
0274 #if HAVE_QTDBUS
0275     if (!isDocumentsPortalAvailable()) {
0276         return false;
0277     }
0278     QList<QUrl> urls = mimeData->urls();
0279 
0280     bool onlyLocalFiles = true;
0281     for (const auto &url : urls) {
0282         const auto isLocal = url.isLocalFile();
0283         if (!isLocal) {
0284             onlyLocalFiles = false;
0285 
0286             // For the time being the fuse redirection is opt-in because we later need to open() the files
0287             // and this is an insanely expensive operation involving a stat() for remote URLs that we can't
0288             // really get rid of. We'll need a way to avoid the open().
0289             // https://bugs.kde.org/show_bug.cgi?id=457529
0290             // https://github.com/flatpak/xdg-desktop-portal/issues/961
0291             static const auto fuseRedirect = qEnvironmentVariableIntValue("KCOREADDONS_FUSE_REDIRECT");
0292             if (!fuseRedirect) {
0293                 return false;
0294             }
0295 
0296             // some remotes, fusing is enabled, but kio-fuse is unavailable -> cannot run this url list through the portal
0297             if (!isKIOFuseAvailable()) {
0298                 qWarning() << "kio-fuse is missing";
0299                 return false;
0300             }
0301         } else {
0302             const QFileInfo info(url.toLocalFile());
0303             if (info.isDir()) {
0304                 // XDG Document Portal doesn't support directories and silently drops them.
0305                 return false;
0306             }
0307             if (info.isSymbolicLink()) {
0308                 // XDG Document Portal also doesn't support symlinks since it doesn't let us open the fd O_NOFOLLOW.
0309                 // https://github.com/flatpak/xdg-desktop-portal/issues/961#issuecomment-1573646299
0310                 return false;
0311             }
0312         }
0313     }
0314 
0315     auto iface =
0316         new OrgFreedesktopPortalFileTransferInterface(portalServiceName(), QStringLiteral("/org/freedesktop/portal/documents"), QDBusConnection::sessionBus());
0317 
0318     // Do not autostop, we'll stop once our mimedata disappears (i.e. the drag operation has finished);
0319     // Otherwise not-wellbehaved clients that read the urls multiple times will trip the automatic-transfer-
0320     // closing-upon-read inside the portal and have any reads, but the first, not properly resolve anymore.
0321     const QString transferId = iface->StartTransfer({{QStringLiteral("autostop"), QVariant::fromValue(false)}});
0322     mimeData->setData(QStringLiteral("application/vnd.portal.filetransfer"), QFile::encodeName(transferId));
0323     setSourceId(mimeData);
0324 
0325     auto optionalPaths = fuseRedirect(urls, onlyLocalFiles);
0326     if (!optionalPaths.has_value()) {
0327         qCWarning(KCOREADDONS_DEBUG) << "Failed to mount with fuse!";
0328         return false;
0329     }
0330 
0331     // Prevent running into "too many open files" errors.
0332     // Because submission of calls happens on the qdbus thread we may be feeding
0333     // it QDBusUnixFileDescriptors faster than it can submit them over the wire, this would eventually
0334     // lead to running into the open file cap since the QDBusUnixFileDescriptor hold
0335     // an open FD until their call has been made.
0336     // To prevent this from happening we collect a submission batch, make the call and **wait** for
0337     // the call to succeed.
0338     FDList pendingFds;
0339     static constexpr decltype(pendingFds.size()) maximumBatchSize = 16;
0340     pendingFds.reserve(maximumBatchSize);
0341 
0342     const auto addFilesAndClear = [transferId, &iface, &pendingFds]() {
0343         if (pendingFds.isEmpty()) {
0344             return;
0345         }
0346         auto reply = iface->AddFiles(transferId, pendingFds, {});
0347         reply.waitForFinished();
0348         if (reply.isError()) {
0349             qCWarning(KCOREADDONS_DEBUG) << "Some files could not be exported. " << reply.error();
0350         }
0351         pendingFds.clear();
0352     };
0353 
0354     for (const auto &path : optionalPaths.value()) {
0355         const int fd = open(QFile::encodeName(path).constData(), O_RDONLY | O_CLOEXEC | O_NONBLOCK);
0356         if (fd == -1) {
0357             const int error = errno;
0358             qCWarning(KCOREADDONS_DEBUG) << "Failed to open" << path << strerror(error);
0359         }
0360         pendingFds << QDBusUnixFileDescriptor(fd);
0361         close(fd);
0362 
0363         if (pendingFds.size() >= maximumBatchSize) {
0364             addFilesAndClear();
0365         }
0366     }
0367     addFilesAndClear();
0368 
0369     QObject::connect(mimeData, &QObject::destroyed, iface, [transferId, iface] {
0370         iface->StopTransfer(transferId);
0371         iface->deleteLater();
0372     });
0373     QObject::connect(iface, &OrgFreedesktopPortalFileTransferInterface::TransferClosed, mimeData, [iface]() {
0374         iface->deleteLater();
0375     });
0376 
0377     return true;
0378 #else
0379     return false;
0380 #endif
0381 }