Continued porting the web browser. QtWebEngine

Sat, 12 Mar 2016 20:05:01 +0100

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Sat, 12 Mar 2016 20:05:01 +0100
branch
QtWebEngine
changeset 4847
a1a8eac81b54
parent 4846
960e5e18894b
child 4856
15739e8eb6c5

Continued porting the web browser.

- started porting the AdBlock code

Preferences/__init__.py file | annotate | diff | comparison | revisions
WebBrowser/AdBlock/AdBlockManager.py file | annotate | diff | comparison | revisions
WebBrowser/AdBlock/AdBlockPage.py file | annotate | diff | comparison | revisions
WebBrowser/AdBlock/AdBlockSubscription.py file | annotate | diff | comparison | revisions
WebBrowser/AdBlock/__init__.py file | annotate | diff | comparison | revisions
WebBrowser/Tools/Scripts.py file | annotate | diff | comparison | revisions
WebBrowser/WebBrowserPage.py file | annotate | diff | comparison | revisions
WebBrowser/WebBrowserWindow.py file | annotate | diff | comparison | revisions
eric6.e4p file | annotate | diff | comparison | revisions
--- 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>

eric ide

mercurial