File indexing completed on 2024-04-21 03:52:37

0001 // -*- c-basic-offset:4; indent-tabs-mode:nil -*-
0002 /*
0003     This file is part of the KDE libraries
0004     SPDX-FileCopyrightText: 2000 David Faure <faure@kde.org>
0005     SPDX-FileCopyrightText: 2003 Alexander Kellett <lypanov@kde.org>
0006     SPDX-FileCopyrightText: 2008 Norbert Frese <nf2@scheinwelt.at>
0007 
0008     SPDX-License-Identifier: LGPL-2.0-only
0009 */
0010 
0011 #include "kbookmarkmanager.h"
0012 #include "kbookmarks_debug.h"
0013 
0014 #include <QDir>
0015 #include <QFile>
0016 #include <QFileInfo>
0017 #include <QRegularExpression>
0018 #include <QSaveFile>
0019 #include <QStandardPaths>
0020 #include <QTextStream>
0021 
0022 #include <KBackup>
0023 #include <KConfig>
0024 #include <KConfigGroup>
0025 #include <KDirWatch>
0026 
0027 namespace
0028 {
0029 namespace Strings
0030 {
0031 QString piData()
0032 {
0033     return QStringLiteral("version=\"1.0\" encoding=\"UTF-8\"");
0034 }
0035 }
0036 }
0037 
0038 class KBookmarkMap : private KBookmarkGroupTraverser
0039 {
0040 public:
0041     KBookmarkMap()
0042         : m_mapNeedsUpdate(true)
0043     {
0044     }
0045     void setNeedsUpdate()
0046     {
0047         m_mapNeedsUpdate = true;
0048     }
0049     void update(KBookmarkManager *);
0050     QList<KBookmark> find(const QString &url) const
0051     {
0052         return m_bk_map.value(url);
0053     }
0054 
0055 private:
0056     void visit(const KBookmark &) override;
0057     void visitEnter(const KBookmarkGroup &) override
0058     {
0059         ;
0060     }
0061     void visitLeave(const KBookmarkGroup &) override
0062     {
0063         ;
0064     }
0065 
0066 private:
0067     typedef QList<KBookmark> KBookmarkList;
0068     QMap<QString, KBookmarkList> m_bk_map;
0069     bool m_mapNeedsUpdate;
0070 };
0071 
0072 void KBookmarkMap::update(KBookmarkManager *manager)
0073 {
0074     if (m_mapNeedsUpdate) {
0075         m_mapNeedsUpdate = false;
0076 
0077         m_bk_map.clear();
0078         KBookmarkGroup root = manager->root();
0079         traverse(root);
0080     }
0081 }
0082 
0083 void KBookmarkMap::visit(const KBookmark &bk)
0084 {
0085     if (!bk.isSeparator()) {
0086         // add bookmark to url map
0087         m_bk_map[bk.internalElement().attribute(QStringLiteral("href"))].append(bk);
0088     }
0089 }
0090 
0091 // #########################
0092 // KBookmarkManagerPrivate
0093 class KBookmarkManagerPrivate
0094 {
0095 public:
0096     KBookmarkManagerPrivate(bool bDocIsloaded)
0097         : m_doc(QStringLiteral("xbel"))
0098         , m_docIsLoaded(bDocIsloaded)
0099         , m_dirWatch(nullptr)
0100     {
0101     }
0102 
0103     mutable QDomDocument m_doc;
0104     mutable QDomDocument m_toolbarDoc;
0105     QString m_bookmarksFile;
0106     mutable bool m_docIsLoaded;
0107 
0108     KDirWatch *m_dirWatch; // for monitoring changes on bookmark files
0109 
0110     KBookmarkMap m_map;
0111 };
0112 
0113 // ################
0114 // KBookmarkManager
0115 
0116 static QDomElement createXbelTopLevelElement(QDomDocument &doc)
0117 {
0118     QDomElement topLevel = doc.createElement(QStringLiteral("xbel"));
0119     topLevel.setAttribute(QStringLiteral("xmlns:mime"), QStringLiteral("http://www.freedesktop.org/standards/shared-mime-info"));
0120     topLevel.setAttribute(QStringLiteral("xmlns:bookmark"), QStringLiteral("http://www.freedesktop.org/standards/desktop-bookmarks"));
0121     topLevel.setAttribute(QStringLiteral("xmlns:kdepriv"), QStringLiteral("http://www.kde.org/kdepriv"));
0122     doc.appendChild(topLevel);
0123     doc.insertBefore(doc.createProcessingInstruction(QStringLiteral("xml"), Strings::piData()), topLevel);
0124     return topLevel;
0125 }
0126 
0127 KBookmarkManager::KBookmarkManager(const QString &bookmarksFile, QObject *parent)
0128     : QObject(parent)
0129     , d(new KBookmarkManagerPrivate(false))
0130 {
0131     Q_ASSERT(!bookmarksFile.isEmpty());
0132     d->m_bookmarksFile = bookmarksFile;
0133 
0134     if (!QFile::exists(d->m_bookmarksFile)) {
0135         createXbelTopLevelElement(d->m_doc);
0136     } else {
0137         parse();
0138     }
0139     d->m_docIsLoaded = true;
0140 
0141     // start KDirWatch
0142     KDirWatch::self()->addFile(d->m_bookmarksFile);
0143     QObject::connect(KDirWatch::self(), &KDirWatch::dirty, this, &KBookmarkManager::slotFileChanged);
0144     QObject::connect(KDirWatch::self(), &KDirWatch::created, this, &KBookmarkManager::slotFileChanged);
0145     QObject::connect(KDirWatch::self(), &KDirWatch::deleted, this, &KBookmarkManager::slotFileChanged);
0146 
0147     // qCDebug(KBOOKMARKS_LOG) << "starting KDirWatch for" << d->m_bookmarksFile;
0148 }
0149 
0150 void KBookmarkManager::slotFileChanged(const QString &path)
0151 {
0152     if (path == d->m_bookmarksFile) {
0153         // qCDebug(KBOOKMARKS_LOG) << "file changed (KDirWatch) " << path ;
0154         // Reparse
0155         parse();
0156         // Tell our GUI
0157         // (emit where group is "" to directly mark the root menu as dirty)
0158         Q_EMIT changed(QLatin1String(""));
0159     }
0160 }
0161 
0162 KBookmarkManager::~KBookmarkManager()
0163 {
0164 }
0165 
0166 QDomDocument KBookmarkManager::internalDocument() const
0167 {
0168     if (!d->m_docIsLoaded) {
0169         parse();
0170         d->m_toolbarDoc.clear();
0171     }
0172     return d->m_doc;
0173 }
0174 
0175 void KBookmarkManager::parse() const
0176 {
0177     d->m_docIsLoaded = true;
0178     // qCDebug(KBOOKMARKS_LOG) << "KBookmarkManager::parse " << d->m_bookmarksFile;
0179     QFile file(d->m_bookmarksFile);
0180     if (!file.open(QIODevice::ReadOnly)) {
0181         qCWarning(KBOOKMARKS_LOG) << "Can't open" << d->m_bookmarksFile;
0182         d->m_doc = QDomDocument(QStringLiteral("xbel"));
0183         createXbelTopLevelElement(d->m_doc);
0184         return;
0185     }
0186     d->m_doc = QDomDocument(QStringLiteral("xbel"));
0187     d->m_doc.setContent(&file);
0188 
0189     if (d->m_doc.documentElement().isNull()) {
0190         qCWarning(KBOOKMARKS_LOG) << "KBookmarkManager::parse : main tag is missing, creating default " << d->m_bookmarksFile;
0191         QDomElement element = d->m_doc.createElement(QStringLiteral("xbel"));
0192         d->m_doc.appendChild(element);
0193     }
0194 
0195     QDomElement docElem = d->m_doc.documentElement();
0196 
0197     QString mainTag = docElem.tagName();
0198     if (mainTag != QLatin1String("xbel")) {
0199         qCWarning(KBOOKMARKS_LOG) << "KBookmarkManager::parse : unknown main tag " << mainTag;
0200     }
0201 
0202     QDomNode n = d->m_doc.documentElement().previousSibling();
0203     if (n.isProcessingInstruction()) {
0204         QDomProcessingInstruction pi = n.toProcessingInstruction();
0205         pi.parentNode().removeChild(pi);
0206     }
0207 
0208     QDomProcessingInstruction pi;
0209     pi = d->m_doc.createProcessingInstruction(QStringLiteral("xml"), Strings::piData());
0210     d->m_doc.insertBefore(pi, docElem);
0211 
0212     file.close();
0213 
0214     d->m_map.setNeedsUpdate();
0215 }
0216 
0217 bool KBookmarkManager::save(bool toolbarCache) const
0218 {
0219     return saveAs(d->m_bookmarksFile, toolbarCache);
0220 }
0221 
0222 bool KBookmarkManager::saveAs(const QString &filename, bool toolbarCache) const
0223 {
0224     // qCDebug(KBOOKMARKS_LOG) << "KBookmarkManager::save " << filename;
0225 
0226     // Save the bookmark toolbar folder for quick loading
0227     // but only when it will actually make things quicker
0228     const QString cacheFilename = filename + QLatin1String(".tbcache");
0229     if (toolbarCache && !root().isToolbarGroup()) {
0230         QSaveFile cacheFile(cacheFilename);
0231         if (cacheFile.open(QIODevice::WriteOnly)) {
0232             QString str;
0233             QTextStream stream(&str, QIODevice::WriteOnly);
0234             stream << root().findToolbar();
0235             const QByteArray cstr = str.toUtf8();
0236             cacheFile.write(cstr.data(), cstr.length());
0237             cacheFile.commit();
0238         }
0239     } else { // remove any (now) stale cache
0240         QFile::remove(cacheFilename);
0241     }
0242 
0243     // Create parent dirs
0244     QFileInfo info(filename);
0245     QDir().mkpath(info.absolutePath());
0246 
0247     QSaveFile file(filename);
0248     if (file.open(QIODevice::WriteOnly)) {
0249         KBackup::simpleBackupFile(file.fileName(), QString(), QStringLiteral(".bak"));
0250         QTextStream stream(&file);
0251         // In Qt6 it's UTF-8 by default
0252         stream << internalDocument().toString();
0253         stream.flush();
0254         if (file.commit()) {
0255             return true;
0256         }
0257     }
0258 
0259     QString err = tr("Unable to save bookmarks in %1. Reported error was: %2. "
0260                      "This error message will only be shown once. The cause "
0261                      "of the error needs to be fixed as quickly as possible, "
0262                      "which is most likely a full hard drive.")
0263                       .arg(filename, file.errorString());
0264     qCCritical(KBOOKMARKS_LOG) << QStringLiteral("Unable to save bookmarks in %1. File reported the following error-code: %2.").arg(filename).arg(file.error());
0265     Q_EMIT const_cast<KBookmarkManager *>(this)->error(err);
0266 
0267     return false;
0268 }
0269 
0270 QString KBookmarkManager::path() const
0271 {
0272     return d->m_bookmarksFile;
0273 }
0274 
0275 KBookmarkGroup KBookmarkManager::root() const
0276 {
0277     return KBookmarkGroup(internalDocument().documentElement());
0278 }
0279 
0280 KBookmarkGroup KBookmarkManager::toolbar()
0281 {
0282     // qCDebug(KBOOKMARKS_LOG) << "KBookmarkManager::toolbar begin";
0283     // Only try to read from a toolbar cache if the full document isn't loaded
0284     if (!d->m_docIsLoaded) {
0285         // qCDebug(KBOOKMARKS_LOG) << "KBookmarkManager::toolbar trying cache";
0286         const QString cacheFilename = d->m_bookmarksFile + QLatin1String(".tbcache");
0287         QFileInfo bmInfo(d->m_bookmarksFile);
0288         QFileInfo cacheInfo(cacheFilename);
0289         if (d->m_toolbarDoc.isNull() && QFile::exists(cacheFilename) && bmInfo.lastModified() < cacheInfo.lastModified()) {
0290             // qCDebug(KBOOKMARKS_LOG) << "KBookmarkManager::toolbar reading file";
0291             QFile file(cacheFilename);
0292 
0293             if (file.open(QIODevice::ReadOnly)) {
0294                 d->m_toolbarDoc = QDomDocument(QStringLiteral("cache"));
0295                 d->m_toolbarDoc.setContent(&file);
0296                 // qCDebug(KBOOKMARKS_LOG) << "KBookmarkManager::toolbar opened";
0297             }
0298         }
0299         if (!d->m_toolbarDoc.isNull()) {
0300             // qCDebug(KBOOKMARKS_LOG) << "KBookmarkManager::toolbar returning element";
0301             QDomElement elem = d->m_toolbarDoc.firstChild().toElement();
0302             return KBookmarkGroup(elem);
0303         }
0304     }
0305 
0306     // Fallback to the normal way if there is no cache or if the bookmark file
0307     // is already loaded
0308     QDomElement elem = root().findToolbar();
0309     if (elem.isNull()) {
0310         // Root is the bookmark toolbar if none has been set.
0311         // Make it explicit to speed up invocations of findToolbar()
0312         root().internalElement().setAttribute(QStringLiteral("toolbar"), QStringLiteral("yes"));
0313         return root();
0314     } else {
0315         return KBookmarkGroup(elem);
0316     }
0317 }
0318 
0319 KBookmark KBookmarkManager::findByAddress(const QString &address)
0320 {
0321     // qCDebug(KBOOKMARKS_LOG) << "KBookmarkManager::findByAddress " << address;
0322     KBookmark result = root();
0323     // The address is something like /5/10/2+
0324     static const QRegularExpression separator(QStringLiteral("[/+]"));
0325     const QStringList addresses = address.split(separator, Qt::SkipEmptyParts);
0326     // qCWarning(KBOOKMARKS_LOG) << addresses.join(",");
0327     for (QStringList::const_iterator it = addresses.begin(); it != addresses.end();) {
0328         bool append = ((*it) == QLatin1String("+"));
0329         uint number = (*it).toUInt();
0330         Q_ASSERT(result.isGroup());
0331         KBookmarkGroup group = result.toGroup();
0332         KBookmark bk = group.first();
0333         KBookmark lbk = bk; // last non-null bookmark
0334         for (uint i = 0; ((i < number) || append) && !bk.isNull(); ++i) {
0335             lbk = bk;
0336             bk = group.next(bk);
0337             // qCWarning(KBOOKMARKS_LOG) << i;
0338         }
0339         it++;
0340         // qCWarning(KBOOKMARKS_LOG) << "found section";
0341         result = bk;
0342     }
0343     if (result.isNull()) {
0344         qCWarning(KBOOKMARKS_LOG) << "KBookmarkManager::findByAddress: couldn't find item " << address;
0345     }
0346     // qCWarning(KBOOKMARKS_LOG) << "found " << result.address();
0347     return result;
0348 }
0349 
0350 void KBookmarkManager::emitChanged()
0351 {
0352     emitChanged(root());
0353 }
0354 
0355 void KBookmarkManager::emitChanged(const KBookmarkGroup &group)
0356 {
0357     (void)save(); // KDE5 TODO: emitChanged should return a bool? Maybe rename it to saveAndEmitChanged?
0358 
0359     // Tell the other processes too
0360     // qCDebug(KBOOKMARKS_LOG) << "KBookmarkManager::emitChanged : broadcasting change " << group.address();
0361 
0362     Q_EMIT changed(group.address());
0363 }
0364 
0365 ///////
0366 bool KBookmarkManager::updateAccessMetadata(const QString &url)
0367 {
0368     d->m_map.update(this);
0369     QList<KBookmark> list = d->m_map.find(url);
0370     if (list.isEmpty()) {
0371         return false;
0372     }
0373 
0374     for (QList<KBookmark>::iterator it = list.begin(); it != list.end(); ++it) {
0375         (*it).updateAccessMetadata();
0376     }
0377 
0378     return true;
0379 }
0380 
0381 #include "moc_kbookmarkmanager.cpp"