KIO

krecentdocument.cpp
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
25static QString xbelPath()
26{
28}
29
30static inline QString stringForRecentDocumentGroup(int val)
31{
32 switch (val) {
33 case KRecentDocument::RecentDocumentGroup::Development:
34 return QStringLiteral("Development");
35 case KRecentDocument::RecentDocumentGroup::Office:
36 return QStringLiteral("Office");
37 case KRecentDocument::RecentDocumentGroup::Database:
38 return QStringLiteral("Database");
39 case KRecentDocument::RecentDocumentGroup::Email:
40 return QStringLiteral("Email");
41 case KRecentDocument::RecentDocumentGroup::Presentation:
42 return QStringLiteral("Presentation");
43 case KRecentDocument::RecentDocumentGroup::Spreadsheet:
44 return QStringLiteral("Spreadsheet");
45 case KRecentDocument::RecentDocumentGroup::WordProcessor:
46 return QStringLiteral("WordProcessor");
47 case KRecentDocument::RecentDocumentGroup::Graphics:
48 return QStringLiteral("Graphics");
49 case KRecentDocument::RecentDocumentGroup::TextEditor:
50 return QStringLiteral("TextEditor");
51 case KRecentDocument::RecentDocumentGroup::Viewer:
52 return QStringLiteral("Viewer");
53 case KRecentDocument::RecentDocumentGroup::Archive:
54 return QStringLiteral("Archive");
55 case KRecentDocument::RecentDocumentGroup::Multimedia:
56 return QStringLiteral("Multimedia");
57 case KRecentDocument::RecentDocumentGroup::Audio:
58 return QStringLiteral("Audio");
59 case KRecentDocument::RecentDocumentGroup::Video:
60 return QStringLiteral("Video");
61 case KRecentDocument::RecentDocumentGroup::Photo:
62 return QStringLiteral("Photo");
63 case KRecentDocument::RecentDocumentGroup::Application:
64 return QStringLiteral("Application");
65 };
66 Q_UNREACHABLE();
67}
68
69static KRecentDocument::RecentDocumentGroups groupsForMimeType(const QString mimeType)
70{
71 // simple heuristics, feel free to expand as needed
72 if (mimeType.startsWith(QStringLiteral("image/"))) {
73 return KRecentDocument::RecentDocumentGroups{KRecentDocument::RecentDocumentGroup::Graphics};
74 }
75 if (mimeType.startsWith(QStringLiteral("video/"))) {
76 return KRecentDocument::RecentDocumentGroups{KRecentDocument::RecentDocumentGroup::Video};
77 }
78 if (mimeType.startsWith(QStringLiteral("audio/"))) {
79 return KRecentDocument::RecentDocumentGroups{KRecentDocument::RecentDocumentGroup::Audio};
80 }
82}
83
84// Marginally more readable to avoid all the QStringLiteral() spam below
85static const QLatin1String xbelTag("xbel");
86static const QLatin1String versionAttribute("version");
87static const QLatin1String expectedVersion("1.0");
88
89static const QLatin1String applicationsBookmarkTag("bookmark:applications");
90static const QLatin1String applicationBookmarkTag("bookmark:application");
91static const QLatin1String bookmarkTag("bookmark");
92static const QLatin1String infoTag("info");
93static const QLatin1String metadataTag("metadata");
94static const QLatin1String mimeTypeTag("mime:mime-type");
95static const QLatin1String bookmarkGroups("bookmark:groups");
96static const QLatin1String bookmarkGroup("bookmark:group");
97
98static const QLatin1String nameAttribute("name");
99static const QLatin1String countAttribute("count");
100static const QLatin1String modifiedAttribute("modified");
101static const QLatin1String visitedAttribute("visited");
102static const QLatin1String hrefAttribute("href");
103static const QLatin1String addedAttribute("added");
104static const QLatin1String execAttribute("exec");
105static const QLatin1String ownerAttribute("owner");
106static const QLatin1String ownerValue("http://freedesktop.org");
107static const QLatin1String typeAttribute("type");
108
109static bool removeOldestEntries(int &maxEntries)
110{
111 QFile input(xbelPath());
112 if (!input.exists()) {
113 return true;
114 }
115
116 // Won't help for GTK applications and whatnot, but we can be good citizens ourselves
117 QLockFile lockFile(xbelPath() + QLatin1String(".lock"));
118 lockFile.setStaleLockTime(0);
119 if (!lockFile.tryLock(100)) { // give it 100ms
120 qCWarning(KIO_CORE) << "Failed to lock recently used";
121 return false;
122 }
123
124 if (!input.open(QIODevice::ReadOnly)) {
125 qCWarning(KIO_CORE) << "Failed to open existing recently used" << input.errorString();
126 return false;
127 }
128
129 QDomDocument document;
130 document.setContent(&input);
131 input.close();
132
133 auto xbelTags = document.elementsByTagName(xbelTag);
134 if (xbelTags.length() != 1) {
135 qCWarning(KIO_CORE) << "Invalid Xbel file" << input.errorString();
136 return false;
137 }
138 auto xbelElement = document.elementsByTagName(xbelTag).item(0);
139 auto bookmarkList = xbelElement.childNodes();
140 if (bookmarkList.length() <= maxEntries) {
141 return true;
142 }
143
144 QMultiMap<QDateTime, QDomNode> bookmarksByModifiedDate;
145 for (int i = 0; i < bookmarkList.length(); ++i) {
146 const auto node = bookmarkList.item(i);
147 const auto modifiedString = node.attributes().namedItem(modifiedAttribute);
148 const auto modifiedTime = QDateTime::fromString(modifiedString.nodeValue(), Qt::ISODate);
149
150 bookmarksByModifiedDate.insert(modifiedTime, node);
151 }
152
153 int i = 0;
154 // entries are traversed in ascending key order
155 for (auto entry = bookmarksByModifiedDate.keyValueBegin(); entry != bookmarksByModifiedDate.keyValueEnd(); ++entry) {
156 // only keep the maxEntries last nodes
157 if (bookmarksByModifiedDate.size() - i > maxEntries) {
158 xbelElement.removeChild(entry->second);
159 }
160 ++i;
161 }
162
163 if (input.open(QIODevice::WriteOnly) && input.write(document.toByteArray(2)) != -1) {
164 input.close();
165 return true;
166 }
167 input.close();
168 return false;
169}
170
171static bool addToXbel(const QUrl &url, const QString &desktopEntryName, KRecentDocument::RecentDocumentGroups groups, int maxEntries, bool ignoreHidden)
172{
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(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(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(versionAttribute)) {
202 qCDebug(KIO_CORE) << "The recently-used.xbel is not an XBEL file, overwriting.";
203 } else if (xml.attributes().value(versionAttribute) != expectedVersion) {
204 qCDebug(KIO_CORE) << "The recently-used.xbel is not an XBEL version 1.0 file but has version: " << xml.attributes().value(versionAttribute)
205 << ", overwriting.";
206 }
207 }
208
209 QSaveFile outputFile(xbelPath());
210 if (!outputFile.open(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(xbelTag);
220
221 output.writeAttribute(versionAttribute, expectedVersion);
222 output.writeNamespace(QStringLiteral("http://www.freedesktop.org/standards/desktop-bookmarks"), QStringLiteral("bookmark"));
223 output.writeNamespace(QStringLiteral("http://www.freedesktop.org/standards/shared-mime-info"), QStringLiteral("mime"));
224
225 const QString newUrl = QString::fromLatin1(url.toEncoded());
226 const QString currentTimestamp = QDateTime::currentDateTimeUtc().toString(Qt::ISODateWithMs).chopped(1) + QStringLiteral("000Z");
227
228 auto addApplicationTag = [&output, desktopEntryName, currentTimestamp, url]() {
229 output.writeEmptyElement(applicationBookmarkTag);
230 output.writeAttribute(nameAttribute, desktopEntryName);
231 auto service = KService::serviceByDesktopName(desktopEntryName);
232 QString exec;
233 bool shouldAddParameter = true;
234 if (service) {
235 exec = service->exec();
236 exec.replace(QLatin1String(" %U"), QLatin1String(" %u"));
237 exec.replace(QLatin1String(" %F"), QLatin1String(" %f"));
238 shouldAddParameter = !exec.contains(QLatin1String(" %u")) && !exec.contains(QLatin1String(" %f"));
239 } else {
241 }
242 if (shouldAddParameter) {
243 if (url.isLocalFile()) {
244 exec += QLatin1String(" %f");
245 } else {
246 exec += QLatin1String(" %u");
247 }
248 }
249 output.writeAttribute(execAttribute, exec);
250 output.writeAttribute(modifiedAttribute, currentTimestamp);
251 output.writeAttribute(countAttribute, QStringLiteral("1"));
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()) {
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(hrefAttribute);
273 inRightBookmark = hrefValue == newUrl;
274
275 // remove hidden files if some were added by GTK
276 if (ignoreHidden && hrefValue.contains(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(old);
293 }
294 newAttributes.append(modifiedAttribute, currentTimestamp);
295 newAttributes.append(visitedAttribute, currentTimestamp);
296 attributes = newAttributes;
297 }
298
299 nbEntries += 1;
300 }
301
302 else if (inRightBookmark && tagName == applicationBookmarkTag && attributes.value(nameAttribute) == desktopEntryName) {
303 // case found right bookmark and same application
304 const int count = attributes.value(countAttribute).toInt();
305
306 QXmlStreamAttributes newAttributes;
307 for (const QXmlStreamAttribute &old : std::as_const(attributes)) {
308 if (old.name() == countAttribute) {
309 continue;
310 }
311 if (old.name() == modifiedAttribute) {
312 continue;
313 }
314 newAttributes.append(old);
315 }
316 newAttributes.append(modifiedAttribute, currentTimestamp);
317 newAttributes.append(countAttribute, QString::number(count + 1));
318 attributes = newAttributes;
319
320 foundExistingApp = true;
321 }
322
323 output.writeStartElement(tagName.toString());
324 output.writeAttributes(attributes);
325 break;
326 }
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 }
337 if (xml.isCDATA()) {
338 output.writeCDATA(xml.text().toString());
339 } else {
340 output.writeCharacters(xml.text().toString());
341 }
342 break;
344 output.writeComment(xml.text().toString());
345 break;
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(QStringLiteral("\n"));
359 }
360 output.writeCharacters(QStringLiteral(" "));
361 output.writeStartElement(bookmarkTag);
362
363 output.writeAttribute(hrefAttribute, newUrl);
364 output.writeAttribute(addedAttribute, currentTimestamp);
365 output.writeAttribute(modifiedAttribute, currentTimestamp);
366 output.writeAttribute(visitedAttribute, currentTimestamp);
367
368 {
369 QMimeDatabase mimeDb;
370 const auto fileMime = mimeDb.mimeTypeForUrl(url).name();
371
372 output.writeStartElement(infoTag);
373 output.writeStartElement(metadataTag);
374 output.writeAttribute(ownerAttribute, ownerValue);
375
376 output.writeEmptyElement(mimeTypeTag);
377 output.writeAttribute(typeAttribute, fileMime);
378
379 // write groups metadata
380 if (groups.isEmpty()) {
381 groups = groupsForMimeType(fileMime);
382 }
383 if (!groups.isEmpty()) {
384 output.writeStartElement(bookmarkGroups);
385 for (const auto &group : std::as_const(groups)) {
386 output.writeTextElement(bookmarkGroup, stringForRecentDocumentGroup(group));
387 }
388 // bookmarkGroups
389 output.writeEndElement();
390 }
391
392 {
393 output.writeStartElement(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
423static QMap<QUrl, QDateTime> xbelRecentlyUsedList()
424{
426 QFile input(xbelPath());
427 if (!input.open(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(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(QLatin1String("href"));
445 if (urlString.isEmpty()) {
446 qCInfo(KIO_CORE) << "Invalid bookmark in" << input.fileName();
447 continue;
448 }
449 const QUrl url = QUrl::fromEncoded(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(attributes.value(QLatin1String("modified")).toString(), Qt::ISODate);
455 const QDateTime visited = QDateTime::fromString(attributes.value(QLatin1String("visited")).toString(), Qt::ISODate);
456 const QDateTime added = QDateTime::fromString(attributes.value(QLatin1String("added")).toString(), 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
474{
475 QMap<QUrl, QDateTime> documents = xbelRecentlyUsedList();
476
477 QList<QUrl> ret = documents.keys();
478 std::sort(ret.begin(), ret.end(), [&](const QUrl &doc1, const QUrl &doc2) {
479 return documents.value(doc1) < documents.value(doc2);
480 });
481
482 return ret;
483}
484
486{
488}
489
491{
492 // desktopFileName is in QGuiApplication but we're in KIO Core here
493 QString desktopEntryName = QCoreApplication::instance()->property("desktopFileName").toString();
494 if (desktopEntryName.isEmpty()) {
495 desktopEntryName = QCoreApplication::applicationName();
496 }
497 add(url, desktopEntryName, groups);
498}
499
500void KRecentDocument::add(const QUrl &url, const QString &desktopEntryName)
501{
502 add(url, desktopEntryName, RecentDocumentGroups());
503}
504
505void KRecentDocument::add(const QUrl &url, const QString &desktopEntryName, KRecentDocument::RecentDocumentGroups groups)
506{
507 if (url.isLocalFile() && url.toLocalFile().startsWith(QDir::tempPath())) {
508 return; // inside tmp resource, do not save
509 }
510
511 // qDebug() << "KRecentDocument::add for " << openStr;
512 KConfigGroup config = KSharedConfig::openConfig()->group(QStringLiteral("RecentDocuments"));
513 bool useRecent = config.readEntry(QStringLiteral("UseRecent"), true);
514 int maxEntries = config.readEntry(QStringLiteral("MaxEntries"), 300);
515 bool ignoreHidden = config.readEntry(QStringLiteral("IgnoreHidden"), true);
516
517 if (!useRecent || maxEntries == 0) {
518 clear();
519 return;
520 }
521 if (ignoreHidden && url.toLocalFile().contains(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
531{
532 QFile(xbelPath()).remove();
533}
534
536{
537 KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("RecentDocuments"));
538 return cg.readEntry(QStringLiteral("MaxEntries"), 10);
539}
KConfigGroup group(const QString &group)
QString readEntry(const char *key, const char *aDefault=nullptr) const
static int maximumItems()
Returns the maximum amount of recent document entries allowed.
static void clear()
Clear the recent document menu of all entries.
static void add(const QUrl &url)
Add a new item to the Recent Document menu.
static QList< QUrl > recentUrls()
Return a list of recent URLs.
static Ptr serviceByDesktopName(const QString &_name)
static KSharedConfig::Ptr openConfig(const QString &fileName=QString(), OpenFlags mode=FullConfig, QStandardPaths::StandardLocation type=QStandardPaths::GenericConfigLocation)
char * toString(const EngineQuery &query)
KCALUTILS_EXPORT QString mimeType()
KIOCORE_EXPORT MkpathJob * mkpath(const QUrl &url, const QUrl &baseUrl=QUrl(), JobFlags flags=DefaultFlags)
Creates a directory, creating parent directories as needed.
bool isEmpty() const const
QCoreApplication * instance()
QDateTime currentDateTimeUtc()
QDateTime fromString(QStringView string, QStringView format, QCalendar cal)
QString toString(QStringView format, QCalendar cal) const const
QString tempPath()
QDomNodeList elementsByTagName(const QString &tagname) const const
ParseResult setContent(QAnyStringView text, ParseOptions options)
QByteArray toByteArray(int indent) const const
QDomNodeList childNodes() const const
QDomNode item(int index) const const
bool exists(const QString &fileName)
bool remove()
iterator begin()
iterator end()
bool isEmpty() const const
QList< Key > keys() const const
T value(const Key &key, const T &defaultValue) const const
QMimeType mimeTypeForUrl(const QUrl &url) const const
iterator insert(const Key &key, const T &value)
key_value_iterator keyValueBegin()
key_value_iterator keyValueEnd()
size_type size() const const
QVariant property(const char *name) const const
QString writableLocation(StandardLocation type)
QString chopped(qsizetype len) const const
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
QString fromLatin1(QByteArrayView str)
bool isEmpty() const const
QString number(double n, char format, int precision)
QString & replace(QChar before, QChar after, Qt::CaseSensitivity cs)
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
bool contains(QChar c, Qt::CaseSensitivity cs) const const
int toInt(bool *ok, int base) const const
QString toString() const const
QUrl fromEncoded(const QByteArray &input, ParsingMode parsingMode)
bool isLocalFile() const const
QByteArray toEncoded(FormattingOptions options) const const
QString toLocalFile() const const
QString toString() const const
void append(const QString &namespaceUri, const QString &name, const QString &value)
QStringView value(QAnyStringView namespaceUri, QAnyStringView name) const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:18:51 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.