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