File indexing completed on 2025-09-14 05:30:45

0001 /*
0002     SPDX-FileCopyrightText: 2007 Glenn Ergeerts <glenn.ergeerts@telenet.be>
0003     SPDX-FileCopyrightText: 2012 Marco Gulino <marco.gulino@xpeppers.com>
0004     SPDX-FileCopyrightText: 2023 Alexander Lohnau <alexander.lohnau@gmx.de>
0005 
0006     SPDX-License-Identifier: LGPL-2.0-or-later
0007 */
0008 
0009 #include "firefox.h"
0010 #include "bookmarkmatch.h"
0011 #include "bookmarks_debug.h"
0012 #include "favicon.h"
0013 #include "faviconfromblob.h"
0014 #include "fetchsqlite.h"
0015 #include <KConfig>
0016 #include <KConfigGroup>
0017 #include <QFile>
0018 #include <QRegularExpression>
0019 #include <QStandardPaths>
0020 
0021 Firefox::Firefox(const QString &firefoxConfigDir, QObject *parent)
0022     : QObject(parent)
0023     , m_dbCacheFile(QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation)
0024                     + QStringLiteral("/bookmarksrunner/bookmarkrunnerfirefoxdbfile.sqlite"))
0025     , m_dbCacheFile_fav(QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation)
0026                         + QStringLiteral("/bookmarksrunner/bookmarkrunnerfirefoxfavdbfile.sqlite"))
0027     , m_favicon(new FallbackFavicon(this))
0028     , m_fetchsqlite(nullptr)
0029     , m_fetchsqlite_fav(nullptr)
0030 {
0031     if (!QSqlDatabase::isDriverAvailable(QStringLiteral("QSQLITE"))) {
0032         qCWarning(RUNNER_BOOKMARKS) << "SQLITE driver isn't available";
0033         return;
0034     }
0035 
0036     // Try to get the right database file, the default profile is used
0037     KConfig firefoxProfile(firefoxConfigDir + "/profiles.ini", KConfig::SimpleConfig);
0038     QStringList profilesList = firefoxProfile.groupList();
0039     profilesList = profilesList.filter(QRegularExpression(QStringLiteral("^Profile\\d+$")));
0040 
0041     QString profilePath;
0042     if (profilesList.size() == 1) {
0043         // There is only 1 profile so we select it
0044         KConfigGroup fGrp = firefoxProfile.group(profilesList.first());
0045         profilePath = fGrp.readEntry("Path");
0046     } else {
0047         const QStringList installConfig = firefoxProfile.groupList().filter(QRegularExpression("^Install.*"));
0048         // The profile with Default=1 is not always the default profile, see BUG: 418526
0049         // If there is only one Install* group it contains the default profile
0050         if (installConfig.size() == 1) {
0051             profilePath = firefoxProfile.group(installConfig.first()).readEntry("Default");
0052         } else {
0053             // There are multiple profiles, find the default one
0054             for (const QString &profileName : std::as_const(profilesList)) {
0055                 KConfigGroup fGrp = firefoxProfile.group(profileName);
0056                 if (fGrp.readEntry<int>("Default", 0)) {
0057                     profilePath = fGrp.readEntry("Path");
0058                     break;
0059                 }
0060             }
0061         }
0062     }
0063     if (profilePath.isEmpty()) {
0064         qCWarning(RUNNER_BOOKMARKS) << "No default firefox profile found";
0065         return;
0066     }
0067     profilePath.prepend(firefoxConfigDir + "/");
0068     m_dbFile = profilePath + "/places.sqlite";
0069     m_dbFile_fav = profilePath + "/favicons.sqlite";
0070 
0071     // We can reuse the favicon instance over the lifetime of the plugin consequently the
0072     // icons that are already written to disk can be reused in multiple match sessions
0073     updateCacheFile(m_dbFile_fav, m_dbCacheFile_fav);
0074     m_fetchsqlite_fav = new FetchSqlite(m_dbCacheFile_fav, this);
0075     delete m_favicon;
0076     m_favicon = FaviconFromBlob::firefox(m_fetchsqlite_fav, this);
0077 }
0078 
0079 Firefox::~Firefox()
0080 {
0081     // Delete the cached databases
0082     if (!m_dbFile.isEmpty()) {
0083         QFile db_CacheFile(m_dbCacheFile);
0084         if (db_CacheFile.exists()) {
0085             db_CacheFile.remove();
0086         }
0087     }
0088     if (!m_dbFile_fav.isEmpty()) {
0089         QFile db_CacheFileFav(m_dbCacheFile_fav);
0090         if (db_CacheFileFav.exists()) {
0091             db_CacheFileFav.remove();
0092         }
0093     }
0094 }
0095 
0096 void Firefox::prepare()
0097 {
0098     if (updateCacheFile(m_dbFile, m_dbCacheFile) != Error) {
0099         m_fetchsqlite = new FetchSqlite(m_dbCacheFile);
0100         m_fetchsqlite->prepare();
0101     }
0102     updateCacheFile(m_dbFile_fav, m_dbCacheFile_fav);
0103     m_favicon->prepare();
0104 }
0105 
0106 QList<BookmarkMatch> Firefox::match(const QString &term, bool addEverything)
0107 {
0108     QList<BookmarkMatch> matches;
0109     if (!m_fetchsqlite) {
0110         return matches;
0111     }
0112 
0113     QString query;
0114     if (addEverything) {
0115         query = QStringLiteral(
0116             "SELECT moz_bookmarks.fk, moz_bookmarks.title, moz_places.url "
0117             "FROM moz_bookmarks, moz_places WHERE "
0118             "moz_bookmarks.type = 1 AND moz_bookmarks.fk = moz_places.id");
0119     } else {
0120         query = QStringLiteral(
0121             "SELECT moz_bookmarks.fk, moz_bookmarks.title, moz_places.url "
0122             "FROM moz_bookmarks, moz_places WHERE "
0123             "moz_bookmarks.type = 1 AND moz_bookmarks.fk = moz_places.id AND "
0124             "(moz_bookmarks.title LIKE :term OR moz_places.url LIKE :term)");
0125     }
0126     const QMap<QString, QVariant> bindVariables{
0127         {QStringLiteral(":term"), QStringLiteral("%%%1%%").arg(term)},
0128     };
0129     const QList<QVariantMap> results = m_fetchsqlite->query(query, bindVariables);
0130     QMultiMap<QString, QString> uniqueResults;
0131     for (const QVariantMap &result : results) {
0132         const QString title = result.value(QStringLiteral("title")).toString();
0133         const QUrl url = result.value(QStringLiteral("url")).toUrl();
0134         if (url.isEmpty() || url.scheme() == QLatin1String("place")) {
0135             // Don't use bookmarks with empty url or Firefox's "place:" scheme,
0136             // e.g. used for "Most Visited" or "Recent Tags"
0137             // qDebug() << "element " << url << " was not added";
0138             continue;
0139         }
0140 
0141         auto urlString = url.toString();
0142         // After joining we may have multiple results for each URL:
0143         // 1) one for each bookmark folder (same or different titles)
0144         // 2) one for each tag (no title for all but the first entry)
0145         auto keyRange = uniqueResults.equal_range(urlString);
0146         auto it = keyRange.first;
0147         if (!title.isEmpty()) {
0148             while (it != keyRange.second) {
0149                 if (*it == title) {
0150                     // same URL and title in multiple bookmark folders
0151                     break;
0152                 }
0153                 if (it->isEmpty()) {
0154                     // add a title if there was none for the URL
0155                     *it = title;
0156                     break;
0157                 }
0158                 ++it;
0159             }
0160         }
0161         if (it == keyRange.second) {
0162             // first or unique entry
0163             uniqueResults.insert(urlString, title);
0164         }
0165     }
0166 
0167     for (auto result = uniqueResults.constKeyValueBegin(); result != uniqueResults.constKeyValueEnd(); ++result) {
0168         const QString url = (*result).first;
0169         BookmarkMatch bookmarkMatch(m_favicon->iconFor(url), term, (*result).second, url);
0170         bookmarkMatch.addTo(matches, addEverything);
0171     }
0172 
0173     return matches;
0174 }
0175 
0176 void Firefox::teardown()
0177 {
0178     if (m_fetchsqlite) {
0179         m_fetchsqlite->teardown();
0180         delete m_fetchsqlite;
0181         m_fetchsqlite = nullptr;
0182     }
0183     m_favicon->teardown();
0184 }