1 /*
2     This file is part of the KDE Baloo Project
3     SPDX-FileCopyrightText: 2012-2014 Vishesh Handa <me@vhanda.in>
4     SPDX-FileCopyrightText: 2017-2018 James D. Smith <smithjd15@gmail.com>
5     SPDX-FileCopyrightText: 2020 Stefan Brüns <bruns@kde.org>
6 
7     SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
8 */
9 
10 #include "kio_tags.h"
11 #include "kio_tags_debug.h"
12 
13 #include <QUrl>
14 
15 #include <KLocalizedString>
16 #include <KUser>
17 #include <KIO/Job>
18 
19 #include <QCoreApplication>
20 #include <QDir>
21 #include <QRegularExpression>
22 
23 #include "file.h"
24 #include "taglistjob.h"
25 #include "../common/udstools.h"
26 
27 #include "term.h"
28 
29 using namespace Baloo;
30 
31 // Pseudo plugin class to embed meta data
32 class KIOPluginForMetaData : public QObject
33 {
34     Q_OBJECT
35     Q_PLUGIN_METADATA(IID "org.kde.kio.slave.tags" FILE "tags.json")
36 };
37 
TagsProtocol(const QByteArray & pool_socket,const QByteArray & app_socket)38 TagsProtocol::TagsProtocol(const QByteArray& pool_socket, const QByteArray& app_socket)
39     : KIO::ForwardingSlaveBase("tags", pool_socket, app_socket)
40 {
41 }
42 
~TagsProtocol()43 TagsProtocol::~TagsProtocol()
44 {
45 }
46 
listDir(const QUrl & url)47 void TagsProtocol::listDir(const QUrl& url)
48 {
49     ParseResult result = parseUrl(url);
50 
51     switch(result.urlType) {
52         case InvalidUrl:
53         case FileUrl:
54             qCWarning(KIO_TAGS) << result.decodedUrl << "list() invalid url";
55             error(KIO::ERR_CANNOT_ENTER_DIRECTORY, result.decodedUrl);
56             return;
57         case TagUrl:
58             listEntries(result.pathUDSResults);
59     }
60 
61     finished();
62 }
63 
stat(const QUrl & url)64 void TagsProtocol::stat(const QUrl& url)
65 {
66     ParseResult result = parseUrl(url);
67 
68     switch(result.urlType) {
69         case InvalidUrl:
70             qCWarning(KIO_TAGS) << result.decodedUrl << "stat() invalid url";
71             error(KIO::ERR_DOES_NOT_EXIST, result.decodedUrl);
72             return;
73         case FileUrl:
74             ForwardingSlaveBase::stat(result.fileUrl);
75             return;
76         case TagUrl:
77             for (const KIO::UDSEntry& entry : std::as_const(result.pathUDSResults)) {
78                 if (entry.stringValue(KIO::UDSEntry::UDS_EXTRA) == result.tag) {
79                     statEntry(entry);
80                 }
81             }
82     }
83 
84     finished();
85 }
86 
copy(const QUrl & src,const QUrl & dest,int permissions,KIO::JobFlags flags)87 void TagsProtocol::copy(const QUrl& src, const QUrl& dest, int permissions, KIO::JobFlags flags)
88 {
89     Q_UNUSED(permissions);
90     Q_UNUSED(flags);
91 
92     ParseResult srcResult = parseUrl(src);
93     ParseResult dstResult = parseUrl(dest, QList<ParseFlags>() << ChopLastSection << LazyValidation);
94 
95     if (srcResult.urlType == InvalidUrl) {
96         qCWarning(KIO_TAGS) << srcResult.decodedUrl << "copy() invalid src url";
97         error(KIO::ERR_DOES_NOT_EXIST, srcResult.decodedUrl);
98         return;
99     } else if (dstResult.urlType == InvalidUrl) {
100         qCWarning(KIO_TAGS) << dstResult.decodedUrl << "copy() invalid dest url";
101         error(KIO::ERR_DOES_NOT_EXIST, dstResult.decodedUrl);
102         return;
103     }
104 
105     auto rewriteTags = [] (KFileMetaData::UserMetaData& md, const QString& newTag) {
106         qCDebug(KIO_TAGS) << md.filePath() << "adding tag" << newTag;
107         QStringList tags = md.tags();
108         tags.append(newTag);
109         md.setTags(tags);
110     };
111 
112     if (srcResult.metaData.tags().contains(dstResult.tag)) {
113         qCWarning(KIO_TAGS) << srcResult.fileUrl.toLocalFile() << "file already has tag" << dstResult.tag;
114         infoMessage(i18n("File %1 already has tag %2", srcResult.fileUrl.toLocalFile(), dstResult.tag));
115     } else if (dstResult.urlType == TagUrl) {
116         rewriteTags(srcResult.metaData, dstResult.tag);
117     }
118 
119     finished();
120 }
121 
get(const QUrl & url)122 void TagsProtocol::get(const QUrl& url)
123 {
124     ParseResult result = parseUrl(url);
125 
126     switch(result.urlType) {
127         case InvalidUrl:
128             qCWarning(KIO_TAGS) << result.decodedUrl << "get() invalid url";
129             error(KIO::ERR_DOES_NOT_EXIST, result.decodedUrl);
130             return;
131         case FileUrl:
132             ForwardingSlaveBase::get(result.fileUrl);
133             return;
134         case TagUrl:
135             error(KIO::ERR_UNSUPPORTED_ACTION, result.decodedUrl);
136             return;
137     }
138 }
139 
rename(const QUrl & src,const QUrl & dest,KIO::JobFlags flags)140 void TagsProtocol::rename(const QUrl& src, const QUrl& dest, KIO::JobFlags flags)
141 {
142     Q_UNUSED(flags);
143 
144     ParseResult srcResult = parseUrl(src);
145     ParseResult dstResult;
146 
147     if (srcResult.urlType == FileUrl) {
148         dstResult = parseUrl(dest, QList<ParseFlags>() << ChopLastSection);
149     } else if (srcResult.urlType == TagUrl) {
150         dstResult = parseUrl(dest, QList<ParseFlags>() << LazyValidation);
151     }
152 
153     if (srcResult.urlType == InvalidUrl) {
154         qCWarning(KIO_TAGS) << srcResult.decodedUrl << "rename() invalid src url";
155         error(KIO::ERR_DOES_NOT_EXIST, srcResult.decodedUrl);
156         return;
157     } else if (dstResult.urlType == InvalidUrl) {
158         qCWarning(KIO_TAGS) << dstResult.decodedUrl << "rename() invalid dest url";
159         error(KIO::ERR_DOES_NOT_EXIST, dstResult.decodedUrl);
160         return;
161     }
162 
163     auto rewriteTags = [] (KFileMetaData::UserMetaData& md, const QString& oldTag, const QString& newTag) {
164         qCDebug(KIO_TAGS) << md.filePath() << "swapping tag" << oldTag << "with" << newTag;
165         QStringList tags = md.tags();
166         tags.removeAll(oldTag);
167         tags.append(newTag);
168         md.setTags(tags);
169     };
170 
171     if (srcResult.metaData.tags().contains(dstResult.tag)) {
172         qCWarning(KIO_TAGS) << srcResult.fileUrl.toLocalFile() << "file already has tag" << dstResult.tag;
173         infoMessage(i18n("File %1 already has tag %2", srcResult.fileUrl.toLocalFile(), dstResult.tag));
174     } else if (srcResult.urlType == FileUrl) {
175         rewriteTags(srcResult.metaData, srcResult.tag, dstResult.tag);
176     } else if (srcResult.urlType == TagUrl) {
177         ResultIterator it = srcResult.query.exec();
178         while (it.next()) {
179             KFileMetaData::UserMetaData md(it.filePath());
180             if (it.filePath() == srcResult.fileUrl.toLocalFile()) {
181                 rewriteTags(md, srcResult.tag, dstResult.tag);
182             } else if (srcResult.fileUrl.isEmpty()) {
183                 const auto tags = md.tags();
184                 for (const QString& tag : tags) {
185                     if (tag == srcResult.tag || (tag.startsWith(srcResult.tag + QLatin1Char('/')))) {
186                         QString newTag = tag;
187                         newTag.replace(srcResult.tag, dstResult.tag, Qt::CaseInsensitive);
188                         rewriteTags(md, tag, newTag);
189                     }
190                 }
191             }
192         }
193     }
194 
195     finished();
196 }
197 
del(const QUrl & url,bool isfile)198 void TagsProtocol::del(const QUrl& url, bool isfile)
199 {
200     Q_UNUSED(isfile);
201 
202     ParseResult result = parseUrl(url);
203 
204     auto rewriteTags = [] (KFileMetaData::UserMetaData& md, const QString& tag) {
205         qCDebug(KIO_TAGS) << md.filePath() << "removing tag" << tag;
206         QStringList tags = md.tags();
207         tags.removeAll(tag);
208         md.setTags(tags);
209     };
210 
211     switch(result.urlType) {
212         case InvalidUrl:
213             qCWarning(KIO_TAGS) << result.decodedUrl << "del() invalid url";
214             error(KIO::ERR_DOES_NOT_EXIST, result.decodedUrl);
215             return;
216         case FileUrl:
217         case TagUrl:
218             ResultIterator it = result.query.exec();
219             while (it.next()) {
220                 KFileMetaData::UserMetaData md(it.filePath());
221                 if (it.filePath() == result.fileUrl.toLocalFile()) {
222                     rewriteTags(md, result.tag);
223                 } else if (result.fileUrl.isEmpty()) {
224                     const auto tags = md.tags();
225                     for (const QString &tag : tags) {
226                         if ((tag == result.tag) || (tag.startsWith(result.tag + QLatin1Char('/'), Qt::CaseInsensitive))) {
227                             rewriteTags(md, tag);
228                         }
229                     }
230                 }
231             }
232     }
233 
234     finished();
235 }
236 
mimetype(const QUrl & url)237 void TagsProtocol::mimetype(const QUrl& url)
238 {
239     ParseResult result = parseUrl(url);
240 
241     switch(result.urlType) {
242         case InvalidUrl:
243             qCWarning(KIO_TAGS) << result.decodedUrl << "mimetype() invalid url";
244             error(KIO::ERR_DOES_NOT_EXIST, result.decodedUrl);
245             return;
246         case FileUrl:
247             ForwardingSlaveBase::mimetype(result.fileUrl);
248             return;
249         case TagUrl:
250             mimeType(QStringLiteral("inode/directory"));
251     }
252 
253     finished();
254 }
255 
mkdir(const QUrl & url,int permissions)256 void TagsProtocol::mkdir(const QUrl& url, int permissions)
257 {
258     Q_UNUSED(permissions);
259 
260     ParseResult result = parseUrl(url, QList<ParseFlags>() << LazyValidation);
261 
262     switch(result.urlType) {
263         case InvalidUrl:
264         case FileUrl:
265             qCWarning(KIO_TAGS) << result.decodedUrl << "mkdir() invalid url";
266             error(KIO::ERR_DOES_NOT_EXIST, result.decodedUrl);
267             return;
268         case TagUrl:
269             m_unassignedTags << result.tag;
270     }
271 
272     finished();
273 }
274 
rewriteUrl(const QUrl & url,QUrl & newURL)275 bool TagsProtocol::rewriteUrl(const QUrl& url, QUrl& newURL)
276 {
277     Q_UNUSED(url);
278     Q_UNUSED(newURL);
279 
280     return false;
281 }
282 
parseUrl(const QUrl & url,const QList<ParseFlags> & flags)283 TagsProtocol::ParseResult TagsProtocol::parseUrl(const QUrl& url, const QList<ParseFlags> &flags)
284 {
285     TagsProtocol::ParseResult result;
286     result.decodedUrl = QUrl::fromPercentEncoding(url.toString().toUtf8());
287 
288     if ((url.scheme() == QLatin1String("tags")) && result.decodedUrl.length()>6 && result.decodedUrl.at(6) == QLatin1Char('/')) {
289         result.urlType = InvalidUrl;
290         return result;
291     }
292 
293     auto createUDSEntryForTag = [] (const QString& tagSection, const QString& tag) {
294         KIO::UDSEntry uds;
295         uds.reserve(9);
296         uds.fastInsert(KIO::UDSEntry::UDS_NAME, tagSection);
297         uds.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR);
298         uds.fastInsert(KIO::UDSEntry::UDS_MIME_TYPE, QStringLiteral("inode/directory"));
299         uds.fastInsert(KIO::UDSEntry::UDS_ACCESS, 0700);
300         uds.fastInsert(KIO::UDSEntry::UDS_USER, KUser().loginName());
301         uds.fastInsert(KIO::UDSEntry::UDS_ICON_NAME, QStringLiteral("tag"));
302         uds.fastInsert(KIO::UDSEntry::UDS_EXTRA, tag);
303 
304         QString displayType;
305         if (tagSection == tag) {
306             displayType = i18n("Tag");
307         } else if (!tag.isEmpty()) {
308             displayType = i18n("Tag Fragment");
309         } else {
310             displayType = i18n("All Tags");
311         }
312 
313         uds.fastInsert(KIO::UDSEntry::UDS_DISPLAY_TYPE, displayType);
314 
315         QString displayName = i18n("All Tags");
316         if (!tag.isEmpty() && ((tagSection == QLatin1Char('.')) || (tagSection == QLatin1String("..")))) {
317             displayName = tag.section(QLatin1Char('/'), -1);
318         } else {
319             displayName = tagSection;
320         }
321 
322         uds.fastInsert(KIO::UDSEntry::UDS_DISPLAY_NAME, displayName);
323 
324         return uds;
325     };
326 
327     TagListJob* tagJob = new TagListJob();
328     if (!tagJob->exec()) {
329         qCWarning(KIO_TAGS) << "tag fetch failed:" << tagJob->errorString();
330         return result;
331     }
332 
333     if (url.isLocalFile()) {
334         result.urlType = FileUrl;
335         result.fileUrl = url;
336         result.metaData = KFileMetaData::UserMetaData(url.toLocalFile());
337     } else if (url.scheme() == QLatin1String("tags")) {
338         bool validTag = flags.contains(LazyValidation);
339 
340         // Determine the tag from the URL.
341         result.tag = result.decodedUrl;
342         result.tag.remove(url.scheme() + QLatin1Char(':'));
343         result.tag = QDir::cleanPath(result.tag);
344         while (result.tag.startsWith(QLatin1Char('/'))) {
345             result.tag.remove(0, 1);
346         }
347 
348         // Extract any local file path from the URL.
349         QString tag = result.tag.section(QDir::separator(), 0, -2);
350         QString fileName = result.tag.section(QDir::separator(), -1, -1);
351         int pos = 0;
352 
353         // Extract and remove any multiple filename suffix from the file name.
354         QRegularExpression regexp(QStringLiteral("\\s\\((\\d+)\\)$"));
355         QRegularExpressionMatch regMatch = regexp.match(fileName);
356         if (regMatch.hasMatch()) {
357             pos = regMatch.captured(1).toInt();
358 
359             fileName.remove(regexp);
360         }
361 
362         Query q;
363         q.setSearchString(QStringLiteral("tag=\"%1\" AND filename=\"%2\"").arg(tag, fileName));
364         ResultIterator it = q.exec();
365 
366         int i = 0;
367         while (it.next()) {
368             result.fileUrl = QUrl::fromLocalFile(it.filePath());
369             result.metaData = KFileMetaData::UserMetaData(it.filePath());
370 
371             if (i == pos) {
372                 break;
373             } else {
374                 i++;
375             }
376         }
377 
378         if (!result.fileUrl.isEmpty() || flags.contains(ChopLastSection)) {
379             result.tag = result.tag.section(QDir::separator(), 0, -2);
380         }
381 
382         validTag = validTag || result.tag.isEmpty();
383 
384         if (!result.tag.isEmpty()) {
385             // Create a query to find files that may be in the operation's scope.
386             QString query = result.tag;
387             query.prepend(QStringLiteral("tag:"));
388             query.replace(QLatin1Char(' '), QStringLiteral(" AND tag:"));
389             query.replace(QLatin1Char('/'), QStringLiteral(" AND tag:"));
390             result.query.setSearchString(query);
391 
392             qCDebug(KIO_TAGS) << result.decodedUrl << "url query:" << query;
393         }
394 
395         // Create the tag directory entries.
396         int index = result.tag.count(QLatin1Char('/')) + (result.tag.isEmpty() ? 0 : 1);
397         QStringList tagPaths;
398 
399         const QStringList tags = QStringList() << tagJob->tags() << m_unassignedTags;
400         for (const QString& tag : tags) {
401             if (result.tag.isEmpty() || (tag.startsWith(result.tag, Qt::CaseInsensitive))) {
402                 QString tagSection = tag.section(QLatin1Char('/'), index, index, QString::SectionSkipEmpty);
403                 if (!tagPaths.contains(tagSection, Qt::CaseInsensitive) && !tagSection.isEmpty()) {
404                     result.pathUDSResults << createUDSEntryForTag(tagSection, tag);
405                     tagPaths << tagSection;
406                 }
407             }
408 
409             validTag = validTag || tag.startsWith(result.tag, Qt::CaseInsensitive);
410         }
411 
412         if (validTag && result.fileUrl.isEmpty()) {
413             result.urlType = TagUrl;
414         } else if (validTag && !result.fileUrl.isEmpty()) {
415             result.urlType = FileUrl;
416         }
417     }
418 
419     if (result.urlType == FileUrl) {
420         return result;
421     } else {
422         result.pathUDSResults << createUDSEntryForTag(QStringLiteral("."), result.tag);
423     }
424 
425     // The root tag url has no file entries.
426     if (result.tag.isEmpty()) {
427         return result;
428     } else {
429         result.pathUDSResults << createUDSEntryForTag(QStringLiteral(".."), result.tag);
430     }
431 
432     // Query for any files associated with the tag.
433     Query q;
434     q.setSearchString(QStringLiteral("tag=\"%1\"").arg(result.tag));
435     ResultIterator it = q.exec();
436     QList<QString> resultNames;
437     UdsFactory udsf;
438 
439     while (it.next()) {
440         KIO::UDSEntry uds = udsf.createUdsEntry(it.filePath());
441         if (uds.count() == 0) {
442 	    continue;
443 	}
444 
445 	const QUrl url(uds.stringValue(KIO::UDSEntry::UDS_URL));
446 	auto dupCount = resultNames.count(url.fileName());
447         if (dupCount > 0) {
448             uds.replace(KIO::UDSEntry::UDS_NAME, url.fileName() + QStringLiteral(" (%1)").arg(dupCount));
449         }
450 
451         qCDebug(KIO_TAGS) << result.tag << "adding file:" << uds.stringValue(KIO::UDSEntry::UDS_NAME);
452 
453         resultNames << url.fileName();
454         result.pathUDSResults << uds;
455     }
456 
457     return result;
458 }
459 
460 extern "C"
461 {
kdemain(int argc,char ** argv)462     Q_DECL_EXPORT int kdemain(int argc, char** argv)
463     {
464         QCoreApplication app(argc, argv);
465         app.setApplicationName(QStringLiteral("kio_tags"));
466         Baloo::TagsProtocol slave(argv[2], argv[3]);
467         slave.dispatchLoop();
468         return 0;
469     }
470 }
471 
472 #include "kio_tags.moc"
473