File indexing completed on 2025-01-19 04:47:00

0001 /*
0002    SPDX-FileCopyrightText: 2023-2024 Laurent Montel <montel.org>
0003 
0004    SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "adblockmanager.h"
0008 #include "adblocklistsmanager.h"
0009 
0010 #include "globalsettings_webengineurlinterceptoradblock.h"
0011 #include "libadblockplugin_debug.h"
0012 
0013 #include <QDir>
0014 #include <QFile>
0015 #include <QNetworkReply>
0016 #include <QNetworkRequest>
0017 #include <QStandardPaths>
0018 
0019 [[nodiscard]] QString filterListPath()
0020 {
0021     static const auto path = []() -> QString {
0022         QString path = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/filterlists/");
0023         QDir(path).mkpath(QStringLiteral("."));
0024         return path;
0025     }();
0026     return path;
0027 }
0028 
0029 AdblockManager::AdblockManager(QObject *parent)
0030     : QObject{parent} // parsing the block lists takes some time, try to do it asynchronously
0031                       // if it is not ready when it's needed, reading the future will block
0032     , mAdblockListManager(new AdblockListsManager(this))
0033     , mAdblockInitFuture(std::async(std::launch::async,
0034                                     [this] {
0035                                         return createOrRestoreAdblock();
0036                                     }))
0037     , mAdblock(std::nullopt)
0038 {
0039     reloadConfig();
0040 
0041     connect(&m_networkManager, &QNetworkAccessManager::finished, this, &AdblockManager::handleListFetched);
0042     m_networkManager.setRedirectPolicy(QNetworkRequest::SameOriginRedirectPolicy);
0043 
0044     if (QDir(filterListPath()).isEmpty()) {
0045         refreshLists();
0046     }
0047 }
0048 
0049 AdblockManager::~AdblockManager()
0050 {
0051     if (mAdblock && (*mAdblock)->isValid() && (*mAdblock)->needsSave()) {
0052         (*mAdblock)->save(adblockCacheLocation().toStdString());
0053     }
0054 }
0055 
0056 QString AdblockManager::adblockCacheLocation() const
0057 {
0058     return QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QStringLiteral("/adblockCache");
0059 }
0060 
0061 rust::Box<Adblock> AdblockManager::createOrRestoreAdblock()
0062 {
0063     rust::Box<Adblock> adb = [this] {
0064         auto cacheLocation = adblockCacheLocation();
0065         if (QFile::exists(cacheLocation)) {
0066             return loadAdblock(cacheLocation.toStdString());
0067         }
0068         return newAdblock(mAdblockListManager->filterListsPath().toStdString());
0069     }();
0070 
0071     Q_EMIT adblockInitialized();
0072     return adb;
0073 }
0074 
0075 void copyStream(QIODevice &input, QIODevice &output)
0076 {
0077     constexpr auto BUFFER_SIZE = 1024;
0078 
0079     QByteArray buffer;
0080     buffer.reserve(BUFFER_SIZE);
0081 
0082     while (true) {
0083         int64_t read = input.read(buffer.data(), BUFFER_SIZE);
0084 
0085         if (read > 0) {
0086             output.write(buffer.data(), read);
0087         } else {
0088             break;
0089         }
0090     }
0091 }
0092 
0093 AdblockManager *AdblockManager::self()
0094 {
0095     static AdblockManager s_self = AdblockManager();
0096     return &s_self;
0097 }
0098 
0099 void AdblockManager::reloadConfig()
0100 {
0101     const bool enabled = AdBlockSettings::self()->adBlockEnabled();
0102     Q_EMIT enabledChanged(enabled);
0103 
0104     mAdblockFilterLists.clear();
0105 
0106     const auto filterUrls = AdBlockSettings::self()->adblockFilterUrls();
0107     const auto filterNames = AdBlockSettings::self()->adblockFilterNames();
0108 
0109     auto namesIt = filterNames.begin();
0110     auto urlsIt = filterUrls.begin();
0111 
0112     // Otherwise list is corrupted, but we will still not crash in release mode
0113     Q_ASSERT(filterNames.size() == filterUrls.size());
0114 
0115     while (namesIt != filterNames.end() && urlsIt != filterUrls.end()) {
0116         AdblockFilter filter;
0117         filter.setName(*namesIt);
0118         filter.setUrl(urlsIt->toDisplayString());
0119 
0120         namesIt++;
0121         urlsIt++;
0122         mAdblockFilterLists << filter;
0123     }
0124 }
0125 
0126 void AdblockManager::writeConfig()
0127 {
0128     QStringList filterNames;
0129     QList<QUrl> filterUrls;
0130 
0131     for (const auto &filterList : std::as_const(mAdblockFilterLists)) {
0132         filterNames.push_back(filterList.name());
0133         filterUrls.push_back(QUrl(filterList.url()));
0134     }
0135 
0136     AdBlockSettings::self()->setAdblockFilterNames(filterNames);
0137     AdBlockSettings::self()->setAdblockFilterUrls(filterUrls);
0138     AdBlockSettings::self()->save();
0139 }
0140 
0141 QString AdblockManager::filterListIdFromUrl(const QString &url) const
0142 {
0143     QCryptographicHash fileNameHash(QCryptographicHash::Sha256);
0144     fileNameHash.addData(url.toUtf8());
0145     return QString::fromUtf8(fileNameHash.result().toHex());
0146 }
0147 
0148 QString AdblockManager::adblockListText(const QString &url)
0149 {
0150     const auto id = filterListIdFromUrl(url);
0151 
0152     QFile file(filterListPath() + id);
0153     if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
0154         qCWarning(LIBADBLOCKPLUGIN_PLUGIN_LOG) << "Impossible to open file " << file.fileName();
0155         return {};
0156     }
0157     const QString list = QString::fromLatin1(file.readAll());
0158     return list;
0159 }
0160 
0161 void AdblockManager::handleListFetched(QNetworkReply *reply)
0162 {
0163     Q_ASSERT(reply);
0164 
0165     m_runningRequests--;
0166 
0167     if (m_runningRequests < 1) {
0168         Q_EMIT refreshFinished();
0169     }
0170 
0171     const auto id = filterListIdFromUrl(reply->url().toString());
0172 
0173     QFile file(filterListPath() + id);
0174     if (!file.open(QIODevice::WriteOnly)) {
0175         qCWarning(LIBADBLOCKPLUGIN_PLUGIN_LOG) << "Failed to open" << file.fileName() << "for writing."
0176                                                << "Filter list not updated";
0177         return;
0178     }
0179 
0180     copyStream(*reply, file);
0181 }
0182 
0183 void AdblockManager::refreshLists()
0184 {
0185     // Delete old lists, in case the names change.
0186     // Otherwise we might not be overwriting all of them.
0187     const QDir dir(filterListPath());
0188     const auto entries = dir.entryList(QDir::Files | QDir::NoDotAndDotDot);
0189     for (const auto &entry : entries) {
0190         const QString path{dir.path() + QDir::separator() + entry};
0191         if (!QFile::remove(path)) {
0192             qCWarning(LIBADBLOCKPLUGIN_PLUGIN_LOG) << "Impossible to remove file: " << path;
0193         }
0194     }
0195 
0196     for (const auto &list : std::as_const(mAdblockFilterLists)) {
0197         m_runningRequests++;
0198         m_networkManager.get(QNetworkRequest(QUrl(list.url())));
0199     }
0200 }
0201 
0202 QList<AdblockFilter> AdblockManager::adblockFilterLists() const
0203 {
0204     return mAdblockFilterLists;
0205 }
0206 
0207 void AdblockManager::setAdblockFilterLists(const QList<AdblockFilter> &newAdblockFilterLists)
0208 {
0209     if (mAdblockFilterLists != newAdblockFilterLists) {
0210         mAdblockFilterLists = newAdblockFilterLists;
0211         writeConfig();
0212     }
0213 }
0214 
0215 inline auto resourceTypeToString(const QWebEngineUrlRequestInfo::ResourceType type)
0216 {
0217     // Strings from https://docs.rs/crate/adblock/0.8.1/source/src/request.rs
0218     using Type = QWebEngineUrlRequestInfo::ResourceType;
0219     switch (type) {
0220     case Type::ResourceTypeMainFrame:
0221         return "main_frame";
0222     case Type::ResourceTypeSubFrame:
0223         return "sub_frame";
0224     case Type::ResourceTypeStylesheet:
0225         return "stylesheet";
0226     case Type::ResourceTypeScript:
0227         return "script";
0228     case Type::ResourceTypeFontResource:
0229         return "font";
0230     case Type::ResourceTypeImage:
0231         return "image";
0232     case Type::ResourceTypeSubResource:
0233         return "object_subrequest"; // TODO CHECK
0234     case Type::ResourceTypeObject:
0235         return "object";
0236     case Type::ResourceTypeMedia:
0237         return "media";
0238     case Type::ResourceTypeFavicon:
0239         return "image"; // almost
0240     case Type::ResourceTypeXhr:
0241         return "xhr";
0242     case Type::ResourceTypePing:
0243         return "ping";
0244     case Type::ResourceTypeCspReport:
0245         return "csp_report";
0246     default:
0247         return "other";
0248     }
0249 }
0250 bool AdblockManager::interceptRequest(QWebEngineUrlRequestInfo &info)
0251 {
0252     // Only wait for the adblock initialization if it isn't ready on first use
0253     if (!mAdblock) {
0254         qCDebug(LIBADBLOCKPLUGIN_PLUGIN_LOG) << "Adblock not yet initialized, blindly allowing request";
0255         return false;
0256     }
0257 
0258     const std::string url = info.requestUrl().toString().toStdString();
0259     const std::string firstPartyUrl = info.firstPartyUrl().toString().toStdString();
0260     const AdblockResult result = (*mAdblock)->shouldBlock(url, firstPartyUrl, resourceTypeToString(info.resourceType()));
0261 
0262     const auto &redirect = result.redirect;
0263     const auto &rewrittenUrl = result.rewrittenUrl;
0264     if (!redirect.empty()) {
0265         info.redirect(QUrl(QString::fromStdString(std::string(redirect))));
0266     } else if (result.matched) {
0267         info.block(result.matched);
0268     } else if (!rewrittenUrl.empty()) {
0269         info.redirect(QUrl(QString::fromStdString(std::string(rewrittenUrl))));
0270     }
0271     return true;
0272 }
0273 
0274 void q_cdebug_adblock(const char *message)
0275 {
0276     qCDebug(LIBADBLOCKPLUGIN_PLUGIN_LOG) << "AdblockManager message: " << message;
0277 }
0278 
0279 #include "moc_adblockmanager.cpp"