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 }