1 /* ============================================================
2 * Falkon - Qt web browser
3 * Copyright (C) 2010-2018 David Rosca <nowrep@gmail.com>
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
17 * ============================================================ */
18 #include "adblockmanager.h"
19 #include "adblockdialog.h"
20 #include "adblockmatcher.h"
21 #include "adblocksubscription.h"
22 #include "adblockurlinterceptor.h"
23 #include "datapaths.h"
24 #include "mainapplication.h"
25 #include "webpage.h"
26 #include "qztools.h"
27 #include "browserwindow.h"
28 #include "settings.h"
29 #include "networkmanager.h"
30 
31 #include <QAction>
32 #include <QDateTime>
33 #include <QTextStream>
34 #include <QDir>
35 #include <QTimer>
36 #include <QMessageBox>
37 #include <QUrlQuery>
38 #include <QMutexLocker>
39 #include <QSaveFile>
40 
41 //#define ADBLOCK_DEBUG
42 
43 #ifdef ADBLOCK_DEBUG
44 #include <QElapsedTimer>
45 #endif
46 
Q_GLOBAL_STATIC(AdBlockManager,qz_adblock_manager)47 Q_GLOBAL_STATIC(AdBlockManager, qz_adblock_manager)
48 
49 AdBlockManager::AdBlockManager(QObject* parent)
50     : QObject(parent)
51     , m_loaded(false)
52     , m_enabled(true)
53     , m_matcher(new AdBlockMatcher(this))
54     , m_interceptor(new AdBlockUrlInterceptor(this))
55 {
56     qRegisterMetaType<AdBlockedRequest>();
57 
58     load();
59 }
60 
~AdBlockManager()61 AdBlockManager::~AdBlockManager()
62 {
63     qDeleteAll(m_subscriptions);
64 }
65 
instance()66 AdBlockManager* AdBlockManager::instance()
67 {
68     return qz_adblock_manager();
69 }
70 
setEnabled(bool enabled)71 void AdBlockManager::setEnabled(bool enabled)
72 {
73     if (m_enabled == enabled) {
74         return;
75     }
76 
77     m_enabled = enabled;
78     emit enabledChanged(enabled);
79 
80     Settings settings;
81     settings.beginGroup(QSL("AdBlock"));
82     settings.setValue(QSL("enabled"), m_enabled);
83     settings.endGroup();
84 
85     load();
86     mApp->reloadUserStyleSheet();
87 
88     QMutexLocker locker(&m_mutex);
89 
90     if (m_enabled) {
91         m_matcher->update();
92     } else {
93         m_matcher->clear();
94     }
95 }
96 
subscriptions() const97 QList<AdBlockSubscription*> AdBlockManager::subscriptions() const
98 {
99     return m_subscriptions;
100 }
101 
block(QWebEngineUrlRequestInfo & request,QString & ruleFilter,QString & ruleSubscription)102 bool AdBlockManager::block(QWebEngineUrlRequestInfo &request, QString &ruleFilter, QString &ruleSubscription)
103 {
104     QMutexLocker locker(&m_mutex);
105 
106     if (!isEnabled()) {
107         return false;
108     }
109 
110 #ifdef ADBLOCK_DEBUG
111     QElapsedTimer timer;
112     timer.start();
113 #endif
114     const QString urlString = request.requestUrl().toEncoded().toLower();
115     const QString urlDomain = request.requestUrl().host().toLower();
116     const QString urlScheme = request.requestUrl().scheme().toLower();
117 
118     if (!canRunOnScheme(urlScheme) || !canBeBlocked(request.firstPartyUrl())) {
119         return false;
120     }
121 
122     const AdBlockRule* blockedRule = m_matcher->match(request, urlDomain, urlString);
123 
124     if (blockedRule) {
125         ruleFilter = blockedRule->filter();
126         ruleSubscription = blockedRule->subscription()->title();
127 #ifdef ADBLOCK_DEBUG
128         qDebug() << "BLOCKED: " << timer.elapsed() << blockedRule->filter() << request.requestUrl();
129 #endif
130     }
131 
132 #ifdef ADBLOCK_DEBUG
133     qDebug() << timer.elapsed() << request.requestUrl();
134 #endif
135 
136     return blockedRule;
137 }
138 
blockedRequestsForUrl(const QUrl & url) const139 QVector<AdBlockedRequest> AdBlockManager::blockedRequestsForUrl(const QUrl &url) const
140 {
141     return m_blockedRequests.value(url);
142 }
143 
clearBlockedRequestsForUrl(const QUrl & url)144 void AdBlockManager::clearBlockedRequestsForUrl(const QUrl &url)
145 {
146     if (m_blockedRequests.remove(url)) {
147         emit blockedRequestsChanged(url);
148     }
149 }
150 
disabledRules() const151 QStringList AdBlockManager::disabledRules() const
152 {
153     return m_disabledRules;
154 }
155 
addDisabledRule(const QString & filter)156 void AdBlockManager::addDisabledRule(const QString &filter)
157 {
158     m_disabledRules.append(filter);
159 }
160 
removeDisabledRule(const QString & filter)161 void AdBlockManager::removeDisabledRule(const QString &filter)
162 {
163     m_disabledRules.removeOne(filter);
164 }
165 
addSubscriptionFromUrl(const QUrl & url)166 bool AdBlockManager::addSubscriptionFromUrl(const QUrl &url)
167 {
168     const QList<QPair<QString, QString> > queryItems = QUrlQuery(url).queryItems(QUrl::FullyDecoded);
169 
170     QString subscriptionTitle;
171     QString subscriptionUrl;
172 
173     for (int i = 0; i < queryItems.count(); ++i) {
174         QPair<QString, QString> pair = queryItems.at(i);
175         if (pair.first.endsWith(QL1S("location")))
176             subscriptionUrl = pair.second;
177         else if (pair.first.endsWith(QL1S("title")))
178             subscriptionTitle = pair.second;
179     }
180 
181     if (subscriptionTitle.isEmpty() || subscriptionUrl.isEmpty())
182         return false;
183 
184     const QString message = AdBlockManager::tr("Do you want to add <b>%1</b> subscription?").arg(subscriptionTitle);
185 
186     QMessageBox::StandardButton result = QMessageBox::question(nullptr, AdBlockManager::tr("AdBlock Subscription"), message, QMessageBox::Yes | QMessageBox::No);
187     if (result == QMessageBox::Yes) {
188         AdBlockManager::instance()->addSubscription(subscriptionTitle, subscriptionUrl);
189         AdBlockManager::instance()->showDialog();
190     }
191 
192     return true;
193 }
194 
addSubscription(const QString & title,const QString & url)195 AdBlockSubscription* AdBlockManager::addSubscription(const QString &title, const QString &url)
196 {
197     if (title.isEmpty() || url.isEmpty()) {
198         return nullptr;
199     }
200 
201     QString fileName = QzTools::filterCharsFromFilename(title.toLower()) + QSL(".txt");
202     QString filePath = QzTools::ensureUniqueFilename(DataPaths::currentProfilePath() + QSL("/adblock/") + fileName);
203 
204     QByteArray data = QSL("Title: %1\nUrl: %2\n[Adblock Plus 1.1.1]").arg(title, url).toLatin1();
205 
206     QSaveFile file(filePath);
207     if (!file.open(QFile::WriteOnly)) {
208         qWarning() << "AdBlockManager: Cannot write to file" << filePath;
209         return nullptr;
210     }
211     file.write(data);
212     file.commit();
213 
214     AdBlockSubscription* subscription = new AdBlockSubscription(title, this);
215     subscription->setUrl(QUrl(url));
216     subscription->setFilePath(filePath);
217     subscription->loadSubscription(m_disabledRules);
218 
219     m_subscriptions.insert(m_subscriptions.count() - 1, subscription);
220     connect(subscription, &AdBlockSubscription::subscriptionUpdated, mApp, &MainApplication::reloadUserStyleSheet);
221     connect(subscription, &AdBlockSubscription::subscriptionChanged, this, &AdBlockManager::updateMatcher);
222 
223     return subscription;
224 }
225 
removeSubscription(AdBlockSubscription * subscription)226 bool AdBlockManager::removeSubscription(AdBlockSubscription* subscription)
227 {
228     QMutexLocker locker(&m_mutex);
229 
230     if (!m_subscriptions.contains(subscription) || !subscription->canBeRemoved()) {
231         return false;
232     }
233 
234     QFile(subscription->filePath()).remove();
235     m_subscriptions.removeOne(subscription);
236 
237     m_matcher->update();
238     delete subscription;
239 
240     return true;
241 }
242 
customList() const243 AdBlockCustomList* AdBlockManager::customList() const
244 {
245     for (AdBlockSubscription* subscription : qAsConst(m_subscriptions)) {
246         AdBlockCustomList* list = qobject_cast<AdBlockCustomList*>(subscription);
247 
248         if (list) {
249             return list;
250         }
251     }
252 
253     return nullptr;
254 }
255 
load()256 void AdBlockManager::load()
257 {
258     QMutexLocker locker(&m_mutex);
259 
260     if (m_loaded) {
261         return;
262     }
263 
264 #ifdef ADBLOCK_DEBUG
265     QElapsedTimer timer;
266     timer.start();
267 #endif
268 
269     Settings settings;
270     settings.beginGroup(QSL("AdBlock"));
271     m_enabled = settings.value(QSL("enabled"), m_enabled).toBool();
272     m_disabledRules = settings.value(QSL("disabledRules"), QStringList()).toStringList();
273     QDateTime lastUpdate = settings.value(QSL("lastUpdate"), QDateTime()).toDateTime();
274     settings.endGroup();
275 
276     if (!m_enabled) {
277         return;
278     }
279 
280     QDir adblockDir(DataPaths::currentProfilePath() + QSL("/adblock"));
281     // Create if necessary
282     if (!adblockDir.exists()) {
283         QDir(DataPaths::currentProfilePath()).mkdir(QSL("adblock"));
284     }
285 
286     const auto fileNames = adblockDir.entryList(QStringList(QSL("*.txt")), QDir::Files);
287     for (const QString &fileName : fileNames) {
288         if (fileName == QLatin1String("customlist.txt")) {
289             continue;
290         }
291 
292         const QString absolutePath = adblockDir.absoluteFilePath(fileName);
293         QFile file(absolutePath);
294         if (!file.open(QFile::ReadOnly)) {
295             continue;
296         }
297 
298         QTextStream textStream(&file);
299         textStream.setCodec("UTF-8");
300         QString title = textStream.readLine(1024).remove(QLatin1String("Title: "));
301         QUrl url = QUrl(textStream.readLine(1024).remove(QLatin1String("Url: ")));
302 
303         if (title.isEmpty() || !url.isValid()) {
304             qWarning() << "AdBlockManager: Invalid subscription file" << absolutePath;
305             continue;
306         }
307 
308         AdBlockSubscription* subscription = new AdBlockSubscription(title, this);
309         subscription->setUrl(url);
310         subscription->setFilePath(absolutePath);
311 
312         m_subscriptions.append(subscription);
313     }
314 
315     // Add EasyList + NoCoinList if subscriptions are empty
316     if (m_subscriptions.isEmpty()) {
317         AdBlockSubscription *easyList = new AdBlockSubscription(tr("EasyList"), this);
318         easyList->setUrl(QUrl(ADBLOCK_EASYLIST_URL));
319         easyList->setFilePath(DataPaths::currentProfilePath() + QLatin1String("/adblock/easylist.txt"));
320         m_subscriptions.append(easyList);
321 
322         AdBlockSubscription *noCoinList = new AdBlockSubscription(tr("NoCoin List"), this);
323         noCoinList->setUrl(QUrl(ADBLOCK_NOCOINLIST_URL));
324         noCoinList->setFilePath(DataPaths::currentProfilePath() + QLatin1String("/adblock/nocoinlist.txt"));
325         m_subscriptions.append(noCoinList);
326     }
327 
328     // Append CustomList
329     AdBlockCustomList* customList = new AdBlockCustomList(this);
330     m_subscriptions.append(customList);
331 
332     // Load all subscriptions
333     for (AdBlockSubscription* subscription : qAsConst(m_subscriptions)) {
334         subscription->loadSubscription(m_disabledRules);
335 
336         connect(subscription, &AdBlockSubscription::subscriptionUpdated, mApp, &MainApplication::reloadUserStyleSheet);
337         connect(subscription, &AdBlockSubscription::subscriptionChanged, this, &AdBlockManager::updateMatcher);
338     }
339 
340     if (lastUpdate.addDays(5) < QDateTime::currentDateTime()) {
341         QTimer::singleShot(1000 * 60, this, &AdBlockManager::updateAllSubscriptions);
342     }
343 
344 #ifdef ADBLOCK_DEBUG
345     qDebug() << "AdBlock loaded in" << timer.elapsed();
346 #endif
347 
348     m_matcher->update();
349     m_loaded = true;
350 
351     connect(m_interceptor, &AdBlockUrlInterceptor::requestBlocked, this, [this](const AdBlockedRequest &request) {
352         m_blockedRequests[request.firstPartyUrl].append(request);
353         emit blockedRequestsChanged(request.firstPartyUrl);
354     });
355 
356     mApp->networkManager()->installUrlInterceptor(m_interceptor);
357 }
358 
updateMatcher()359 void AdBlockManager::updateMatcher()
360 {
361     QMutexLocker locker(&m_mutex);
362 
363     mApp->networkManager()->removeUrlInterceptor(m_interceptor);
364     m_matcher->update();
365     mApp->networkManager()->installUrlInterceptor(m_interceptor);
366 }
367 
updateAllSubscriptions()368 void AdBlockManager::updateAllSubscriptions()
369 {
370     for (AdBlockSubscription* subscription : qAsConst(m_subscriptions)) {
371         subscription->updateSubscription();
372     }
373 
374     Settings settings;
375     settings.beginGroup(QSL("AdBlock"));
376     settings.setValue(QSL("lastUpdate"), QDateTime::currentDateTime());
377     settings.endGroup();
378 }
379 
save()380 void AdBlockManager::save()
381 {
382     if (!m_loaded) {
383         return;
384     }
385 
386     for (AdBlockSubscription* subscription : qAsConst(m_subscriptions)) {
387         subscription->saveSubscription();
388     }
389 
390     Settings settings;
391     settings.beginGroup(QSL("AdBlock"));
392     settings.setValue(QSL("enabled"), m_enabled);
393     settings.setValue(QSL("disabledRules"), m_disabledRules);
394     settings.endGroup();
395 }
396 
isEnabled() const397 bool AdBlockManager::isEnabled() const
398 {
399     return m_enabled;
400 }
401 
canRunOnScheme(const QString & scheme) const402 bool AdBlockManager::canRunOnScheme(const QString &scheme) const
403 {
404     return !(scheme == QL1S("file") || scheme == QL1S("qrc") || scheme == QL1S("view-source")
405              || scheme == QL1S("falkon") || scheme == QL1S("data") || scheme == QL1S("abp"));
406 }
407 
canBeBlocked(const QUrl & url) const408 bool AdBlockManager::canBeBlocked(const QUrl &url) const
409 {
410     return !m_matcher->adBlockDisabledForUrl(url);
411 }
412 
elementHidingRules(const QUrl & url) const413 QString AdBlockManager::elementHidingRules(const QUrl &url) const
414 {
415     if (!isEnabled() || !canRunOnScheme(url.scheme()) || !canBeBlocked(url))
416         return QString();
417 
418     return m_matcher->elementHidingRules();
419 }
420 
elementHidingRulesForDomain(const QUrl & url) const421 QString AdBlockManager::elementHidingRulesForDomain(const QUrl &url) const
422 {
423     if (!isEnabled() || !canRunOnScheme(url.scheme()) || !canBeBlocked(url))
424         return QString();
425 
426     return m_matcher->elementHidingRulesForDomain(url.host());
427 }
428 
subscriptionByName(const QString & name) const429 AdBlockSubscription* AdBlockManager::subscriptionByName(const QString &name) const
430 {
431     for (AdBlockSubscription* subscription : qAsConst(m_subscriptions)) {
432         if (subscription->title() == name) {
433             return subscription;
434         }
435     }
436 
437     return nullptr;
438 }
439 
showDialog(QWidget * parent)440 AdBlockDialog *AdBlockManager::showDialog(QWidget *parent)
441 {
442     if (!m_adBlockDialog) {
443         m_adBlockDialog = new AdBlockDialog(parent ? parent : mApp->getWindow());
444     }
445 
446     m_adBlockDialog.data()->show();
447     m_adBlockDialog.data()->raise();
448     m_adBlockDialog.data()->activateWindow();
449 
450     return m_adBlockDialog.data();
451 }
452 
showRule()453 void AdBlockManager::showRule()
454 {
455     if (QAction* action = qobject_cast<QAction*>(sender())) {
456         const AdBlockRule* rule = static_cast<const AdBlockRule*>(action->data().value<void*>());
457 
458         if (rule) {
459             showDialog()->showRule(rule);
460         }
461     }
462 }
463