eric6/WebBrowser/AdBlock/AdBlockManager.py

changeset 6942
2602857055c5
parent 6645
ad476851d7e0
child 7229
53054eb5b15a
equal deleted inserted replaced
6941:f99d60d6b59b 6942:2602857055c5
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2009 - 2019 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing the AdBlock manager.
8 """
9
10 from __future__ import unicode_literals
11
12 import os
13
14 from PyQt5.QtCore import pyqtSignal, QObject, QUrl, QUrlQuery, QFile, \
15 QByteArray, QMutex, QMutexLocker
16 from PyQt5.QtWebEngineCore import QWebEngineUrlRequestInfo
17
18 from E5Gui import E5MessageBox
19
20 from .AdBlockSubscription import AdBlockSubscription
21 from .AdBlockUrlInterceptor import AdBlockUrlInterceptor
22 from .AdBlockMatcher import AdBlockMatcher
23
24 from Utilities.AutoSaver import AutoSaver
25 import Utilities
26 import Preferences
27
28
29 class AdBlockManager(QObject):
30 """
31 Class implementing the AdBlock manager.
32
33 @signal rulesChanged() emitted after some rule has changed
34 @signal requiredSubscriptionLoaded(subscription) emitted to indicate
35 loading of a required subscription is finished (AdBlockSubscription)
36 @signal enabledChanged(enabled) emitted to indicate a change of the
37 enabled state
38 """
39 rulesChanged = pyqtSignal()
40 requiredSubscriptionLoaded = pyqtSignal(AdBlockSubscription)
41 enabledChanged = pyqtSignal(bool)
42
43 def __init__(self, parent=None):
44 """
45 Constructor
46
47 @param parent reference to the parent object
48 @type QObject
49 """
50 super(AdBlockManager, self).__init__(parent)
51
52 self.__loaded = False
53 self.__subscriptionsLoaded = False
54 self.__enabled = False
55 self.__adBlockDialog = None
56 self.__adBlockExceptionsDialog = None
57 self.__adBlockNetwork = None
58 self.__adBlockPage = None
59 self.__subscriptions = []
60 self.__exceptedHosts = Preferences.getWebBrowser("AdBlockExceptions")
61 self.__saveTimer = AutoSaver(self, self.save)
62 self.__limitedEasyList = Preferences.getWebBrowser(
63 "AdBlockUseLimitedEasyList")
64
65 self.__defaultSubscriptionUrlString = \
66 "abp:subscribe?location=" \
67 "https://easylist-downloads.adblockplus.org/easylist.txt&"\
68 "title=EasyList"
69 self.__additionalDefaultSubscriptionUrlStrings = (
70 "abp:subscribe?location=https://raw.githubusercontent.com/"
71 "hoshsadiq/adblock-nocoin-list/master/nocoin.txt&"
72 "title=NoCoin",
73 )
74 self.__customSubscriptionUrlString = \
75 bytes(self.__customSubscriptionUrl().toEncoded()).decode()
76
77 self.__mutex = QMutex()
78 self.__matcher = AdBlockMatcher(self)
79
80 self.rulesChanged.connect(self.__saveTimer.changeOccurred)
81 self.rulesChanged.connect(self.__rulesChanged)
82
83 self.__interceptor = AdBlockUrlInterceptor(self)
84
85 from WebBrowser.WebBrowserWindow import WebBrowserWindow
86 WebBrowserWindow.networkManager().installUrlInterceptor(
87 self.__interceptor)
88
89 def __rulesChanged(self):
90 """
91 Private slot handling a change of the AdBlock rules.
92 """
93 from WebBrowser.WebBrowserWindow import WebBrowserWindow
94 WebBrowserWindow.mainWindow().reloadUserStyleSheet()
95 self.__updateMatcher()
96
97 def close(self):
98 """
99 Public method to close the open search engines manager.
100 """
101 self.__adBlockDialog and self.__adBlockDialog.close()
102 self.__adBlockExceptionsDialog and \
103 self.__adBlockExceptionsDialog.close()
104
105 self.__saveTimer.saveIfNeccessary()
106
107 def isEnabled(self):
108 """
109 Public method to check, if blocking ads is enabled.
110
111 @return flag indicating the enabled state
112 @rtype bool
113 """
114 if not self.__loaded:
115 self.load()
116
117 return self.__enabled
118
119 def setEnabled(self, enabled):
120 """
121 Public slot to set the enabled state.
122
123 @param enabled flag indicating the enabled state
124 @type bool
125 """
126 if self.isEnabled() == enabled:
127 return
128
129 from WebBrowser.WebBrowserWindow import WebBrowserWindow
130 self.__enabled = enabled
131 for mainWindow in WebBrowserWindow.mainWindows():
132 mainWindow.adBlockIcon().setEnabled(enabled)
133 if enabled:
134 self.__loadSubscriptions()
135
136 self.rulesChanged.emit()
137 self.enabledChanged.emit(enabled)
138
139 def block(self, info):
140 """
141 Public method to check, if a request should be blocked.
142
143 @param info request info object
144 @type QWebEngineUrlRequestInfo
145 @return flag indicating to block the request
146 @rtype bool
147 """
148 locker = QMutexLocker(self.__mutex) # __IGNORE_WARNING__
149
150 if not self.isEnabled():
151 return False
152
153 urlString = bytes(info.requestUrl().toEncoded()).decode().lower()
154 urlDomain = info.requestUrl().host().lower()
155 urlScheme = info.requestUrl().scheme().lower()
156
157 if not self.canRunOnScheme(urlScheme) or \
158 not self.__canBeBlocked(info.firstPartyUrl()):
159 return False
160
161 res = False
162 blockedRule = self.__matcher.match(info, urlDomain, urlString)
163
164 if blockedRule:
165 res = True
166 if info.resourceType() == \
167 QWebEngineUrlRequestInfo.ResourceTypeMainFrame:
168 url = QUrl("eric:adblock")
169 query = QUrlQuery()
170 query.addQueryItem("rule", blockedRule.filter())
171 query.addQueryItem(
172 "subscription", blockedRule.subscription().title())
173 url.setQuery(query)
174 info.redirect(url)
175 else:
176 info.block(True)
177
178 return res
179
180 def canRunOnScheme(self, scheme):
181 """
182 Public method to check, if AdBlock can be performed on the scheme.
183
184 @param scheme scheme to check
185 @type str
186 @return flag indicating, that AdBlock can be performed
187 @rtype bool
188 """
189 return scheme not in ["data", "eric", "qthelp", "qrc", "file", "abp"]
190
191 def page(self):
192 """
193 Public method to get a reference to the page block object.
194
195 @return reference to the page block object
196 @rtype AdBlockPage
197 """
198 if self.__adBlockPage is None:
199 from .AdBlockPage import AdBlockPage
200 self.__adBlockPage = AdBlockPage(self)
201 return self.__adBlockPage
202
203 def __customSubscriptionLocation(self):
204 """
205 Private method to generate the path for custom subscriptions.
206
207 @return URL for custom subscriptions
208 @rtype QUrl
209 """
210 dataDir = os.path.join(Utilities.getConfigDir(), "web_browser",
211 "subscriptions")
212 if not os.path.exists(dataDir):
213 os.makedirs(dataDir)
214 fileName = os.path.join(dataDir, "adblock_subscription_custom")
215 return QUrl.fromLocalFile(fileName)
216
217 def __customSubscriptionUrl(self):
218 """
219 Private method to generate the URL for custom subscriptions.
220
221 @return URL for custom subscriptions
222 @rtype QUrl
223 """
224 location = self.__customSubscriptionLocation()
225 encodedUrl = bytes(location.toEncoded()).decode()
226 url = QUrl("abp:subscribe?location={0}&title={1}".format(
227 encodedUrl, self.tr("Custom Rules")))
228 return url
229
230 def customRules(self):
231 """
232 Public method to get a subscription for custom rules.
233
234 @return subscription object for custom rules
235 @rtype AdBlockSubscription
236 """
237 location = self.__customSubscriptionLocation()
238 for subscription in self.__subscriptions:
239 if subscription.location() == location:
240 return subscription
241
242 url = self.__customSubscriptionUrl()
243 customAdBlockSubscription = AdBlockSubscription(url, True, self)
244 self.addSubscription(customAdBlockSubscription)
245 return customAdBlockSubscription
246
247 def subscriptions(self):
248 """
249 Public method to get all subscriptions.
250
251 @return list of subscriptions
252 @rtype list of AdBlockSubscription
253 """
254 if not self.__loaded:
255 self.load()
256
257 return self.__subscriptions[:]
258
259 def subscription(self, location):
260 """
261 Public method to get a subscription based on its location.
262
263 @param location location of the subscription to search for
264 @type str
265 @return subscription or None
266 @rtype AdBlockSubscription
267 """
268 if location != "":
269 for subscription in self.__subscriptions:
270 if subscription.location().toString() == location:
271 return subscription
272
273 return None
274
275 def updateAllSubscriptions(self):
276 """
277 Public method to update all subscriptions.
278 """
279 for subscription in self.__subscriptions:
280 subscription.updateNow()
281
282 def removeSubscription(self, subscription, emitSignal=True):
283 """
284 Public method to remove an AdBlock subscription.
285
286 @param subscription AdBlock subscription to be removed
287 @type AdBlockSubscription
288 @param emitSignal flag indicating to send a signal
289 @type bool
290 """
291 if subscription is None:
292 return
293
294 if subscription.url().toString().startswith(
295 (self.__defaultSubscriptionUrlString,
296 self.__customSubscriptionUrlString)):
297 return
298
299 try:
300 self.__subscriptions.remove(subscription)
301 rulesFileName = subscription.rulesFileName()
302 QFile.remove(rulesFileName)
303 requiresSubscriptions = self.getRequiresSubscriptions(subscription)
304 for requiresSubscription in requiresSubscriptions:
305 self.removeSubscription(requiresSubscription, False)
306 if emitSignal:
307 self.rulesChanged.emit()
308 except ValueError:
309 pass
310
311 def addSubscriptionFromUrl(self, url):
312 """
313 Public method to ad an AdBlock subscription given the abp URL.
314
315 @param url URL to subscribe an AdBlock subscription
316 @type QUrl
317 @return flag indicating success
318 @rtype bool
319 """
320 if url.path() != "subscribe":
321 return False
322
323 title = QUrl.fromPercentEncoding(
324 QByteArray(QUrlQuery(url).queryItemValue("title").encode()))
325 if not title:
326 return False
327
328 res = E5MessageBox.yesNo(
329 None,
330 self.tr("Subscribe?"),
331 self.tr(
332 """<p>Subscribe to this AdBlock subscription?</p>"""
333 """<p>{0}</p>""").format(title))
334 if res:
335 from .AdBlockSubscription import AdBlockSubscription
336 from WebBrowser.WebBrowserWindow import WebBrowserWindow
337
338 dlg = WebBrowserWindow.adBlockManager().showDialog()
339 subscription = AdBlockSubscription(
340 url, False,
341 WebBrowserWindow.adBlockManager())
342 WebBrowserWindow.adBlockManager().addSubscription(subscription)
343 dlg.addSubscription(subscription, False)
344 dlg.setFocus()
345 dlg.raise_()
346
347 return res
348
349 def addSubscription(self, subscription):
350 """
351 Public method to add an AdBlock subscription.
352
353 @param subscription AdBlock subscription to be added
354 @type AdBlockSubscription
355 """
356 if subscription is None:
357 return
358
359 self.__subscriptions.insert(-1, subscription)
360
361 subscription.rulesChanged.connect(self.rulesChanged)
362 subscription.changed.connect(self.rulesChanged)
363 subscription.enabledChanged.connect(self.rulesChanged)
364
365 self.rulesChanged.emit()
366
367 def save(self):
368 """
369 Public method to save the AdBlock subscriptions.
370 """
371 if not self.__loaded:
372 return
373
374 Preferences.setWebBrowser("AdBlockEnabled", self.__enabled)
375 if self.__subscriptionsLoaded:
376 subscriptions = []
377 requiresSubscriptions = []
378 # intermediate store for subscription requiring others
379 for subscription in self.__subscriptions:
380 if subscription is None:
381 continue
382 urlString = bytes(subscription.url().toEncoded()).decode()
383 if "requiresLocation" in urlString:
384 requiresSubscriptions.append(urlString)
385 else:
386 subscriptions.append(urlString)
387 subscription.saveRules()
388 for subscription in requiresSubscriptions:
389 subscriptions.insert(-1, subscription) # custom should be last
390 Preferences.setWebBrowser("AdBlockSubscriptions", subscriptions)
391
392 def load(self):
393 """
394 Public method to load the AdBlock subscriptions.
395 """
396 if self.__loaded:
397 return
398
399 self.__loaded = True
400
401 self.__enabled = Preferences.getWebBrowser("AdBlockEnabled")
402 if self.__enabled:
403 self.__loadSubscriptions()
404
405 def __loadSubscriptions(self):
406 """
407 Private method to load the set of subscriptions.
408 """
409 if self.__subscriptionsLoaded:
410 return
411
412 subscriptions = Preferences.getWebBrowser("AdBlockSubscriptions")
413 if subscriptions:
414 for subscription in subscriptions:
415 if subscription.startswith(self.__customSubscriptionUrlString):
416 break
417 else:
418 subscriptions.append(self.__customSubscriptionUrlString)
419 else:
420 subscriptions = [self.__defaultSubscriptionUrlString] + \
421 self.__additionalDefaultSubscriptionUrlStrings + \
422 [self.__customSubscriptionUrlString]
423 for subscription in subscriptions:
424 url = QUrl.fromEncoded(subscription.encode("utf-8"))
425 adBlockSubscription = AdBlockSubscription(
426 url,
427 subscription.startswith(self.__customSubscriptionUrlString),
428 self,
429 subscription.startswith(self.__defaultSubscriptionUrlString))
430 adBlockSubscription.rulesChanged.connect(self.rulesChanged)
431 adBlockSubscription.changed.connect(self.rulesChanged)
432 adBlockSubscription.enabledChanged.connect(self.rulesChanged)
433 adBlockSubscription.rulesEnabledChanged.connect(
434 self.__updateMatcher)
435 adBlockSubscription.rulesEnabledChanged.connect(
436 self.__saveTimer.changeOccurred)
437 self.__subscriptions.append(adBlockSubscription)
438
439 self.__subscriptionsLoaded = True
440
441 self.__updateMatcher()
442
443 def loadRequiredSubscription(self, location, title):
444 """
445 Public method to load a subscription required by another one.
446
447 @param location location of the required subscription
448 @type str
449 @param title title of the required subscription
450 @type str
451 """
452 # Step 1: check, if the subscription is in the list of subscriptions
453 urlString = "abp:subscribe?location={0}&title={1}".format(
454 location, title)
455 for subscription in self.__subscriptions:
456 if subscription.url().toString().startswith(urlString):
457 # We found it!
458 return
459
460 # Step 2: if it is not, get it
461 url = QUrl.fromEncoded(urlString.encode("utf-8"))
462 adBlockSubscription = AdBlockSubscription(url, False, self)
463 self.addSubscription(adBlockSubscription)
464 self.requiredSubscriptionLoaded.emit(adBlockSubscription)
465
466 def getRequiresSubscriptions(self, subscription):
467 """
468 Public method to get a list of subscriptions, that require the given
469 one.
470
471 @param subscription subscription to check for
472 @type AdBlockSubscription
473 @return list of subscription requiring the given one
474 @rtype list of AdBlockSubscription
475 """
476 subscriptions = []
477 location = subscription.location().toString()
478 for subscription in self.__subscriptions:
479 if subscription.requiresLocation() == location:
480 subscriptions.append(subscription)
481
482 return subscriptions
483
484 def showDialog(self):
485 """
486 Public slot to show the AdBlock subscription management dialog.
487
488 @return reference to the dialog
489 @rtype AdBlockDialog
490 """
491 if self.__adBlockDialog is None:
492 from .AdBlockDialog import AdBlockDialog
493 self.__adBlockDialog = AdBlockDialog(self)
494
495 self.__adBlockDialog.show()
496 return self.__adBlockDialog
497
498 def elementHidingRules(self, url):
499 """
500 Public method to get the element hiding rules.
501
502
503 @param url URL to get hiding rules for
504 @type QUrl
505 @return element hiding rules
506 @rtype str
507 """
508 if not self.isEnabled() or \
509 not self.canRunOnScheme(url.scheme()) or \
510 not self.__canBeBlocked(url):
511 return ""
512
513 return self.__matcher.elementHidingRules()
514
515 def elementHidingRulesForDomain(self, url):
516 """
517 Public method to get the element hiding rules for a domain.
518
519 @param url URL to get hiding rules for
520 @type QUrl
521 @return element hiding rules
522 @rtype str
523 """
524 if not self.isEnabled() or \
525 not self.canRunOnScheme(url.scheme()) or \
526 not self.__canBeBlocked(url):
527 return ""
528
529 return self.__matcher.elementHidingRulesForDomain(url.host())
530
531 def exceptions(self):
532 """
533 Public method to get a list of excepted hosts.
534
535 @return list of excepted hosts
536 @rtype list of str
537 """
538 return self.__exceptedHosts
539
540 def setExceptions(self, hosts):
541 """
542 Public method to set the list of excepted hosts.
543
544 @param hosts list of excepted hosts
545 @type list of str
546 """
547 self.__exceptedHosts = [host.lower() for host in hosts]
548 Preferences.setWebBrowser("AdBlockExceptions", self.__exceptedHosts)
549
550 def addException(self, host):
551 """
552 Public method to add an exception.
553
554 @param host to be excepted
555 @type str
556 """
557 host = host.lower()
558 if host and host not in self.__exceptedHosts:
559 self.__exceptedHosts.append(host)
560 Preferences.setWebBrowser(
561 "AdBlockExceptions", self.__exceptedHosts)
562
563 def removeException(self, host):
564 """
565 Public method to remove an exception.
566
567 @param host to be removed from the list of exceptions
568 @type str
569 """
570 host = host.lower()
571 if host in self.__exceptedHosts:
572 self.__exceptedHosts.remove(host)
573 Preferences.setWebBrowser(
574 "AdBlockExceptions", self.__exceptedHosts)
575
576 def isHostExcepted(self, host):
577 """
578 Public slot to check, if a host is excepted.
579
580 @param host host to check
581 @type str
582 @return flag indicating an exception
583 @rtype bool
584 """
585 host = host.lower()
586 return host in self.__exceptedHosts
587
588 def showExceptionsDialog(self):
589 """
590 Public method to show the AdBlock Exceptions dialog.
591
592 @return reference to the exceptions dialog
593 @rtype AdBlockExceptionsDialog
594 """
595 if self.__adBlockExceptionsDialog is None:
596 from .AdBlockExceptionsDialog import AdBlockExceptionsDialog
597 self.__adBlockExceptionsDialog = AdBlockExceptionsDialog()
598
599 self.__adBlockExceptionsDialog.load(self.__exceptedHosts)
600 self.__adBlockExceptionsDialog.show()
601 return self.__adBlockExceptionsDialog
602
603 def useLimitedEasyList(self):
604 """
605 Public method to test, if limited EasyList rules shall be used.
606
607 @return flag indicating limited EasyList rules
608 @rtype bool
609 """
610 return self.__limitedEasyList
611
612 def setUseLimitedEasyList(self, limited):
613 """
614 Public method to set the limited EasyList flag.
615
616 @param limited flag indicating to use limited EasyList
617 @type bool
618 """
619 self.__limitedEasyList = limited
620
621 for subscription in self.__subscriptions:
622 if subscription.url().toString().startswith(
623 self.__defaultSubscriptionUrlString):
624 subscription.updateNow()
625
626 Preferences.setWebBrowser("AdBlockUseLimitedEasyList", limited)
627
628 def getDefaultSubscriptionUrl(self):
629 """
630 Public method to get the default subscription URL.
631
632 @return default subscription URL
633 @rtype str
634 """
635 return self.__defaultSubscriptionUrlString
636
637 def __updateMatcher(self):
638 """
639 Private slot to update the adblock matcher.
640 """
641 from WebBrowser.WebBrowserWindow import WebBrowserWindow
642 WebBrowserWindow.networkManager().removeUrlInterceptor(
643 self.__interceptor)
644
645 if self.__enabled:
646 self.__matcher.update()
647 else:
648 self.__matcher.clear()
649
650 WebBrowserWindow.networkManager().installUrlInterceptor(
651 self.__interceptor)
652
653 def __canBeBlocked(self, url):
654 """
655 Private method to check, if the given URL could be blocked (i.e. is
656 not whitelisted).
657
658 @param url URL to be checked
659 @type QUrl
660 @return flag indicating that the given URL can be blocked
661 @rtype bool
662 """
663 return not self.__matcher.adBlockDisabledForUrl(url)

eric ide

mercurial