1 | /* -*- c++ -*- |
2 | SPDX-FileCopyrightText: 2000 Daniel M. Duley <mosfet@kde.org> |
3 | SPDX-FileCopyrightText: 2021 Martin Tobias Holmedahl Sandsmark |
4 | SPDX-FileCopyrightText: 2022 Méven Car <meven.car@kdemail.net> |
5 | |
6 | SPDX-License-Identifier: BSD-2-Clause |
7 | */ |
8 | |
9 | #include "krecentdocument.h" |
10 | |
11 | #include "kiocoredebug.h" |
12 | |
13 | #include <QCoreApplication> |
14 | #include <QDir> |
15 | #include <QDomDocument> |
16 | #include <QLockFile> |
17 | #include <QMimeDatabase> |
18 | #include <QSaveFile> |
19 | #include <QXmlStreamWriter> |
20 | |
21 | #include <KConfigGroup> |
22 | #include <KService> |
23 | #include <KSharedConfig> |
24 | |
25 | using namespace Qt::StringLiterals; |
26 | |
27 | static QString xbelPath() |
28 | { |
29 | return QStandardPaths::writableLocation(type: QStandardPaths::GenericDataLocation) + QLatin1String("/recently-used.xbel" ); |
30 | } |
31 | |
32 | static inline QString stringForRecentDocumentGroup(int val) |
33 | { |
34 | switch (val) { |
35 | case KRecentDocument::RecentDocumentGroup::Development: |
36 | return "Development"_L1 ; |
37 | case KRecentDocument::RecentDocumentGroup::Office: |
38 | return "Office"_L1 ; |
39 | case KRecentDocument::RecentDocumentGroup::Database: |
40 | return "Database"_L1 ; |
41 | case KRecentDocument::RecentDocumentGroup::Email: |
42 | return "Email"_L1 ; |
43 | case KRecentDocument::RecentDocumentGroup::Presentation: |
44 | return "Presentation"_L1 ; |
45 | case KRecentDocument::RecentDocumentGroup::Spreadsheet: |
46 | return "Spreadsheet"_L1 ; |
47 | case KRecentDocument::RecentDocumentGroup::WordProcessor: |
48 | return "WordProcessor"_L1 ; |
49 | case KRecentDocument::RecentDocumentGroup::Graphics: |
50 | return "Graphics"_L1 ; |
51 | case KRecentDocument::RecentDocumentGroup::TextEditor: |
52 | return "TextEditor"_L1 ; |
53 | case KRecentDocument::RecentDocumentGroup::Viewer: |
54 | return "Viewer"_L1 ; |
55 | case KRecentDocument::RecentDocumentGroup::Archive: |
56 | return "Archive"_L1 ; |
57 | case KRecentDocument::RecentDocumentGroup::Multimedia: |
58 | return "Multimedia"_L1 ; |
59 | case KRecentDocument::RecentDocumentGroup::Audio: |
60 | return "Audio"_L1 ; |
61 | case KRecentDocument::RecentDocumentGroup::Video: |
62 | return "Video"_L1 ; |
63 | case KRecentDocument::RecentDocumentGroup::Photo: |
64 | return "Photo"_L1 ; |
65 | case KRecentDocument::RecentDocumentGroup::Application: |
66 | return "Application"_L1 ; |
67 | }; |
68 | Q_UNREACHABLE(); |
69 | } |
70 | |
71 | static KRecentDocument::RecentDocumentGroups groupsForMimeType(const QString mimeType) |
72 | { |
73 | // simple heuristics, feel free to expand as needed |
74 | if (mimeType.startsWith(s: "image/"_L1 )) { |
75 | return KRecentDocument::RecentDocumentGroups{KRecentDocument::RecentDocumentGroup::Graphics}; |
76 | } |
77 | if (mimeType.startsWith(s: "video/"_L1 )) { |
78 | return KRecentDocument::RecentDocumentGroups{KRecentDocument::RecentDocumentGroup::Video}; |
79 | } |
80 | if (mimeType.startsWith(s: "audio/"_L1 )) { |
81 | return KRecentDocument::RecentDocumentGroups{KRecentDocument::RecentDocumentGroup::Audio}; |
82 | } |
83 | return KRecentDocument::RecentDocumentGroups{}; |
84 | } |
85 | |
86 | // Marginally more readable to avoid all the QStringLiteral() spam below |
87 | static const QLatin1String xbelTag("xbel" ); |
88 | static const QLatin1String versionAttribute("version" ); |
89 | static const QLatin1String expectedVersion("1.0" ); |
90 | |
91 | static const QLatin1String applicationsBookmarkTag("bookmark:applications" ); |
92 | static const QLatin1String applicationBookmarkTag("bookmark:application" ); |
93 | static const QLatin1String bookmarkTag("bookmark" ); |
94 | static const QLatin1String infoTag("info" ); |
95 | static const QLatin1String metadataTag("metadata" ); |
96 | static const QLatin1String mimeTypeTag("mime:mime-type" ); |
97 | static const QLatin1String bookmarkGroups("bookmark:groups" ); |
98 | static const QLatin1String bookmarkGroup("bookmark:group" ); |
99 | |
100 | static const QLatin1String nameAttribute("name" ); |
101 | static const QLatin1String countAttribute("count" ); |
102 | static const QLatin1String modifiedAttribute("modified" ); |
103 | static const QLatin1String visitedAttribute("visited" ); |
104 | static const QLatin1String hrefAttribute("href" ); |
105 | static const QLatin1String addedAttribute("added" ); |
106 | static const QLatin1String execAttribute("exec" ); |
107 | static const QLatin1String ownerAttribute("owner" ); |
108 | static const QLatin1String ownerValue("http://freedesktop.org" ); |
109 | static const QLatin1String typeAttribute("type" ); |
110 | |
111 | static bool removeOldestEntries(int &maxEntries) |
112 | { |
113 | QFile input(xbelPath()); |
114 | if (!input.exists()) { |
115 | return true; |
116 | } |
117 | |
118 | // Won't help for GTK applications and whatnot, but we can be good citizens ourselves |
119 | QLockFile lockFile(xbelPath() + QLatin1String(".lock" )); |
120 | lockFile.setStaleLockTime(0); |
121 | if (!lockFile.tryLock(timeout: 100)) { // give it 100ms |
122 | qCWarning(KIO_CORE) << "Failed to lock recently used" ; |
123 | return false; |
124 | } |
125 | |
126 | if (!input.open(flags: QIODevice::ReadOnly)) { |
127 | qCWarning(KIO_CORE) << "Failed to open existing recently used" << input.errorString(); |
128 | return false; |
129 | } |
130 | |
131 | QDomDocument document; |
132 | document.setContent(device: &input); |
133 | input.close(); |
134 | |
135 | auto xbelTags = document.elementsByTagName(tagname: xbelTag); |
136 | if (xbelTags.length() != 1) { |
137 | qCWarning(KIO_CORE) << "Invalid Xbel file, missing xbel element" ; |
138 | return false; |
139 | } |
140 | auto xbelElement = xbelTags.item(index: 0); |
141 | auto bookmarkList = xbelElement.childNodes(); |
142 | if (bookmarkList.length() <= maxEntries) { |
143 | return true; |
144 | } |
145 | |
146 | QMultiMap<QDateTime, QDomNode> bookmarksByModifiedDate; |
147 | for (int i = 0; i < bookmarkList.length(); ++i) { |
148 | const auto node = bookmarkList.item(index: i); |
149 | const auto modifiedString = node.attributes().namedItem(name: modifiedAttribute); |
150 | const auto modifiedTime = QDateTime::fromString(string: modifiedString.nodeValue(), format: Qt::ISODate); |
151 | |
152 | bookmarksByModifiedDate.insert(key: modifiedTime, value: node); |
153 | } |
154 | |
155 | int i = 0; |
156 | // entries are traversed in ascending key order |
157 | for (auto entry = bookmarksByModifiedDate.keyValueBegin(); entry != bookmarksByModifiedDate.keyValueEnd(); ++entry) { |
158 | // only keep the maxEntries last nodes |
159 | if (bookmarksByModifiedDate.size() - i > maxEntries) { |
160 | xbelElement.removeChild(oldChild: entry->second); |
161 | } |
162 | ++i; |
163 | } |
164 | |
165 | if (input.open(flags: QIODevice::WriteOnly) && input.write(data: document.toByteArray(indent: 2)) != -1) { |
166 | return true; |
167 | } |
168 | return false; |
169 | } |
170 | |
171 | static bool addToXbel(const QUrl &url, const QString &desktopEntryName, KRecentDocument::RecentDocumentGroups groups, int maxEntries, bool ignoreHidden) |
172 | { |
173 | if (!QDir().mkpath(dirPath: QStandardPaths::writableLocation(type: QStandardPaths::GenericDataLocation))) { |
174 | qCWarning(KIO_CORE) << "Could not create GenericDataLocation" ; |
175 | return false; |
176 | } |
177 | |
178 | // Won't help for GTK applications and whatnot, but we can be good citizens ourselves |
179 | QLockFile lockFile(xbelPath() + QLatin1String(".lock" )); |
180 | lockFile.setStaleLockTime(0); |
181 | if (!lockFile.tryLock(timeout: 100)) { // give it 100ms |
182 | qCWarning(KIO_CORE) << "Failed to lock recently used" ; |
183 | return false; |
184 | } |
185 | |
186 | QByteArray existingContent; |
187 | QFile input(xbelPath()); |
188 | if (input.open(flags: QIODevice::ReadOnly)) { |
189 | existingContent = input.readAll(); |
190 | } else if (!input.exists()) { // That it doesn't exist is a very uncommon case |
191 | qCDebug(KIO_CORE) << input.fileName() << "does not exist, creating new" ; |
192 | } else { |
193 | qCWarning(KIO_CORE) << "Failed to open existing recently used" << input.errorString(); |
194 | return false; |
195 | } |
196 | |
197 | QXmlStreamReader xml(existingContent); |
198 | |
199 | xml.readNextStartElement(); |
200 | if (!existingContent.isEmpty()) { |
201 | if (xml.name().isEmpty() || xml.name() != xbelTag || !xml.attributes().hasAttribute(qualifiedName: versionAttribute)) { |
202 | qCDebug(KIO_CORE) << "The recently-used.xbel is not an XBEL file, overwriting." ; |
203 | } else if (xml.attributes().value(qualifiedName: versionAttribute) != expectedVersion) { |
204 | qCDebug(KIO_CORE) << "The recently-used.xbel is not an XBEL version 1.0 file but has version: " << xml.attributes().value(qualifiedName: versionAttribute) |
205 | << ", overwriting." ; |
206 | } |
207 | } |
208 | |
209 | QSaveFile outputFile(xbelPath()); |
210 | if (!outputFile.open(flags: QIODevice::WriteOnly)) { |
211 | qCWarning(KIO_CORE) << "Failed to recently-used.xbel for writing:" << outputFile.errorString(); |
212 | return false; |
213 | } |
214 | |
215 | QXmlStreamWriter output(&outputFile); |
216 | output.setAutoFormatting(true); |
217 | output.setAutoFormattingIndent(2); |
218 | output.writeStartDocument(); |
219 | output.writeStartElement(qualifiedName: xbelTag); |
220 | |
221 | output.writeAttribute(qualifiedName: versionAttribute, value: expectedVersion); |
222 | output.writeNamespace(namespaceUri: "http://www.freedesktop.org/standards/desktop-bookmarks"_L1 , prefix: bookmarkTag); |
223 | output.writeNamespace(namespaceUri: "http://www.freedesktop.org/standards/shared-mime-info"_L1 , prefix: "mime"_L1 ); |
224 | |
225 | const QString newUrl = QString::fromLatin1(ba: url.toEncoded()); |
226 | const QString currentTimestamp = QDateTime::currentDateTimeUtc().toString(format: Qt::ISODateWithMs).chopped(n: 1) + "000Z"_L1 ; |
227 | |
228 | auto addApplicationTag = [&output, desktopEntryName, currentTimestamp, url]() { |
229 | output.writeEmptyElement(qualifiedName: applicationBookmarkTag); |
230 | output.writeAttribute(qualifiedName: nameAttribute, value: desktopEntryName); |
231 | auto service = KService::serviceByDesktopName(name: desktopEntryName); |
232 | QString exec; |
233 | bool shouldAddParameter = true; |
234 | if (service) { |
235 | exec = service->exec(); |
236 | exec.replace(before: QLatin1String(" %U" ), after: QLatin1String(" %u" )); |
237 | exec.replace(before: QLatin1String(" %F" ), after: QLatin1String(" %f" )); |
238 | shouldAddParameter = !exec.contains(s: QLatin1String(" %u" )) && !exec.contains(s: QLatin1String(" %f" )); |
239 | } else { |
240 | exec = QCoreApplication::instance()->applicationName(); |
241 | } |
242 | if (shouldAddParameter) { |
243 | if (url.isLocalFile()) { |
244 | exec += QLatin1String(" %f" ); |
245 | } else { |
246 | exec += QLatin1String(" %u" ); |
247 | } |
248 | } |
249 | output.writeAttribute(qualifiedName: execAttribute, value: exec); |
250 | output.writeAttribute(qualifiedName: modifiedAttribute, value: currentTimestamp); |
251 | output.writeAttribute(qualifiedName: countAttribute, value: "1"_L1 ); |
252 | }; |
253 | |
254 | bool foundExistingApp = false; |
255 | bool inRightBookmark = false; |
256 | bool foundMatchingBookmark = false; |
257 | bool firstBookmark = true; |
258 | int nbEntries = 0; |
259 | while (!xml.atEnd() && !xml.hasError()) { |
260 | if (xml.readNext() == QXmlStreamReader::EndElement && xml.name() == xbelTag) { |
261 | break; |
262 | } |
263 | switch (xml.tokenType()) { |
264 | case QXmlStreamReader::StartElement: { |
265 | const QStringView tagName = xml.qualifiedName(); |
266 | QXmlStreamAttributes attributes = xml.attributes(); |
267 | |
268 | if (tagName == bookmarkTag) { |
269 | foundExistingApp = false; |
270 | firstBookmark = false; |
271 | |
272 | const QStringView hrefValue = attributes.value(qualifiedName: hrefAttribute); |
273 | inRightBookmark = hrefValue == newUrl; |
274 | |
275 | // remove hidden files if some were added by GTK |
276 | if (ignoreHidden && hrefValue.contains(s: QLatin1String("/." ))) { |
277 | xml.skipCurrentElement(); |
278 | break; |
279 | } |
280 | |
281 | if (inRightBookmark) { |
282 | foundMatchingBookmark = true; |
283 | |
284 | QXmlStreamAttributes newAttributes; |
285 | for (const QXmlStreamAttribute &old : attributes) { |
286 | if (old.name() == modifiedAttribute) { |
287 | continue; |
288 | } |
289 | if (old.name() == visitedAttribute) { |
290 | continue; |
291 | } |
292 | newAttributes.append(t: old); |
293 | } |
294 | newAttributes.append(qualifiedName: modifiedAttribute, value: currentTimestamp); |
295 | newAttributes.append(qualifiedName: visitedAttribute, value: currentTimestamp); |
296 | attributes = newAttributes; |
297 | } |
298 | |
299 | nbEntries += 1; |
300 | } |
301 | |
302 | else if (inRightBookmark && tagName == applicationBookmarkTag && attributes.value(qualifiedName: nameAttribute) == desktopEntryName) { |
303 | // case found right bookmark and same application |
304 | const int count = attributes.value(qualifiedName: countAttribute).toInt(); |
305 | |
306 | QXmlStreamAttributes newAttributes; |
307 | for (const QXmlStreamAttribute &old : std::as_const(t&: attributes)) { |
308 | if (old.name() == countAttribute) { |
309 | continue; |
310 | } |
311 | if (old.name() == modifiedAttribute) { |
312 | continue; |
313 | } |
314 | newAttributes.append(t: old); |
315 | } |
316 | newAttributes.append(qualifiedName: modifiedAttribute, value: currentTimestamp); |
317 | newAttributes.append(qualifiedName: countAttribute, value: QString::number(count + 1)); |
318 | attributes = newAttributes; |
319 | |
320 | foundExistingApp = true; |
321 | } |
322 | |
323 | output.writeStartElement(qualifiedName: tagName.toString()); |
324 | output.writeAttributes(attributes); |
325 | break; |
326 | } |
327 | case QXmlStreamReader::EndElement: { |
328 | const QStringView tagName = xml.qualifiedName(); |
329 | if (tagName == applicationsBookmarkTag && inRightBookmark && !foundExistingApp) { |
330 | // add an application to the applications already known for the bookmark |
331 | addApplicationTag(); |
332 | } |
333 | output.writeEndElement(); |
334 | break; |
335 | } |
336 | case QXmlStreamReader::Characters: |
337 | if (xml.isCDATA()) { |
338 | output.writeCDATA(text: xml.text().toString()); |
339 | } else { |
340 | output.writeCharacters(text: xml.text().toString()); |
341 | } |
342 | break; |
343 | case QXmlStreamReader::Comment: |
344 | output.writeComment(text: xml.text().toString()); |
345 | break; |
346 | case QXmlStreamReader::EndDocument: |
347 | qCWarning(KIO_CORE) << "Malformed, got end document before end of xbel" << xml.tokenString() << url; |
348 | return false; |
349 | default: |
350 | qCWarning(KIO_CORE) << "unhandled token" << xml.tokenString() << url; |
351 | break; |
352 | } |
353 | } |
354 | |
355 | if (!foundMatchingBookmark) { |
356 | // must create new bookmark tag |
357 | if (firstBookmark) { |
358 | output.writeCharacters(text: "\n"_L1 ); |
359 | } |
360 | output.writeCharacters(text: " "_L1 ); |
361 | output.writeStartElement(qualifiedName: bookmarkTag); |
362 | |
363 | output.writeAttribute(qualifiedName: hrefAttribute, value: newUrl); |
364 | output.writeAttribute(qualifiedName: addedAttribute, value: currentTimestamp); |
365 | output.writeAttribute(qualifiedName: modifiedAttribute, value: currentTimestamp); |
366 | output.writeAttribute(qualifiedName: visitedAttribute, value: currentTimestamp); |
367 | |
368 | { |
369 | QMimeDatabase mimeDb; |
370 | const auto fileMime = mimeDb.mimeTypeForUrl(url).name(); |
371 | |
372 | output.writeStartElement(qualifiedName: infoTag); |
373 | output.writeStartElement(qualifiedName: metadataTag); |
374 | output.writeAttribute(qualifiedName: ownerAttribute, value: ownerValue); |
375 | |
376 | output.writeEmptyElement(qualifiedName: mimeTypeTag); |
377 | output.writeAttribute(qualifiedName: typeAttribute, value: fileMime); |
378 | |
379 | // write groups metadata |
380 | if (groups.isEmpty()) { |
381 | groups = groupsForMimeType(mimeType: fileMime); |
382 | } |
383 | if (!groups.isEmpty()) { |
384 | output.writeStartElement(qualifiedName: bookmarkGroups); |
385 | for (const auto &group : std::as_const(t&: groups)) { |
386 | output.writeTextElement(qualifiedName: bookmarkGroup, text: stringForRecentDocumentGroup(val: group)); |
387 | } |
388 | // bookmarkGroups |
389 | output.writeEndElement(); |
390 | } |
391 | |
392 | { |
393 | output.writeStartElement(qualifiedName: applicationsBookmarkTag); |
394 | addApplicationTag(); |
395 | // end applicationsBookmarkTag |
396 | output.writeEndElement(); |
397 | } |
398 | |
399 | // end infoTag |
400 | output.writeEndElement(); |
401 | // end metadataTag |
402 | output.writeEndElement(); |
403 | } |
404 | |
405 | // end bookmarkTag |
406 | output.writeEndElement(); |
407 | } |
408 | |
409 | // end xbelTag |
410 | output.writeEndElement(); |
411 | |
412 | // end document |
413 | output.writeEndDocument(); |
414 | |
415 | if (outputFile.commit()) { |
416 | lockFile.unlock(); |
417 | // tolerate 10 more entries than threshold to limit overhead of cleaning old data |
418 | return nbEntries - maxEntries > 10 || removeOldestEntries(maxEntries); |
419 | } |
420 | return false; |
421 | } |
422 | |
423 | static QMap<QUrl, QDateTime> xbelRecentlyUsedList() |
424 | { |
425 | QMap<QUrl, QDateTime> ret; |
426 | QFile input(xbelPath()); |
427 | if (!input.open(flags: QIODevice::ReadOnly)) { |
428 | qCWarning(KIO_CORE) << "Failed to open" << input.fileName() << input.errorString(); |
429 | return ret; |
430 | } |
431 | |
432 | QXmlStreamReader xml(&input); |
433 | xml.readNextStartElement(); |
434 | if (xml.name() != QLatin1String("xbel" ) || xml.attributes().value(qualifiedName: QLatin1String("version" )) != QLatin1String("1.0" )) { |
435 | qCWarning(KIO_CORE) << "The file is not an XBEL version 1.0 file." ; |
436 | return ret; |
437 | } |
438 | |
439 | while (!xml.atEnd() && !xml.hasError()) { |
440 | if (xml.readNext() != QXmlStreamReader::StartElement || xml.name() != QLatin1String("bookmark" )) { |
441 | continue; |
442 | } |
443 | |
444 | const auto urlString = xml.attributes().value(qualifiedName: QLatin1String("href" )); |
445 | if (urlString.isEmpty()) { |
446 | qCInfo(KIO_CORE) << "Invalid bookmark in" << input.fileName(); |
447 | continue; |
448 | } |
449 | const QUrl url = QUrl::fromEncoded(input: urlString.toLatin1()); |
450 | if (url.isLocalFile() && !QFile(url.toLocalFile()).exists()) { |
451 | continue; |
452 | } |
453 | const auto attributes = xml.attributes(); |
454 | const QDateTime modified = QDateTime::fromString(string: attributes.value(qualifiedName: QLatin1String("modified" )).toString(), format: Qt::ISODate); |
455 | const QDateTime visited = QDateTime::fromString(string: attributes.value(qualifiedName: QLatin1String("visited" )).toString(), format: Qt::ISODate); |
456 | const QDateTime added = QDateTime::fromString(string: attributes.value(qualifiedName: QLatin1String("added" )).toString(), format: Qt::ISODate); |
457 | if (modified > visited && modified > added) { |
458 | ret[url] = modified; |
459 | } else if (visited > added) { |
460 | ret[url] = visited; |
461 | } else { |
462 | ret[url] = added; |
463 | } |
464 | } |
465 | |
466 | if (xml.hasError()) { |
467 | qCWarning(KIO_CORE) << "Failed to read" << input.fileName() << xml.errorString(); |
468 | } |
469 | |
470 | return ret; |
471 | } |
472 | |
473 | QList<QUrl> KRecentDocument::recentUrls() |
474 | { |
475 | QMap<QUrl, QDateTime> documents = xbelRecentlyUsedList(); |
476 | |
477 | QList<QUrl> ret = documents.keys(); |
478 | std::sort(first: ret.begin(), last: ret.end(), comp: [&](const QUrl &doc1, const QUrl &doc2) { |
479 | return documents.value(key: doc1) < documents.value(key: doc2); |
480 | }); |
481 | |
482 | return ret; |
483 | } |
484 | |
485 | void KRecentDocument::add(const QUrl &url) |
486 | { |
487 | add(url, groups: RecentDocumentGroups()); |
488 | } |
489 | |
490 | void KRecentDocument::add(const QUrl &url, KRecentDocument::RecentDocumentGroups groups) |
491 | { |
492 | // desktopFileName is in QGuiApplication but we're in KIO Core here |
493 | QString desktopEntryName = QCoreApplication::instance()->property(name: "desktopFileName" ).toString(); |
494 | if (desktopEntryName.isEmpty()) { |
495 | desktopEntryName = QCoreApplication::applicationName(); |
496 | } |
497 | add(url, desktopEntryName, groups); |
498 | } |
499 | |
500 | void KRecentDocument::add(const QUrl &url, const QString &desktopEntryName) |
501 | { |
502 | add(url, desktopEntryName, groups: RecentDocumentGroups()); |
503 | } |
504 | |
505 | void KRecentDocument::add(const QUrl &url, const QString &desktopEntryName, KRecentDocument::RecentDocumentGroups groups) |
506 | { |
507 | if (url.isLocalFile() && url.toLocalFile().startsWith(s: QDir::tempPath())) { |
508 | return; // inside tmp resource, do not save |
509 | } |
510 | |
511 | // qDebug() << "KRecentDocument::add for " << openStr; |
512 | KConfigGroup config = KSharedConfig::openConfig()->group(group: "RecentDocuments"_L1 ); |
513 | bool useRecent = config.readEntry(key: "UseRecent"_L1 , aDefault: true); |
514 | int maxEntries = config.readEntry(key: "MaxEntries"_L1 , aDefault: 300); |
515 | bool ignoreHidden = config.readEntry(key: "IgnoreHidden"_L1 , aDefault: true); |
516 | |
517 | if (!useRecent || maxEntries == 0) { |
518 | clear(); |
519 | return; |
520 | } |
521 | if (ignoreHidden && url.toLocalFile().contains(s: QLatin1String("/." ))) { |
522 | return; |
523 | } |
524 | |
525 | if (!addToXbel(url, desktopEntryName, groups, maxEntries, ignoreHidden)) { |
526 | qCWarning(KIO_CORE) << "Failed to add to recently used bookmark file" ; |
527 | } |
528 | } |
529 | |
530 | void KRecentDocument::clear() |
531 | { |
532 | QFile(xbelPath()).remove(); |
533 | } |
534 | |
535 | int KRecentDocument::maximumItems() |
536 | { |
537 | KConfigGroup cg(KSharedConfig::openConfig(), "RecentDocuments"_L1 ); |
538 | return cg.readEntry(key: "MaxEntries"_L1 , aDefault: 300); |
539 | } |
540 | |
541 | void KRecentDocument::removeFile(const QUrl &url) |
542 | { |
543 | QFile input(xbelPath()); |
544 | if (!input.exists()) { |
545 | return; |
546 | } |
547 | |
548 | // Won't help for GTK applications and whatnot, but we can be good citizens ourselves |
549 | QLockFile lockFile(xbelPath() + QLatin1String(".lock" )); |
550 | lockFile.setStaleLockTime(0); |
551 | if (!lockFile.tryLock(timeout: 100)) { // give it 100ms |
552 | qCWarning(KIO_CORE) << "Failed to lock recently used" ; |
553 | return; |
554 | } |
555 | |
556 | if (!input.open(flags: QIODevice::ReadOnly)) { |
557 | qCWarning(KIO_CORE) << "Failed to open existing recently used" << input.errorString(); |
558 | return; |
559 | } |
560 | |
561 | QDomDocument document; |
562 | document.setContent(device: &input); |
563 | input.close(); |
564 | |
565 | auto xbelTags = document.elementsByTagName(tagname: xbelTag); |
566 | if (xbelTags.length() != 1) { |
567 | qCWarning(KIO_CORE) << "Invalid Xbel file, missing xbel elememt" ; |
568 | return; |
569 | } |
570 | auto xbelElement = xbelTags.item(index: 0); |
571 | auto bookmarkList = xbelElement.childNodes(); |
572 | |
573 | bool fileChanged = false; |
574 | for (int i = 0; i < bookmarkList.length(); ++i) { |
575 | const auto node = bookmarkList.item(index: i); |
576 | |
577 | const auto hrefValue = node.attributes().namedItem(name: hrefAttribute); |
578 | if (!hrefValue.isAttr() || hrefValue.nodeValue().isEmpty()) { |
579 | qCInfo(KIO_CORE) << "Invalid bookmark in" << input.fileName() << "invalid href attribute" ; |
580 | continue; |
581 | } |
582 | |
583 | const QUrl hrefUrl = QUrl::fromEncoded(input: hrefValue.nodeValue().toLatin1()); |
584 | if (hrefUrl == url) { |
585 | xbelElement.removeChild(oldChild: node); |
586 | fileChanged = true; |
587 | } |
588 | } |
589 | |
590 | if (fileChanged) { |
591 | if (!input.open(flags: QIODevice::WriteOnly) || (input.write(data: document.toByteArray(indent: 2)) < 0)) { |
592 | qCWarning(KIO_CORE) << "Couldn't save bookmark file " << input.fileName(); |
593 | } |
594 | } |
595 | } |
596 | |
597 | void KRecentDocument::removeApplication(const QString &desktopEntryName) |
598 | { |
599 | QFile input(xbelPath()); |
600 | if (!input.exists()) { |
601 | return; |
602 | } |
603 | |
604 | // Won't help for GTK applications and whatnot, but we can be good citizens ourselves |
605 | QLockFile lockFile(xbelPath() + QLatin1String(".lock" )); |
606 | lockFile.setStaleLockTime(0); |
607 | if (!lockFile.tryLock(timeout: 100)) { // give it 100ms |
608 | qCWarning(KIO_CORE) << "Failed to lock recently used" ; |
609 | return; |
610 | } |
611 | |
612 | if (!input.open(flags: QIODevice::ReadOnly)) { |
613 | qCWarning(KIO_CORE) << "Failed to open existing recently used" << input.errorString(); |
614 | return; |
615 | } |
616 | |
617 | QDomDocument document; |
618 | document.setContent(device: &input); |
619 | input.close(); |
620 | |
621 | auto xbelTags = document.elementsByTagName(tagname: xbelTag); |
622 | if (xbelTags.length() != 1) { |
623 | qCWarning(KIO_CORE) << "Invalid Xbel file, missing xbel element" ; |
624 | return; |
625 | } |
626 | auto xbelElement = xbelTags.item(index: 0); |
627 | auto bookmarkList = xbelElement.childNodes(); |
628 | |
629 | bool fileChanged = false; |
630 | for (int i = 0; i < bookmarkList.length(); ++i) { |
631 | const auto bookmarkNode = bookmarkList.item(index: i); |
632 | const auto infoNode = bookmarkNode.firstChild(); |
633 | if (!infoNode.isElement()) { |
634 | qCWarning(KIO_CORE) << "Invalid Xbel file, missing info element" ; |
635 | return; |
636 | } |
637 | const auto metadataElement = infoNode.firstChild(); |
638 | if (!metadataElement.isElement()) { |
639 | qCWarning(KIO_CORE) << "Invalid Xbel file, missing metadata element" ; |
640 | return; |
641 | } |
642 | |
643 | auto bookmarksElement = metadataElement.firstChildElement(tagName: applicationsBookmarkTag); |
644 | if (!bookmarksElement.isElement()) { |
645 | qCWarning(KIO_CORE) << "Invalid Xbel file, missing bookmarks element" ; |
646 | return; |
647 | } |
648 | |
649 | auto applicationList = bookmarksElement.childNodes(); |
650 | for (int i = 0; i < applicationList.length(); ++i) { |
651 | auto appNode = applicationList.item(index: i); |
652 | const auto appName = appNode.attributes().namedItem(name: nameAttribute).nodeValue(); |
653 | |
654 | if (appName == desktopEntryName) { |
655 | bookmarksElement.removeChild(oldChild: appNode); |
656 | fileChanged = true; |
657 | } |
658 | } |
659 | if (bookmarksElement.childNodes().length() == 0) { |
660 | // no more application associated with the file |
661 | xbelElement.removeChild(oldChild: bookmarkNode); |
662 | } |
663 | } |
664 | |
665 | if (fileChanged) { |
666 | if (!input.open(flags: QIODevice::WriteOnly) || (input.write(data: document.toByteArray(indent: 2)) < 0)) { |
667 | qCWarning(KIO_CORE) << "Couldn't save bookmark file " << input.fileName(); |
668 | } |
669 | } |
670 | } |
671 | |
672 | void KRecentDocument::removeBookmarksModifiedSince(const QDateTime &since) |
673 | { |
674 | QFile input(xbelPath()); |
675 | if (!input.exists()) { |
676 | return; |
677 | } |
678 | |
679 | // Won't help for GTK applications and whatnot, but we can be good citizens ourselves |
680 | QLockFile lockFile(xbelPath() + QLatin1String(".lock" )); |
681 | lockFile.setStaleLockTime(0); |
682 | if (!lockFile.tryLock(timeout: 100)) { // give it 100ms |
683 | qCWarning(KIO_CORE) << "Failed to lock recently used" ; |
684 | return; |
685 | } |
686 | |
687 | if (!input.open(flags: QIODevice::ReadOnly)) { |
688 | qCWarning(KIO_CORE) << "Failed to open existing recently used" << input.errorString(); |
689 | return; |
690 | } |
691 | |
692 | QDomDocument document; |
693 | document.setContent(device: &input); |
694 | input.close(); |
695 | |
696 | auto xbelTags = document.elementsByTagName(tagname: xbelTag); |
697 | if (xbelTags.length() != 1) { |
698 | qCWarning(KIO_CORE) << "Invalid Xbel file, missing xbel element" ; |
699 | return; |
700 | } |
701 | auto xbelElement = xbelTags.item(index: 0); |
702 | auto bookmarkList = xbelElement.childNodes(); |
703 | |
704 | bool fileChanged = false; |
705 | for (int i = 0; i < bookmarkList.length(); ++i) { |
706 | const auto node = bookmarkList.item(index: i); |
707 | const auto modifiedString = node.attributes().namedItem(name: modifiedAttribute); |
708 | const auto modifiedTime = QDateTime::fromString(string: modifiedString.nodeValue(), format: Qt::ISODate); |
709 | |
710 | if (modifiedTime >= since) { |
711 | fileChanged = true; |
712 | xbelElement.removeChild(oldChild: node); |
713 | } |
714 | } |
715 | if (fileChanged) { |
716 | if (!input.open(flags: QIODevice::WriteOnly) || (input.write(data: document.toByteArray(indent: 2)) < 0)) { |
717 | qCWarning(KIO_CORE) << "Couldn't save bookmark file " << input.fileName(); |
718 | } |
719 | } |
720 | } |
721 | |