| 1 | // -*- c-basic-offset:4; indent-tabs-mode:nil -*- |
| 2 | /* |
| 3 | This file is part of the KDE libraries |
| 4 | SPDX-FileCopyrightText: 2000 David Faure <faure@kde.org> |
| 5 | SPDX-FileCopyrightText: 2003 Alexander Kellett <lypanov@kde.org> |
| 6 | SPDX-FileCopyrightText: 2008 Norbert Frese <nf2@scheinwelt.at> |
| 7 | |
| 8 | SPDX-License-Identifier: LGPL-2.0-only |
| 9 | */ |
| 10 | |
| 11 | #include "kbookmarkmanager.h" |
| 12 | #include "kbookmarks_debug.h" |
| 13 | |
| 14 | #include <QDir> |
| 15 | #include <QFile> |
| 16 | #include <QFileInfo> |
| 17 | #include <QRegularExpression> |
| 18 | #include <QSaveFile> |
| 19 | #include <QStandardPaths> |
| 20 | #include <QTextStream> |
| 21 | |
| 22 | #include <KBackup> |
| 23 | #include <KConfig> |
| 24 | #include <KConfigGroup> |
| 25 | #include <KDirWatch> |
| 26 | |
| 27 | namespace |
| 28 | { |
| 29 | namespace Strings |
| 30 | { |
| 31 | QString piData() |
| 32 | { |
| 33 | return QStringLiteral("version=\"1.0\" encoding=\"UTF-8\"" ); |
| 34 | } |
| 35 | } |
| 36 | } |
| 37 | |
| 38 | class KBookmarkMap : private KBookmarkGroupTraverser |
| 39 | { |
| 40 | public: |
| 41 | KBookmarkMap() |
| 42 | : m_mapNeedsUpdate(true) |
| 43 | { |
| 44 | } |
| 45 | void setNeedsUpdate() |
| 46 | { |
| 47 | m_mapNeedsUpdate = true; |
| 48 | } |
| 49 | void update(KBookmarkManager *); |
| 50 | QList<KBookmark> find(const QString &url) const |
| 51 | { |
| 52 | return m_bk_map.value(key: url); |
| 53 | } |
| 54 | |
| 55 | private: |
| 56 | void visit(const KBookmark &) override; |
| 57 | void visitEnter(const KBookmarkGroup &) override |
| 58 | { |
| 59 | ; |
| 60 | } |
| 61 | void visitLeave(const KBookmarkGroup &) override |
| 62 | { |
| 63 | ; |
| 64 | } |
| 65 | |
| 66 | private: |
| 67 | typedef QList<KBookmark> KBookmarkList; |
| 68 | QMap<QString, KBookmarkList> m_bk_map; |
| 69 | bool m_mapNeedsUpdate; |
| 70 | }; |
| 71 | |
| 72 | void KBookmarkMap::update(KBookmarkManager *manager) |
| 73 | { |
| 74 | if (m_mapNeedsUpdate) { |
| 75 | m_mapNeedsUpdate = false; |
| 76 | |
| 77 | m_bk_map.clear(); |
| 78 | KBookmarkGroup root = manager->root(); |
| 79 | traverse(root); |
| 80 | } |
| 81 | } |
| 82 | |
| 83 | void KBookmarkMap::visit(const KBookmark &bk) |
| 84 | { |
| 85 | if (!bk.isSeparator()) { |
| 86 | // add bookmark to url map |
| 87 | m_bk_map[bk.internalElement().attribute(QStringLiteral("href" ))].append(t: bk); |
| 88 | } |
| 89 | } |
| 90 | |
| 91 | // ######################### |
| 92 | // KBookmarkManagerPrivate |
| 93 | class KBookmarkManagerPrivate |
| 94 | { |
| 95 | public: |
| 96 | KBookmarkManagerPrivate(bool bDocIsloaded) |
| 97 | : m_doc(QStringLiteral("xbel" )) |
| 98 | , m_docIsLoaded(bDocIsloaded) |
| 99 | , m_dirWatch(nullptr) |
| 100 | { |
| 101 | } |
| 102 | |
| 103 | mutable QDomDocument m_doc; |
| 104 | mutable QDomDocument m_toolbarDoc; |
| 105 | QString m_bookmarksFile; |
| 106 | mutable bool m_docIsLoaded; |
| 107 | |
| 108 | KDirWatch *m_dirWatch; // for monitoring changes on bookmark files |
| 109 | |
| 110 | KBookmarkMap m_map; |
| 111 | }; |
| 112 | |
| 113 | // ################ |
| 114 | // KBookmarkManager |
| 115 | |
| 116 | static QDomElement createXbelTopLevelElement(QDomDocument &doc) |
| 117 | { |
| 118 | QDomElement topLevel = doc.createElement(QStringLiteral("xbel" )); |
| 119 | topLevel.setAttribute(QStringLiteral("xmlns:mime" ), QStringLiteral("http://www.freedesktop.org/standards/shared-mime-info" )); |
| 120 | topLevel.setAttribute(QStringLiteral("xmlns:bookmark" ), QStringLiteral("http://www.freedesktop.org/standards/desktop-bookmarks" )); |
| 121 | topLevel.setAttribute(QStringLiteral("xmlns:kdepriv" ), QStringLiteral("http://www.kde.org/kdepriv" )); |
| 122 | doc.appendChild(newChild: topLevel); |
| 123 | doc.insertBefore(newChild: doc.createProcessingInstruction(QStringLiteral("xml" ), data: Strings::piData()), refChild: topLevel); |
| 124 | return topLevel; |
| 125 | } |
| 126 | |
| 127 | KBookmarkManager::KBookmarkManager(const QString &bookmarksFile, QObject *parent) |
| 128 | : QObject(parent) |
| 129 | , d(new KBookmarkManagerPrivate(false)) |
| 130 | { |
| 131 | Q_ASSERT(!bookmarksFile.isEmpty()); |
| 132 | d->m_bookmarksFile = bookmarksFile; |
| 133 | |
| 134 | if (!QFile::exists(fileName: d->m_bookmarksFile)) { |
| 135 | createXbelTopLevelElement(doc&: d->m_doc); |
| 136 | } else { |
| 137 | parse(); |
| 138 | } |
| 139 | d->m_docIsLoaded = true; |
| 140 | |
| 141 | // start KDirWatch |
| 142 | KDirWatch::self()->addFile(file: d->m_bookmarksFile); |
| 143 | QObject::connect(sender: KDirWatch::self(), signal: &KDirWatch::dirty, context: this, slot: &KBookmarkManager::slotFileChanged); |
| 144 | QObject::connect(sender: KDirWatch::self(), signal: &KDirWatch::created, context: this, slot: &KBookmarkManager::slotFileChanged); |
| 145 | QObject::connect(sender: KDirWatch::self(), signal: &KDirWatch::deleted, context: this, slot: &KBookmarkManager::slotFileChanged); |
| 146 | |
| 147 | // qCDebug(KBOOKMARKS_LOG) << "starting KDirWatch for" << d->m_bookmarksFile; |
| 148 | } |
| 149 | |
| 150 | void KBookmarkManager::slotFileChanged(const QString &path) |
| 151 | { |
| 152 | if (path == d->m_bookmarksFile) { |
| 153 | // qCDebug(KBOOKMARKS_LOG) << "file changed (KDirWatch) " << path ; |
| 154 | // Reparse |
| 155 | parse(); |
| 156 | // Tell our GUI |
| 157 | // (emit where group is "" to directly mark the root menu as dirty) |
| 158 | Q_EMIT changed(groupAddress: QLatin1String("" )); |
| 159 | } |
| 160 | } |
| 161 | |
| 162 | KBookmarkManager::~KBookmarkManager() |
| 163 | { |
| 164 | } |
| 165 | |
| 166 | QDomDocument KBookmarkManager::internalDocument() const |
| 167 | { |
| 168 | if (!d->m_docIsLoaded) { |
| 169 | parse(); |
| 170 | d->m_toolbarDoc.clear(); |
| 171 | } |
| 172 | return d->m_doc; |
| 173 | } |
| 174 | |
| 175 | void KBookmarkManager::parse() const |
| 176 | { |
| 177 | d->m_docIsLoaded = true; |
| 178 | // qCDebug(KBOOKMARKS_LOG) << "KBookmarkManager::parse " << d->m_bookmarksFile; |
| 179 | QFile file(d->m_bookmarksFile); |
| 180 | if (!file.open(flags: QIODevice::ReadOnly)) { |
| 181 | qCWarning(KBOOKMARKS_LOG) << "Can't open" << d->m_bookmarksFile; |
| 182 | d->m_doc = QDomDocument(QStringLiteral("xbel" )); |
| 183 | createXbelTopLevelElement(doc&: d->m_doc); |
| 184 | return; |
| 185 | } |
| 186 | d->m_doc = QDomDocument(QStringLiteral("xbel" )); |
| 187 | d->m_doc.setContent(device: &file); |
| 188 | |
| 189 | if (d->m_doc.documentElement().isNull()) { |
| 190 | qCWarning(KBOOKMARKS_LOG) << "KBookmarkManager::parse : main tag is missing, creating default " << d->m_bookmarksFile; |
| 191 | QDomElement element = d->m_doc.createElement(QStringLiteral("xbel" )); |
| 192 | d->m_doc.appendChild(newChild: element); |
| 193 | } |
| 194 | |
| 195 | QDomElement docElem = d->m_doc.documentElement(); |
| 196 | |
| 197 | QString mainTag = docElem.tagName(); |
| 198 | if (mainTag != QLatin1String("xbel" )) { |
| 199 | qCWarning(KBOOKMARKS_LOG) << "KBookmarkManager::parse : unknown main tag " << mainTag; |
| 200 | } |
| 201 | |
| 202 | QDomNode n = d->m_doc.documentElement().previousSibling(); |
| 203 | if (n.isProcessingInstruction()) { |
| 204 | QDomProcessingInstruction pi = n.toProcessingInstruction(); |
| 205 | pi.parentNode().removeChild(oldChild: pi); |
| 206 | } |
| 207 | |
| 208 | QDomProcessingInstruction pi; |
| 209 | pi = d->m_doc.createProcessingInstruction(QStringLiteral("xml" ), data: Strings::piData()); |
| 210 | d->m_doc.insertBefore(newChild: pi, refChild: docElem); |
| 211 | |
| 212 | file.close(); |
| 213 | |
| 214 | d->m_map.setNeedsUpdate(); |
| 215 | } |
| 216 | |
| 217 | bool KBookmarkManager::save(bool toolbarCache) const |
| 218 | { |
| 219 | return saveAs(filename: d->m_bookmarksFile, toolbarCache); |
| 220 | } |
| 221 | |
| 222 | bool KBookmarkManager::saveAs(const QString &filename, bool toolbarCache) const |
| 223 | { |
| 224 | // qCDebug(KBOOKMARKS_LOG) << "KBookmarkManager::save " << filename; |
| 225 | |
| 226 | // Save the bookmark toolbar folder for quick loading |
| 227 | // but only when it will actually make things quicker |
| 228 | const QString cacheFilename = filename + QLatin1String(".tbcache" ); |
| 229 | if (toolbarCache && !root().isToolbarGroup()) { |
| 230 | QSaveFile cacheFile(cacheFilename); |
| 231 | if (cacheFile.open(flags: QIODevice::WriteOnly)) { |
| 232 | QString str; |
| 233 | QTextStream stream(&str, QIODevice::WriteOnly); |
| 234 | stream << root().findToolbar(); |
| 235 | const QByteArray cstr = str.toUtf8(); |
| 236 | cacheFile.write(data: cstr.data(), len: cstr.length()); |
| 237 | cacheFile.commit(); |
| 238 | } |
| 239 | } else { // remove any (now) stale cache |
| 240 | QFile::remove(fileName: cacheFilename); |
| 241 | } |
| 242 | |
| 243 | // Create parent dirs |
| 244 | QFileInfo info(filename); |
| 245 | QDir().mkpath(dirPath: info.absolutePath()); |
| 246 | |
| 247 | if (filename == d->m_bookmarksFile) { |
| 248 | KDirWatch::self()->removeFile(file: d->m_bookmarksFile); |
| 249 | } |
| 250 | |
| 251 | QSaveFile file(filename); |
| 252 | bool success = false; |
| 253 | if (file.open(flags: QIODevice::WriteOnly)) { |
| 254 | KBackup::simpleBackupFile(filename: file.fileName(), backupDir: QString(), QStringLiteral(".bak" )); |
| 255 | QTextStream stream(&file); |
| 256 | // In Qt6 it's UTF-8 by default |
| 257 | stream << internalDocument().toString(); |
| 258 | stream.flush(); |
| 259 | success = file.commit(); |
| 260 | } |
| 261 | |
| 262 | if (filename == d->m_bookmarksFile) { |
| 263 | KDirWatch::self()->addFile(file: d->m_bookmarksFile); |
| 264 | } |
| 265 | |
| 266 | if (!success) { |
| 267 | QString err = tr(s: "Unable to save bookmarks in %1. Reported error was: %2. " |
| 268 | "This error message will only be shown once. The cause " |
| 269 | "of the error needs to be fixed as quickly as possible, " |
| 270 | "which is most likely a full hard drive." ) |
| 271 | .arg(args: filename, args: file.errorString()); |
| 272 | qCCritical(KBOOKMARKS_LOG) |
| 273 | << QStringLiteral("Unable to save bookmarks in %1. File reported the following error-code: %2." ).arg(a: filename).arg(a: file.error()); |
| 274 | Q_EMIT const_cast<KBookmarkManager *>(this)->error(errorMessage: err); |
| 275 | } |
| 276 | |
| 277 | return success; |
| 278 | } |
| 279 | |
| 280 | QString KBookmarkManager::path() const |
| 281 | { |
| 282 | return d->m_bookmarksFile; |
| 283 | } |
| 284 | |
| 285 | KBookmarkGroup KBookmarkManager::root() const |
| 286 | { |
| 287 | return KBookmarkGroup(internalDocument().documentElement()); |
| 288 | } |
| 289 | |
| 290 | KBookmarkGroup KBookmarkManager::toolbar() |
| 291 | { |
| 292 | // qCDebug(KBOOKMARKS_LOG) << "KBookmarkManager::toolbar begin"; |
| 293 | // Only try to read from a toolbar cache if the full document isn't loaded |
| 294 | if (!d->m_docIsLoaded) { |
| 295 | // qCDebug(KBOOKMARKS_LOG) << "KBookmarkManager::toolbar trying cache"; |
| 296 | const QString cacheFilename = d->m_bookmarksFile + QLatin1String(".tbcache" ); |
| 297 | QFileInfo bmInfo(d->m_bookmarksFile); |
| 298 | QFileInfo cacheInfo(cacheFilename); |
| 299 | if (d->m_toolbarDoc.isNull() && QFile::exists(fileName: cacheFilename) && bmInfo.lastModified() < cacheInfo.lastModified()) { |
| 300 | // qCDebug(KBOOKMARKS_LOG) << "KBookmarkManager::toolbar reading file"; |
| 301 | QFile file(cacheFilename); |
| 302 | |
| 303 | if (file.open(flags: QIODevice::ReadOnly)) { |
| 304 | d->m_toolbarDoc = QDomDocument(QStringLiteral("cache" )); |
| 305 | d->m_toolbarDoc.setContent(device: &file); |
| 306 | // qCDebug(KBOOKMARKS_LOG) << "KBookmarkManager::toolbar opened"; |
| 307 | } |
| 308 | } |
| 309 | if (!d->m_toolbarDoc.isNull()) { |
| 310 | // qCDebug(KBOOKMARKS_LOG) << "KBookmarkManager::toolbar returning element"; |
| 311 | QDomElement elem = d->m_toolbarDoc.firstChild().toElement(); |
| 312 | return KBookmarkGroup(elem); |
| 313 | } |
| 314 | } |
| 315 | |
| 316 | // Fallback to the normal way if there is no cache or if the bookmark file |
| 317 | // is already loaded |
| 318 | QDomElement elem = root().findToolbar(); |
| 319 | if (elem.isNull()) { |
| 320 | // Root is the bookmark toolbar if none has been set. |
| 321 | // Make it explicit to speed up invocations of findToolbar() |
| 322 | root().internalElement().setAttribute(QStringLiteral("toolbar" ), QStringLiteral("yes" )); |
| 323 | return root(); |
| 324 | } else { |
| 325 | return KBookmarkGroup(elem); |
| 326 | } |
| 327 | } |
| 328 | |
| 329 | KBookmark KBookmarkManager::findByAddress(const QString &address) |
| 330 | { |
| 331 | // qCDebug(KBOOKMARKS_LOG) << "KBookmarkManager::findByAddress " << address; |
| 332 | KBookmark result = root(); |
| 333 | // The address is something like /5/10/2+ |
| 334 | static const QRegularExpression separator(QStringLiteral("[/+]" )); |
| 335 | const QStringList addresses = address.split(sep: separator, behavior: Qt::SkipEmptyParts); |
| 336 | // qCWarning(KBOOKMARKS_LOG) << addresses.join(","); |
| 337 | for (QStringList::const_iterator it = addresses.begin(); it != addresses.end();) { |
| 338 | bool append = ((*it) == QLatin1String("+" )); |
| 339 | uint number = (*it).toUInt(); |
| 340 | Q_ASSERT(result.isGroup()); |
| 341 | KBookmarkGroup group = result.toGroup(); |
| 342 | KBookmark bk = group.first(); |
| 343 | KBookmark lbk = bk; // last non-null bookmark |
| 344 | for (uint i = 0; ((i < number) || append) && !bk.isNull(); ++i) { |
| 345 | lbk = bk; |
| 346 | bk = group.next(current: bk); |
| 347 | // qCWarning(KBOOKMARKS_LOG) << i; |
| 348 | } |
| 349 | it++; |
| 350 | // qCWarning(KBOOKMARKS_LOG) << "found section"; |
| 351 | result = bk; |
| 352 | } |
| 353 | if (result.isNull()) { |
| 354 | qCWarning(KBOOKMARKS_LOG) << "KBookmarkManager::findByAddress: couldn't find item " << address; |
| 355 | } |
| 356 | // qCWarning(KBOOKMARKS_LOG) << "found " << result.address(); |
| 357 | return result; |
| 358 | } |
| 359 | |
| 360 | void KBookmarkManager::emitChanged() |
| 361 | { |
| 362 | emitChanged(group: root()); |
| 363 | } |
| 364 | |
| 365 | void KBookmarkManager::emitChanged(const KBookmarkGroup &group) |
| 366 | { |
| 367 | (void)save(); // KDE5 TODO: emitChanged should return a bool? Maybe rename it to saveAndEmitChanged? |
| 368 | |
| 369 | // Tell the other processes too |
| 370 | // qCDebug(KBOOKMARKS_LOG) << "KBookmarkManager::emitChanged : broadcasting change " << group.address(); |
| 371 | |
| 372 | Q_EMIT changed(groupAddress: group.address()); |
| 373 | } |
| 374 | |
| 375 | /////// |
| 376 | bool KBookmarkManager::updateAccessMetadata(const QString &url) |
| 377 | { |
| 378 | d->m_map.update(manager: this); |
| 379 | QList<KBookmark> list = d->m_map.find(url); |
| 380 | if (list.isEmpty()) { |
| 381 | return false; |
| 382 | } |
| 383 | |
| 384 | for (QList<KBookmark>::iterator it = list.begin(); it != list.end(); ++it) { |
| 385 | (*it).updateAccessMetadata(); |
| 386 | } |
| 387 | |
| 388 | return true; |
| 389 | } |
| 390 | |
| 391 | #include "moc_kbookmarkmanager.cpp" |
| 392 | |