File indexing completed on 2023-10-03 10:29:19

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 "kbookmarkmenu_p.h"
0013 #include "kbookmarks_debug.h"
0014 
0015 #include <QApplication>
0016 #include <QDir>
0017 #include <QFile>
0018 #include <QFileInfo>
0019 #include <QMessageBox>
0020 #include <QProcess>
0021 #include <QReadWriteLock>
0022 #include <QRegularExpression>
0023 #include <QTextStream>
0024 #include <QThread>
0025 
0026 #include <KBackup>
0027 #include <KConfig>
0028 #include <KConfigGroup>
0029 #include <KDirWatch>
0030 #include <QSaveFile>
0031 #include <QStandardPaths>
0032 
0033 namespace
0034 {
0035 namespace Strings
0036 {
0037 QString piData()
0038 {
0039     return QStringLiteral("version=\"1.0\" encoding=\"UTF-8\"");
0040 }
0041 }
0042 }
0043 
0044 class KBookmarkManagerList : public QList<KBookmarkManager *>
0045 {
0046 public:
0047     KBookmarkManagerList();
0048     ~KBookmarkManagerList()
0049     {
0050         cleanup();
0051     }
0052     void cleanup()
0053     {
0054         QList<KBookmarkManager *> copy = *this;
0055         qDeleteAll(copy); // auto-delete functionality
0056         clear();
0057     }
0058 
0059     QReadWriteLock lock;
0060 };
0061 
0062 Q_GLOBAL_STATIC(KBookmarkManagerList, s_pSelf)
0063 
0064 KBookmarkManagerList::KBookmarkManagerList()
0065 {
0066     if (s_pSelf.exists()) {
0067         s_pSelf->cleanup();
0068     }
0069 }
0070 
0071 class KBookmarkMap : private KBookmarkGroupTraverser
0072 {
0073 public:
0074     KBookmarkMap()
0075         : m_mapNeedsUpdate(true)
0076     {
0077     }
0078     void setNeedsUpdate()
0079     {
0080         m_mapNeedsUpdate = true;
0081     }
0082     void update(KBookmarkManager *);
0083     QList<KBookmark> find(const QString &url) const
0084     {
0085         return m_bk_map.value(url);
0086     }
0087 
0088 private:
0089     void visit(const KBookmark &) override;
0090     void visitEnter(const KBookmarkGroup &) override
0091     {
0092         ;
0093     }
0094     void visitLeave(const KBookmarkGroup &) override
0095     {
0096         ;
0097     }
0098 
0099 private:
0100     typedef QList<KBookmark> KBookmarkList;
0101     QMap<QString, KBookmarkList> m_bk_map;
0102     bool m_mapNeedsUpdate;
0103 };
0104 
0105 void KBookmarkMap::update(KBookmarkManager *manager)
0106 {
0107     if (m_mapNeedsUpdate) {
0108         m_mapNeedsUpdate = false;
0109 
0110         m_bk_map.clear();
0111         KBookmarkGroup root = manager->root();
0112         traverse(root);
0113     }
0114 }
0115 
0116 void KBookmarkMap::visit(const KBookmark &bk)
0117 {
0118     if (!bk.isSeparator()) {
0119         // add bookmark to url map
0120         m_bk_map[bk.internalElement().attribute(QStringLiteral("href"))].append(bk);
0121     }
0122 }
0123 
0124 // #########################
0125 // KBookmarkManagerPrivate
0126 class KBookmarkManagerPrivate
0127 {
0128 public:
0129     KBookmarkManagerPrivate(bool bDocIsloaded)
0130         : m_doc(QStringLiteral("xbel"))
0131         , m_docIsLoaded(bDocIsloaded)
0132         , m_update(false)
0133         , m_dialogAllowed(true)
0134         , m_dialogParent(nullptr)
0135         , m_dirWatch(nullptr)
0136     {
0137     }
0138 
0139     mutable QDomDocument m_doc;
0140     mutable QDomDocument m_toolbarDoc;
0141     QString m_bookmarksFile;
0142     mutable bool m_docIsLoaded;
0143     bool m_update;
0144     bool m_dialogAllowed;
0145     QWidget *m_dialogParent;
0146 
0147     KDirWatch *m_dirWatch; // for monitoring changes on bookmark files
0148 
0149     KBookmarkMap m_map;
0150 };
0151 
0152 // ################
0153 // KBookmarkManager
0154 
0155 static KBookmarkManager *lookupExisting(const QString &bookmarksFile)
0156 {
0157     for (KBookmarkManagerList::ConstIterator bmit = s_pSelf()->constBegin(), bmend = s_pSelf()->constEnd(); bmit != bmend; ++bmit) {
0158         if ((*bmit)->path() == bookmarksFile) {
0159             return *bmit;
0160         }
0161     }
0162     return nullptr;
0163 }
0164 
0165 KBookmarkManager *KBookmarkManager::managerForFile(const QString &bookmarksFile)
0166 {
0167     KBookmarkManager *mgr(nullptr);
0168     {
0169         QReadLocker readLock(&s_pSelf()->lock);
0170         mgr = lookupExisting(bookmarksFile);
0171         if (mgr) {
0172             return mgr;
0173         }
0174     }
0175 
0176     QWriteLocker writeLock(&s_pSelf()->lock);
0177     mgr = lookupExisting(bookmarksFile);
0178     if (mgr) {
0179         return mgr;
0180     }
0181 
0182     mgr = new KBookmarkManager(bookmarksFile);
0183     s_pSelf()->append(mgr);
0184     return mgr;
0185 }
0186 
0187 static QDomElement createXbelTopLevelElement(QDomDocument &doc)
0188 {
0189     QDomElement topLevel = doc.createElement(QStringLiteral("xbel"));
0190     topLevel.setAttribute(QStringLiteral("xmlns:mime"), QStringLiteral("http://www.freedesktop.org/standards/shared-mime-info"));
0191     topLevel.setAttribute(QStringLiteral("xmlns:bookmark"), QStringLiteral("http://www.freedesktop.org/standards/desktop-bookmarks"));
0192     topLevel.setAttribute(QStringLiteral("xmlns:kdepriv"), QStringLiteral("http://www.kde.org/kdepriv"));
0193     doc.appendChild(topLevel);
0194     doc.insertBefore(doc.createProcessingInstruction(QStringLiteral("xml"), Strings::piData()), topLevel);
0195     return topLevel;
0196 }
0197 
0198 KBookmarkManager::KBookmarkManager(const QString &bookmarksFile)
0199     : d(new KBookmarkManagerPrivate(false))
0200 {
0201     d->m_update = true;
0202 
0203     Q_ASSERT(!bookmarksFile.isEmpty());
0204     d->m_bookmarksFile = bookmarksFile;
0205 
0206     if (!QFile::exists(d->m_bookmarksFile)) {
0207         createXbelTopLevelElement(d->m_doc);
0208     } else {
0209         parse();
0210     }
0211     d->m_docIsLoaded = true;
0212 
0213     // start KDirWatch
0214     KDirWatch::self()->addFile(d->m_bookmarksFile);
0215     QObject::connect(KDirWatch::self(), &KDirWatch::dirty, this, &KBookmarkManager::slotFileChanged);
0216     QObject::connect(KDirWatch::self(), &KDirWatch::created, this, &KBookmarkManager::slotFileChanged);
0217     QObject::connect(KDirWatch::self(), &KDirWatch::deleted, this, &KBookmarkManager::slotFileChanged);
0218 
0219     // qCDebug(KBOOKMARKS_LOG) << "starting KDirWatch for" << d->m_bookmarksFile;
0220 }
0221 
0222 KBookmarkManager::KBookmarkManager()
0223     : d(new KBookmarkManagerPrivate(true))
0224 {
0225     d->m_update = false; // TODO - make it read/write
0226 
0227     createXbelTopLevelElement(d->m_doc);
0228 }
0229 
0230 void KBookmarkManager::slotFileChanged(const QString &path)
0231 {
0232     if (path == d->m_bookmarksFile) {
0233         // qCDebug(KBOOKMARKS_LOG) << "file changed (KDirWatch) " << path ;
0234         // Reparse
0235         parse();
0236         // Tell our GUI
0237         // (emit where group is "" to directly mark the root menu as dirty)
0238         Q_EMIT changed(QLatin1String(""), QString());
0239     }
0240 }
0241 
0242 KBookmarkManager::~KBookmarkManager()
0243 {
0244     if (!s_pSelf.isDestroyed()) {
0245         s_pSelf()->removeAll(this);
0246     }
0247 }
0248 
0249 bool KBookmarkManager::autoErrorHandlingEnabled() const
0250 {
0251     return d->m_dialogAllowed;
0252 }
0253 
0254 void KBookmarkManager::setAutoErrorHandlingEnabled(bool enable, QWidget *parent)
0255 {
0256     d->m_dialogAllowed = enable;
0257     d->m_dialogParent = parent;
0258 }
0259 
0260 void KBookmarkManager::setUpdate(bool update)
0261 {
0262     d->m_update = update;
0263 }
0264 
0265 QDomDocument KBookmarkManager::internalDocument() const
0266 {
0267     if (!d->m_docIsLoaded) {
0268         parse();
0269         d->m_toolbarDoc.clear();
0270     }
0271     return d->m_doc;
0272 }
0273 
0274 void KBookmarkManager::parse() const
0275 {
0276     d->m_docIsLoaded = true;
0277     // qCDebug(KBOOKMARKS_LOG) << "KBookmarkManager::parse " << d->m_bookmarksFile;
0278     QFile file(d->m_bookmarksFile);
0279     if (!file.open(QIODevice::ReadOnly)) {
0280         qCWarning(KBOOKMARKS_LOG) << "Can't open" << d->m_bookmarksFile;
0281         d->m_doc = QDomDocument(QStringLiteral("xbel"));
0282         createXbelTopLevelElement(d->m_doc);
0283         return;
0284     }
0285     d->m_doc = QDomDocument(QStringLiteral("xbel"));
0286     d->m_doc.setContent(&file);
0287 
0288     if (d->m_doc.documentElement().isNull()) {
0289         qCWarning(KBOOKMARKS_LOG) << "KBookmarkManager::parse : main tag is missing, creating default " << d->m_bookmarksFile;
0290         QDomElement element = d->m_doc.createElement(QStringLiteral("xbel"));
0291         d->m_doc.appendChild(element);
0292     }
0293 
0294     QDomElement docElem = d->m_doc.documentElement();
0295 
0296     QString mainTag = docElem.tagName();
0297     if (mainTag != QLatin1String("xbel")) {
0298         qCWarning(KBOOKMARKS_LOG) << "KBookmarkManager::parse : unknown main tag " << mainTag;
0299     }
0300 
0301     QDomNode n = d->m_doc.documentElement().previousSibling();
0302     if (n.isProcessingInstruction()) {
0303         QDomProcessingInstruction pi = n.toProcessingInstruction();
0304         pi.parentNode().removeChild(pi);
0305     }
0306 
0307     QDomProcessingInstruction pi;
0308     pi = d->m_doc.createProcessingInstruction(QStringLiteral("xml"), Strings::piData());
0309     d->m_doc.insertBefore(pi, docElem);
0310 
0311     file.close();
0312 
0313     d->m_map.setNeedsUpdate();
0314 }
0315 
0316 bool KBookmarkManager::save(bool toolbarCache) const
0317 {
0318     return saveAs(d->m_bookmarksFile, toolbarCache);
0319 }
0320 
0321 bool KBookmarkManager::saveAs(const QString &filename, bool toolbarCache) const
0322 {
0323     // qCDebug(KBOOKMARKS_LOG) << "KBookmarkManager::save " << filename;
0324 
0325     // Save the bookmark toolbar folder for quick loading
0326     // but only when it will actually make things quicker
0327     const QString cacheFilename = filename + QLatin1String(".tbcache");
0328     if (toolbarCache && !root().isToolbarGroup()) {
0329         QSaveFile cacheFile(cacheFilename);
0330         if (cacheFile.open(QIODevice::WriteOnly)) {
0331             QString str;
0332             QTextStream stream(&str, QIODevice::WriteOnly);
0333             stream << root().findToolbar();
0334             const QByteArray cstr = str.toUtf8();
0335             cacheFile.write(cstr.data(), cstr.length());
0336             cacheFile.commit();
0337         }
0338     } else { // remove any (now) stale cache
0339         QFile::remove(cacheFilename);
0340     }
0341 
0342     // Create parent dirs
0343     QFileInfo info(filename);
0344     QDir().mkpath(info.absolutePath());
0345 
0346     QSaveFile file(filename);
0347     if (file.open(QIODevice::WriteOnly)) {
0348         KBackup::simpleBackupFile(file.fileName(), QString(), QStringLiteral(".bak"));
0349         QTextStream stream(&file);
0350         // In Qt6 it's UTF-8 by default
0351         stream << internalDocument().toString();
0352         stream.flush();
0353         if (file.commit()) {
0354             return true;
0355         }
0356     }
0357 
0358     static int hadSaveError = false;
0359     if (!hadSaveError) {
0360         QString err = tr("Unable to save bookmarks in %1. Reported error was: %2. "
0361                          "This error message will only be shown once. The cause "
0362                          "of the error needs to be fixed as quickly as possible, "
0363                          "which is most likely a full hard drive.")
0364                           .arg(filename, file.errorString());
0365 
0366         if (d->m_dialogAllowed && qobject_cast<QApplication *>(qApp) && QThread::currentThread() == qApp->thread()) {
0367             QMessageBox::critical(QApplication::activeWindow(), QApplication::applicationName(), err);
0368         }
0369 
0370         qCCritical(KBOOKMARKS_LOG)
0371             << QStringLiteral("Unable to save bookmarks in %1. File reported the following error-code: %2.").arg(filename).arg(file.error());
0372         Q_EMIT const_cast<KBookmarkManager *>(this)->error(err);
0373     }
0374     hadSaveError = true;
0375     return false;
0376 }
0377 
0378 QString KBookmarkManager::path() const
0379 {
0380     return d->m_bookmarksFile;
0381 }
0382 
0383 KBookmarkGroup KBookmarkManager::root() const
0384 {
0385     return KBookmarkGroup(internalDocument().documentElement());
0386 }
0387 
0388 KBookmarkGroup KBookmarkManager::toolbar()
0389 {
0390     // qCDebug(KBOOKMARKS_LOG) << "KBookmarkManager::toolbar begin";
0391     // Only try to read from a toolbar cache if the full document isn't loaded
0392     if (!d->m_docIsLoaded) {
0393         // qCDebug(KBOOKMARKS_LOG) << "KBookmarkManager::toolbar trying cache";
0394         const QString cacheFilename = d->m_bookmarksFile + QLatin1String(".tbcache");
0395         QFileInfo bmInfo(d->m_bookmarksFile);
0396         QFileInfo cacheInfo(cacheFilename);
0397         if (d->m_toolbarDoc.isNull() && QFile::exists(cacheFilename) && bmInfo.lastModified() < cacheInfo.lastModified()) {
0398             // qCDebug(KBOOKMARKS_LOG) << "KBookmarkManager::toolbar reading file";
0399             QFile file(cacheFilename);
0400 
0401             if (file.open(QIODevice::ReadOnly)) {
0402                 d->m_toolbarDoc = QDomDocument(QStringLiteral("cache"));
0403                 d->m_toolbarDoc.setContent(&file);
0404                 // qCDebug(KBOOKMARKS_LOG) << "KBookmarkManager::toolbar opened";
0405             }
0406         }
0407         if (!d->m_toolbarDoc.isNull()) {
0408             // qCDebug(KBOOKMARKS_LOG) << "KBookmarkManager::toolbar returning element";
0409             QDomElement elem = d->m_toolbarDoc.firstChild().toElement();
0410             return KBookmarkGroup(elem);
0411         }
0412     }
0413 
0414     // Fallback to the normal way if there is no cache or if the bookmark file
0415     // is already loaded
0416     QDomElement elem = root().findToolbar();
0417     if (elem.isNull()) {
0418         // Root is the bookmark toolbar if none has been set.
0419         // Make it explicit to speed up invocations of findToolbar()
0420         root().internalElement().setAttribute(QStringLiteral("toolbar"), QStringLiteral("yes"));
0421         return root();
0422     } else {
0423         return KBookmarkGroup(elem);
0424     }
0425 }
0426 
0427 KBookmark KBookmarkManager::findByAddress(const QString &address)
0428 {
0429     // qCDebug(KBOOKMARKS_LOG) << "KBookmarkManager::findByAddress " << address;
0430     KBookmark result = root();
0431     // The address is something like /5/10/2+
0432     static const QRegularExpression separator(QStringLiteral("[/+]"));
0433     const QStringList addresses = address.split(separator, Qt::SkipEmptyParts);
0434     // qCWarning(KBOOKMARKS_LOG) << addresses.join(",");
0435     for (QStringList::const_iterator it = addresses.begin(); it != addresses.end();) {
0436         bool append = ((*it) == QLatin1String("+"));
0437         uint number = (*it).toUInt();
0438         Q_ASSERT(result.isGroup());
0439         KBookmarkGroup group = result.toGroup();
0440         KBookmark bk = group.first();
0441         KBookmark lbk = bk; // last non-null bookmark
0442         for (uint i = 0; ((i < number) || append) && !bk.isNull(); ++i) {
0443             lbk = bk;
0444             bk = group.next(bk);
0445             // qCWarning(KBOOKMARKS_LOG) << i;
0446         }
0447         it++;
0448         // qCWarning(KBOOKMARKS_LOG) << "found section";
0449         result = bk;
0450     }
0451     if (result.isNull()) {
0452         qCWarning(KBOOKMARKS_LOG) << "KBookmarkManager::findByAddress: couldn't find item " << address;
0453     }
0454     // qCWarning(KBOOKMARKS_LOG) << "found " << result.address();
0455     return result;
0456 }
0457 
0458 void KBookmarkManager::emitChanged()
0459 {
0460     emitChanged(root());
0461 }
0462 
0463 void KBookmarkManager::emitChanged(const KBookmarkGroup &group)
0464 {
0465     (void)save(); // KDE5 TODO: emitChanged should return a bool? Maybe rename it to saveAndEmitChanged?
0466 
0467     // Tell the other processes too
0468     // qCDebug(KBOOKMARKS_LOG) << "KBookmarkManager::emitChanged : broadcasting change " << group.address();
0469 
0470     Q_EMIT bookmarksChanged(group.address());
0471 
0472     // We do get our own broadcast, so no need for this anymore
0473     // emit changed( group );
0474 }
0475 
0476 void KBookmarkManager::emitConfigChanged()
0477 {
0478     Q_EMIT bookmarkConfigChanged();
0479 }
0480 
0481 void KBookmarkManager::notifyCompleteChange(const QString &caller) // DBUS call
0482 {
0483     if (!d->m_update) {
0484         return;
0485     }
0486 
0487     // qCDebug(KBOOKMARKS_LOG) << "KBookmarkManager::notifyCompleteChange";
0488     // The bk editor tells us we should reload everything
0489     // Reparse
0490     parse();
0491     // Tell our GUI
0492     // (emit where group is "" to directly mark the root menu as dirty)
0493     Q_EMIT changed(QLatin1String(""), caller);
0494 }
0495 
0496 ///////
0497 bool KBookmarkManager::updateAccessMetadata(const QString &url)
0498 {
0499     d->m_map.update(this);
0500     QList<KBookmark> list = d->m_map.find(url);
0501     if (list.isEmpty()) {
0502         return false;
0503     }
0504 
0505     for (QList<KBookmark>::iterator it = list.begin(); it != list.end(); ++it) {
0506         (*it).updateAccessMetadata();
0507     }
0508 
0509     return true;
0510 }
0511 
0512 KBookmarkSettings *KBookmarkSettings::s_self = nullptr;
0513 
0514 void KBookmarkSettings::readSettings()
0515 {
0516     KConfig config(QStringLiteral("kbookmarkrc"), KConfig::NoGlobals);
0517     KConfigGroup cg(&config, "Bookmarks");
0518 
0519     // add bookmark dialog usage - no reparse
0520     s_self->m_advancedaddbookmark = cg.readEntry("AdvancedAddBookmarkDialog", false);
0521 
0522     // this one alters the menu, therefore it needs a reparse
0523     s_self->m_contextmenu = cg.readEntry("ContextMenuActions", true);
0524 }
0525 
0526 KBookmarkSettings *KBookmarkSettings::self()
0527 {
0528     if (!s_self) {
0529         s_self = new KBookmarkSettings;
0530         readSettings();
0531     }
0532     return s_self;
0533 }
0534 
0535 #include "moc_kbookmarkmanager.cpp"