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 "kbookmark.h"
12
13#include <KStringHandler>
14#include <kurlmimedata.h>
15
16#include <QCoreApplication>
17#include <QMimeDatabase>
18#include <QStack>
19#include <QDateTime>
20#include <QMimeData>
21
22namespace
23{
24namespace Strings
25{
26QString metaDataKDEOwner()
27{
28 return QStringLiteral("http://www.kde.org");
29}
30QString metaDataFreedesktopOwner()
31{
32 return QStringLiteral("http://freedesktop.org");
33}
34QString metaDataMimeOwner()
35{
36 return QStringLiteral("http://www.freedesktop.org/standards/shared-mime-info");
37}
38
39QString xbelMimeType()
40{
41 return QStringLiteral("application/x-xbel");
42}
43}
44}
45
46////// utility functions
47
48static QDomNode cd(QDomNode node, const QString &name, bool create)
49{
50 QDomNode subnode = node.namedItem(name);
51 if (create && subnode.isNull()) {
52 subnode = node.ownerDocument().createElement(tagName: name);
53 node.appendChild(newChild: subnode);
54 }
55 return subnode;
56}
57
58static QDomNode cd_or_create(const QDomNode &node, const QString &name)
59{
60 return cd(node, name, create: true);
61}
62
63static QDomText get_or_create_text(QDomNode node)
64{
65 QDomNode subnode = node.firstChild();
66 if (subnode.isNull()) {
67 subnode = node.ownerDocument().createTextNode(data: QLatin1String(""));
68 node.appendChild(newChild: subnode);
69 }
70 return subnode.toText();
71}
72
73static QDomNode findMetadata(const QString &forOwner, QDomNode &parent, bool create)
74{
75 const bool forOwnerIsKDE = (forOwner == Strings::metaDataKDEOwner());
76
77 QDomElement metadataElement;
78 for (QDomNode _node = parent.firstChild(); !_node.isNull(); _node = _node.nextSibling()) {
79 QDomElement elem = _node.toElement();
80 if (!elem.isNull() && elem.tagName() == QLatin1String("metadata")) {
81 const QString owner = elem.attribute(QStringLiteral("owner"));
82 if (owner == forOwner) {
83 return elem;
84 }
85 if (owner.isEmpty() && forOwnerIsKDE) {
86 metadataElement = elem;
87 }
88 }
89 }
90 if (create && metadataElement.isNull()) {
91 metadataElement = parent.ownerDocument().createElement(QStringLiteral("metadata"));
92 parent.appendChild(newChild: metadataElement);
93 metadataElement.setAttribute(QStringLiteral("owner"), value: forOwner);
94
95 } else if (!metadataElement.isNull() && forOwnerIsKDE) {
96 // i'm not sure if this is good, we shouldn't take over foreign metadata
97 metadataElement.setAttribute(QStringLiteral("owner"), value: Strings::metaDataKDEOwner());
98 }
99 return metadataElement;
100}
101
102//////
103
104KBookmarkGroup::KBookmarkGroup()
105 : KBookmark(QDomElement())
106{
107}
108
109KBookmarkGroup::KBookmarkGroup(const QDomElement &elem)
110 : KBookmark(elem)
111{
112}
113
114bool KBookmarkGroup::isOpen() const
115{
116 return element.attribute(QStringLiteral("folded")) == QLatin1String("no"); // default is: folded
117}
118
119KBookmark KBookmarkGroup::first() const
120{
121 return KBookmark(nextKnownTag(start: element.firstChildElement(), goNext: true));
122}
123
124KBookmark KBookmarkGroup::previous(const KBookmark &current) const
125{
126 return KBookmark(nextKnownTag(start: current.element.previousSiblingElement(), goNext: false));
127}
128
129KBookmark KBookmarkGroup::next(const KBookmark &current) const
130{
131 return KBookmark(nextKnownTag(start: current.element.nextSiblingElement(), goNext: true));
132}
133
134int KBookmarkGroup::indexOf(const KBookmark &child) const
135{
136 int counter = 0;
137 for (KBookmark bk = first(); !bk.isNull(); bk = next(current: bk), ++counter) {
138 if (bk.element == child.element) {
139 return counter;
140 }
141 }
142 return -1;
143}
144
145QDomElement KBookmarkGroup::nextKnownTag(const QDomElement &start, bool goNext) const
146{
147 for (QDomElement elem = start; !elem.isNull();) {
148 QString tag = elem.tagName();
149 if (tag == QLatin1String("folder") || tag == QLatin1String("bookmark") || tag == QLatin1String("separator")) {
150 return elem;
151 }
152 if (goNext) {
153 elem = elem.nextSiblingElement();
154 } else {
155 elem = elem.previousSiblingElement();
156 }
157 }
158 return QDomElement();
159}
160
161KBookmarkGroup KBookmarkGroup::createNewFolder(const QString &text)
162{
163 if (isNull()) {
164 return KBookmarkGroup();
165 }
166 QDomDocument doc = element.ownerDocument();
167 QDomElement groupElem = doc.createElement(QStringLiteral("folder"));
168 element.appendChild(newChild: groupElem);
169 QDomElement textElem = doc.createElement(QStringLiteral("title"));
170 groupElem.appendChild(newChild: textElem);
171 textElem.appendChild(newChild: doc.createTextNode(data: text));
172 return KBookmarkGroup(groupElem);
173}
174
175KBookmark KBookmarkGroup::createNewSeparator()
176{
177 if (isNull()) {
178 return KBookmark();
179 }
180 QDomDocument doc = element.ownerDocument();
181 Q_ASSERT(!doc.isNull());
182 QDomElement sepElem = doc.createElement(QStringLiteral("separator"));
183 element.appendChild(newChild: sepElem);
184 return KBookmark(sepElem);
185}
186
187bool KBookmarkGroup::moveBookmark(const KBookmark &item, const KBookmark &after)
188{
189 QDomNode n;
190 if (!after.isNull()) {
191 n = element.insertAfter(newChild: item.element, refChild: after.element);
192 } else { // first child
193 if (element.firstChild().isNull()) { // Empty element -> set as real first child
194 n = element.insertBefore(newChild: item.element, refChild: QDomElement());
195 }
196
197 // we have to skip everything up to the first valid child
198 QDomElement firstChild = nextKnownTag(start: element.firstChild().toElement(), goNext: true);
199 if (!firstChild.isNull()) {
200 if (firstChild == item.element) { // item is already the first child, done
201 return true;
202 }
203 n = element.insertBefore(newChild: item.element, refChild: firstChild);
204 } else {
205 // No real first child -> append after the <title> etc.
206 n = element.appendChild(newChild: item.element);
207 }
208 }
209 return (!n.isNull());
210}
211
212KBookmark KBookmarkGroup::addBookmark(const KBookmark &bm)
213{
214 element.appendChild(newChild: bm.internalElement());
215 return bm;
216}
217
218KBookmark KBookmarkGroup::addBookmark(const QString &text, const QUrl &url, const QString &icon)
219{
220 if (isNull()) {
221 return KBookmark();
222 }
223 QDomDocument doc = element.ownerDocument();
224 QDomElement elem = doc.createElement(QStringLiteral("bookmark"));
225 elem.setAttribute(QStringLiteral("href"), value: url.toString(options: QUrl::FullyEncoded));
226
227 QDomElement textElem = doc.createElement(QStringLiteral("title"));
228 elem.appendChild(newChild: textElem);
229 textElem.appendChild(newChild: doc.createTextNode(data: text));
230
231 KBookmark newBookmark = addBookmark(bm: KBookmark(elem));
232
233 // as icons are moved to metadata, we have to use the KBookmark API for this
234 newBookmark.setIcon(icon);
235 return newBookmark;
236}
237
238void KBookmarkGroup::deleteBookmark(const KBookmark &bk)
239{
240 element.removeChild(oldChild: bk.element);
241}
242
243bool KBookmarkGroup::isToolbarGroup() const
244{
245 return (element.attribute(QStringLiteral("toolbar")) == QLatin1String("yes"));
246}
247
248QDomElement KBookmarkGroup::findToolbar() const
249{
250 if (element.attribute(QStringLiteral("toolbar")) == QLatin1String("yes")) {
251 return element;
252 }
253 for (QDomElement e = element.firstChildElement(QStringLiteral("folder")); !e.isNull(); e = e.nextSiblingElement(QStringLiteral("folder"))) {
254 QDomElement result = KBookmarkGroup(e).findToolbar();
255 if (!result.isNull()) {
256 return result;
257 }
258 }
259 return QDomElement();
260}
261
262QList<QUrl> KBookmarkGroup::groupUrlList() const
263{
264 QList<QUrl> urlList;
265 for (KBookmark bm = first(); !bm.isNull(); bm = next(current: bm)) {
266 if (bm.isSeparator() || bm.isGroup()) {
267 continue;
268 }
269 urlList << bm.url();
270 }
271 return urlList;
272}
273
274//////
275
276KBookmark::KBookmark()
277{
278}
279
280KBookmark::KBookmark(const QDomElement &elem)
281 : element(elem)
282{
283}
284
285bool KBookmark::isGroup() const
286{
287 QString tag = element.tagName();
288 return tag == QLatin1String("folder") //
289 || tag == QLatin1String("xbel"); // don't forget the toplevel group
290}
291
292bool KBookmark::isSeparator() const
293{
294 return (element.tagName() == QLatin1String("separator"));
295}
296
297bool KBookmark::isNull() const
298{
299 return element.isNull();
300}
301
302bool KBookmark::hasParent() const
303{
304 QDomElement parent = element.parentNode().toElement();
305 return !parent.isNull();
306}
307
308QString KBookmark::text() const
309{
310 return KStringHandler::csqueeze(str: fullText());
311}
312
313QString KBookmark::fullText() const
314{
315 if (isSeparator()) {
316 return QCoreApplication::translate(context: "KBookmark", key: "--- separator ---", disambiguation: "Bookmark separator");
317 }
318
319 QString text = element.namedItem(QStringLiteral("title")).toElement().text();
320 text.replace(before: QLatin1Char('\n'), after: QLatin1Char(' ')); // #140673
321 return text;
322}
323
324void KBookmark::setFullText(const QString &fullText)
325{
326 QDomNode titleNode = element.namedItem(QStringLiteral("title"));
327 if (titleNode.isNull()) {
328 titleNode = element.ownerDocument().createElement(QStringLiteral("title"));
329 element.appendChild(newChild: titleNode);
330 }
331
332 if (titleNode.firstChild().isNull()) {
333 QDomText domtext = titleNode.ownerDocument().createTextNode(data: QLatin1String(""));
334 titleNode.appendChild(newChild: domtext);
335 }
336
337 QDomText domtext = titleNode.firstChild().toText();
338 domtext.setData(fullText);
339}
340
341QUrl KBookmark::url() const
342{
343 return QUrl(element.attribute(QStringLiteral("href")));
344}
345
346void KBookmark::setUrl(const QUrl &url)
347{
348 element.setAttribute(QStringLiteral("href"), value: url.toString());
349}
350
351QString KBookmark::icon() const
352{
353 QDomNode metaDataNode = metaData(owner: Strings::metaDataFreedesktopOwner(), create: false);
354 QDomElement iconElement = cd(node: metaDataNode, QStringLiteral("bookmark:icon"), create: false).toElement();
355
356 QString icon = iconElement.attribute(QStringLiteral("name"));
357
358 // migration code
359 if (icon.isEmpty()) {
360 icon = element.attribute(QStringLiteral("icon"));
361 }
362 if (icon == QLatin1String("www")) { // common icon for kde3 bookmarks
363 return QStringLiteral("internet-web-browser");
364 }
365 // end migration code
366
367 if (icon == QLatin1String("bookmark_folder")) {
368 return QStringLiteral("folder-bookmarks");
369 }
370 if (icon.isEmpty()) {
371 // Default icon depends on URL for bookmarks, and is default directory
372 // icon for groups.
373 if (isGroup()) {
374 icon = QStringLiteral("folder-bookmarks");
375 } else {
376 if (isSeparator()) {
377 icon = QStringLiteral("edit-clear"); // whatever
378 } else {
379 // get icon from mimeType
380 QMimeDatabase db;
381 QMimeType mime;
382 QString _mimeType = mimeType();
383 if (!_mimeType.isEmpty()) {
384 mime = db.mimeTypeForName(nameOrAlias: _mimeType);
385 } else {
386 mime = db.mimeTypeForUrl(url: url());
387 }
388 if (mime.isValid()) {
389 icon = mime.iconName();
390 }
391 }
392 }
393 }
394 return icon;
395}
396
397void KBookmark::setIcon(const QString &icon)
398{
399 QDomNode metaDataNode = metaData(owner: Strings::metaDataFreedesktopOwner(), create: true);
400 QDomElement iconElement = cd_or_create(node: metaDataNode, QStringLiteral("bookmark:icon")).toElement();
401 iconElement.setAttribute(QStringLiteral("name"), value: icon);
402
403 // migration code
404 if (!element.attribute(QStringLiteral("icon")).isEmpty()) {
405 element.removeAttribute(QStringLiteral("icon"));
406 }
407}
408
409QString KBookmark::description() const
410{
411 if (isSeparator()) {
412 return QString();
413 }
414
415 QString description = element.namedItem(QStringLiteral("desc")).toElement().text();
416 description.replace(before: QLatin1Char('\n'), after: QLatin1Char(' ')); // #140673
417 return description;
418}
419
420void KBookmark::setDescription(const QString &description)
421{
422 QDomNode descNode = element.namedItem(QStringLiteral("desc"));
423 if (descNode.isNull()) {
424 descNode = element.ownerDocument().createElement(QStringLiteral("desc"));
425 element.appendChild(newChild: descNode);
426 }
427
428 if (descNode.firstChild().isNull()) {
429 QDomText domtext = descNode.ownerDocument().createTextNode(data: QString());
430 descNode.appendChild(newChild: domtext);
431 }
432
433 QDomText domtext = descNode.firstChild().toText();
434 domtext.setData(description);
435}
436
437QString KBookmark::mimeType() const
438{
439 QDomNode metaDataNode = metaData(owner: Strings::metaDataMimeOwner(), create: false);
440 QDomElement mimeTypeElement = cd(node: metaDataNode, QStringLiteral("mime:mime-type"), create: false).toElement();
441 return mimeTypeElement.attribute(QStringLiteral("type"));
442}
443
444void KBookmark::setMimeType(const QString &mimeType)
445{
446 QDomNode metaDataNode = metaData(owner: Strings::metaDataMimeOwner(), create: true);
447 QDomElement iconElement = cd_or_create(node: metaDataNode, QStringLiteral("mime:mime-type")).toElement();
448 iconElement.setAttribute(QStringLiteral("type"), value: mimeType);
449}
450
451bool KBookmark::showInToolbar() const
452{
453 if (element.hasAttribute(QStringLiteral("showintoolbar"))) {
454 bool show = element.attribute(QStringLiteral("showintoolbar")) == QLatin1String("yes");
455 const_cast<QDomElement *>(&element)->removeAttribute(QStringLiteral("showintoolbar"));
456 const_cast<KBookmark *>(this)->setShowInToolbar(show);
457 }
458 return metaDataItem(QStringLiteral("showintoolbar")) == QLatin1String("yes");
459}
460
461void KBookmark::setShowInToolbar(bool show)
462{
463 setMetaDataItem(QStringLiteral("showintoolbar"), value: show ? QStringLiteral("yes") : QStringLiteral("no"));
464}
465
466KBookmarkGroup KBookmark::parentGroup() const
467{
468 return KBookmarkGroup(element.parentNode().toElement());
469}
470
471KBookmarkGroup KBookmark::toGroup() const
472{
473 Q_ASSERT(isGroup());
474 return KBookmarkGroup(element);
475}
476
477QString KBookmark::address() const
478{
479 if (element.tagName() == QLatin1String("xbel")) {
480 return QLatin1String(""); // not QString() !
481 } else {
482 // Use keditbookmarks's DEBUG_ADDRESSES flag to debug this code :)
483 if (element.parentNode().isNull()) {
484 Q_ASSERT(false);
485 return QStringLiteral("ERROR"); // Avoid an infinite loop
486 }
487 KBookmarkGroup group = parentGroup();
488 QString parentAddress = group.address();
489 int pos = group.indexOf(child: *this);
490 Q_ASSERT(pos != -1);
491 return parentAddress + QLatin1Char('/') + QString::number(pos);
492 }
493}
494
495int KBookmark::positionInParent() const
496{
497 return parentGroup().indexOf(child: *this);
498}
499
500QDomElement KBookmark::internalElement() const
501{
502 return element;
503}
504
505KBookmark KBookmark::standaloneBookmark(const QString &text, const QUrl &url, const QString &icon)
506{
507 QDomDocument doc(QStringLiteral("xbel"));
508 QDomElement elem = doc.createElement(QStringLiteral("xbel"));
509 doc.appendChild(newChild: elem);
510 KBookmarkGroup grp(elem);
511 grp.addBookmark(text, url, icon);
512 return grp.first();
513}
514
515QString KBookmark::commonParent(const QString &first, const QString &second)
516{
517 QString A = first;
518 QString B = second;
519 QString error(QStringLiteral("ERROR"));
520 if (A == error || B == error) {
521 return error;
522 }
523
524 A += QLatin1Char('/');
525 B += QLatin1Char('/');
526
527 int lastCommonSlash = 0;
528 int lastPos = A.length() < B.length() ? A.length() : B.length();
529 for (int i = 0; i < lastPos; ++i) {
530 if (A[i] != B[i]) {
531 return A.left(n: lastCommonSlash);
532 }
533 if (A[i] == QLatin1Char('/')) {
534 lastCommonSlash = i;
535 }
536 }
537 return A.left(n: lastCommonSlash);
538}
539
540void KBookmark::updateAccessMetadata()
541{
542 // qCDebug(KBOOKMARKS_LOG) << "KBookmark::updateAccessMetadata " << address() << " " << url();
543
544 const uint timet = QDateTime::currentDateTimeUtc().toSecsSinceEpoch();
545 setMetaDataItem(QStringLiteral("time_added"), value: QString::number(timet), mode: DontOverwriteMetaData);
546 setMetaDataItem(QStringLiteral("time_visited"), value: QString::number(timet));
547
548 QString countStr = metaDataItem(QStringLiteral("visit_count")); // TODO use spec'ed name
549 bool ok;
550 int currentCount = countStr.toInt(ok: &ok);
551 if (!ok) {
552 currentCount = 0;
553 }
554 currentCount++;
555 setMetaDataItem(QStringLiteral("visit_count"), value: QString::number(currentCount));
556
557 // TODO - time_modified
558}
559
560QString KBookmark::parentAddress(const QString &address)
561{
562 return address.left(n: address.lastIndexOf(c: QLatin1Char('/')));
563}
564
565uint KBookmark::positionInParent(const QString &address)
566{
567 return QStringView(address).mid(pos: address.lastIndexOf(c: QLatin1Char('/')) + 1).toInt();
568}
569
570QString KBookmark::previousAddress(const QString &address)
571{
572 uint pp = positionInParent(address);
573 return pp > 0 ? parentAddress(address) + QLatin1Char('/') + QString::number(pp - 1) : QString();
574}
575
576QString KBookmark::nextAddress(const QString &address)
577{
578 return parentAddress(address) + QLatin1Char('/') + QString::number(positionInParent(address) + 1);
579}
580
581QDomNode KBookmark::metaData(const QString &owner, bool create) const
582{
583 QDomNode infoNode = cd(node: internalElement(), QStringLiteral("info"), create);
584 if (infoNode.isNull()) {
585 return QDomNode();
586 }
587 return findMetadata(forOwner: owner, parent&: infoNode, create);
588}
589
590QString KBookmark::metaDataItem(const QString &key) const
591{
592 QDomNode metaDataNode = metaData(owner: Strings::metaDataKDEOwner(), create: false);
593 for (QDomElement e = metaDataNode.firstChildElement(); !e.isNull(); e = e.nextSiblingElement()) {
594 if (e.tagName() == key) {
595 return e.text();
596 }
597 }
598 return QString();
599}
600
601void KBookmark::setMetaDataItem(const QString &key, const QString &value, MetaDataOverwriteMode mode)
602{
603 QDomNode metaDataNode = metaData(owner: Strings::metaDataKDEOwner(), create: true);
604 QDomNode item = cd_or_create(node: metaDataNode, name: key);
605 QDomText text = get_or_create_text(node: item);
606 if (mode == DontOverwriteMetaData && !text.data().isEmpty()) {
607 return;
608 }
609
610 text.setData(value);
611}
612
613bool KBookmark::operator==(const KBookmark &rhs) const
614{
615 return element == rhs.element;
616}
617
618////
619
620KBookmarkGroupTraverser::~KBookmarkGroupTraverser()
621{
622}
623
624void KBookmarkGroupTraverser::traverse(const KBookmarkGroup &root)
625{
626 QStack<KBookmarkGroup> stack;
627 stack.push(t: root);
628 KBookmark bk = root.first();
629 for (;;) {
630 if (bk.isNull()) {
631 if (stack.count() == 1) { // only root is on the stack
632 return;
633 }
634 if (!stack.isEmpty()) {
635 visitLeave(stack.top());
636 bk = stack.pop();
637 }
638 bk = stack.top().next(current: bk);
639 } else if (bk.isGroup()) {
640 KBookmarkGroup gp = bk.toGroup();
641 visitEnter(gp);
642 bk = gp.first();
643 stack.push(t: gp);
644 } else {
645 visit(bk);
646 bk = stack.top().next(current: bk);
647 }
648 }
649}
650
651void KBookmarkGroupTraverser::visit(const KBookmark &)
652{
653}
654
655void KBookmarkGroupTraverser::visitEnter(const KBookmarkGroup &)
656{
657}
658
659void KBookmarkGroupTraverser::visitLeave(const KBookmarkGroup &)
660{
661}
662
663void KBookmark::populateMimeData(QMimeData *mimeData) const
664{
665 KBookmark::List bookmarkList;
666 bookmarkList.append(t: *this);
667 bookmarkList.populateMimeData(mimeData);
668}
669
670KBookmark::List::List()
671 : QList<KBookmark>()
672{
673}
674
675void KBookmark::List::populateMimeData(QMimeData *mimeData) const
676{
677 QList<QUrl> urls;
678
679 QDomDocument doc(QStringLiteral("xbel"));
680 QDomElement elem = doc.createElement(QStringLiteral("xbel"));
681 doc.appendChild(newChild: elem);
682
683 for (const_iterator it = begin(), end = this->end(); it != end; ++it) {
684 urls.append(t: (*it).url());
685 elem.appendChild(newChild: (*it).internalElement().cloneNode(deep: true /* deep */));
686 }
687
688 // This sets text/uri-list and text/plain into the mimedata
689 mimeData->setUrls(urls);
690
691 mimeData->setData(mimetype: Strings::xbelMimeType(), data: doc.toByteArray());
692}
693
694bool KBookmark::List::canDecode(const QMimeData *mimeData)
695{
696 return mimeData->hasFormat(mimetype: Strings::xbelMimeType()) || mimeData->hasUrls();
697}
698
699QStringList KBookmark::List::mimeDataTypes()
700{
701 return QStringList() << Strings::xbelMimeType() << KUrlMimeData::mimeDataTypes();
702}
703
704KBookmark::List KBookmark::List::fromMimeData(const QMimeData *mimeData, QDomDocument &doc)
705{
706 KBookmark::List bookmarks;
707 const QByteArray payload = mimeData->data(mimetype: Strings::xbelMimeType());
708 if (!payload.isEmpty()) {
709 doc.setContent(data: payload);
710 QDomElement elem = doc.documentElement();
711 const QDomNodeList children = elem.childNodes();
712 bookmarks.reserve(asize: children.count());
713 for (int childno = 0; childno < children.count(); childno++) {
714 bookmarks.append(t: KBookmark(children.item(index: childno).toElement()));
715 }
716 return bookmarks;
717 }
718 const QList<QUrl> urls = KUrlMimeData::urlsFromMimeData(mimeData);
719 bookmarks.reserve(asize: urls.size());
720 for (int i = 0; i < urls.size(); ++i) {
721 const QUrl url = urls.at(i);
722 bookmarks.append(t: KBookmark::standaloneBookmark(text: url.toDisplayString(), url, icon: QString() /*TODO icon*/));
723 }
724 return bookmarks;
725}
726

source code of kbookmarks/src/kbookmark.cpp