// For license of this file, see /LICENSE.md. #include "network-web/adblock/adblockmanager.h" #include "exceptions/applicationexception.h" #include "exceptions/networkexception.h" #include "miscellaneous/application.h" #include "miscellaneous/settings.h" #include "network-web/adblock/adblockdialog.h" #include "network-web/adblock/adblockicon.h" #include "network-web/adblock/adblockrequestinfo.h" #include "network-web/adblock/adblockurlinterceptor.h" #include "network-web/networkfactory.h" #include "network-web/networkurlinterceptor.h" #include "network-web/webfactory.h" #include #include #include #include #include #include #include #include #include AdBlockManager::AdBlockManager(QObject* parent) : QObject(parent), m_loaded(false), m_enabled(false), m_interceptor(new AdBlockUrlInterceptor(this)), m_serverProcess(nullptr), m_cacheBlocks({}) { m_adblockIcon = new AdBlockIcon(this); m_adblockIcon->setObjectName(QSL("m_adblockIconAction")); m_unifiedFiltersFile = qApp->userDataFolder() + QDir::separator() + QSL("adblock-unified-filters.txt"); } AdBlockManager::~AdBlockManager() { if (m_serverProcess != nullptr && m_serverProcess->state() == QProcess::ProcessState::Running) { m_serverProcess->kill(); } } BlockingResult AdBlockManager::block(const AdblockRequestInfo& request) { if (!isEnabled()) { return { false }; } const QString url_string = request.requestUrl().toEncoded().toLower(); const QString firstparty_url_string = request.firstPartyUrl().toEncoded().toLower(); const QString url_scheme = request.requestUrl().scheme().toLower(); const QPair url_pair = { firstparty_url_string, url_string }; const QString url_type = request.resourceType(); if (!canRunOnScheme(url_scheme)) { return { false }; } else { if (m_serverProcess != nullptr && m_serverProcess->state() == QProcess::ProcessState::Running) { if (m_cacheBlocks.contains(url_pair)) { qDebugNN << LOGSEC_ADBLOCK << "Found blocking data in cache, URL:" << QUOTE_W_SPACE_DOT(url_pair); return m_cacheBlocks.value(url_pair); } try { auto result = askServerIfBlocked(firstparty_url_string, url_string, url_type); m_cacheBlocks.insert(url_pair, result); qDebugNN << LOGSEC_ADBLOCK << "Inserted blocking data to cache for:" << QUOTE_W_SPACE_DOT(url_pair); return result; } catch (const ApplicationException& ex) { qCriticalNN << LOGSEC_ADBLOCK << "HTTP error when calling server for blocking rules:" << QUOTE_W_SPACE_DOT(ex.message()); return { false }; } } else { return { false }; } } } void AdBlockManager::load(bool initial_load) { auto new_enabled = qApp->settings()->value(GROUP(AdBlock), SETTING(AdBlock::AdBlockEnabled)).toBool(); if (!initial_load) { new_enabled = !new_enabled; } if (new_enabled != m_enabled) { emit enabledChanged(new_enabled); qApp->settings()->setValue(GROUP(AdBlock), AdBlock::AdBlockEnabled, new_enabled); } else if (!initial_load) { return; } m_enabled = new_enabled; if (!m_loaded) { qApp->web()->urlIinterceptor()->installUrlInterceptor(m_interceptor); m_loaded = true; } if (m_enabled) { try { updateUnifiedFiltersFile(); } catch (const ApplicationException& ex) { qCriticalNN << LOGSEC_ADBLOCK << "Failed to write unified filters to file or re-start server, error:" << QUOTE_W_SPACE_DOT(ex.message()); qApp->showGuiMessage(Notification::Event::GeneralEvent, tr("AdBlock needs to be configured"), tr("AdBlock component is not configured properly."), QSystemTrayIcon::MessageIcon::Warning, true, {}, [=]() { showDialog(); }); } } } bool AdBlockManager::isEnabled() const { return m_enabled; } bool AdBlockManager::canRunOnScheme(const QString& scheme) const { return !(scheme == QSL("file") || scheme == QSL("qrc") || scheme == QSL("data") || scheme == QSL("abp")); } QString AdBlockManager::elementHidingRulesForDomain(const QUrl& url) const { if (m_serverProcess != nullptr && m_serverProcess->state() == QProcess::ProcessState::Running) { try { auto result = askServerForCosmeticRules(url.toString()); return result; } catch (const ApplicationException& ex) { qCriticalNN << LOGSEC_ADBLOCK << "HTTP error when calling server for cosmetic rules:" << QUOTE_W_SPACE_DOT(ex.message()); return {}; } } else { return {}; } } QStringList AdBlockManager::filterLists() const { return qApp->settings()->value(GROUP(AdBlock), SETTING(AdBlock::FilterLists)).toStringList(); } void AdBlockManager::setFilterLists(const QStringList& filter_lists) { qApp->settings()->setValue(GROUP(AdBlock), AdBlock::FilterLists, filter_lists); } QStringList AdBlockManager::customFilters() const { return qApp->settings()->value(GROUP(AdBlock), SETTING(AdBlock::CustomFilters)).toStringList(); } void AdBlockManager::setCustomFilters(const QStringList& custom_filters) { qApp->settings()->setValue(GROUP(AdBlock), AdBlock::CustomFilters, custom_filters); } QString AdBlockManager::generateJsForElementHiding(const QString& css) { QString source = QL1S("(function() {" "var head = document.getElementsByTagName('head')[0];" "if (!head) return;" "var css = document.createElement('style');" "css.setAttribute('type', 'text/css');" "css.appendChild(document.createTextNode('%1'));" "head.appendChild(css);" "})()"); QString style = css; style.replace(QL1S("'"), QL1S("\\'")); style.replace(QL1S("\n"), QL1S("\\n")); return source.arg(style); } void AdBlockManager::showDialog() { AdBlockDialog(qApp->mainFormWidget()).exec(); } BlockingResult AdBlockManager::askServerIfBlocked(const QString& fp_url, const QString& url, const QString& url_type) const { QJsonObject req_obj; QByteArray out; QElapsedTimer tmr; req_obj["fp_url"] = fp_url; req_obj["url"] = url; req_obj["url_type"] = url_type, req_obj["filter"] = true; tmr.start(); auto network_res = NetworkFactory::performNetworkOperation(QSL("http://%1:%2").arg(QHostAddress(QHostAddress::SpecialAddress::LocalHost).toString(), QString::number(ADBLOCK_SERVER_PORT)), 500, QJsonDocument(req_obj).toJson(), out, QNetworkAccessManager::Operation::PostOperation, { { QSL(HTTP_HEADERS_CONTENT_TYPE).toLocal8Bit(), QSL("application/json").toLocal8Bit() } }); if (network_res.first == QNetworkReply::NetworkError::NoError) { qDebugNN << LOGSEC_ADBLOCK << "Query for blocking info to server took " << tmr.elapsed() << " ms."; QJsonObject out_obj = QJsonDocument::fromJson(out).object(); bool blocking = out_obj["filter"].toObject()["match"].toBool(); return { blocking, blocking ? out_obj["filter"].toObject()["filter"].toObject()["filter"].toString() : QString() }; } else { throw NetworkException(network_res.first); } } QString AdBlockManager::askServerForCosmeticRules(const QString& url) const { QJsonObject req_obj; QByteArray out; QElapsedTimer tmr; req_obj["url"] = url; req_obj["cosmetic"] = true; tmr.start(); auto network_res = NetworkFactory::performNetworkOperation(QSL("http://%1:%2").arg(QHostAddress(QHostAddress::SpecialAddress::LocalHost).toString(), QString::number(ADBLOCK_SERVER_PORT)), 500, QJsonDocument(req_obj).toJson(), out, QNetworkAccessManager::Operation::PostOperation, { { QSL(HTTP_HEADERS_CONTENT_TYPE).toLocal8Bit(), QSL("application/json").toLocal8Bit() } }); if (network_res.first == QNetworkReply::NetworkError::NoError) { qDebugNN << LOGSEC_ADBLOCK << "Query for cosmetic rules to server took " << tmr.elapsed() << " ms."; QJsonObject out_obj = QJsonDocument::fromJson(out).object(); return out_obj["cosmetic"].toObject()["styles"].toString(); } else { throw NetworkException(network_res.first); } } QProcess* AdBlockManager::restartServer(int port) { QString temp_server = QDir::toNativeSeparators(IOFactory::getSystemFolder(QStandardPaths::StandardLocation::TempLocation)) + QDir::separator() + QSL("adblock-server.js"); if (!IOFactory::copyFile(QSL(":/scripts/adblock/adblock-server.js"), temp_server)) { qWarningNN << LOGSEC_ADBLOCK << "Failed to copy server file to TEMP."; } QProcess* proc = new QProcess(this); #if defined(Q_OS_WIN) proc->setProgram(QSL("node.exe")); #else proc->setProgram(QSL("node")); #endif proc->setArguments({ QDir::toNativeSeparators(temp_server), QString::number(port), QDir::toNativeSeparators(m_unifiedFiltersFile) }); proc->setProcessEnvironment(QProcessEnvironment::systemEnvironment()); auto pe = proc->processEnvironment(); QString default_node_path = #if defined(Q_OS_WIN) pe.value(QSL("APPDATA")) + QDir::separator() + QSL("npm") + QDir::separator() + QSL("node_modules"); #elif defined(Q_OS_LINUX) QSL("/usr/lib/node_modules"); #elif defined(Q_OS_MACOS) QSL("/usr/local/lib/node_modules"); #else QSL(""); #endif if (!pe.contains(QSL("NODE_PATH")) && !default_node_path.isEmpty()) { pe.insert(QSL("NODE_PATH"), default_node_path); } proc->setProcessEnvironment(pe); proc->setProcessChannelMode(QProcess::ProcessChannelMode::ForwardedErrorChannel); if (!proc->open() || proc->state() == QProcess::ProcessState::NotRunning || proc->error() != QProcess::ProcessError::UnknownError) { auto ers = proc->errorString(); proc->deleteLater(); throw ApplicationException(ers); } else { qDebugNN << LOGSEC_ADBLOCK << "Started server."; return proc; } } void AdBlockManager::updateUnifiedFiltersFile() { m_cacheBlocks.clear(); if (QFile::exists(m_unifiedFiltersFile)) { QFile::remove(m_unifiedFiltersFile); } QString unified_contents; auto filter_lists = filterLists(); // Download filters one by one and append. for (const QString& filter_list_url : qAsConst(filter_lists)) { if (filter_list_url.simplified().isEmpty()) { continue; } QByteArray out; auto res = NetworkFactory::performNetworkOperation(filter_list_url, 2000, {}, out, QNetworkAccessManager::Operation::GetOperation); if (res.first == QNetworkReply::NetworkError::NoError) { unified_contents = unified_contents.append(QString::fromUtf8(out)); unified_contents = unified_contents.append('\n'); qDebugNN << LOGSEC_ADBLOCK << "Downloaded filter list from" << QUOTE_W_SPACE_DOT(filter_list_url); } else { throw NetworkException(res.first, tr("failed to download filter list '%1'").arg(filter_list_url)); } } unified_contents = unified_contents.append(customFilters().join(QSL("\n"))); // Save. m_unifiedFiltersFile = IOFactory::getSystemFolder(QStandardPaths::StandardLocation::TempLocation) + QDir::separator() + QSL("adblock.filters"); IOFactory::writeFile(m_unifiedFiltersFile, unified_contents.toUtf8()); if (m_enabled) { if (m_serverProcess != nullptr && m_serverProcess->state() == QProcess::ProcessState::Running) { m_serverProcess->kill(); m_serverProcess->waitForFinished(1000); m_serverProcess->deleteLater(); m_serverProcess = nullptr; } m_serverProcess = restartServer(ADBLOCK_SERVER_PORT); } }