Sat, 12 Mar 2016 20:05:01 +0100
Continued porting the web browser.
- started porting the AdBlock code
--- a/Preferences/__init__.py Sat Mar 12 17:02:04 2016 +0100 +++ b/Preferences/__init__.py Sat Mar 12 20:05:01 2016 +0100 @@ -1065,6 +1065,11 @@ "SyncFtpPort": 21, "SyncFtpIdleTimeout": 30, "SyncDirectoryPath": "", + # AdBlock + "AdBlockEnabled": False, + "AdBlockSubscriptions": [], + "AdBlockUpdatePeriod": 1, + "AdBlockExceptions": [], # Flash Cookie Manager: identical to helpDefaults # PIM: identical to helpDefaults # VirusTotal: identical to helpDefaults @@ -2787,7 +2792,6 @@ ## elif key in ["StartupBehavior", ## "OfflineStorageDatabaseQuota", ## "OfflineWebApplicationCacheQuota", "CachePolicy", -## "AdBlockUpdatePeriod", ## ]: elif key in ["StartupBehavior", "HistoryLimit", "DownloadManagerRemovePolicy","SyncType", "SyncFtpPort", @@ -2796,10 +2800,11 @@ "DefaultFontSize", "DefaultFixedFontSize", "MinimumFontSize", "MinimumLogicalFontSize", "DiskCacheSize", "AcceptCookies", "KeepCookiesUntil", + "AdBlockUpdatePeriod", ]: return int(prefClass.settings.value( "WebBrowser/" + key, prefClass.webBrowserDefaults[key])) -## elif key in ["PrintBackgrounds", "AdBlockEnabled" +## elif key in ["PrintBackgrounds", ## "JavaEnabled", ## "JavaScriptCanCloseWindows", ## "PluginsEnabled", "DnsPrefetchEnabled", @@ -2822,13 +2827,14 @@ "SyncEncryptData", "SyncEncryptPasswordsOnly", "ShowPreview", "WebInspectorEnabled", "DiskCacheEnabled", "DoNotTrack", "SendReferer", "FilterTrackingCookies", + "AdBlockEnabled", ]: return toBool(prefClass.settings.value( "WebBrowser/" + key, prefClass.webBrowserDefaults[key])) -## elif key in ["AdBlockSubscriptions", "AdBlockExceptions", -## "ClickToFlashWhitelist", +## elif key in ["ClickToFlashWhitelist", ## "NoCacheHosts", elif key in ["GreaseMonkeyDisabledScripts", "SendRefererWhitelist", + "AdBlockSubscriptions", "AdBlockExceptions", ]: return toList(prefClass.settings.value( "WebBrowser/" + key, prefClass.helpDefaults[key]))
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/AdBlock/AdBlockManager.py Sat Mar 12 20:05:01 2016 +0100 @@ -0,0 +1,481 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the AdBlock manager. +""" + +from __future__ import unicode_literals + +import os + +from PyQt5.QtCore import pyqtSignal, QObject, QUrl, QFile + +from .AdBlockSubscription import AdBlockSubscription + +from Utilities.AutoSaver import AutoSaver +import Utilities +import Preferences + + +class AdBlockManager(QObject): + """ + Class implementing the AdBlock manager. + + @signal rulesChanged() emitted after some rule has changed + """ + rulesChanged = pyqtSignal() + requiredSubscriptionLoaded = pyqtSignal(AdBlockSubscription) + + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent object (QObject) + """ + super(AdBlockManager, self).__init__(parent) + + self.__loaded = False + self.__subscriptionsLoaded = False + self.__enabled = False + self.__adBlockDialog = None + self.__adBlockExceptionsDialog = None + self.__adBlockNetwork = None + self.__adBlockPage = None + self.__subscriptions = [] + self.__exceptedHosts = Preferences.getWebBrowser("AdBlockExceptions") + self.__saveTimer = AutoSaver(self, self.save) + + self.__defaultSubscriptionUrlString = \ + "abp:subscribe?location=" \ + "https://easylist-downloads.adblockplus.org/easylist.txt&"\ + "title=EasyList" + self.__customSubscriptionUrlString = \ + bytes(self.__customSubscriptionUrl().toEncoded()).decode() + + self.rulesChanged.connect(self.__saveTimer.changeOccurred) + + def close(self): + """ + Public method to close the open search engines manager. + """ + self.__adBlockDialog and self.__adBlockDialog.close() + self.__adBlockExceptionsDialog and \ + self.__adBlockExceptionsDialog.close() + + self.__saveTimer.saveIfNeccessary() + + def isEnabled(self): + """ + Public method to check, if blocking ads is enabled. + + @return flag indicating the enabled state (boolean) + """ + if not self.__loaded: + self.load() + + return self.__enabled + + def setEnabled(self, enabled): + """ + Public slot to set the enabled state. + + @param enabled flag indicating the enabled state (boolean) + """ + if self.isEnabled() == enabled: + return + + from WebBrowser.WebBrowserWindow import WebBrowserWindow + self.__enabled = enabled + for mainWindow in WebBrowserWindow.mainWindows(): + mainWindow.adBlockIcon().setEnabled(enabled) + if enabled: + self.__loadSubscriptions() + self.rulesChanged.emit() +## +## def network(self): +## """ +## Public method to get a reference to the network block object. +## +## @return reference to the network block object (AdBlockNetwork) +## """ +## if self.__adBlockNetwork is None: +## from .AdBlockNetwork import AdBlockNetwork +## self.__adBlockNetwork = AdBlockNetwork(self) +## return self.__adBlockNetwork + + def page(self): + """ + Public method to get a reference to the page block object. + + @return reference to the page block object (AdBlockPage) + """ + if self.__adBlockPage is None: + from .AdBlockPage import AdBlockPage + self.__adBlockPage = AdBlockPage(self) + return self.__adBlockPage + + def __customSubscriptionLocation(self): + """ + Private method to generate the path for custom subscriptions. + + @return URL for custom subscriptions (QUrl) + """ + dataDir = os.path.join(Utilities.getConfigDir(), "web_browser", + "subscriptions") + if not os.path.exists(dataDir): + os.makedirs(dataDir) + fileName = os.path.join(dataDir, "adblock_subscription_custom") + return QUrl.fromLocalFile(fileName) + + def __customSubscriptionUrl(self): + """ + Private method to generate the URL for custom subscriptions. + + @return URL for custom subscriptions (QUrl) + """ + location = self.__customSubscriptionLocation() + encodedUrl = bytes(location.toEncoded()).decode() + url = QUrl("abp:subscribe?location={0}&title={1}".format( + encodedUrl, self.tr("Custom Rules"))) + return url + + def customRules(self): + """ + Public method to get a subscription for custom rules. + + @return subscription object for custom rules (AdBlockSubscription) + """ + location = self.__customSubscriptionLocation() + for subscription in self.__subscriptions: + if subscription.location() == location: + return subscription + + url = self.__customSubscriptionUrl() + customAdBlockSubscription = AdBlockSubscription(url, True, self) + self.addSubscription(customAdBlockSubscription) + return customAdBlockSubscription + + def subscriptions(self): + """ + Public method to get all subscriptions. + + @return list of subscriptions (list of AdBlockSubscription) + """ + if not self.__loaded: + self.load() + + return self.__subscriptions[:] + + def subscription(self, location): + """ + Public method to get a subscription based on its location. + + @param location location of the subscription to search for (string) + @return subscription or None (AdBlockSubscription) + """ + if location != "": + for subscription in self.__subscriptions: + if subscription.location().toString() == location: + return subscription + + return None + + def updateAllSubscriptions(self): + """ + Public method to update all subscriptions. + """ + for subscription in self.__subscriptions: + subscription.updateNow() + + def removeSubscription(self, subscription, emitSignal=True): + """ + Public method to remove an AdBlock subscription. + + @param subscription AdBlock subscription to be removed + (AdBlockSubscription) + @param emitSignal flag indicating to send a signal (boolean) + """ + if subscription is None: + return + + if subscription.url().toString().startswith( + (self.__defaultSubscriptionUrlString, + self.__customSubscriptionUrlString)): + return + + try: + self.__subscriptions.remove(subscription) + rulesFileName = subscription.rulesFileName() + QFile.remove(rulesFileName) + requiresSubscriptions = self.getRequiresSubscriptions(subscription) + for requiresSubscription in requiresSubscriptions: + self.removeSubscription(requiresSubscription, False) + if emitSignal: + self.rulesChanged.emit() + except ValueError: + pass + + def addSubscription(self, subscription): + """ + Public method to add an AdBlock subscription. + + @param subscription AdBlock subscription to be added + (AdBlockSubscription) + """ + if subscription is None: + return + + self.__subscriptions.insert(-1, subscription) + + subscription.rulesChanged.connect(self.rulesChanged) + subscription.changed.connect(self.rulesChanged) + subscription.enabledChanged.connect(self.rulesChanged) + + self.rulesChanged.emit() + + def save(self): + """ + Public method to save the AdBlock subscriptions. + """ + if not self.__loaded: + return + + Preferences.setWebBrowser("AdBlockEnabled", self.__enabled) + if self.__subscriptionsLoaded: + subscriptions = [] + requiresSubscriptions = [] + # intermediate store for subscription requiring others + for subscription in self.__subscriptions: + if subscription is None: + continue + urlString = bytes(subscription.url().toEncoded()).decode() + if "requiresLocation" in urlString: + requiresSubscriptions.append(urlString) + else: + subscriptions.append(urlString) + subscription.saveRules() + for subscription in requiresSubscriptions: + subscriptions.insert(-1, subscription) # custom should be last + Preferences.setWebBrowser("AdBlockSubscriptions", subscriptions) + + def load(self): + """ + Public method to load the AdBlock subscriptions. + """ + if self.__loaded: + return + + self.__loaded = True + + self.__enabled = Preferences.getWebBrowser("AdBlockEnabled") + if self.__enabled: + self.__loadSubscriptions() + + def __loadSubscriptions(self): + """ + Private method to load the set of subscriptions. + """ + if self.__subscriptionsLoaded: + return + + subscriptions = Preferences.getWebBrowser("AdBlockSubscriptions") + if subscriptions: + for subscription in subscriptions: + if subscription.startswith( + self.__defaultSubscriptionUrlString): + break + else: + subscriptions.insert(0, self.__defaultSubscriptionUrlString) + for subscription in subscriptions: + if subscription.startswith(self.__customSubscriptionUrlString): + break + else: + subscriptions.append(self.__customSubscriptionUrlString) + else: + subscriptions = [self.__defaultSubscriptionUrlString, + self.__customSubscriptionUrlString] + for subscription in subscriptions: + url = QUrl.fromEncoded(subscription.encode("utf-8")) + adBlockSubscription = AdBlockSubscription( + url, + subscription.startswith(self.__customSubscriptionUrlString), + self, + subscription.startswith(self.__defaultSubscriptionUrlString)) + adBlockSubscription.rulesChanged.connect(self.rulesChanged) + adBlockSubscription.changed.connect(self.rulesChanged) + adBlockSubscription.enabledChanged.connect(self.rulesChanged) + self.__subscriptions.append(adBlockSubscription) + + self.__subscriptionsLoaded = True + + def loadRequiredSubscription(self, location, title): + """ + Public method to load a subscription required by another one. + + @param location location of the required subscription (string) + @param title title of the required subscription (string) + """ + # Step 1: check, if the subscription is in the list of subscriptions + urlString = "abp:subscribe?location={0}&title={1}".format( + location, title) + for subscription in self.__subscriptions: + if subscription.url().toString().startswith(urlString): + # We found it! + return + + # Step 2: if it is not, get it + url = QUrl.fromEncoded(urlString.encode("utf-8")) + adBlockSubscription = AdBlockSubscription(url, False, self) + self.addSubscription(adBlockSubscription) + self.requiredSubscriptionLoaded.emit(adBlockSubscription) + + def getRequiresSubscriptions(self, subscription): + """ + Public method to get a list of subscriptions, that require the given + one. + + @param subscription subscription to check for (AdBlockSubscription) + @return list of subscription requiring the given one (list of + AdBlockSubscription) + """ + subscriptions = [] + location = subscription.location().toString() + for subscription in self.__subscriptions: + if subscription.requiresLocation() == location: + subscriptions.append(subscription) + + return subscriptions + + def showDialog(self): + """ + Public slot to show the AdBlock subscription management dialog. + + @return reference to the dialog (AdBlockDialog) + """ + if self.__adBlockDialog is None: + from .AdBlockDialog import AdBlockDialog + self.__adBlockDialog = AdBlockDialog() + + self.__adBlockDialog.show() + return self.__adBlockDialog + + def showRule(self): + """ + Public slot to show an AdBlock rule. + """ + act = self.sender() + if act is not None: + rule = act.data() + if rule: + self.showDialog().showRule(rule) + + def elementHidingRules(self): + """ + Public method to get the element hiding rules. + + @return element hiding rules (string) + """ + if not self.__enabled: + return "" + + rules = "" + + for subscription in self.__subscriptions: + rules += subscription.elementHidingRules() + + if rules: + # remove last ", + rules = rules[:-1] + + return rules + + def elementHidingRulesForDomain(self, url): + """ + Public method to get the element hiding rules for a domain. + + @param url URL to get hiding rules for (QUrl) + @return element hiding rules (string) + """ + if not self.__enabled: + return "" + + rules = "" + + for subscription in self.__subscriptions: + if subscription.elemHideDisabledForUrl(url): + continue + + rules += subscription.elementHidingRulesForDomain(url.host()) + + if rules: + # remove last "," + rules = rules[:-1] + + rules += "{display:none !important;}\n" + + return rules + + def exceptions(self): + """ + Public method to get a list of excepted hosts. + + @return list of excepted hosts (list of string) + """ + return self.__exceptedHosts + + def setExceptions(self, hosts): + """ + Public method to set the list of excepted hosts. + + @param hosts list of excepted hosts (list of string) + """ + self.__exceptedHosts = hosts[:] + Preferences.setWebBrowser("AdBlockExceptions", self.__exceptedHosts) + + def addException(self, host): + """ + Public method to add an exception. + + @param host to be excepted (string) + """ + if host and host not in self.__exceptedHosts: + self.__exceptedHosts.append(host) + Preferences.setWebBrowser( + "AdBlockExceptions", self.__exceptedHosts) + + def removeException(self, host): + """ + Public method to remove an exception. + + @param host to be removed from the list of exceptions (string) + """ + if host in self.__exceptedHosts: + self.__exceptedHosts.remove(host) + Preferences.setWebBrowser( + "AdBlockExceptions", self.__exceptedHosts) + + def isHostExcepted(self, host): + """ + Public slot to check, if a host is excepted. + + @param host host to check (string) + @return flag indicating an exception (boolean) + """ + return host in self.__exceptedHosts + + def showExceptionsDialog(self): + """ + Public method to show the AdBlock Exceptions dialog. + + @return reference to the exceptions dialog (AdBlockExceptionsDialog) + """ + if self.__adBlockExceptionsDialog is None: + from .AdBlockExceptionsDialog import AdBlockExceptionsDialog + self.__adBlockExceptionsDialog = AdBlockExceptionsDialog() + + self.__adBlockExceptionsDialog.load(self.__exceptedHosts) + self.__adBlockExceptionsDialog.show() + return self.__adBlockExceptionsDialog
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/AdBlock/AdBlockPage.py Sat Mar 12 20:05:01 2016 +0100 @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a class to apply AdBlock rules to a web page. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import QObject, QUrl + +from ..Tools import Scripts + + +class AdBlockPage(QObject): + """ + Class to apply AdBlock rules to a web page. + """ + def hideBlockedPageEntries(self, page): + """ + Public method to apply AdBlock rules to a web page. + + @param page reference to the web page (HelpWebPage) + """ + if page is None: + return + + from WebBrowser.WebBrowserWindow import WebBrowserWindow + manager = WebBrowserWindow.adBlockManager() + if not manager.isEnabled(): + return + + # apply domain specific element hiding rules + elementHiding = manager.elementHidingRulesForDomain(page.url()) + if elementHiding: + script = Scripts.setCss(elementHiding) + page.runJavaScript(script) +## docElement = page.mainFrame().documentElement() +## +## for entry in page.getAdBlockedPageEntries(): +## urlString = entry.urlString() +## if urlString.endswith((".js", ".css")): +## continue +## +## urlEnd = "" +## pos = urlString.rfind("/") +## if pos >= 0: +## urlEnd = urlString[pos + 1:] +## if urlString.endswith("/"): +## urlEnd = urlString[:-1] +## +## selector = \ +## 'img[src$="{0}"], iframe[src$="{0}"], embed[src$="{0}"]'\ +## .format(urlEnd) +## elements = docElement.findAll(selector) +## +## for element in elements: +## src = element.attribute("src") +## src = src.replace("../", "") +## if src in urlString: +## element.setStyleProperty("display", "none") +## +## +## elementHiding += "{display: none !important;}\n</style>" +## +## bodyElement = docElement.findFirst("body") +## bodyElement.appendInside( +## '<style type="text/css">\n/* AdBlock for eric */\n' + +## elementHiding) +## +## +##class AdBlockedPageEntry(object): +## """ +## Class implementing a data structure for web page rules. +## """ +## def __init__(self, rule, url): +## """ +## Constructor +## +## @param rule AdBlock rule to add (AdBlockRule) +## @param url URL that matched the rule (QUrl) +## """ +## self.rule = rule +## self.url = QUrl(url) +## +## def __eq__(self, other): +## """ +## Special method to test equality. +## +## @param other reference to the other entry (AdBlockedPageEntry) +## @return flag indicating equality (boolean) +## """ +## return self.rule == other.rule and self.url == other.url +## +## def urlString(self): +## """ +## Public method to get the URL as a string. +## +## @return URL as a string (string) +## """ +## return self.url.toString()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/AdBlock/AdBlockSubscription.py Sat Mar 12 20:05:01 2016 +0100 @@ -0,0 +1,696 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the AdBlock subscription class. +""" + +from __future__ import unicode_literals + +import os +import re +import hashlib +import base64 + +from PyQt5.QtCore import pyqtSignal, Qt, QObject, QByteArray, QDateTime, \ + QUrl, QUrlQuery, QCryptographicHash, QFile, QIODevice, QTextStream, \ + QDate, QTime, qVersion +from PyQt5.QtNetwork import QNetworkReply + +from E5Gui import E5MessageBox + +import Utilities +import Preferences + + +class AdBlockSubscription(QObject): + """ + Class implementing the AdBlock subscription. + + @signal changed() emitted after the subscription has changed + @signal rulesChanged() emitted after the subscription's rules have changed + @signal enabledChanged(bool) emitted after the enabled state was changed + """ + changed = pyqtSignal() + rulesChanged = pyqtSignal() + enabledChanged = pyqtSignal(bool) + + def __init__(self, url, custom, parent=None, default=False): + """ + Constructor + + @param url AdBlock URL for the subscription (QUrl) + @param custom flag indicating a custom subscription (boolean) + @param parent reference to the parent object (QObject) + @param default flag indicating a default subscription (boolean) + """ + super(AdBlockSubscription, self).__init__(parent) + + self.__custom = custom + self.__url = url.toEncoded() + self.__enabled = False + self.__downloading = None + self.__defaultSubscription = default + + self.__title = "" + self.__location = QByteArray() + self.__lastUpdate = QDateTime() + self.__requiresLocation = "" + self.__requiresTitle = "" + + self.__updatePeriod = 0 # update period in hours, 0 = use default + self.__remoteModified = QDateTime() + + self.__rules = [] # list containing all AdBlock rules + + self.__networkExceptionRules = [] + self.__networkBlockRules = [] + self.__domainRestrictedCssRules = [] + self.__elementHidingRules = "" + self.__documentRules = [] + self.__elemhideRules = [] + + self.__checksumRe = re.compile( + r"""^\s*!\s*checksum[\s\-:]+([\w\+\/=]+).*\n""", + re.IGNORECASE | re.MULTILINE) + self.__expiresRe = re.compile( + r"""(?:expires:|expires after)\s*(\d+)\s*(hour|h)?""", + re.IGNORECASE) + self.__remoteModifiedRe = re.compile( + r"""!\s*(?:Last modified|Updated):\s*(\d{1,2})\s*""" + r"""(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s*""" + r"""(\d{2,4})\s*((\d{1,2}):(\d{2}))?""", + re.IGNORECASE) + + self.__monthNameToNumber = { + "Jan": 1, + "Feb": 2, + "Mar": 3, + "Apr": 4, + "May": 5, + "Jun": 6, + "Jul": 7, + "Aug": 8, + "Sep": 9, + "Oct": 10, + "Nov": 11, + "Dec": 12 + } + + self.__parseUrl(url) + + def __parseUrl(self, url): + """ + Private method to parse the AdBlock URL for the subscription. + + @param url AdBlock URL for the subscription (QUrl) + """ + if url.scheme() != "abp": + return + + if url.path() != "subscribe": + return + + urlQuery = QUrlQuery(url) + self.__title = QUrl.fromPercentEncoding( + QByteArray(urlQuery.queryItemValue("title").encode())) + self.__enabled = urlQuery.queryItemValue("enabled") != "false" + self.__location = QByteArray(QUrl.fromPercentEncoding( + QByteArray(urlQuery.queryItemValue("location").encode())) + .encode("utf-8")) + + # Check for required subscription + self.__requiresLocation = QUrl.fromPercentEncoding( + QByteArray(urlQuery.queryItemValue( + "requiresLocation").encode())) + self.__requiresTitle = QUrl.fromPercentEncoding( + QByteArray(urlQuery.queryItemValue("requiresTitle").encode())) + if self.__requiresLocation and self.__requiresTitle: + import Helpviewer.HelpWindow + Helpviewer.HelpWindow.HelpWindow.adBlockManager()\ + .loadRequiredSubscription(self.__requiresLocation, + self.__requiresTitle) + + lastUpdateString = urlQuery.queryItemValue("lastUpdate") + self.__lastUpdate = QDateTime.fromString(lastUpdateString, + Qt.ISODate) + + self.__loadRules() + + def url(self): + """ + Public method to generate the URL for this subscription. + + @return AdBlock URL for the subscription (QUrl) + """ + url = QUrl() + url.setScheme("abp") + url.setPath("subscribe") + + queryItems = [] + queryItems.append(("location", bytes(self.__location).decode())) + queryItems.append(("title", self.__title)) + if self.__requiresLocation and self.__requiresTitle: + queryItems.append(("requiresLocation", self.__requiresLocation)) + queryItems.append(("requiresTitle", self.__requiresTitle)) + if not self.__enabled: + queryItems.append(("enabled", "false")) + if self.__lastUpdate.isValid(): + queryItems.append(("lastUpdate", + self.__lastUpdate.toString(Qt.ISODate))) + + query = QUrlQuery() + query.setQueryItems(queryItems) + url.setQuery(query) + return url + + def isEnabled(self): + """ + Public method to check, if the subscription is enabled. + + @return flag indicating the enabled status (boolean) + """ + return self.__enabled + + def setEnabled(self, enabled): + """ + Public method to set the enabled status. + + @param enabled flag indicating the enabled status (boolean) + """ + if self.__enabled == enabled: + return + + self.__enabled = enabled + self.enabledChanged.emit(enabled) + + def title(self): + """ + Public method to get the subscription title. + + @return subscription title (string) + """ + return self.__title + + def setTitle(self, title): + """ + Public method to set the subscription title. + + @param title subscription title (string) + """ + if self.__title == title: + return + + self.__title = title + self.changed.emit() + + def location(self): + """ + Public method to get the subscription location. + + @return URL of the subscription location (QUrl) + """ + return QUrl.fromEncoded(self.__location) + + def setLocation(self, url): + """ + Public method to set the subscription location. + + @param url URL of the subscription location (QUrl) + """ + if url == self.location(): + return + + self.__location = url.toEncoded() + self.__lastUpdate = QDateTime() + self.changed.emit() + + def requiresLocation(self): + """ + Public method to get the location of a required subscription. + + @return location of a required subscription (string) + """ + return self.__requiresLocation + + def lastUpdate(self): + """ + Public method to get the date and time of the last update. + + @return date and time of the last update (QDateTime) + """ + return self.__lastUpdate + + def rulesFileName(self): + """ + Public method to get the name of the rules file. + + @return name of the rules file (string) + """ + if self.location().scheme() == "file": + return self.location().toLocalFile() + + if self.__location.isEmpty(): + return "" + + sha1 = bytes(QCryptographicHash.hash( + self.__location, QCryptographicHash.Sha1).toHex()).decode() + dataDir = os.path.join( + Utilities.getConfigDir(), "web_browser", "subscriptions") + if not os.path.exists(dataDir): + os.makedirs(dataDir) + fileName = os.path.join( + dataDir, "adblock_subscription_{0}".format(sha1)) + return fileName + + def __loadRules(self): + """ + Private method to load the rules of the subscription. + """ + fileName = self.rulesFileName() + f = QFile(fileName) + if f.exists(): + if not f.open(QIODevice.ReadOnly): + E5MessageBox.warning( + None, + self.tr("Load subscription rules"), + self.tr( + """Unable to open AdBlock file '{0}' for reading.""") + .format(fileName)) + else: + textStream = QTextStream(f) + header = textStream.readLine(1024) + if not header.startswith("[Adblock"): + E5MessageBox.warning( + None, + self.tr("Load subscription rules"), + self.tr("""AdBlock file '{0}' does not start""" + """ with [Adblock.""") + .format(fileName)) + f.close() + f.remove() + self.__lastUpdate = QDateTime() + else: + from .AdBlockRule import AdBlockRule + + self.__updatePeriod = 0 + self.__remoteModified = QDateTime() + self.__rules = [] + self.__rules.append(AdBlockRule(header, self)) + while not textStream.atEnd(): + line = textStream.readLine() + self.__rules.append(AdBlockRule(line, self)) + expires = self.__expiresRe.search(line) + if expires: + period, kind = expires.groups() + if kind: + # hours + self.__updatePeriod = int(period) + else: + # days + self.__updatePeriod = int(period) * 24 + remoteModified = self.__remoteModifiedRe.search(line) + if remoteModified: + day, month, year, time, hour, minute = \ + remoteModified.groups() + self.__remoteModified.setDate( + QDate(int(year), + self.__monthNameToNumber[month], + int(day)) + ) + if time: + self.__remoteModified.setTime( + QTime(int(hour), int(minute))) + self.__populateCache() + self.changed.emit() + elif not fileName.endswith("_custom"): + self.__lastUpdate = QDateTime() + + self.checkForUpdate() + + def checkForUpdate(self): + """ + Public method to check for an update. + """ + if self.__updatePeriod: + updatePeriod = self.__updatePeriod + else: + updatePeriod = \ + Preferences.getWebBrowser("AdBlockUpdatePeriod") * 24 + if not self.__lastUpdate.isValid() or \ + (self.__remoteModified.isValid() and + self.__remoteModified.addSecs(updatePeriod * 3600) < + QDateTime.currentDateTime()) or \ + self.__lastUpdate.addSecs(updatePeriod * 3600) < \ + QDateTime.currentDateTime(): + self.updateNow() + + def updateNow(self): + """ + Public method to update the subscription immediately. + """ + if self.__downloading is not None: + return + + if not self.location().isValid(): + return + + if self.location().scheme() == "file": + self.__lastUpdate = QDateTime.currentDateTime() + self.__loadRules() + return + + from WebBrowser.WebBrowserWindow import WebBrowserWindow + from WebBrowser.Network.FollowRedirectReply import FollowRedirectReply + self.__downloading = FollowRedirectReply( + self.location(), + WebBrowserWindow.networkManager()) + self.__downloading.finished.connect(self.__rulesDownloaded) + + def __rulesDownloaded(self): + """ + Private slot to deal with the downloaded rules. + """ + reply = self.sender() + + response = reply.readAll() + reply.close() + self.__downloading = None + + if reply.error() != QNetworkReply.NoError: + if not self.__defaultSubscription: + # don't show error if we try to load the default + E5MessageBox.warning( + None, + self.tr("Downloading subscription rules"), + self.tr( + """<p>Subscription rules could not be""" + """ downloaded.</p><p>Error: {0}</p>""") + .format(reply.errorString())) + else: + # reset after first download attempt + self.__defaultSubscription = False + return + + if response.isEmpty(): + E5MessageBox.warning( + None, + self.tr("Downloading subscription rules"), + self.tr("""Got empty subscription rules.""")) + return + + fileName = self.rulesFileName() + QFile.remove(fileName) + f = QFile(fileName) + if not f.open(QIODevice.ReadWrite): + E5MessageBox.warning( + None, + self.tr("Downloading subscription rules"), + self.tr( + """Unable to open AdBlock file '{0}' for writing.""") + .file(fileName)) + return + f.write(response) + f.close() + self.__lastUpdate = QDateTime.currentDateTime() + if self.__validateCheckSum(fileName): + self.__loadRules() + else: + QFile.remove(fileName) + self.__downloading = None + reply.deleteLater() + + def __validateCheckSum(self, fileName): + """ + Private method to check the subscription file's checksum. + + @param fileName name of the file containing the subscription (string) + @return flag indicating a valid file (boolean). A file is considered + valid, if the checksum is OK or the file does not contain a + checksum (i.e. cannot be checked). + """ + try: + f = open(fileName, "r", encoding="utf-8") + data = f.read() + f.close() + except (IOError, OSError): + return False + + match = re.search(self.__checksumRe, data) + if match: + expectedChecksum = match.group(1) + else: + # consider it as valid + return True + + # normalize the data + data = re.sub(r"\r", "", data) # normalize eol + data = re.sub(r"\n+", "\n", data) # remove empty lines + data = re.sub(self.__checksumRe, "", data) # remove checksum line + + # calculate checksum + md5 = hashlib.md5() + md5.update(data.encode("utf-8")) + calculatedChecksum = base64.b64encode(md5.digest()).decode()\ + .rstrip("=") + if calculatedChecksum == expectedChecksum: + return True + else: + res = E5MessageBox.yesNo( + None, + self.tr("Downloading subscription rules"), + self.tr( + """<p>AdBlock subscription <b>{0}</b> has a wrong""" + """ checksum.<br/>""" + """Found: {1}<br/>""" + """Calculated: {2}<br/>""" + """Use it anyway?</p>""") + .format(self.__title, expectedChecksum, + calculatedChecksum)) + return res + + def saveRules(self): + """ + Public method to save the subscription rules. + """ + fileName = self.rulesFileName() + if not fileName: + return + + f = QFile(fileName) + if not f.open(QIODevice.ReadWrite | QIODevice.Truncate): + E5MessageBox.warning( + None, + self.tr("Saving subscription rules"), + self.tr( + """Unable to open AdBlock file '{0}' for writing.""") + .format(fileName)) + return + + textStream = QTextStream(f) + if not self.__rules or not self.__rules[0].isHeader(): + textStream << "[Adblock Plus 1.1.1]\n" + for rule in self.__rules: + textStream << rule.filter() << "\n" + + def match(self, req, urlDomain, urlString): + """ + Public method to check the subscription for a matching rule. + + @param req reference to the network request (QNetworkRequest) + @param urlDomain domain of the URL (string) + @param urlString URL (string) + @return reference to the rule object or None (AdBlockRule) + """ + for rule in self.__networkExceptionRules: + if rule.networkMatch(req, urlDomain, urlString): + return None + + for rule in self.__networkBlockRules: + if rule.networkMatch(req, urlDomain, urlString): + return rule + + return None + + def adBlockDisabledForUrl(self, url): + """ + Public method to check, if AdBlock is disabled for the given URL. + + @param url URL to check (QUrl) + @return flag indicating disabled state (boolean) + """ + for rule in self.__documentRules: + if rule.urlMatch(url): + return True + + return False + + def elemHideDisabledForUrl(self, url): + """ + Public method to check, if element hiding is disabled for the given + URL. + + @param url URL to check (QUrl) + @return flag indicating disabled state (boolean) + """ + if self.adBlockDisabledForUrl(url): + return True + + for rule in self.__elemhideRules: + if rule.urlMatch(url): + return True + + return False + + def elementHidingRules(self): + """ + Public method to get the element hiding rules. + + @return element hiding rules (string) + """ + return self.__elementHidingRules + + def elementHidingRulesForDomain(self, domain): + """ + Public method to get the element hiding rules for the given domain. + + @param domain domain name (string) + @return element hiding rules (string) + """ + rules = "" + + for rule in self.__domainRestrictedCssRules: + if rule.matchDomain(domain): + rules += rule.cssSelector() + "," + + return rules + + def rule(self, offset): + """ + Public method to get a specific rule. + + @param offset offset of the rule (integer) + @return requested rule (AdBlockRule) + """ + if offset >= len(self.__rules): + return None + + return self.__rules[offset] + + def allRules(self): + """ + Public method to get the list of rules. + + @return list of rules (list of AdBlockRule) + """ + return self.__rules[:] + + def addRule(self, rule): + """ + Public method to add a rule. + + @param rule reference to the rule to add (AdBlockRule) + @return offset of the rule (integer) + """ + self.__rules.append(rule) + self.__populateCache() + self.rulesChanged.emit() + + return len(self.__rules) - 1 + + def removeRule(self, offset): + """ + Public method to remove a rule given the offset. + + @param offset offset of the rule to remove (integer) + """ + if offset < 0 or offset > len(self.__rules): + return + + del self.__rules[offset] + self.__populateCache() + self.rulesChanged.emit() + + def replaceRule(self, rule, offset): + """ + Public method to replace a rule given the offset. + + @param rule reference to the rule to set (AdBlockRule) + @param offset offset of the rule to remove (integer) + @return requested rule (AdBlockRule) + """ + if offset >= len(self.__rules): + return None + + self.__rules[offset] = rule + self.__populateCache() + self.rulesChanged.emit() + + return self.__rules[offset] + + def __populateCache(self): + """ + Private method to populate the various rule caches. + """ + self.__networkExceptionRules = [] + self.__networkBlockRules = [] + self.__domainRestrictedCssRules = [] + self.__elementHidingRules = "" + self.__documentRules = [] + self.__elemhideRules = [] + + for rule in self.__rules: + if not rule.isEnabled(): + continue + + if rule.isCSSRule(): + if rule.isDomainRestricted(): + self.__domainRestrictedCssRules.append(rule) + else: + self.__elementHidingRules += rule.cssSelector() + "," + elif rule.isDocument(): + self.__documentRules.append(rule) + elif rule.isElementHiding(): + self.__elemhideRules.append(rule) + elif rule.isException(): + self.__networkExceptionRules.append(rule) + else: + self.__networkBlockRules.append(rule) + + def canEditRules(self): + """ + Public method to check, if rules can be edited. + + @return flag indicating rules may be edited (boolean) + """ + return self.__custom + + def canBeRemoved(self): + """ + Public method to check, if the subscription can be removed. + + @return flag indicating removal is allowed (boolean) + """ + return not self.__custom and not self.__defaultSubscription + + def setRuleEnabled(self, offset, enabled): + """ + Public method to enable a specific rule. + + @param offset offset of the rule (integer) + @param enabled new enabled state (boolean) + @return reference to the changed rule (AdBlockRule) + """ + if offset >= len(self.__rules): + return None + + rule = self.__rules[offset] + rule.setEnabled(enabled) + if rule.isCSSRule(): + from WebBrowser.WebBrowserWindow import WebBrowserWindow + self.__populateCache() + WebBrowserWindow.mainWindow().reloadUserStyleSheet() + + return rule
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/AdBlock/__init__.py Sat Mar 12 20:05:01 2016 +0100 @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Package implementing the advertisements blocker functionality. +"""
--- a/WebBrowser/Tools/Scripts.py Sat Mar 12 17:02:04 2016 +0100 +++ b/WebBrowser/Tools/Scripts.py Sat Mar 12 20:05:01 2016 +0100 @@ -376,6 +376,26 @@ data = data.replace("'", "\\'") return source.format(data) + +def setCss(css): + """ + Function generating a script to set a given CSS style sheet. + + @param css style sheet + @type str + @return script to set the style sheet + @rtype str + """ + source = """ + (function() {{ + var css = document.createElement('style'); + css.setAttribute('type', 'text/css'); + css.appendChild(document.createTextNode('{0}')); + document.getElementsByTagName('head')[0].appendChild(css); + }})()""" + style = css.replace("'", "\\'").replace("\n", "\\n") + return source.format(style) + ########################################################################### ## scripts below are specific for eric ###########################################################################
--- a/WebBrowser/WebBrowserPage.py Sat Mar 12 17:02:04 2016 +0100 +++ b/WebBrowser/WebBrowserPage.py Sat Mar 12 20:05:01 2016 +0100 @@ -152,13 +152,8 @@ self.__sslConfiguration = None ## self.__proxy.finished.connect(self.__managerFinished) ## - self.__adBlockedEntries = [] - self.loadStarted.connect(self.__loadStarted) -## -## self.saveFrameStateRequested.connect( -## self.__saveFrameStateRequested) -## self.restoreFrameStateRequested.connect( -## self.__restoreFrameStateRequested) +## self.__adBlockedEntries = [] +## self.loadStarted.connect(self.__loadStarted) self.featurePermissionRequested.connect( self.__featurePermissionRequested) @@ -397,12 +392,12 @@ ## return True ## ## return QWebPage.extension(self, extension, option, output) - - def __loadStarted(self): - """ - Private slot to handle the loadStarted signal. - """ - self.__adBlockedEntries = [] +## +## def __loadStarted(self): +## """ +## Private slot to handle the loadStarted signal. +## """ +## self.__adBlockedEntries = [] ## ## def addAdBlockRule(self, rule, url): ## """ @@ -609,58 +604,6 @@ ## return super(HelpWebPage, self).event(fakeEvent) ## ## return super(HelpWebPage, self).event(evt) -## -## def __saveFrameStateRequested(self, frame, itm): -## """ -## Private slot to save the page state (i.e. zoom level and scroll -## position). -## -## Note: Code is based on qutebrowser. -## -## @param frame frame to be saved -## @type QWebFrame -## @param itm web history item to be saved -## @type QWebHistoryItem -## """ -## try: -## if frame != self.mainFrame(): -## return -## except RuntimeError: -## # With Qt 5.2.1 (Ubuntu Trusty) we get this when closing a tab: -## # RuntimeError: wrapped C/C++ object of type BrowserPage has -## # been deleted -## # Since the information here isn't that important for closing web -## # views anyways, we ignore this error. -## return -## data = { -## 'zoom': frame.zoomFactor(), -## 'scrollPos': frame.scrollPosition(), -## } -## itm.setUserData(data) -## -## def __restoreFrameStateRequested(self, frame): -## """ -## Private slot to restore scroll position and zoom level from -## history. -## -## Note: Code is based on qutebrowser. -## -## @param frame frame to be restored -## @type QWebFrame -## """ -## if frame != self.mainFrame(): -## return -## -## data = self.history().currentItem().userData() -## if data is None: -## return -## -## if 'zoom' in data: -## frame.page().view().setZoomValue(int(data['zoom'] * 100), -## saveValue=False) -## -## if 'scrollPos' in data and frame.scrollPosition() == QPoint(0, 0): -## frame.setScrollPosition(data['scrollPos']) def __featurePermissionRequested(self, url, feature): """
--- a/WebBrowser/WebBrowserWindow.py Sat Mar 12 17:02:04 2016 +0100 +++ b/WebBrowser/WebBrowserWindow.py Sat Mar 12 20:05:01 2016 +0100 @@ -4079,10 +4079,8 @@ @param styleSheetFile name of the user style sheet file (string) """ - # TODO: AdBlock - userStyle = "" -## userStyle = \ -## self.adBlockManager().elementHidingRules().replace('"', '\\"') + userStyle = \ + self.adBlockManager().elementHidingRules().replace('"', '\\"') userStyle += WebBrowserTools.readAllFileContents(styleSheetFile)\ .replace("\n", "")
--- a/eric6.e4p Sat Mar 12 17:02:04 2016 +0100 +++ b/eric6.e4p Sat Mar 12 20:05:01 2016 +0100 @@ -1266,6 +1266,10 @@ <Source>ViewManager/BookmarkedFilesDialog.py</Source> <Source>ViewManager/ViewManager.py</Source> <Source>ViewManager/__init__.py</Source> + <Source>WebBrowser/AdBlock/AdBlockManager.py</Source> + <Source>WebBrowser/AdBlock/AdBlockPage.py</Source> + <Source>WebBrowser/AdBlock/AdBlockSubscription.py</Source> + <Source>WebBrowser/AdBlock/__init__.py</Source> <Source>WebBrowser/Bookmarks/AddBookmarkDialog.py</Source> <Source>WebBrowser/Bookmarks/BookmarkNode.py</Source> <Source>WebBrowser/Bookmarks/BookmarkPropertiesDialog.py</Source>