File indexing completed on 2024-05-05 16:13:19

0001 /* -*- c++ -*-
0002     SPDX-FileCopyrightText: 2000 Daniel M. Duley <mosfet@kde.org>
0003     SPDX-FileCopyrightText: 2021 Martin Tobias Holmedahl Sandsmark
0004     SPDX-FileCopyrightText: 2022 Méven Car <meven.car@kdemail.net>
0005 
0006     SPDX-License-Identifier: BSD-2-Clause
0007 */
0008 
0009 #include "krecentdocument.h"
0010 
0011 #include "kiocoredebug.h"
0012 
0013 #ifdef Q_OS_WIN
0014 #include <sys/utime.h>
0015 #else
0016 #include <utime.h>
0017 #endif
0018 
0019 #include <KDesktopFile>
0020 #include <KService>
0021 #include <QCoreApplication>
0022 #include <QDir>
0023 #include <QLockFile>
0024 #include <QMimeDatabase>
0025 #include <QRegularExpression>
0026 #include <QSaveFile>
0027 #include <QXmlStreamWriter>
0028 #include <kio/global.h>
0029 
0030 #include <KConfigGroup>
0031 #include <KSharedConfig>
0032 
0033 static QString xbelPath()
0034 {
0035     return QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/recently-used.xbel");
0036 }
0037 
0038 static inline QString stringForRecentDocumentGroup(int val)
0039 {
0040     switch (val) {
0041     case KRecentDocument::RecentDocumentGroup::Development:
0042         return QStringLiteral("Development");
0043     case KRecentDocument::RecentDocumentGroup::Office:
0044         return QStringLiteral("Office");
0045     case KRecentDocument::RecentDocumentGroup::Database:
0046         return QStringLiteral("Database");
0047     case KRecentDocument::RecentDocumentGroup::Email:
0048         return QStringLiteral("Email");
0049     case KRecentDocument::RecentDocumentGroup::Presentation:
0050         return QStringLiteral("Presentation");
0051     case KRecentDocument::RecentDocumentGroup::Spreadsheet:
0052         return QStringLiteral("Spreadsheet");
0053     case KRecentDocument::RecentDocumentGroup::WordProcessor:
0054         return QStringLiteral("WordProcessor");
0055     case KRecentDocument::RecentDocumentGroup::Graphics:
0056         return QStringLiteral("Graphics");
0057     case KRecentDocument::RecentDocumentGroup::TextEditor:
0058         return QStringLiteral("TextEditor");
0059     case KRecentDocument::RecentDocumentGroup::Viewer:
0060         return QStringLiteral("Viewer");
0061     case KRecentDocument::RecentDocumentGroup::Archive:
0062         return QStringLiteral("Archive");
0063     case KRecentDocument::RecentDocumentGroup::Multimedia:
0064         return QStringLiteral("Multimedia");
0065     case KRecentDocument::RecentDocumentGroup::Audio:
0066         return QStringLiteral("Audio");
0067     case KRecentDocument::RecentDocumentGroup::Video:
0068         return QStringLiteral("Video");
0069     case KRecentDocument::RecentDocumentGroup::Photo:
0070         return QStringLiteral("Photo");
0071     case KRecentDocument::RecentDocumentGroup::Application:
0072         return QStringLiteral("Application");
0073     };
0074     Q_UNREACHABLE();
0075 }
0076 
0077 static KRecentDocument::RecentDocumentGroups groupsForMimeType(const QString mimeType)
0078 {
0079     // simple heuristics, feel free to expand as needed
0080     if (mimeType.startsWith(QStringLiteral("image/"))) {
0081         return KRecentDocument::RecentDocumentGroups{KRecentDocument::RecentDocumentGroup::Graphics};
0082     }
0083     if (mimeType.startsWith(QStringLiteral("video/"))) {
0084         return KRecentDocument::RecentDocumentGroups{KRecentDocument::RecentDocumentGroup::Video};
0085     }
0086     if (mimeType.startsWith(QStringLiteral("audio/"))) {
0087         return KRecentDocument::RecentDocumentGroups{KRecentDocument::RecentDocumentGroup::Audio};
0088     }
0089     return KRecentDocument::RecentDocumentGroups{};
0090 }
0091 
0092 static bool addToXbel(const QUrl &url, const QString &desktopEntryName, KRecentDocument::RecentDocumentGroups groups)
0093 {
0094     QDir().mkpath(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation));
0095 
0096     // Won't help for GTK applications and whatnot, but we can be good citizens ourselves
0097     QLockFile lockFile(xbelPath() + QLatin1String(".lock"));
0098     lockFile.setStaleLockTime(0);
0099     if (!lockFile.tryLock(100)) { // give it 100ms
0100         qCWarning(KIO_CORE) << "Failed to lock recently used";
0101         return false;
0102     }
0103 
0104     QByteArray existingContent;
0105     QFile input(xbelPath());
0106     if (input.open(QIODevice::ReadOnly)) {
0107         existingContent = input.readAll();
0108     } else if (!input.exists()) { // That it doesn't exist is a very uncommon case
0109         qCDebug(KIO_CORE) << input.fileName() << "does not exist, creating new";
0110     } else {
0111         qCWarning(KIO_CORE) << "Failed to open existing recently used" << input.errorString();
0112         return false;
0113     }
0114 
0115     // Marginally more readable to avoid all the QStringLiteral() spam below
0116     static const QLatin1String xbelTag("xbel");
0117     static const QLatin1String versionAttribute("version");
0118     static const QLatin1String expectedVersion("1.0");
0119 
0120     static const QLatin1String applicationsBookmarkTag("bookmark:applications");
0121     static const QLatin1String applicationBookmarkTag("bookmark:application");
0122     static const QLatin1String bookmarkTag("bookmark");
0123     static const QLatin1String infoTag("info");
0124     static const QLatin1String metadataTag("metadata");
0125     static const QLatin1String mimeTypeTag("mime:mime-type");
0126     static const QLatin1String bookmarkGroups("bookmark:groups");
0127     static const QLatin1String bookmarkGroup("bookmark:group");
0128 
0129     static const QLatin1String nameAttribute("name");
0130     static const QLatin1String countAttribute("count");
0131     static const QLatin1String modifiedAttribute("modified");
0132     static const QLatin1String visitedAttribute("visited");
0133     static const QLatin1String hrefAttribute("href");
0134     static const QLatin1String addedAttribute("added");
0135     static const QLatin1String execAttribute("exec");
0136     static const QLatin1String ownerAttribute("owner");
0137     static const QLatin1String ownerValue("http://freedesktop.org");
0138     static const QLatin1String typeAttribute("type");
0139 
0140     QXmlStreamReader xml(existingContent);
0141 
0142     xml.readNextStartElement();
0143     if (!existingContent.isEmpty()) {
0144         if (xml.name().isEmpty() || xml.name() != xbelTag || !xml.attributes().hasAttribute(versionAttribute)) {
0145             qCDebug(KIO_CORE) << "The recently-used.xbel is not an XBEL file, overwriting.";
0146         } else if (xml.attributes().value(versionAttribute) != expectedVersion) {
0147             qCDebug(KIO_CORE) << "The recently-used.xbel is not an XBEL version 1.0 file but has version: " << xml.attributes().value(versionAttribute)
0148                               << ", overwriting.";
0149         }
0150     }
0151 
0152     QSaveFile outputFile(xbelPath());
0153     if (!outputFile.open(QIODevice::WriteOnly)) {
0154         qCWarning(KIO_CORE) << "Failed to recently-used.xbel for writing:" << outputFile.errorString();
0155         return false;
0156     }
0157 
0158     QXmlStreamWriter output(&outputFile);
0159     output.setAutoFormatting(true);
0160     output.setAutoFormattingIndent(2);
0161     output.writeStartDocument();
0162     output.writeStartElement(xbelTag);
0163 
0164     output.writeAttribute(versionAttribute, expectedVersion);
0165     output.writeNamespace(QStringLiteral("http://www.freedesktop.org/standards/desktop-bookmarks"), QStringLiteral("bookmark"));
0166     output.writeNamespace(QStringLiteral("http://www.freedesktop.org/standards/shared-mime-info"), QStringLiteral("mime"));
0167 
0168     const QString newUrl = QString::fromLatin1(url.toEncoded());
0169     const QString currentTimestamp = QDateTime::currentDateTimeUtc().toString(Qt::ISODateWithMs).chopped(1) + QStringLiteral("000Z");
0170 
0171     auto addApplicationTag = [&output, desktopEntryName, currentTimestamp]() {
0172         output.writeEmptyElement(applicationBookmarkTag);
0173         output.writeAttribute(nameAttribute, desktopEntryName);
0174         auto service = KService::serviceByDesktopName(desktopEntryName);
0175         if (service) {
0176             output.writeAttribute(execAttribute, service->exec() + QLatin1String(" %u"));
0177         } else {
0178             output.writeAttribute(execAttribute, QCoreApplication::instance()->applicationName() + QLatin1String(" %u"));
0179         }
0180         output.writeAttribute(modifiedAttribute, currentTimestamp);
0181         output.writeAttribute(countAttribute, QStringLiteral("1"));
0182     };
0183 
0184     bool foundExistingApp = false;
0185     bool inRightBookmark = false;
0186     bool foundMatchingBookmark = false;
0187     bool firstBookmark = true;
0188     while (!xml.atEnd() && !xml.hasError()) {
0189         if (xml.readNext() == QXmlStreamReader::EndElement && xml.name() == xbelTag) {
0190             break;
0191         }
0192         switch (xml.tokenType()) {
0193         case QXmlStreamReader::StartElement: {
0194             QString tagName = xml.qualifiedName().toString();
0195             QXmlStreamAttributes attributes = xml.attributes();
0196 
0197             if (xml.name() == bookmarkTag) {
0198                 foundExistingApp = false;
0199                 firstBookmark = false;
0200 
0201                 inRightBookmark = attributes.value(hrefAttribute) == newUrl;
0202 
0203                 if (inRightBookmark) {
0204                     foundMatchingBookmark = true;
0205 
0206                     QXmlStreamAttributes newAttributes;
0207                     for (const QXmlStreamAttribute &old : attributes) {
0208                         if (old.qualifiedName() == modifiedAttribute) {
0209                             continue;
0210                         }
0211                         if (old.qualifiedName() == visitedAttribute) {
0212                             continue;
0213                         }
0214                         newAttributes.append(old);
0215                     }
0216                     newAttributes.append(modifiedAttribute, currentTimestamp);
0217                     newAttributes.append(visitedAttribute, currentTimestamp);
0218                     attributes = newAttributes;
0219                 }
0220             }
0221 
0222             if (inRightBookmark && tagName == applicationBookmarkTag && attributes.value(nameAttribute) == desktopEntryName) {
0223                 // case found right bookmark and same application
0224                 const int count = attributes.value(countAttribute).toInt();
0225 
0226                 QXmlStreamAttributes newAttributes;
0227                 for (const QXmlStreamAttribute &old : std::as_const(attributes)) {
0228                     if (old.qualifiedName() == countAttribute) {
0229                         continue;
0230                     }
0231                     if (old.qualifiedName() == modifiedAttribute) {
0232                         continue;
0233                     }
0234                     newAttributes.append(old);
0235                 }
0236                 newAttributes.append(modifiedAttribute, currentTimestamp);
0237                 newAttributes.append(countAttribute, QString::number(count + 1));
0238                 attributes = newAttributes;
0239 
0240                 foundExistingApp = true;
0241             }
0242 
0243             output.writeStartElement(tagName);
0244             output.writeAttributes(attributes);
0245             break;
0246         }
0247         case QXmlStreamReader::EndElement: {
0248             QString tagName = xml.qualifiedName().toString();
0249             if (tagName == applicationsBookmarkTag && inRightBookmark && !foundExistingApp) {
0250                 // add an application to the applications already known for the bookmark
0251                 addApplicationTag();
0252             }
0253             output.writeEndElement();
0254             break;
0255         }
0256         case QXmlStreamReader::Characters:
0257             if (xml.isCDATA()) {
0258                 output.writeCDATA(xml.text().toString());
0259             } else {
0260                 output.writeCharacters(xml.text().toString());
0261             }
0262             break;
0263         case QXmlStreamReader::Comment:
0264             output.writeComment(xml.text().toString());
0265             break;
0266         case QXmlStreamReader::EndDocument:
0267             qCWarning(KIO_CORE) << "Malformed, got end document before end of xbel" << xml.tokenString() << url;
0268             return false;
0269         default:
0270             qCWarning(KIO_CORE) << "unhandled token" << xml.tokenString() << url;
0271             break;
0272         }
0273     }
0274 
0275     if (!foundMatchingBookmark) {
0276         // must create new bookmark tag
0277         if (firstBookmark) {
0278             output.writeCharacters(QStringLiteral("\n"));
0279         }
0280         output.writeCharacters(QStringLiteral("  "));
0281         output.writeStartElement(bookmarkTag);
0282 
0283         output.writeAttribute(hrefAttribute, newUrl);
0284         output.writeAttribute(addedAttribute, currentTimestamp);
0285         output.writeAttribute(modifiedAttribute, currentTimestamp);
0286         output.writeAttribute(visitedAttribute, currentTimestamp);
0287 
0288         {
0289             QMimeDatabase mimeDb;
0290             const auto fileMime = mimeDb.mimeTypeForUrl(url).name();
0291 
0292             output.writeStartElement(infoTag);
0293             output.writeStartElement(metadataTag);
0294             output.writeAttribute(ownerAttribute, ownerValue);
0295 
0296             output.writeEmptyElement(mimeTypeTag);
0297             output.writeAttribute(typeAttribute, fileMime);
0298 
0299             // write groups metadata
0300             if (groups.isEmpty()) {
0301                 groups = groupsForMimeType(fileMime);
0302             }
0303             if (!groups.isEmpty()) {
0304                 output.writeStartElement(bookmarkGroups);
0305                 for (const auto &group : std::as_const(groups)) {
0306                     output.writeTextElement(bookmarkGroup, stringForRecentDocumentGroup(group));
0307                 }
0308                 // bookmarkGroups
0309                 output.writeEndElement();
0310             }
0311 
0312             {
0313                 output.writeStartElement(applicationsBookmarkTag);
0314                 addApplicationTag();
0315                 // end applicationsBookmarkTag
0316                 output.writeEndElement();
0317             }
0318 
0319             // end infoTag
0320             output.writeEndElement();
0321             // end metadataTag
0322             output.writeEndElement();
0323         }
0324 
0325         // end bookmarkTag
0326         output.writeEndElement();
0327     }
0328 
0329     // end xbelTag
0330     output.writeEndElement();
0331 
0332     // end document
0333     output.writeEndDocument();
0334 
0335     return outputFile.commit();
0336 }
0337 
0338 static QMap<QUrl, QDateTime> xbelRecentlyUsedList()
0339 {
0340     QMap<QUrl, QDateTime> ret;
0341     QFile input(xbelPath());
0342     if (!input.open(QIODevice::ReadOnly)) {
0343         qCWarning(KIO_CORE) << "Failed to open" << input.fileName() << input.errorString();
0344         return ret;
0345     }
0346 
0347     QXmlStreamReader xml(&input);
0348     xml.readNextStartElement();
0349     if (xml.name() != QLatin1String("xbel") || xml.attributes().value(QLatin1String("version")) != QLatin1String("1.0")) {
0350         qCWarning(KIO_CORE) << "The file is not an XBEL version 1.0 file.";
0351         return ret;
0352     }
0353 
0354     while (!xml.atEnd() && !xml.hasError()) {
0355         if (xml.readNext() != QXmlStreamReader::StartElement || xml.name() != QLatin1String("bookmark")) {
0356             continue;
0357         }
0358 
0359         const auto urlString = xml.attributes().value(QLatin1String("href"));
0360         if (urlString.isEmpty()) {
0361             qCInfo(KIO_CORE) << "Invalid bookmark in" << input.fileName();
0362             continue;
0363         }
0364         const QUrl url = QUrl::fromEncoded(urlString.toLatin1());
0365         if (url.isLocalFile() && !QFile(url.toLocalFile()).exists()) {
0366             continue;
0367         }
0368         const auto attributes = xml.attributes();
0369         const QDateTime modified = QDateTime::fromString(attributes.value(QLatin1String("modified")).toString(), Qt::ISODate);
0370         const QDateTime visited = QDateTime::fromString(attributes.value(QLatin1String("visited")).toString(), Qt::ISODate);
0371         const QDateTime added = QDateTime::fromString(attributes.value(QLatin1String("added")).toString(), Qt::ISODate);
0372         if (modified > visited && modified > added) {
0373             ret[url] = modified;
0374         } else if (visited > added) {
0375             ret[url] = visited;
0376         } else {
0377             ret[url] = added;
0378         }
0379     }
0380 
0381     if (xml.hasError()) {
0382         qCWarning(KIO_CORE) << "Failed to read" << input.fileName() << xml.errorString();
0383     }
0384 
0385     return ret;
0386 }
0387 
0388 QString KRecentDocument::recentDocumentDirectory()
0389 {
0390     // need to change this path, not sure where
0391     return QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/RecentDocuments/");
0392 }
0393 
0394 QList<QUrl> KRecentDocument::recentUrls()
0395 {
0396     QMap<QUrl, QDateTime> documents = xbelRecentlyUsedList();
0397 
0398     // TODO KF6: Revisit if we should still continue to fetch the old recentDocuments()
0399     // We need to do it to be compatible with older versions of ourselves, and
0400     // possibly others who for some reason did the same as us, but it could
0401     // possibly also be done as a one-time migration.
0402     const auto recentDocs = recentDocuments();
0403     for (const QString &pathDesktop : recentDocs) {
0404         const KDesktopFile tmpDesktopFile(pathDesktop);
0405         const QUrl url(tmpDesktopFile.readUrl());
0406         if (url.isEmpty()) {
0407             continue;
0408         }
0409         const QDateTime lastModified = QFileInfo(pathDesktop).lastModified();
0410         const QDateTime documentLastModified = documents.value(url);
0411         if (documentLastModified.isValid() && documentLastModified > lastModified) {
0412             continue;
0413         }
0414         documents[url] = lastModified;
0415     }
0416     QList<QUrl> ret = documents.keys();
0417     std::sort(ret.begin(), ret.end(), [&](const QUrl &doc1, const QUrl &doc2) {
0418         return documents.value(doc1) < documents.value(doc2);
0419     });
0420 
0421     return ret;
0422 }
0423 
0424 QStringList KRecentDocument::recentDocuments()
0425 {
0426     // TODO KF6: Consider deprecating this, also see the comment above in recentUrls()
0427     static const auto flags = QDir::Files | QDir::Readable | QDir::Hidden;
0428     QDir d(recentDocumentDirectory(), QStringLiteral("*.desktop"), QDir::Time, flags);
0429 
0430     if (!d.exists()) {
0431         d.mkdir(recentDocumentDirectory());
0432     }
0433 
0434     const QStringList list = d.entryList();
0435     QStringList fullList;
0436 
0437     for (const QString &fileName : list) {
0438         QString pathDesktop;
0439         if (fileName.startsWith(QLatin1Char(':'))) {
0440             // See: https://bugreports.qt.io/browse/QTBUG-11223
0441             pathDesktop = KRecentDocument::recentDocumentDirectory() + fileName;
0442         } else {
0443             pathDesktop = d.absoluteFilePath(fileName);
0444         }
0445         KDesktopFile tmpDesktopFile(pathDesktop);
0446         QUrl urlDesktopFile(tmpDesktopFile.desktopGroup().readPathEntry("URL", QString()));
0447         if (urlDesktopFile.isLocalFile() && !QFile(urlDesktopFile.toLocalFile()).exists()) {
0448             d.remove(pathDesktop);
0449         } else {
0450             fullList.append(pathDesktop);
0451         }
0452     }
0453 
0454     return fullList;
0455 }
0456 
0457 void KRecentDocument::add(const QUrl &url)
0458 {
0459     add(url, RecentDocumentGroups());
0460 }
0461 
0462 void KRecentDocument::add(const QUrl &url, KRecentDocument::RecentDocumentGroups groups)
0463 {
0464     // desktopFileName is in QGuiApplication but we're in KIO Core here
0465     QString desktopEntryName = QCoreApplication::instance()->property("desktopFileName").toString();
0466     if (desktopEntryName.isEmpty()) {
0467         desktopEntryName = QCoreApplication::applicationName();
0468     }
0469     add(url, desktopEntryName, groups);
0470 }
0471 
0472 void KRecentDocument::add(const QUrl &url, const QString &desktopEntryName)
0473 {
0474     add(url, desktopEntryName, RecentDocumentGroups());
0475 }
0476 
0477 void KRecentDocument::add(const QUrl &url, const QString &desktopEntryName, KRecentDocument::RecentDocumentGroups groups)
0478 {
0479     if (url.isLocalFile() && url.toLocalFile().startsWith(QDir::tempPath())) {
0480         return; // inside tmp resource, do not save
0481     }
0482 
0483     if (!addToXbel(url, desktopEntryName, groups)) {
0484         qCWarning(KIO_CORE) << "Failed to add to recently used bookmark file";
0485     }
0486 
0487     QString openStr = url.toDisplayString();
0488     openStr.replace(QRegularExpression(QStringLiteral("\\$")), QStringLiteral("$$")); // Desktop files with type "Link" are $-variable expanded
0489 
0490     // qDebug() << "KRecentDocument::add for " << openStr;
0491     KConfigGroup config = KSharedConfig::openConfig()->group(QByteArray("RecentDocuments"));
0492     bool useRecent = config.readEntry(QStringLiteral("UseRecent"), true);
0493     int maxEntries = config.readEntry(QStringLiteral("MaxEntries"), 10);
0494     bool ignoreHidden = config.readEntry(QStringLiteral("IgnoreHidden"), true);
0495 
0496     if (!useRecent || maxEntries <= 0) {
0497         return;
0498     }
0499     if (ignoreHidden && QRegularExpression(QStringLiteral("/\\.")).match(url.toLocalFile()).hasMatch()) {
0500         return;
0501     }
0502 
0503     const QString path = recentDocumentDirectory();
0504     const QString fileName = url.fileName();
0505     // don't create a file called ".desktop", it will lead to an empty name in kio_recentdocuments
0506     const QString dStr = path + (fileName.isEmpty() ? QStringLiteral("unnamed") : fileName);
0507 
0508     QString ddesktop = dStr + QLatin1String(".desktop");
0509 
0510     int i = 1;
0511     // check for duplicates
0512     while (QFile::exists(ddesktop)) {
0513         // see if it points to the same file and application
0514         KDesktopFile tmp(ddesktop);
0515         if (tmp.desktopGroup().readPathEntry("URL", QString()) == url.toDisplayString()
0516             && tmp.desktopGroup().readEntry("X-KDE-LastOpenedWith") == desktopEntryName) {
0517             // Set access and modification time to current time
0518             ::utime(QFile::encodeName(ddesktop).constData(), nullptr);
0519             return;
0520         }
0521         // if not append a (num) to it
0522         ++i;
0523         if (i > maxEntries) {
0524             break;
0525         }
0526         ddesktop = dStr + QStringLiteral("[%1].desktop").arg(i);
0527     }
0528 
0529     QDir dir(path);
0530     // check for max entries, delete oldest files if exceeded
0531     const QStringList list = dir.entryList(QDir::Files | QDir::Hidden, QFlags<QDir::SortFlag>(QDir::Time | QDir::Reversed));
0532     i = list.count();
0533     if (i > maxEntries - 1) {
0534         QStringList::ConstIterator it;
0535         it = list.begin();
0536         while (i > maxEntries - 1) {
0537             QFile::remove(dir.absolutePath() + QLatin1Char('/') + (*it));
0538             --i;
0539             ++it;
0540         }
0541     }
0542 
0543     // create the applnk
0544     KDesktopFile configFile(ddesktop);
0545     KConfigGroup conf = configFile.desktopGroup();
0546     conf.writeEntry("Type", QStringLiteral("Link"));
0547     conf.writePathEntry("URL", openStr);
0548     // If you change the line below, change the test in the above loop
0549     conf.writeEntry("X-KDE-LastOpenedWith", desktopEntryName);
0550     conf.writeEntry("Name", url.fileName());
0551     conf.writeEntry("Icon", KIO::iconNameForUrl(url));
0552 }
0553 
0554 void KRecentDocument::clear()
0555 {
0556     const QStringList list = recentDocuments();
0557     QDir dir;
0558     for (const QString &desktopFilePath : list) {
0559         dir.remove(desktopFilePath);
0560     }
0561     QFile(xbelPath()).remove();
0562 }
0563 
0564 int KRecentDocument::maximumItems()
0565 {
0566     KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("RecentDocuments"));
0567     return cg.readEntry(QStringLiteral("MaxEntries"), 10);
0568 }