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 | |