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
27namespace
28{
29namespace Strings
30{
31QString piData()
32{
33 return QStringLiteral("version=\"1.0\" encoding=\"UTF-8\"");
34}
35}
36}
37
38class KBookmarkMap : private KBookmarkGroupTraverser
39{
40public:
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
55private:
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
66private:
67 typedef QList<KBookmark> KBookmarkList;
68 QMap<QString, KBookmarkList> m_bk_map;
69 bool m_mapNeedsUpdate;
70};
71
72void 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
83void 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
93class KBookmarkManagerPrivate
94{
95public:
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
116static 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
127KBookmarkManager::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
150void 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
162KBookmarkManager::~KBookmarkManager()
163{
164}
165
166QDomDocument KBookmarkManager::internalDocument() const
167{
168 if (!d->m_docIsLoaded) {
169 parse();
170 d->m_toolbarDoc.clear();
171 }
172 return d->m_doc;
173}
174
175void 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
217bool KBookmarkManager::save(bool toolbarCache) const
218{
219 return saveAs(filename: d->m_bookmarksFile, toolbarCache);
220}
221
222bool 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
280QString KBookmarkManager::path() const
281{
282 return d->m_bookmarksFile;
283}
284
285KBookmarkGroup KBookmarkManager::root() const
286{
287 return KBookmarkGroup(internalDocument().documentElement());
288}
289
290KBookmarkGroup 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
329KBookmark 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
360void KBookmarkManager::emitChanged()
361{
362 emitChanged(group: root());
363}
364
365void 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///////
376bool 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

source code of kbookmarks/src/kbookmarkmanager.cpp