7#include <QStandardPaths>
12#include <QLoggingCategory>
14#include "BookmarkDatabase.h"
16static Q_LOGGING_CATEGORY(log,
"WebBrowser.Bookmark.DB")
23 g_pDatabase->SetDatabase(database);
24 g_pDatabase->OnInitializeDatabase();
35 bool bRet = g_pDatabase->OpenSQLiteDatabase(
"bookmarks_connect", szFile);
38 g_pDatabase =
nullptr;
45CBookmarkDatabase::CBookmarkDatabase(QObject *parent)
47 , m_TreeDB(
"bookmarks")
49 qDebug(log) << Q_FUNC_INFO;
52CBookmarkDatabase::~CBookmarkDatabase()
54 qDebug(log) << Q_FUNC_INFO;
57bool CBookmarkDatabase::OnInitializeDatabase()
59 m_UrlDB.SetDatabase(GetDatabase());
60 m_UrlDB.OnInitializeDatabase();
61 m_TreeDB.SetDatabase(GetDatabase());
62 m_TreeDB.OnInitializeDatabase();
64 if(m_TreeDB.GetNodeCount() == 0) {
65 m_TreeDB.AddNode(tr(
"Bookmarks"), 0);
66 m_TreeDB.AddNode(tr(
"Other"), 0);
67 m_TreeDB.AddNode(tr(
"Favorites"), 1);
68 m_TreeDB.AddNode(tr(
"Frequently Used Websites"), 1);
73int CBookmarkDatabase::addBookmark(
const BookmarkItem &item)
75 int urlId = m_UrlDB.AddUrl(item.url, item.title, item.icon);
76 auto tree = BookmarkToTree(item);
78 return m_TreeDB.
Add(tree);
81bool CBookmarkDatabase::updateBookmark(
const BookmarkItem &item)
83 bool success = m_UrlDB.UpdateUrl(item.url, item.title, item.icon);
85 qCritical(log) <<
"Failed to update bookmark" << item.url;
88 TreeItem tree = BookmarkToTree(item,
true);
89 success = m_TreeDB.Update(tree);
91 qCritical(log) <<
"Failed to update bookmark" << item.url;
96bool CBookmarkDatabase::deleteBookmark(
int id)
98 return m_TreeDB.Delete(
id);
101bool CBookmarkDatabase::deleteBookmark(
const QList<BookmarkItem> &items)
103 if(items.isEmpty()) {
104 qWarning(log) <<
"The items is empty";
108 foreach(
auto item, items) {
109 lstItems.push_back(item.id);
111 return m_TreeDB.Delete(lstItems);
114bool CBookmarkDatabase::moveBookmark(
int id,
int newFolderId)
116 return m_TreeDB.Move(
id, newFolderId);
119int CBookmarkDatabase::addFolder(
const QString &name,
int parentId)
121 return m_TreeDB.AddNode(name, parentId);
124bool CBookmarkDatabase::renameFolder(
int folderId,
const QString &newName)
126 return m_TreeDB.RenameNode(folderId, newName);
129bool CBookmarkDatabase::deleteFolder(
int folderId)
131 return m_TreeDB.DeleteNode(folderId);
134bool CBookmarkDatabase::moveFolder(
int folderId,
int newParentId)
136 return m_TreeDB.MoveNode(folderId, newParentId);
143 auto leaf = m_TreeDB.GetLeaf(
id);
144 if(leaf.GetId() == 0)
return item;
145 item = TreeToBookmark(leaf);
149QList<BookmarkItem> CBookmarkDatabase::getBookmarkByUrl(
const QString &url)
151 QList<BookmarkItem> lstItems;
154 auto urlItem = m_UrlDB.GetItem(url);
156 foreach(
auto leaf, leaves) {
157 item = TreeToBookmark(leaf, urlItem);
158 lstItems.push_back(item);
163QList<BookmarkItem> CBookmarkDatabase::getAllBookmarks(
int folderId)
165 QList<BookmarkItem> bookmarks;
167 auto leaves = m_TreeDB.
GetLeaves(folderId);
168 foreach(
auto leaf, leaves) {
169 auto item = TreeToBookmark(leaf);
175QList<BookmarkItem> CBookmarkDatabase::searchBookmarks(
const QString &keyword)
177 QList<BookmarkItem> bookmarks;
179 auto urlIds = m_UrlDB.Search(keyword);
180 foreach(
auto urlItem, urlIds) {
184 foreach(
auto t, trees) {
185 auto item = TreeToBookmark(t, urlItem);
195QList<BookmarkItem> CBookmarkDatabase::getAllFolders()
197 QList<BookmarkItem> folders;
199 auto items = m_TreeDB.GetAllNodes();
200 foreach(
auto it, items) {
201 auto book = TreeToBookmark(it);
207QList<BookmarkItem> CBookmarkDatabase::getSubFolders(
int folderId)
209 QList<BookmarkItem> folders;
211 auto items = m_TreeDB.GetSubNodes(folderId);
212 foreach(
auto it, items) {
213 auto book = TreeToBookmark(it);
219bool CBookmarkDatabase::exportToHtml(
const QString &filename)
221 QFile file(filename);
222 if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
229 buildBookmarkDocument(doc);
232 QTextStream out(&file);
233#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
234 out.setEncoding(QStringConverter::Utf8);
236 out.setCodec(
"UTF-8");
238 out << doc.toString(4);
244void CBookmarkDatabase::buildBookmarkDocument(QDomDocument &doc)
247 QDomProcessingInstruction xmlDecl = doc.createProcessingInstruction(
248 "xml",
"version=\"1.0\" encoding=\"UTF-8\""
250 doc.appendChild(xmlDecl);
253 QDomComment comment = doc.createComment(
254 " This is an automatically generated file by Rabbit Remote Control.\n"
255 " It will be read and overwritten.\n"
258 doc.appendChild(comment);
261 QDomDocumentType doctype = doc.implementation().createDocumentType(
262 "NETSCAPE-Bookmark-file-1",
266 doc.appendChild(doctype);
269 QDomElement htmlElement = doc.createElement(
"HTML");
270 doc.appendChild(htmlElement);
273 QDomElement headElement = doc.createElement(
"HEAD");
274 htmlElement.appendChild(headElement);
277 QDomElement metaElement = doc.createElement(
"META");
278 metaElement.setAttribute(
"HTTP-EQUIV",
"Content-Type");
279 metaElement.setAttribute(
"CONTENT",
"text/html; charset=UTF-8");
280 headElement.appendChild(metaElement);
283 QDomElement titleElement = doc.createElement(
"TITLE");
284 QDomText titleText = doc.createTextNode(
"Rabbit Remote Control - Browser: Bookmarks");
285 titleElement.appendChild(titleText);
286 headElement.appendChild(titleElement);
289 QDomElement bodyElement = doc.createElement(
"BODY");
290 htmlElement.appendChild(bodyElement);
293 QDomElement h1Element = doc.createElement(
"H1");
294 QDomText h1Text = doc.createTextNode(
"Bookmarks");
295 h1Element.appendChild(h1Text);
296 bodyElement.appendChild(h1Element);
299 QDomElement dlElement = doc.createElement(
"DL");
300 bodyElement.appendChild(dlElement);
303 buildBookmarkTree(doc, dlElement, 0);
306void CBookmarkDatabase::buildBookmarkTree(QDomDocument &doc,
307 QDomElement &parentElement,
311 QList<BookmarkItem> bookmarks = getAllBookmarks(folderId);
313 for (
const auto &bookmark : bookmarks) {
314 QDomElement dtElement = doc.createElement(
"DT");
315 parentElement.appendChild(dtElement);
317 QDomElement aElement = createBookmarkDomElement(doc, bookmark);
318 dtElement.appendChild(aElement);
322 QList<BookmarkItem> subFolders = getSubFolders(folderId);
324 for (
const auto &folder : subFolders) {
326 QDomElement dtElement = doc.createElement(
"DT");
327 parentElement.appendChild(dtElement);
330 QDomElement h3Element = doc.createElement(
"H3");
333 if (folder.createdTime.isValid()) {
334 qint64 timestamp = folder.createdTime.toSecsSinceEpoch();
335 h3Element.setAttribute(
"ADD_DATE", QString::number(timestamp));
337 h3Element.setAttribute(
"ADD_DATE",
"0");
341 QDomText folderNameText = doc.createTextNode(folder.title);
342 h3Element.appendChild(folderNameText);
344 dtElement.appendChild(h3Element);
347 QDomElement childDlElement = doc.createElement(
"DL");
348 dtElement.appendChild(childDlElement);
351 buildBookmarkTree(doc, childDlElement, folder.id);
355QDomElement CBookmarkDatabase::createBookmarkDomElement(
358 QDomElement aElement = doc.createElement(
"A");
361 aElement.setAttribute(
"HREF", bookmark.url);
364 QDateTime defaultTime = QDateTime::currentDateTime();
366 qint64 addDate = bookmark.createdTime.isValid() ?
367 bookmark.createdTime.toSecsSinceEpoch() :
368 defaultTime.toSecsSinceEpoch();
369 qint64 lastVisit = bookmark.lastVisitTime.isValid() ?
370 bookmark.lastVisitTime.toSecsSinceEpoch() : 0;
371 qint64 lastModified = bookmark.modifiedTime.isValid() ?
372 bookmark.modifiedTime.toSecsSinceEpoch() :
375 aElement.setAttribute(
"ADD_DATE", QString::number(addDate));
376 aElement.setAttribute(
"LAST_VISIT_TIME", QString::number(lastVisit));
377 aElement.setAttribute(
"MODIFIED_TIME", QString::number(lastModified));
380 QString displayTitle = bookmark.title.isEmpty() ? bookmark.url : bookmark.title;
381 QDomText titleText = doc.createTextNode(displayTitle);
382 aElement.appendChild(titleText);
387bool CBookmarkDatabase::importFromHtml(
const QString &filename)
389 QFile file(filename);
390 if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
391 qWarning() <<
"Failed to open file for import:" << filename;
395 QTextStream in(&file);
396#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
397 in.setEncoding(QStringConverter::Utf8);
399 in.setCodec(
"UTF-8");
402 QString content = in.readAll();
405 qDebug(log) <<
"Importing HTML file:" << filename
406 <<
"File size:" << content.size() <<
"bytes";
409 if (!content.contains(
"<!DOCTYPE", Qt::CaseInsensitive) ||
410 !content.contains(
"NETSCAPE-Bookmark-file-1", Qt::CaseInsensitive)) {
411 qWarning() <<
"Not a valid Netscape bookmark file:" << filename;
412 QMessageBox::warning(
nullptr, tr(
"Format error"),
413 tr(
"This is not a valid bookmark file format.\n"
414 "Please select the HTML bookmark file exported from your browser."));
419 GetDatabase().transaction();
422 int importedCount = parseHtmlBookmarks(content);
424 if (!GetDatabase().commit()) {
425 throw QString(
"Failed to commit transaction: %1").arg(GetDatabase().lastError().text());
428 emit bookmarksChanged();
430 qDebug(log) <<
"Successfully imported" << importedCount <<
"bookmarks";
433 }
catch (
const QString &error) {
434 GetDatabase().rollback();
435 qWarning(log) <<
"Import failed:" << error;
440int CBookmarkDatabase::parseHtmlBookmarks(
const QString &htmlContent)
442 int importedCount = 0;
446 if (!doc.setContent(htmlContent)) {
447 throw QString(
"Invalid HTML format");
451 QDomElement root = doc.documentElement();
453 throw QString(
"No root element found");
457 QDomElement bodyElement = findFirstElement(root,
"BODY");
458 if (bodyElement.isNull()) {
459 throw QString(
"No body node found");
463 QDomElement dlElement = findFirstElement(bodyElement,
"DL");
464 if (dlElement.isNull()) {
465 throw QString(
"No bookmarks list found");
469 QMap<QString, int> folderMap;
472 importedCount = parseBookmarkList(dlElement,
"/", folderMap);
474 return importedCount;
477int CBookmarkDatabase::parseBookmarkList(
const QDomElement &dlElement,
478 const QString ¤tPath,
479 QMap<QString, int> &folderMap)
481 int importedCount = 0;
482 QDomNode child = dlElement.firstChild();
484 while (!child.isNull()) {
485 QDomElement element = child.toElement();
487 if (!element.isNull()) {
488 QString tagName = element.tagName().toUpper();
490 if (tagName ==
"DT") {
492 importedCount += parseDtElement(element, currentPath, folderMap);
493 }
else if (tagName ==
"DL") {
495 importedCount += parseBookmarkList(element, currentPath, folderMap);
499 child = child.nextSibling();
502 return importedCount;
505int CBookmarkDatabase::parseDtElement(
const QDomElement &dtElement,
506 const QString ¤tPath,
507 QMap<QString, int> &folderMap)
509 int importedCount = 0;
511 QString parentPath = currentPath;
514 QDomNode child = dtElement.firstChild();
515 while (!child.isNull()) {
516 QDomElement element = child.toElement();
518 if (!element.isNull()) {
519 QString tagName = element.tagName().toUpper();
521 if (tagName ==
"A") {
523 importedCount += importBookmark(element, currentPath, folderMap);
524 }
else if (tagName ==
"H3") {
526 parentPath = importFolder(element, currentPath, folderMap);
527 }
else if (tagName ==
"DL") {
529 importedCount += parseBookmarkList(element, parentPath, folderMap);
533 child = child.nextSibling();
536 return importedCount;
539int CBookmarkDatabase::importBookmark(
const QDomElement &aElement,
540 const QString &folderPath,
541 QMap<QString, int> &folderMap)
544 QString url = aElement.attribute(
"HREF");
545 QString title = aElement.text();
547 if (url.isEmpty() || title.isEmpty()) {
552 QString addDateStr = aElement.attribute(
"ADD_DATE");
553 QString lastVisitStr = aElement.attribute(
"LAST_VISIT");
554 QString lastModifiedStr = aElement.attribute(
"LAST_MODIFIED");
556 QDateTime addDate = parseTimestamp(addDateStr);
557 QDateTime lastVisit = parseTimestamp(lastVisitStr);
558 QDateTime lastModified = parseTimestamp(lastModifiedStr);
564 item.createdTime = addDate.isValid() ? addDate : QDateTime::currentDateTime();
565 item.lastVisitTime = lastVisit;
566 item.modifiedTime = lastModified.isValid() ? lastModified : item.createdTime;
569 int folderId = getOrCreateFolder(folderPath, folderMap);
570 item.folderId = folderId;
573 auto lstExisting = getBookmarkByUrl(url);
574 if (lstExisting.isEmpty()) {
576 if (addBookmark(item)) {
577 qDebug(log) <<
"Imported bookmark:" << title << folderPath <<
"parentID:" << folderId;
581 foreach(
auto exist, lstExisting) {
583 exist.title = item.title;
584 exist.folderId = item.folderId;
585 exist.lastVisitTime = item.lastVisitTime;
586 exist.modifiedTime = QDateTime::currentDateTime();
588 if (updateBookmark(exist)) {
589 qDebug(log) <<
"Updated existing bookmark:" << title << folderPath <<
"parentID:" << folderId;
593 return lstExisting.count();
599QString CBookmarkDatabase::importFolder(
const QDomElement &h3Element,
600 const QString &parentPath,
601 QMap<QString, int> &folderMap)
603 QString folderName = h3Element.text();
604 if (folderName.isEmpty()) {
609 QString folderPath = parentPath;
610 if (!parentPath.endsWith(
"/")) {
613 folderPath += folderName;
616 int parentFolderId = folderMap.value(parentPath, 0);
619 int folderId = getOrCreateFolder(folderPath, parentFolderId);
620 folderMap[folderPath] = folderId;
622 qDebug(log) <<
"Imported folder:" << folderName <<
"Path:" << folderPath <<
"ID:" << folderId;
627int CBookmarkDatabase::getOrCreateFolder(
const QString &folderPath,
int parentFolderId)
629 if (folderPath.isEmpty() || folderPath ==
"/") {
634 QStringList pathParts = folderPath.split(
"/", Qt::SkipEmptyParts);
635 if (pathParts.isEmpty()) {
639 QString folderName = pathParts.last();
642 QSqlQuery query(GetDatabase());
643 query.prepare(
"SELECT id FROM bookmark_folders WHERE name = :name AND parent_id = :parent_id");
644 query.bindValue(
":name", folderName);
645 query.bindValue(
":parent_id", parentFolderId);
647 if (query.exec() && query.next()) {
648 return query.value(0).toInt();
653 "INSERT INTO bookmark_folders (name, parent_id, created_time) "
654 "VALUES (:name, :parent_id, :created_time)"
656 query.bindValue(
":name", folderName);
657 query.bindValue(
":parent_id", parentFolderId);
658 query.bindValue(
":created_time", QDateTime::currentDateTime());
661 return query.lastInsertId().toInt();
664 qCritical(log) <<
"Failed to create folder:" << folderName << query.lastError().text();
668int CBookmarkDatabase::getOrCreateFolder(
const QString &folderPath,
669 QMap<QString, int> &folderMap)
671 if (folderPath.isEmpty() || folderPath ==
"/") {
675 if(folderMap.contains(folderPath))
676 return folderMap[folderPath];
678 QStringList pathParts = folderPath.split(
"/", Qt::SkipEmptyParts);
679 int currentParentId = 0;
682 for (
const QString &part : pathParts) {
683 if (part.isEmpty())
continue;
685 currentParentId = getOrCreateFolder(part, currentParentId);
688 return currentParentId;
691QDateTime CBookmarkDatabase::parseTimestamp(
const QString ×tampStr)
693 if (timestampStr.isEmpty()) {
698 qint64 timestamp = timestampStr.toLongLong(&ok);
705 if (timestamp > 10000000000) {
707 return QDateTime::fromMSecsSinceEpoch(timestamp);
710 return QDateTime::fromSecsSinceEpoch(timestamp);
714QDomElement CBookmarkDatabase::findFirstElement(
const QDomElement &parent,
const QString &tagName)
716 QDomNode child = parent.firstChild();
718 while (!child.isNull()) {
719 QDomElement element = child.toElement();
721 if (!element.isNull() && element.tagName().toUpper() == tagName.toUpper()) {
725 child = child.nextSibling();
728 return QDomElement();
735 type = tree.type == BookmarkType_Folder ? TreeItem::Node: TreeItem::Leaf;
738 item.SetParentId(tree.folderId);
739 item.SetCreateTime(tree.createdTime);
740 item.SetModifyTime(tree.modifiedTime);
741 item.SetLastVisitTime(tree.lastVisitTime);
742 if(setKey && BookmarkType_Bookmark == tree.type && !tree.url.isEmpty()) {
743 int id = m_UrlDB.GetId(tree.url);
753 item = TreeToBookmark(tree, url);
761 item.id = tree.GetId();
762 item.folderId = tree.GetParentId();
763 item.createdTime = tree.GetCreateTime();
764 item.modifiedTime = tree.GetModifyTime();
765 item.lastVisitTime = tree.GetLastVisitTime();
768 item.type = BookmarkType_Folder;
769 item.title = tree.GetName();
771 item.type = BookmarkType_Bookmark;
772 item.title = url.szTitle;
773 item.url = url.szUrl;
774 item.icon = url.icon;
780bool CBookmarkDatabase::ExportToJson(QJsonObject &obj)
785bool CBookmarkDatabase::ImportFromJson(
const QJsonObject &obj)
virtual int Add(const TreeItem &item)
Add item
QList< TreeItem > GetLeavesByKey(int key)
Get leaves
QList< TreeItem > GetLeaves(int nodeId)
Get the leaves under nodeId