This file is part of the KDE libraries
SPDX-FileCopyrightText: 2005-2012 David Faure <faure@kde.org>
SPDX-FileCopyrightText: 2022-2023 Harald Sitter <sitter@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "kurlmimedata.h"
#include "config-kdirwatch.h"
#if HAVE_QTDBUS
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#endif
#include <optional>
#include <QMimeData>
#include <QStringList>
#include "kcoreaddons_debug.h"
#if HAVE_QTDBUS
#include "org.freedesktop.portal.FileTransfer.h"
#include "org.kde.KIOFuse.VFS.h"
#endif
#include "kurlmimedata_p.h"
static QString kdeUriListMime()
{
return QStringLiteral("application/x-kde4-urilist");
}
static QByteArray uriListData(const QList<QUrl> &urls)
{
QByteArray result;
for (int i = 0; i < urls.size(); ++i) {
result += urls.at(i).toEncoded();
result += "\r\n";
}
return result;
}
void KUrlMimeData::setUrls(const QList<QUrl> &urls, const QList<QUrl> &mostLocalUrls, QMimeData *mimeData)
{
mimeData->setUrls(mostLocalUrls);
mimeData->setData(kdeUriListMime(), uriListData(urls));
}
void KUrlMimeData::setMetaData(const MetaDataMap &metaData, QMimeData *mimeData)
{
QByteArray metaDataData;
for (auto it = metaData.cbegin(); it != metaData.cend(); ++it) {
metaDataData += it.key().toUtf8();
metaDataData += "$@@$";
metaDataData += it.value().toUtf8();
metaDataData += "$@@$";
}
mimeData->setData(QStringLiteral("application/x-kio-metadata"), metaDataData);
}
QStringList KUrlMimeData::mimeDataTypes()
{
return QStringList{kdeUriListMime(), QStringLiteral("text/uri-list")};
}
static QList<QUrl> extractKdeUriList(const QMimeData *mimeData)
{
QList<QUrl> uris;
const QByteArray ba = mimeData->data(kdeUriListMime());
QList<QByteArray> urls = ba.split('\n');
uris.reserve(urls.size());
for (int i = 0; i < urls.size(); ++i) {
QByteArray data = urls.at(i).trimmed();
if (!data.isEmpty()) {
uris.append(QUrl::fromEncoded(data));
}
}
return uris;
}
#if HAVE_QTDBUS
static QString kioFuseServiceName()
{
return QStringLiteral("org.kde.KIOFuse");
}
static QString portalServiceName()
{
return QStringLiteral("org.freedesktop.portal.Documents");
}
static bool isKIOFuseAvailable()
{
static bool available = QDBusConnection::sessionBus().interface()
&& QDBusConnection::sessionBus().interface()->activatableServiceNames().value().contains(kioFuseServiceName());
return available;
}
bool KUrlMimeData::isDocumentsPortalAvailable()
{
static bool available =
QDBusConnection::sessionBus().interface() && QDBusConnection::sessionBus().interface()->activatableServiceNames().value().contains(portalServiceName());
return available;
}
static QString portalFormat()
{
return QStringLiteral("application/vnd.portal.filetransfer");
}
static QList<QUrl> extractPortalUriList(const QMimeData *mimeData)
{
Q_ASSERT(QCoreApplication::instance()->thread() == QThread::currentThread());
static std::pair<QByteArray, QList<QUrl>> cache;
const auto transferId = mimeData->data(portalFormat());
qCDebug(KCOREADDONS_DEBUG) << "Picking up portal urls from transfer" << transferId;
if (std::get<QByteArray>(cache) == transferId) {
const auto uris = std::get<QList<QUrl>>(cache);
qCDebug(KCOREADDONS_DEBUG) << "Urls from portal cache" << uris;
return uris;
}
auto iface =
new OrgFreedesktopPortalFileTransferInterface(portalServiceName(), QStringLiteral("/org/freedesktop/portal/documents"), QDBusConnection::sessionBus());
const QDBusReply<QStringList> reply = iface->RetrieveFiles(QString::fromUtf8(transferId), {});
if (!reply.isValid()) {
qCWarning(KCOREADDONS_DEBUG) << "Failed to retrieve files from portal:" << reply.error();
return {};
}
const QStringList list = reply.value();
QList<QUrl> uris;
uris.reserve(list.size());
for (const auto &path : list) {
uris.append(QUrl::fromLocalFile(path));
}
qCDebug(KCOREADDONS_DEBUG) << "Urls from portal" << uris;
cache = std::make_pair(transferId, uris);
return uris;
}
static QString sourceIdMime()
{
return QStringLiteral("application/x-kde-source-id");
}
static QString sourceId()
{
return QDBusConnection::sessionBus().baseService();
}
void KUrlMimeData::setSourceId(QMimeData *mimeData)
{
mimeData->setData(sourceIdMime(), sourceId().toUtf8());
}
static bool hasSameSourceId(const QMimeData *mimeData)
{
return mimeData->hasFormat(sourceIdMime()) && mimeData->data(sourceIdMime()) == sourceId().toUtf8();
}
#endif
QList<QUrl> KUrlMimeData::urlsFromMimeData(const QMimeData *mimeData, DecodeOptions decodeOptions, MetaDataMap *metaData)
{
QList<QUrl> uris;
#if HAVE_QTDBUS
if (!hasSameSourceId(mimeData) && isDocumentsPortalAvailable() && mimeData->hasFormat(portalFormat())) {
uris = extractPortalUriList(mimeData);
if (static const auto force = qEnvironmentVariableIntValue("KCOREADDONS_FORCE_DOCUMENTS_PORTAL"); force == 1) {
return uris;
}
}
#endif
if (uris.isEmpty()) {
if (decodeOptions.testFlag(PreferLocalUrls)) {
uris = mimeData->urls();
if (uris.isEmpty()) {
uris = extractKdeUriList(mimeData);
}
} else {
uris = extractKdeUriList(mimeData);
if (uris.isEmpty()) {
uris = mimeData->urls();
}
}
}
if (metaData) {
const QByteArray metaDataPayload = mimeData->data(QStringLiteral("application/x-kio-metadata"));
if (!metaDataPayload.isEmpty()) {
QString str = QString::fromUtf8(metaDataPayload.constData());
Q_ASSERT(str.endsWith(QLatin1String("$@@$")));
str.chop(4);
const QStringList lst = str.split(QStringLiteral("$@@$"));
bool readingKey = true;
QString key;
for (const QString &s : lst) {
if (readingKey) {
key = s;
} else {
metaData->insert(key, s);
}
readingKey = !readingKey;
}
Q_ASSERT(readingKey);
}
}
return uris;
}
#if HAVE_QTDBUS
static QStringList urlListToStringList(const QList<QUrl> urls)
{
QStringList list;
for (const auto &url : urls) {
list << url.toLocalFile();
}
return list;
}
static std::optional<QStringList> fuseRedirect(QList<QUrl> urls, bool onlyLocalFiles)
{
qCDebug(KCOREADDONS_DEBUG) << "mounting urls with fuse" << urls;
if (onlyLocalFiles) {
return urlListToStringList(urls);
}
OrgKdeKIOFuseVFSInterface kiofuse_iface(kioFuseServiceName(), QStringLiteral("/org/kde/KIOFuse"), QDBusConnection::sessionBus());
struct MountRequest {
QDBusPendingReply<QString> reply;
int urlIndex;
QString basename;
};
QList<MountRequest> requests;
requests.reserve(urls.count());
for (int i = 0; i < urls.count(); ++i) {
QUrl url = urls.at(i);
if (!url.isLocalFile()) {
const QString path(url.path());
const int slashes = path.count(QLatin1Char('/'));
QString basename;
if (slashes > 1) {
url.setPath(path.section(QLatin1Char('/'), 0, slashes - 1));
basename = path.section(QLatin1Char('/'), slashes, slashes);
}
requests.push_back({kiofuse_iface.mountUrl(url.toString()), i, basename});
}
}
for (auto &request : requests) {
request.reply.waitForFinished();
if (request.reply.isError()) {
qWarning() << "FUSE request failed:" << request.reply.error();
return std::nullopt;
}
urls[request.urlIndex] = QUrl::fromLocalFile(request.reply.value() + QLatin1Char('/') + request.basename);
};
qCDebug(KCOREADDONS_DEBUG) << "mounted urls with fuse, maybe" << urls;
return urlListToStringList(urls);
}
#endif
bool KUrlMimeData::exportUrlsToPortal(QMimeData *mimeData)
{
#if HAVE_QTDBUS
if (!isDocumentsPortalAvailable()) {
return false;
}
QList<QUrl> urls = mimeData->urls();
bool onlyLocalFiles = true;
for (const auto &url : urls) {
const auto isLocal = url.isLocalFile();
if (!isLocal) {
onlyLocalFiles = false;
static const auto fuseRedirect = qEnvironmentVariableIntValue("KCOREADDONS_FUSE_REDIRECT");
if (!fuseRedirect) {
return false;
}
if (!isKIOFuseAvailable()) {
qWarning() << "kio-fuse is missing";
return false;
}
} else {
const QFileInfo info(url.toLocalFile());
if (info.isSymbolicLink()) {
return false;
}
}
}
auto iface =
new OrgFreedesktopPortalFileTransferInterface(portalServiceName(), QStringLiteral("/org/freedesktop/portal/documents"), QDBusConnection::sessionBus());
const QString transferId = iface->StartTransfer({{QStringLiteral("autostop"), QVariant::fromValue(false)}});
auto cleanup = qScopeGuard([transferId, iface] {
iface->StopTransfer(transferId);
iface->deleteLater();
});
auto optionalPaths = fuseRedirect(urls, onlyLocalFiles);
if (!optionalPaths.has_value()) {
qCWarning(KCOREADDONS_DEBUG) << "Failed to mount with fuse!";
return false;
}
FDList pendingFds;
static constexpr decltype(pendingFds.size()) maximumBatchSize = 16;
pendingFds.reserve(maximumBatchSize);
const auto addFilesAndClear = [transferId, &iface, &pendingFds]() {
if (pendingFds.isEmpty()) {
return true;
}
auto reply = iface->AddFiles(transferId, pendingFds, {});
reply.waitForFinished();
if (reply.isError()) {
qCWarning(KCOREADDONS_DEBUG) << "Some files could not be exported. " << reply.error();
return false;
}
pendingFds.clear();
return true;
};
for (const auto &path : optionalPaths.value()) {
const int fd = open(QFile::encodeName(path).constData(), O_RDONLY | O_CLOEXEC | O_NONBLOCK);
if (fd == -1) {
const int error = errno;
qCWarning(KCOREADDONS_DEBUG) << "Failed to open" << path << strerror(error);
return false;
}
pendingFds << QDBusUnixFileDescriptor(fd);
close(fd);
if (pendingFds.size() >= maximumBatchSize) {
if (!addFilesAndClear()) {
return false;
}
}
}
if (!addFilesAndClear()) {
return false;
}
cleanup.dismiss();
QObject::connect(mimeData, &QObject::destroyed, iface, [transferId, iface] {
iface->StopTransfer(transferId);
iface->deleteLater();
});
QObject::connect(iface, &OrgFreedesktopPortalFileTransferInterface::TransferClosed, mimeData, [iface]() {
iface->deleteLater();
});
mimeData->setData(QStringLiteral("application/vnd.portal.filetransfer"), QFile::encodeName(transferId));
setSourceId(mimeData);
return true;
#else
Q_UNUSED(mimeData);
return false;
#endif
}