Started implementing the safe browsing manager and management dialog. safe_browsing

Sat, 29 Jul 2017 19:41:16 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Sat, 29 Jul 2017 19:41:16 +0200
branch
safe_browsing
changeset 5820
b610cb5b501a
parent 5819
69fa45e95673
child 5821
6c7766cde4c1

Started implementing the safe browsing manager and management dialog.

Preferences/ConfigurationPages/WebBrowserPage.py file | annotate | diff | comparison | revisions
Preferences/ConfigurationPages/WebBrowserPage.ui file | annotate | diff | comparison | revisions
WebBrowser/SafeBrowsing/SafeBrowsingAPIClient.py file | annotate | diff | comparison | revisions
WebBrowser/SafeBrowsing/SafeBrowsingCache.py file | annotate | diff | comparison | revisions
WebBrowser/SafeBrowsing/SafeBrowsingDialog.py file | annotate | diff | comparison | revisions
WebBrowser/SafeBrowsing/SafeBrowsingDialog.ui file | annotate | diff | comparison | revisions
WebBrowser/SafeBrowsing/SafeBrowsingManager.py file | annotate | diff | comparison | revisions
WebBrowser/WebBrowserWindow.py file | annotate | diff | comparison | revisions
eric6.e4p file | annotate | diff | comparison | revisions
icons/default/safeBrowsing.png file | annotate | diff | comparison | revisions
icons/default/safeBrowsing48.png file | annotate | diff | comparison | revisions
--- a/Preferences/ConfigurationPages/WebBrowserPage.py	Wed Jul 26 19:46:17 2017 +0200
+++ b/Preferences/ConfigurationPages/WebBrowserPage.py	Sat Jul 29 19:41:16 2017 +0200
@@ -49,8 +49,6 @@
         defaultSchemes = ["file://", "http://", "https://"]
         self.defaultSchemeCombo.addItems(defaultSchemes)
         
-        self.__gsbHelpDialog = None
-        
         # set initial values
         self.singleHelpWindowCheckBox.setChecked(
             Preferences.getWebBrowser("SingleWebBrowserWindow"))
@@ -175,10 +173,6 @@
                 Preferences.getWebBrowser("AllowRunningInsecureContent"))
         except KeyError:
             self.insecureContentsCheckBox.setEnabled(False)
-        self.gsbGroupBox.setChecked(
-            Preferences.getWebBrowser("SafeBrowsingEnabled"))
-        self.gsbApiKeyEdit.setText(
-            Preferences.getWebBrowser("SafeBrowsingApiKey"))
         
         try:
             # Qt 5.8
@@ -325,12 +319,6 @@
             Preferences.setWebBrowser(
                 "AllowRunningInsecureContent",
                 self.insecureContentsCheckBox.isChecked())
-        Preferences.setWebBrowser(
-            "SafeBrowsingEnabled",
-            self.gsbGroupBox.isChecked())
-        Preferences.setWebBrowser(
-            "SafeBrowsingApiKey",
-            self.gsbApiKeyEdit.text())
         
         if self.printBackgroundCheckBox.isEnabled():
             Preferences.setWebBrowser(
@@ -393,23 +381,6 @@
         from WebBrowser.Network.SendRefererWhitelistDialog import \
             SendRefererWhitelistDialog
         SendRefererWhitelistDialog(self).exec_()
-    
-    @pyqtSlot()
-    def on_gsbHelpButton_clicked(self):
-        """
-        Private slot to show some help text "How to create a safe
-        browsing API key.".
-        """
-        if self.__gsbHelpDialog is None:
-            from E5Gui.E5SimpleHelpDialog import E5SimpleHelpDialog
-            from WebBrowser.SafeBrowsing import SafeBrowsingHelp
-            
-            helpStr = SafeBrowsingHelp()
-            self.__gsbHelpDialog = E5SimpleHelpDialog(
-                title=self.tr("Google Safe Browsing API Help"),
-                helpStr=helpStr, parent=self)
-        
-        self.__gsbHelpDialog.show()
 
 
 def create(dlg):
--- a/Preferences/ConfigurationPages/WebBrowserPage.ui	Wed Jul 26 19:46:17 2017 +0200
+++ b/Preferences/ConfigurationPages/WebBrowserPage.ui	Sat Jul 29 19:41:16 2017 +0200
@@ -504,45 +504,6 @@
         </property>
        </widget>
       </item>
-      <item row="1" column="0" colspan="2">
-       <widget class="QGroupBox" name="gsbGroupBox">
-        <property name="toolTip">
-         <string>Select to enable the Google sage browsing support</string>
-        </property>
-        <property name="title">
-         <string>Google Safe Browsing</string>
-        </property>
-        <property name="checkable">
-         <bool>true</bool>
-        </property>
-        <layout class="QGridLayout" name="gridLayout_7">
-         <item row="0" column="0">
-          <widget class="QLabel" name="label_14">
-           <property name="text">
-            <string>API Key:</string>
-           </property>
-          </widget>
-         </item>
-         <item row="0" column="1">
-          <widget class="QLineEdit" name="gsbApiKeyEdit">
-           <property name="toolTip">
-            <string>Enter the Google Safe Browsing API key</string>
-           </property>
-          </widget>
-         </item>
-         <item row="1" column="0" colspan="2">
-          <widget class="QPushButton" name="gsbHelpButton">
-           <property name="toolTip">
-            <string>Press to get some help about obtaining the API key</string>
-           </property>
-           <property name="text">
-            <string>Google Safe Browsing API Help</string>
-           </property>
-          </widget>
-         </item>
-        </layout>
-       </widget>
-      </item>
      </layout>
     </widget>
    </item>
@@ -987,9 +948,6 @@
   <tabstop>refererWhitelistButton</tabstop>
   <tabstop>xssAuditingCheckBox</tabstop>
   <tabstop>insecureContentsCheckBox</tabstop>
-  <tabstop>gsbGroupBox</tabstop>
-  <tabstop>gsbApiKeyEdit</tabstop>
-  <tabstop>gsbHelpButton</tabstop>
   <tabstop>expireHistory</tabstop>
   <tabstop>diskCacheCheckBox</tabstop>
   <tabstop>cacheSizeSpinBox</tabstop>
--- a/WebBrowser/SafeBrowsing/SafeBrowsingAPIClient.py	Wed Jul 26 19:46:17 2017 +0200
+++ b/WebBrowser/SafeBrowsing/SafeBrowsingAPIClient.py	Sat Jul 29 19:41:16 2017 +0200
@@ -53,6 +53,15 @@
         self.__nextRequestNoSoonerThan = QDateTime()
         self.__failCount = 0
     
+    def setApiKey(self, apiKey):
+        """
+        Public method to set the API key.
+        
+        @param apiKey API key to be set
+        @type str
+        """
+        self.__apiKey = apiKey
+    
     def getThreatLists(self):
         """
         Public method to retrieve all available threat lists.
@@ -79,11 +88,11 @@
         reply.deleteLater()
         return res
     
-    def getThreatsUpdate(self, clientState):
+    def getThreatsUpdate(self, clientStates):
         """
         Public method to fetch hash prefix updates for the given threat list.
         
-        @param clientState dictionary of client states with keys like
+        @param clientStates dictionary of client states with keys like
             (threatType, platformType, threatEntryType)
         @type dict
         @return list of threat updates
@@ -98,7 +107,7 @@
         }
         
         for (threatType, platformType, threatEntryType), currentState in \
-                clientState.items():
+                clientStates.items():
             requestBody["listUpdateRequests"].append(
                 {
                     "threatType": threatType,
@@ -223,3 +232,15 @@
             waitDuration = int(minimumWaitDuration.rstrip("s"))
             self.__nextRequestNoSoonerThan = \
                 QDateTime.currentDateTime().addSecs(waitDuration)
+    
+    def fairUseDelayExpired(self):
+        """
+        Public method to check, if the fair use wait period has expired.
+        
+        @return flag indicating expiration
+        @rtype bool
+        """
+        return (
+            self.__fairUse and
+            QDateTime.currentDateTime() >= self.__nextRequestNoSoonerThan
+        ) or not self.__fairUse
--- a/WebBrowser/SafeBrowsing/SafeBrowsingCache.py	Wed Jul 26 19:46:17 2017 +0200
+++ b/WebBrowser/SafeBrowsing/SafeBrowsingCache.py	Sat Jul 29 19:41:16 2017 +0200
@@ -14,12 +14,11 @@
 # https://github.com/afilipovich/gglsbl
 #
 
-from __future__ import unicode_literals
+from __future__ import unicode_literals, division
 
 import os
-import hashlib
 
-from PyQt5.QtCore import QObject
+from PyQt5.QtCore import QObject, QByteArray, QCryptographicHash
 from PyQt5.QtSql import QSql, QSqlDatabase, QSqlQuery
 
 from .SafeBrowsingUtilities import toHex
@@ -77,6 +76,45 @@
         return '/'.join(self.asTuple())
 
 
+class HashPrefixList(object):
+    """
+    Class implementing a container for threat list data.
+    """
+    def __init__(self, prefixLength, rawHashes):
+        """
+        Constructor
+        
+        @param prefixLength length of each hash prefix
+        @type int
+        @param rawHashes raw hash prefixes of given length concatenated and
+            sorted in lexicographical order
+        @type str
+        """
+        self.__prefixLength = prefixLength
+        self.__rawHashes = rawHashes
+    
+    def __len__(self):
+        """
+        Special method to calculate the number of entries.
+        
+        @return length
+        @rtype int
+        """
+        return len(self.__rawHashes) // self.__prefixLength
+    
+    def __iter__(self):
+        """
+        Special method to iterate over the raw hashes.
+        
+        @return iterator object
+        @rtype iterator
+        """
+        n = self.__prefixLength
+        return (self.__rawHashes[index:index + n]
+                for index in range(0, len(self.__rawHashes), n)
+                )
+
+
 class SafeBrowsingCache(QObject):
     """
     Class implementing a cache for Google Safe Browsing.
@@ -247,7 +285,8 @@
                 query.prepare(
                     queryStr.format(",".join(["?" * len(hashValues)])))
                 for hashValue in hashValues:
-                    query.addBindValue(hashValue, QSql.In | QSql.Binary)
+                    query.addBindValue(QByteArray(hashValue),
+                                       QSql.In | QSql.Binary)
                 
                 query.exec_()
                 
@@ -299,7 +338,7 @@
                     threatType = query.value(1)
                     platformType = query.value(2)
                     threatEntryType = query.value(3)
-                    negativeCacheExpired = query.value(4)
+                    negativeCacheExpired = query.value(4)  # TODO: check if bool
                     threatList = ThreatList(threatType, platformType,
                                             threatEntryType)
                     output.append((threatList, fullHash, negativeCacheExpired))
@@ -343,7 +382,8 @@
             try:
                 query = QSqlQuery(db)
                 query.prepare(insertQueryStr)
-                query.addBindValue(hashValue, QSql.In | QSql.Binary)
+                query.addBindValue(QByteArray(hashValue),
+                                   QSql.In | QSql.Binary)
                 query.addBindValue(threatList.threatType)
                 query.addBindValue(threatList.platformType)
                 query.addBindValue(threatList.threatEntryType)
@@ -353,7 +393,8 @@
                 
                 query = QSqlQuery(db)
                 query.prepare(updateQueryStr)
-                query.addBindValue(hashValue, QSql.In | QSql.Binary)
+                query.addBindValue(QByteArray(hashValue),
+                                   QSql.In | QSql.Binary)
                 query.addBindValue(threatList.threatType)
                 query.addBindValue(threatList.platformType)
                 query.addBindValue(threatList.threatEntryType)
@@ -438,7 +479,8 @@
             try:
                 query = QSqlQuery(db)
                 query.prepare(queryStr)
-                query.addBindValue(hashPrefix, QSql.In | QSql.Binary)
+                query.addBindValue(QByteArray(hashPrefix),
+                                   QSql.In | QSql.Binary)
                 query.addBindValue(threatList.threatType)
                 query.addBindValue(threatList.platformType)
                 query.addBindValue(threatList.threatEntryType)
@@ -514,7 +556,7 @@
         """
         Public method to delete a threat list from the cache.
         
-        @param threatlist threat list to be deleted
+        @param threatList threat list to be deleted
         @type ThreatList
         """
         queryStr = """
@@ -585,7 +627,7 @@
         db = QSqlDatabase.database(self.__connectionName)
         if db.isOpen():
             db.transaction()
-            allHashes = b""
+            hash = QCryptographicHash(QCryptographicHash.Sha256)
             try:
                 query = QSqlQuery(db)
                 query.prepare(queryStr)
@@ -596,12 +638,12 @@
                 query.exec_()
                 
                 while query.next():
-                    allHashes += bytes(query.value(0))
+                    hash.addData(query.value(0))
                 del query
             finally:
                 db.commit()
             
-            checksum = hashlib.sha256(allHashes).digest()
+            checksum = bytes(hash.result())
         
         return checksum
     
@@ -611,8 +653,8 @@
         
         @param threatList threat list of the hash prefixes
         @type ThreatList
-        @param prefixes hash prefixes to be inserted
-        @type bytes
+        @param prefixes list of hash prefixes to be inserted
+        @type HashPrefixList
         """
         queryStr = """
             INSERT INTO hash_prefix
@@ -628,7 +670,8 @@
                 for prefix in prefixes:
                     query = QSqlQuery(db)
                     query.prepare(queryStr)
-                    query.addBindValue(prefix, QSql.In | QSql.Binary)
+                    query.addBindValue(QByteArray(prefix),
+                                       QSql.In | QSql.Binary)
                     query.addBindValue(toHex(prefix[:4]))
                     query.addBindValue(threatList.threatType)
                     query.addBindValue(threatList.platformType)
@@ -717,7 +760,8 @@
                         query.addBindValue(threatList.platformType)
                         query.addBindValue(threatList.threatEntryType)
                         for prefix in removeBatch:
-                            query.addBindValue(prefix, QSql.In | QSql.Binary)
+                            query.addBindValue(QByteArray(prefix),
+                                               QSql.In | QSql.Binary)
                         query.exec_()
                         del query
                 finally:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/WebBrowser/SafeBrowsing/SafeBrowsingDialog.py	Sat Jul 29 19:41:16 2017 +0200
@@ -0,0 +1,197 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2017 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a dialog to configure safe browsing support.
+"""
+
+from __future__ import unicode_literals
+
+from PyQt5.QtCore import pyqtSlot, Qt
+from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QAbstractButton, \
+    QApplication
+
+from E5Gui import E5MessageBox
+
+from .Ui_SafeBrowsingDialog import Ui_SafeBrowsingDialog
+
+import UI.PixmapCache
+import Preferences
+
+
+class SafeBrowsingDialog(QDialog, Ui_SafeBrowsingDialog):
+    """
+    Class implementing a dialog to configure safe browsing support.
+    """
+    def __init__(self, manager, parent=None):
+        """
+        Constructor
+        
+        @param manager reference to the safe browsing manager
+        @type SafeBrowsingManager
+        @param parent reference to the parent widget
+        @type QWidget
+        """
+        super(SafeBrowsingDialog, self).__init__(parent)
+        self.setupUi(self)
+        self.setWindowFlags(Qt.Window)
+        
+        self.__manager = manager
+        
+        self.__saveButton = self.buttonBox.addButton(
+            self.tr("Save"), QDialogButtonBox.ActionRole)
+        
+        self.iconLabel.setPixmap(
+            UI.PixmapCache.getPixmap("safeBrowsing48.png"))
+        
+        self.__gsbHelpDialog = None
+        
+        self.__enabled = Preferences.getWebBrowser("SafeBrowsingEnabled")
+        self.__apiKey = Preferences.getWebBrowser("SafeBrowsingApiKey")
+        
+        self.buttonBox.setFocus()
+        
+        msh = self.minimumSizeHint()
+        self.resize(max(self.width(), msh.width()), msh.height())
+    
+    def show(self):
+        """
+        Public slot to show the dialog.
+        """
+        self.gsbGroupBox.setChecked(self.__enabled)
+        self.gsbApiKeyEdit.setText(self.__apiKey)
+        
+        self.__updateCacheButtons()
+        
+        super(SafeBrowsingDialog, self).show()
+    
+    @pyqtSlot()
+    def on_gsbHelpButton_clicked(self):
+        """
+        Private slot to show some help text "How to create a safe
+        browsing API key.".
+        """
+        if self.__gsbHelpDialog is None:
+            from E5Gui.E5SimpleHelpDialog import E5SimpleHelpDialog
+            from . import SafeBrowsingHelp
+            
+            helpStr = SafeBrowsingHelp()
+            self.__gsbHelpDialog = E5SimpleHelpDialog(
+                title=self.tr("Google Safe Browsing API Help"),
+                helpStr=helpStr, parent=self)
+        
+        self.__gsbHelpDialog.show()
+    
+    @pyqtSlot(QAbstractButton)
+    def on_buttonBox_clicked(self, button):
+        """
+        Private slot called by a button of the button box clicked.
+        
+        @param button button that was clicked (QAbstractButton)
+        """
+        if button == self.buttonBox.button(QDialogButtonBox.Close):
+            self.close()
+        elif button == self.__saveButton:
+            self.__save()
+    
+    @pyqtSlot()
+    def __save(self):
+        """
+        Private slot to save the configuration.
+        
+        @return flag indicating success
+        @rtype bool
+        """
+        self.__enabled = self.gsbGroupBox.isChecked()
+        self.__apiKey = self.gsbApiKeyEdit.text()
+        
+        Preferences.setWebBrowser("SafeBrowsingEnabled", self.__enabled)
+        Preferences.setWebBrowser("SafeBrowsingApiKey", self.__apiKey)
+        
+        self.__manager.configurationChanged()
+        
+        self.__updateCacheButtons()
+        
+        return True
+    
+    def closeEvent(self, evt):
+        """
+        Protected method to handle close events.
+        
+        @param evt reference to the close event
+        @type QCloseEvent
+        """
+        if self.__okToClose():
+            evt.accept()
+        else:
+            evt.ignore()
+    
+    def __isModified(self):
+        """
+        Private method to check, if the dialog contains modified data.
+        
+        @return flag indicating the presence of modified data
+        @rtype bool
+        """
+        return (
+            self.__enabled != self.gsbGroupBox.isChecked() or
+            self.__apiKey != self.gsbApiKeyEdit.text()
+        )
+    
+    def __okToClose(self):
+        """
+        Private method to check, if it is safe to close the dialog.
+        
+        @return flag indicating safe to close
+        @rtype bool
+        """
+        if self.__isModified():
+            res = E5MessageBox.okToClearData(
+                self,
+                self.tr("Safe Browsing Management"),
+                self.tr("""The dialog contains unsaved changes."""),
+                self.__save)
+            if not res:
+                return False
+        return True
+    
+    def __updateCacheButtons(self):
+        """
+        Private method to set enabled state of the cache buttons.
+        """
+        enable = self.__enabled and bool(self.__apiKey)
+        
+        self.updateCacheButton.setEnabled(enable)
+        self.clearCacheButton.setEnabled(enable)
+    
+    @pyqtSlot()
+    def on_updateCacheButton_clicked(self):
+        """
+        Private slot to update the local cache database.
+        """
+        QApplication.setOverrideCursor(Qt.WaitCursor)
+        ok, error = self.__manager.updateHashPrefixCache()
+        QApplication.restoreOverrideCursor()
+##        QApplication.processEvents()
+        if not ok:
+            if error:
+                E5MessageBox.critical(
+                    self,
+                    self.tr("Update Safe Browsing Cache"),
+                    self.tr("""<p>Updating the Safe Browsing cache failed."""
+                            """</p><p>Reason: {0}</p>""").format(error))
+            else:
+                E5MessageBox.critical(
+                    self,
+                    self.tr("Update Safe Browsing Cache"),
+                    self.tr("""<p>Updating the Safe Browsing cache failed."""
+                            """</p>"""))
+    
+    @pyqtSlot()
+    def on_clearCacheButton_clicked(self):
+        """
+        Private slot to clear the local cache database.
+        """
+        self.__manager.fullCacheCleanup()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/WebBrowser/SafeBrowsing/SafeBrowsingDialog.ui	Sat Jul 29 19:41:16 2017 +0200
@@ -0,0 +1,115 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>SafeBrowsingDialog</class>
+ <widget class="QDialog" name="SafeBrowsingDialog">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>650</width>
+    <height>187</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Safe Browsing Management</string>
+  </property>
+  <property name="sizeGripEnabled">
+   <bool>true</bool>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <item>
+    <widget class="QGroupBox" name="gsbGroupBox">
+     <property name="toolTip">
+      <string>Select to enable the Google sage browsing support</string>
+     </property>
+     <property name="title">
+      <string>Enable Google Safe Browsing</string>
+     </property>
+     <property name="checkable">
+      <bool>true</bool>
+     </property>
+     <layout class="QGridLayout" name="gridLayout">
+      <item row="0" column="0" rowspan="2">
+       <widget class="QLabel" name="iconLabel">
+        <property name="minimumSize">
+         <size>
+          <width>48</width>
+          <height>48</height>
+         </size>
+        </property>
+        <property name="text">
+         <string notr="true">Icon</string>
+        </property>
+       </widget>
+      </item>
+      <item row="0" column="1">
+       <widget class="QLabel" name="label_14">
+        <property name="text">
+         <string>API Key:</string>
+        </property>
+       </widget>
+      </item>
+      <item row="0" column="2">
+       <widget class="QLineEdit" name="gsbApiKeyEdit">
+        <property name="toolTip">
+         <string>Enter the Google Safe Browsing API key</string>
+        </property>
+       </widget>
+      </item>
+      <item row="1" column="1" colspan="2">
+       <widget class="QPushButton" name="gsbHelpButton">
+        <property name="toolTip">
+         <string>Press to get some help about obtaining the API key</string>
+        </property>
+        <property name="text">
+         <string>Google Safe Browsing API Help</string>
+        </property>
+       </widget>
+      </item>
+     </layout>
+    </widget>
+   </item>
+   <item>
+    <layout class="QHBoxLayout" name="horizontalLayout">
+     <item>
+      <widget class="QPushButton" name="updateCacheButton">
+       <property name="toolTip">
+        <string>Press to update the local cache database</string>
+       </property>
+       <property name="text">
+        <string>Update Cache</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QPushButton" name="clearCacheButton">
+       <property name="toolTip">
+        <string>Press to clear the local cache database</string>
+       </property>
+       <property name="text">
+        <string>Clear Cache</string>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </item>
+   <item>
+    <widget class="QDialogButtonBox" name="buttonBox">
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+     <property name="standardButtons">
+      <set>QDialogButtonBox::Close</set>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <tabstops>
+  <tabstop>gsbGroupBox</tabstop>
+  <tabstop>gsbApiKeyEdit</tabstop>
+  <tabstop>gsbHelpButton</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/WebBrowser/SafeBrowsing/SafeBrowsingManager.py	Sat Jul 29 19:41:16 2017 +0200
@@ -0,0 +1,201 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2017 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing the interface for Google Safe Browsing.
+"""
+
+#
+# Some part of this code were ported from gglsbl.client and adapted
+# to Qt.
+#
+# https://github.com/afilipovich/gglsbl
+#
+
+from __future__ import unicode_literals
+
+import os
+import base64
+
+from PyQt5.QtCore import QObject
+
+import Preferences
+import Utilities
+
+from .SafeBrowsingAPIClient import SafeBrowsingAPIClient
+from .SafeBrowsingCache import SafeBrowsingCache, ThreatList, HashPrefixList
+
+
+class SafeBrowsingManager(QObject):
+    """
+    Class implementing the interface for Google Safe Browsing.
+    """
+    def __init__(self):
+        """
+        Constructor
+        """
+        super(SafeBrowsingManager, self).__init__()
+        
+        self.__apiKey = Preferences.getWebBrowser("SafeBrowsingApiKey")
+        if self.__apiKey:
+##            self.__apiClient = SafeBrowsingAPIClient(self.__apiKey,
+##                                                     parent=self)
+            # TODO: switch these after debugging is finished
+            self.__apiClient = SafeBrowsingAPIClient(self.__apiKey,
+                                                     parent=self,
+                                                     fairUse=False)
+        else:
+            self.__apiClient = None
+        
+        self.__enabled = (
+            Preferences.getWebBrowser("SafeBrowsingEnabled") and
+            bool(self.__apiKey))
+        
+        gsbCachePath = os.path.join(
+            Utilities.getConfigDir(), "web_browser", "safe_browsing")
+        self.__cache = SafeBrowsingCache(gsbCachePath, self)
+        
+        self.__gsbDialog = None
+        self.__platforms = None     # TODO: delete if not needed
+    
+    def configurationChanged(self):
+        """
+        Public method to handle changes of the settings.
+        """
+        apiKey = Preferences.getWebBrowser("SafeBrowsingApiKey")
+        if apiKey != self.__apiKey:
+            self.__apiKey = apiKey
+            if self.__apiKey:
+                if self.__apiClient:
+                    self.__apiClient.setApiKey(self.__apiKey)
+                else:
+##                    self.__apiClient = SafeBrowsingAPIClient(self.__apiKey,
+##                                                             parent=self)
+                    # TODO: switch these after debugging is finished
+                    self.__apiClient = SafeBrowsingAPIClient(self.__apiKey,
+                                                             parent=self,
+                                                             fairUse=False)
+        
+        self.__enabled = (
+            Preferences.getWebBrowser("SafeBrowsingEnabled") and
+            bool(self.__apiKey))
+    
+    def isEnabled(self):
+        """
+        Public method to check, if safe browsing is enabled.
+        
+        @return flag indicating the enabled state
+        @rtype bool
+        """
+        return self.__enabled
+    
+    def close(self):
+        """
+        Public method to close the safe browsing interface.
+        """
+        self.__cache.close()
+    
+    def fairUseDelayExpired(self):
+        """
+        Public method to check, if the fair use wait period has expired.
+        
+        @return flag indicating expiration
+        @rtype bool
+        """
+        return self.__enabled and self.__apiClient.fairUseDelayExpired()
+    
+    def updateHashPrefixCache(self):
+        """
+        Public method to load or update the locally cached threat lists.
+        
+        @return flag indicating success and an error message
+        @rtype tuple of (bool, str)
+        """
+        if not self.__enabled:
+            return False, self.tr("Safe Browsing is disabled.")
+        
+        if not self.__apiClient.fairUseDelayExpired():
+            return False, \
+                self.tr("The fair use wait period has not expired yet.")
+        
+        # step 1: remove expired hashes
+        self.__cache.cleanupFullHashes()
+        
+        # step 2: update threat lists
+        threatListsForRemove = {}
+        for threatList, clientState in self.__cache.getThreatLists():
+            threatListsForRemove[repr(threatList)] = threatList
+        threatLists = self.__apiClient.getThreatLists()
+        for entry in threatLists:
+            threatList = ThreatList.fromApiEntry(entry)
+            if self.__platforms is None or \
+                    threatList.platformType in self.__platforms:
+                self.__cache.addThreatList(threatList)
+                key = repr(threatList)
+                if key in threatListsForRemove:
+                    del threatListsForRemove[key]
+        for threatList in threatListsForRemove.values():
+            self.__cache.deleteHashPrefixList(threatList)
+            self.__cache.deleteThreatList(threatList)
+        del threatListsForRemove
+        
+        # step 3: update threats
+        threatLists = self.__cache.getThreatLists()
+        clientStates = {}
+        for threatList, clientState in threatLists:
+            clientStates[threatList.asTuple()] = clientState
+        threatsUpdateResponses = \
+            self.__apiClient.getThreatsUpdate(clientStates)
+        for response in threatsUpdateResponses:
+            responseThreatList = ThreatList.fromApiEntry(response)
+            if response["responseType"] == "FULL_UPDATE":
+                self.__cache.deleteHashPrefixList(responseThreatList)
+            for removal in response.get("removals", []):
+                self.__cache.removeHashPrefixIndices(
+                    responseThreatList, removal["rawIndices"]["indices"])
+            for addition in response.get("additions", []):
+                hashPrefixList = HashPrefixList(
+                    addition["rawHashes"]["prefixSize"],
+                    base64.b64decode(addition["rawHashes"]["rawHashes"]))
+                self.__cache.populateHashPrefixList(responseThreatList,
+                                                    hashPrefixList)
+            expectedChecksum = base64.b64decode(response["checksum"]["sha256"])
+            if self.__verifyThreatListChecksum(responseThreatList,
+                                               expectedChecksum):
+                self.__cache.updateThreatListClientState(
+                    responseThreatList, response["newClientState"])
+            else:
+                return False, \
+                    self.tr("Local cache checksum does not match the server."
+                            " Consider cleaning the cache. Threat update has"
+                            " been aborted.")
+        
+        return True, ""
+    
+    def __verifyThreatListChecksum(self, threatList, remoteChecksum):
+        """
+        Private method to verify the local checksum of a threat list with the
+        checksum of the safe browsing server.
+        """
+        localChecksum = self.__cache.hashPrefixListChecksum(threatList)
+        return remoteChecksum == localChecksum
+    
+    def fullCacheCleanup(self):
+        """
+        Public method to clean up the cache completely.
+        """
+        self.__cache.prepareCacheDb()
+    
+    def showSafeBrowsingDialog(self):
+        """
+        Public slot to show the safe browsing management dialog.
+        """
+        if self.__gsbDialog is None:
+            from WebBrowser.WebBrowserWindow import WebBrowserWindow
+            from .SafeBrowsingDialog import SafeBrowsingDialog
+            self.__gsbDialog = SafeBrowsingDialog(
+                self, parent=WebBrowserWindow.mainWindow())
+        
+        self.__gsbDialog.show()
--- a/WebBrowser/WebBrowserWindow.py	Wed Jul 26 19:46:17 2017 +0200
+++ b/WebBrowser/WebBrowserWindow.py	Sat Jul 29 19:41:16 2017 +0200
@@ -108,6 +108,7 @@
     _autoScroller = None
     _tabManager = None
     _sessionManager = None
+    _safeBrowsingManager = None
     
     _performingStartup = True
     _performingShutdown = False
@@ -1757,6 +1758,23 @@
                 self.__showCertificateErrorsDialog)
         self.__actions.append(self.certificateErrorsAct)
         
+        self.safeBrowsingAct = E5Action(
+            self.tr('Manage Safe Browsing'),
+            UI.PixmapCache.getIcon("safeBrowsing.png"),
+            self.tr('Manage Safe Browsing...'), 0, 0, self,
+            'webbrowser_manage_safe_browsing')
+        self.safeBrowsingAct.setStatusTip(self.tr(
+            'Configure Safe Browsing and manage local cache'))
+        self.safeBrowsingAct.setWhatsThis(self.tr(
+            """<b>Manage Safe Browsing</b>"""
+            """<p>This opens a dialog to configure Safe Browsing and"""
+            """ to manage the local cache.</p>"""
+        ))
+        if not self.__initShortcutsOnly:
+            self.safeBrowsingAct.triggered.connect(
+                self.__showSafeBrowsingDialog)
+        self.__actions.append(self.safeBrowsingAct)
+        
         self.showDownloadManagerAct = E5Action(
             self.tr('Downloads'),
             self.tr('Downloads'),
@@ -2084,6 +2102,8 @@
         menu.addSeparator()
         menu.addAction(self.adblockAct)
         menu.addSeparator()
+        menu.addAction(self.safeBrowsingAct)
+        menu.addSeparator()
         self.__settingsMenu = menu
         self.__settingsMenu.aboutToShow.connect(
             self.__aboutToShowSettingsMenu)
@@ -2205,6 +2225,8 @@
         menu.addSeparator()
         menu.addAction(self.adblockAct)
         menu.addSeparator()
+        menu.addAction(self.safeBrowsingAct)
+        menu.addSeparator()
         menu.addMenu(self.__userAgentMenu)
         menu.addAction(self.userAgentManagerAct)
         menu.addSeparator()
@@ -2946,6 +2968,8 @@
         
         self.networkManager().shutdown()
         
+        self.safeBrowsingManager().close()
+        
         for browser in WebBrowserWindow.BrowserWindows:
             if browser != self:
                 browser.close()
@@ -4833,3 +4857,27 @@
         Private slot to show the session manager dialog.
         """
         self.sessionManager().showSessionManagerDialog()
+    
+    ##########################################################
+    ## Methods below implement safe browsing related functions
+    ##########################################################
+    
+    @classmethod
+    def safeBrowsingManager(cls):
+        """
+        Class method to get a reference to the safe browsing interface.
+        
+        @return reference to the safe browsing manager
+        @rtype SafeBrowsingManager
+        """
+        if cls._safeBrowsingManager is None:
+            from .SafeBrowsing.SafeBrowsingManager import SafeBrowsingManager
+            cls._safeBrowsingManager = SafeBrowsingManager()
+        
+        return cls._safeBrowsingManager
+    
+    def __showSafeBrowsingDialog(self):
+        """
+        Private slot to show the safe browsing management dialog.
+        """
+        self.safeBrowsingManager().showSafeBrowsingDialog()
--- a/eric6.e4p	Wed Jul 26 19:46:17 2017 +0200
+++ b/eric6.e4p	Sat Jul 29 19:41:16 2017 +0200
@@ -1406,6 +1406,8 @@
     <Source>WebBrowser/QtHelp/__init__.py</Source>
     <Source>WebBrowser/SafeBrowsing/SafeBrowsingAPIClient.py</Source>
     <Source>WebBrowser/SafeBrowsing/SafeBrowsingCache.py</Source>
+    <Source>WebBrowser/SafeBrowsing/SafeBrowsingDialog.py</Source>
+    <Source>WebBrowser/SafeBrowsing/SafeBrowsingManager.py</Source>
     <Source>WebBrowser/SafeBrowsing/SafeBrowsingUrl.py</Source>
     <Source>WebBrowser/SafeBrowsing/SafeBrowsingUtilities.py</Source>
     <Source>WebBrowser/SafeBrowsing/__init__.py</Source>
@@ -1938,6 +1940,7 @@
     <Form>WebBrowser/QtHelp/QtHelpDocumentationDialog.ui</Form>
     <Form>WebBrowser/QtHelp/QtHelpDocumentationSelectionDialog.ui</Form>
     <Form>WebBrowser/QtHelp/QtHelpFiltersDialog.ui</Form>
+    <Form>WebBrowser/SafeBrowsing/SafeBrowsingDialog.ui</Form>
     <Form>WebBrowser/SearchWidget.ui</Form>
     <Form>WebBrowser/Session/SessionManagerDialog.ui</Form>
     <Form>WebBrowser/SiteInfo/SiteInfoDialog.ui</Form>
@@ -2002,14 +2005,14 @@
   <Interfaces/>
   <Others>
     <Other>.hgignore</Other>
+    <Other>APIs/Python/zope-2.10.7.api</Other>
+    <Other>APIs/Python/zope-2.11.2.api</Other>
+    <Other>APIs/Python/zope-3.3.1.api</Other>
     <Other>APIs/Python3/PyQt4.bas</Other>
     <Other>APIs/Python3/PyQt5.bas</Other>
     <Other>APIs/Python3/QScintilla2.bas</Other>
     <Other>APIs/Python3/eric6.api</Other>
     <Other>APIs/Python3/eric6.bas</Other>
-    <Other>APIs/Python/zope-2.10.7.api</Other>
-    <Other>APIs/Python/zope-2.11.2.api</Other>
-    <Other>APIs/Python/zope-3.3.1.api</Other>
     <Other>APIs/QSS/qss.api</Other>
     <Other>APIs/Ruby/Ruby-1.8.7.api</Other>
     <Other>APIs/Ruby/Ruby-1.8.7.bas</Other>
Binary file icons/default/safeBrowsing.png has changed
Binary file icons/default/safeBrowsing48.png has changed

eric ide

mercurial