eric7/WebBrowser/AdBlock/AdBlockManager.py

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

eric ide

mercurial