File indexing completed on 2024-04-28 03:51:50

0001 /*
0002     This file is part of the KDE Baloo Project
0003     SPDX-FileCopyrightText: 2012-2014 Vishesh Handa <me@vhanda.in>
0004     SPDX-FileCopyrightText: 2017-2018 James D. Smith <smithjd15@gmail.com>
0005     SPDX-FileCopyrightText: 2020 Stefan BrĂ¼ns <bruns@kde.org>
0006 
0007     SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
0008 */
0009 
0010 #include "kio_tags.h"
0011 #include "kio_tags_debug.h"
0012 
0013 #include <QUrl>
0014 
0015 #include <KLocalizedString>
0016 #include <KUser>
0017 #include <KIO/Job>
0018 
0019 #include <QCoreApplication>
0020 #include <QDir>
0021 #include <QRegularExpression>
0022 
0023 #include "file.h"
0024 #include "taglistjob.h"
0025 #include "../common/udstools.h"
0026 
0027 #include "term.h"
0028 
0029 using namespace Baloo;
0030 
0031 // Pseudo plugin class to embed meta data
0032 class KIOPluginForMetaData : public QObject
0033 {
0034     Q_OBJECT
0035     Q_PLUGIN_METADATA(IID "org.kde.kio.worker.tags" FILE "tags.json")
0036 };
0037 
0038 TagsProtocol::TagsProtocol(const QByteArray& pool_socket, const QByteArray& app_socket)
0039     : KIO::ForwardingWorkerBase("tags", pool_socket, app_socket)
0040 {
0041 }
0042 
0043 TagsProtocol::~TagsProtocol()
0044 {
0045 }
0046 
0047 KIO::WorkerResult TagsProtocol::listDir(const QUrl& url)
0048 {
0049     ParseResult result = parseUrl(url);
0050 
0051     switch(result.urlType) {
0052         case InvalidUrl:
0053         case FileUrl:
0054             qCWarning(KIO_TAGS) << result.decodedUrl << "list() invalid url";
0055             return KIO::WorkerResult::fail(KIO::ERR_CANNOT_ENTER_DIRECTORY, result.decodedUrl);
0056         case TagUrl:
0057             listEntries(result.pathUDSResults);
0058     }
0059 
0060     return KIO::WorkerResult::pass();
0061 }
0062 
0063 KIO::WorkerResult TagsProtocol::stat(const QUrl& url)
0064 {
0065     ParseResult result = parseUrl(url);
0066 
0067     switch(result.urlType) {
0068         case InvalidUrl:
0069             qCWarning(KIO_TAGS) << result.decodedUrl << "stat() invalid url";
0070             return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, result.decodedUrl);
0071         case FileUrl:
0072             return ForwardingWorkerBase::stat(result.fileUrl);
0073         case TagUrl:
0074             for (const KIO::UDSEntry& entry : std::as_const(result.pathUDSResults)) {
0075                 if (entry.stringValue(KIO::UDSEntry::UDS_EXTRA) == result.tag) {
0076                     statEntry(entry);
0077                     break;
0078                 }
0079             }
0080     }
0081 
0082     return KIO::WorkerResult::pass();
0083 }
0084 
0085 KIO::WorkerResult TagsProtocol::copy(const QUrl& src, const QUrl& dest, int permissions, KIO::JobFlags flags)
0086 {
0087     Q_UNUSED(permissions);
0088     Q_UNUSED(flags);
0089 
0090     ParseResult srcResult = parseUrl(src);
0091     ParseResult dstResult = parseUrl(dest, QList<ParseFlags>() << ChopLastSection << LazyValidation);
0092 
0093     if (srcResult.urlType == InvalidUrl) {
0094         qCWarning(KIO_TAGS) << srcResult.decodedUrl << "copy() invalid src url";
0095         return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, srcResult.decodedUrl);
0096     } else if (dstResult.urlType == InvalidUrl) {
0097         qCWarning(KIO_TAGS) << dstResult.decodedUrl << "copy() invalid dest url";
0098         return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, dstResult.decodedUrl);
0099     }
0100 
0101     auto rewriteTags = [] (KFileMetaData::UserMetaData& md, const QString& newTag) {
0102         qCDebug(KIO_TAGS) << md.filePath() << "adding tag" << newTag;
0103         QStringList tags = md.tags();
0104         tags.append(newTag);
0105         md.setTags(tags);
0106     };
0107 
0108     if (srcResult.metaData.tags().contains(dstResult.tag)) {
0109         qCWarning(KIO_TAGS) << srcResult.fileUrl.toLocalFile() << "file already has tag" << dstResult.tag;
0110         infoMessage(i18n("File %1 already has tag %2", srcResult.fileUrl.toLocalFile(), dstResult.tag));
0111     } else if (dstResult.urlType == TagUrl) {
0112         rewriteTags(srcResult.metaData, dstResult.tag);
0113     }
0114 
0115     return KIO::WorkerResult::pass();
0116 }
0117 
0118 KIO::WorkerResult TagsProtocol::get(const QUrl& url)
0119 {
0120     ParseResult result = parseUrl(url);
0121 
0122     switch(result.urlType) {
0123         case InvalidUrl:
0124             qCWarning(KIO_TAGS) << result.decodedUrl << "get() invalid url";
0125             return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, result.decodedUrl);
0126         case FileUrl:
0127             return ForwardingWorkerBase::get(result.fileUrl);
0128         case TagUrl:
0129             return KIO::WorkerResult::fail(KIO::ERR_UNSUPPORTED_ACTION, result.decodedUrl);
0130     }
0131     Q_UNREACHABLE();
0132     return KIO::WorkerResult::pass();
0133 }
0134 
0135 KIO::WorkerResult TagsProtocol::rename(const QUrl& src, const QUrl& dest, KIO::JobFlags flags)
0136 {
0137     Q_UNUSED(flags);
0138 
0139     ParseResult srcResult = parseUrl(src);
0140     ParseResult dstResult;
0141 
0142     if (srcResult.urlType == FileUrl) {
0143         dstResult = parseUrl(dest, QList<ParseFlags>() << ChopLastSection);
0144     } else if (srcResult.urlType == TagUrl) {
0145         dstResult = parseUrl(dest, QList<ParseFlags>() << LazyValidation);
0146     }
0147 
0148     if (srcResult.urlType == InvalidUrl) {
0149         qCWarning(KIO_TAGS) << srcResult.decodedUrl << "rename() invalid src url";
0150         return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, srcResult.decodedUrl);
0151     } else if (dstResult.urlType == InvalidUrl) {
0152         qCWarning(KIO_TAGS) << dstResult.decodedUrl << "rename() invalid dest url";
0153         return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, dstResult.decodedUrl);
0154     }
0155 
0156     auto rewriteTags = [] (KFileMetaData::UserMetaData& md, const QString& oldTag, const QString& newTag) {
0157         qCDebug(KIO_TAGS) << md.filePath() << "swapping tag" << oldTag << "with" << newTag;
0158         QStringList tags = md.tags();
0159         tags.removeAll(oldTag);
0160         tags.append(newTag);
0161         md.setTags(tags);
0162     };
0163 
0164     if (srcResult.metaData.tags().contains(dstResult.tag)) {
0165         qCWarning(KIO_TAGS) << srcResult.fileUrl.toLocalFile() << "file already has tag" << dstResult.tag;
0166         infoMessage(i18n("File %1 already has tag %2", srcResult.fileUrl.toLocalFile(), dstResult.tag));
0167     } else if (srcResult.urlType == FileUrl) {
0168         rewriteTags(srcResult.metaData, srcResult.tag, dstResult.tag);
0169     } else if (srcResult.urlType == TagUrl) {
0170         ResultIterator it = srcResult.query.exec();
0171         while (it.next()) {
0172             KFileMetaData::UserMetaData md(it.filePath());
0173             if (it.filePath() == srcResult.fileUrl.toLocalFile()) {
0174                 rewriteTags(md, srcResult.tag, dstResult.tag);
0175             } else if (srcResult.fileUrl.isEmpty()) {
0176                 const auto tags = md.tags();
0177                 for (const QString& tag : tags) {
0178                     if (tag == srcResult.tag || (tag.startsWith(srcResult.tag + QLatin1Char('/')))) {
0179                         QString newTag = tag;
0180                         newTag.replace(srcResult.tag, dstResult.tag, Qt::CaseInsensitive);
0181                         rewriteTags(md, tag, newTag);
0182                     }
0183                 }
0184             }
0185         }
0186     }
0187 
0188     return KIO::WorkerResult::pass();
0189 }
0190 
0191 KIO::WorkerResult TagsProtocol::del(const QUrl& url, bool isfile)
0192 {
0193     Q_UNUSED(isfile);
0194 
0195     ParseResult result = parseUrl(url);
0196 
0197     auto rewriteTags = [] (KFileMetaData::UserMetaData& md, const QString& tag) {
0198         qCDebug(KIO_TAGS) << md.filePath() << "removing tag" << tag;
0199         QStringList tags = md.tags();
0200         tags.removeAll(tag);
0201         md.setTags(tags);
0202     };
0203 
0204     switch(result.urlType) {
0205         case InvalidUrl:
0206             qCWarning(KIO_TAGS) << result.decodedUrl << "del() invalid url";
0207             return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, result.decodedUrl);
0208         case FileUrl:
0209         case TagUrl:
0210             ResultIterator it = result.query.exec();
0211             while (it.next()) {
0212                 KFileMetaData::UserMetaData md(it.filePath());
0213                 if (it.filePath() == result.fileUrl.toLocalFile()) {
0214                     rewriteTags(md, result.tag);
0215                 } else if (result.fileUrl.isEmpty()) {
0216                     const auto tags = md.tags();
0217                     for (const QString &tag : tags) {
0218                         if ((tag == result.tag) || (tag.startsWith(result.tag + QLatin1Char('/'), Qt::CaseInsensitive))) {
0219                             rewriteTags(md, tag);
0220                         }
0221                     }
0222                 }
0223             }
0224     }
0225 
0226     return KIO::WorkerResult::pass();
0227 }
0228 
0229 KIO::WorkerResult TagsProtocol::mimetype(const QUrl& url)
0230 {
0231     ParseResult result = parseUrl(url);
0232 
0233     switch(result.urlType) {
0234         case InvalidUrl:
0235             qCWarning(KIO_TAGS) << result.decodedUrl << "mimetype() invalid url";
0236             return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, result.decodedUrl);
0237         case FileUrl:
0238             return ForwardingWorkerBase::mimetype(result.fileUrl);
0239         case TagUrl:
0240             mimeType(QStringLiteral("inode/directory"));
0241     }
0242 
0243     return KIO::WorkerResult::pass();
0244 }
0245 
0246 KIO::WorkerResult TagsProtocol::mkdir(const QUrl& url, int permissions)
0247 {
0248     Q_UNUSED(permissions);
0249 
0250     ParseResult result = parseUrl(url, QList<ParseFlags>() << LazyValidation);
0251 
0252     switch(result.urlType) {
0253         case InvalidUrl:
0254         case FileUrl:
0255             qCWarning(KIO_TAGS) << result.decodedUrl << "mkdir() invalid url";
0256             return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, result.decodedUrl);
0257         case TagUrl:
0258             m_unassignedTags << result.tag;
0259     }
0260 
0261     return KIO::WorkerResult::pass();
0262 }
0263 
0264 bool TagsProtocol::rewriteUrl(const QUrl& url, QUrl& newURL)
0265 {
0266     Q_UNUSED(url);
0267     Q_UNUSED(newURL);
0268 
0269     return false;
0270 }
0271 
0272 TagsProtocol::ParseResult TagsProtocol::parseUrl(const QUrl& url, const QList<ParseFlags> &flags)
0273 {
0274     TagsProtocol::ParseResult result;
0275     result.decodedUrl = QUrl::fromPercentEncoding(url.toString().toUtf8());
0276 
0277     if ((url.scheme() == QLatin1String("tags")) && result.decodedUrl.length()>6 && result.decodedUrl.at(6) == QLatin1Char('/')) {
0278         result.urlType = InvalidUrl;
0279         return result;
0280     }
0281 
0282     auto createUDSEntryForTag = [] (const QString& tagSection, const QString& tag) {
0283         KIO::UDSEntry uds;
0284         uds.reserve(9);
0285         uds.fastInsert(KIO::UDSEntry::UDS_NAME, tagSection);
0286         uds.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR);
0287         uds.fastInsert(KIO::UDSEntry::UDS_MIME_TYPE, QStringLiteral("inode/directory"));
0288         uds.fastInsert(KIO::UDSEntry::UDS_ACCESS, 0700);
0289         uds.fastInsert(KIO::UDSEntry::UDS_USER, KUser().loginName());
0290         uds.fastInsert(KIO::UDSEntry::UDS_ICON_NAME, QStringLiteral("tag"));
0291         uds.fastInsert(KIO::UDSEntry::UDS_EXTRA, tag);
0292 
0293         QString displayType;
0294         QString displayName;
0295 
0296         // a tag/folder
0297         if (tagSection == tag) {
0298             displayType = i18nc("This is a noun", "Tag");
0299             displayName = tag.section(QLatin1Char('/'), -1);
0300         }
0301 
0302         // a tagged file
0303         else if (!tag.isEmpty()) {
0304             displayType = i18nc("This is a noun", "Tag Fragment");
0305             if (tagSection == QStringLiteral("..")) {
0306                 displayName = tag.section(QLatin1Char('/'), -2);
0307             } else if (tagSection == QStringLiteral(".")) {
0308                 displayName = tag.section(QLatin1Char('/'), -1);
0309             } else {
0310                 displayName = tagSection;
0311             }
0312         }
0313 
0314         // The root folder
0315         else {
0316             displayType = i18n("All Tags");
0317             displayName = i18n("All Tags");
0318         }
0319 
0320         uds.fastInsert(KIO::UDSEntry::UDS_DISPLAY_TYPE, displayType);
0321         uds.fastInsert(KIO::UDSEntry::UDS_DISPLAY_NAME, displayName);
0322 
0323         return uds;
0324     };
0325 
0326     TagListJob* tagJob = new TagListJob();
0327     if (!tagJob->exec()) {
0328         qCWarning(KIO_TAGS) << "tag fetch failed:" << tagJob->errorString();
0329         return result;
0330     }
0331 
0332     if (url.isLocalFile()) {
0333         result.urlType = FileUrl;
0334         result.fileUrl = url;
0335         result.metaData = KFileMetaData::UserMetaData(url.toLocalFile());
0336     } else if (url.scheme() == QLatin1String("tags")) {
0337         bool validTag = flags.contains(LazyValidation);
0338 
0339         // Determine the tag from the URL.
0340         result.tag = result.decodedUrl;
0341         result.tag.remove(url.scheme() + QLatin1Char(':'));
0342         result.tag = QDir::cleanPath(result.tag);
0343         while (result.tag.startsWith(QLatin1Char('/'))) {
0344             result.tag.remove(0, 1);
0345         }
0346 
0347         // Extract any local file path from the URL.
0348         QString tag = result.tag.section(QDir::separator(), 0, -2);
0349         QString fileName = result.tag.section(QDir::separator(), -1, -1);
0350         int pos = 0;
0351 
0352         // Extract and remove any multiple filename suffix from the file name.
0353         QRegularExpression regexp(QStringLiteral("\\s\\((\\d+)\\)$"));
0354         QRegularExpressionMatch regMatch = regexp.match(fileName);
0355         if (regMatch.hasMatch()) {
0356             pos = regMatch.captured(1).toInt();
0357 
0358             fileName.remove(regexp);
0359         }
0360 
0361         Query q;
0362         q.setSearchString(QStringLiteral("tag=\"%1\" AND filename=\"%2\"").arg(tag, fileName));
0363         ResultIterator it = q.exec();
0364 
0365         int i = 0;
0366         while (it.next()) {
0367             result.fileUrl = QUrl::fromLocalFile(it.filePath());
0368             result.metaData = KFileMetaData::UserMetaData(it.filePath());
0369 
0370             if (i == pos) {
0371                 break;
0372             } else {
0373                 i++;
0374             }
0375         }
0376 
0377         if (!result.fileUrl.isEmpty() || flags.contains(ChopLastSection)) {
0378             result.tag = result.tag.section(QDir::separator(), 0, -2);
0379         }
0380 
0381         validTag = validTag || result.tag.isEmpty();
0382 
0383         if (!result.tag.isEmpty()) {
0384             // Create a query to find files that may be in the operation's scope.
0385             QString query = result.tag;
0386             query.prepend(QStringLiteral("tag:"));
0387             query.replace(QLatin1Char(' '), QStringLiteral(" AND tag:"));
0388             query.replace(QLatin1Char('/'), QStringLiteral(" AND tag:"));
0389             result.query.setSearchString(query);
0390 
0391             qCDebug(KIO_TAGS) << result.decodedUrl << "url query:" << query;
0392         }
0393 
0394         // Create the tag directory entries.
0395         int index = result.tag.count(QLatin1Char('/')) + (result.tag.isEmpty() ? 0 : 1);
0396         QStringList tagPaths;
0397 
0398         const QStringList tags = QStringList() << tagJob->tags() << m_unassignedTags;
0399         for (const QString& tag : tags) {
0400             if (result.tag.isEmpty() || (tag.startsWith(result.tag, Qt::CaseInsensitive))) {
0401                 QString tagSection = tag.section(QLatin1Char('/'), index, index, QString::SectionSkipEmpty);
0402                 if (!tagPaths.contains(tagSection, Qt::CaseInsensitive) && !tagSection.isEmpty()) {
0403                     result.pathUDSResults << createUDSEntryForTag(tagSection, tag);
0404                     tagPaths << tagSection;
0405                 }
0406             }
0407 
0408             validTag = validTag || tag.startsWith(result.tag, Qt::CaseInsensitive);
0409         }
0410 
0411         if (validTag && result.fileUrl.isEmpty()) {
0412             result.urlType = TagUrl;
0413         } else if (validTag && !result.fileUrl.isEmpty()) {
0414             result.urlType = FileUrl;
0415         }
0416     }
0417 
0418     if (result.urlType == FileUrl) {
0419         return result;
0420     } else {
0421         result.pathUDSResults << createUDSEntryForTag(QStringLiteral("."), result.tag);
0422     }
0423 
0424     // The root tag url has no file entries.
0425     if (result.tag.isEmpty()) {
0426         return result;
0427     } else {
0428         result.pathUDSResults << createUDSEntryForTag(QStringLiteral(".."), result.tag);
0429     }
0430 
0431     // Query for any files associated with the tag.
0432     Query q;
0433     q.setSearchString(QStringLiteral("tag=\"%1\"").arg(result.tag));
0434     ResultIterator it = q.exec();
0435     QList<QString> resultNames;
0436     UdsFactory udsf;
0437 
0438     while (it.next()) {
0439         KIO::UDSEntry uds = udsf.createUdsEntry(it.filePath());
0440         if (uds.count() == 0) {
0441         continue;
0442     }
0443 
0444     const QUrl url(uds.stringValue(KIO::UDSEntry::UDS_URL));
0445     auto dupCount = resultNames.count(url.fileName());
0446         if (dupCount > 0) {
0447             uds.replace(KIO::UDSEntry::UDS_NAME, url.fileName() + QStringLiteral(" (%1)").arg(dupCount));
0448         }
0449 
0450         qCDebug(KIO_TAGS) << result.tag << "adding file:" << uds.stringValue(KIO::UDSEntry::UDS_NAME);
0451 
0452         resultNames << url.fileName();
0453         result.pathUDSResults << uds;
0454     }
0455 
0456     return result;
0457 }
0458 
0459 extern "C"
0460 {
0461     Q_DECL_EXPORT int kdemain(int argc, char** argv)
0462     {
0463         QCoreApplication app(argc, argv);
0464         app.setApplicationName(QStringLiteral("kio_tags"));
0465         Baloo::TagsProtocol worker(argv[2], argv[3]);
0466         worker.dispatchLoop();
0467         return 0;
0468     }
0469 }
0470 
0471 #include "kio_tags.moc"
0472 #include "moc_kio_tags.cpp"