Sun, 06 Mar 2016 16:59:26 +0100
Continued porting the web browser.
- added capability to save the SSL certificate errors exceptions to the network manager
--- a/Preferences/__init__.py Sun Mar 06 14:12:58 2016 +0100 +++ b/Preferences/__init__.py Sun Mar 06 16:59:26 2016 +0100 @@ -1032,6 +1032,7 @@ "WebInspectorEnabled": False, "DiskCacheEnabled": True, "DiskCacheSize": 50, # 50 MB + "SslExceptionsDB": "{}", # empty JSON dictionary # Grease Monkey "GreaseMonkeyDisabledScripts": [], # Downloads
--- a/WebBrowser/Network/NetworkManager.py Sun Mar 06 14:12:58 2016 +0100 +++ b/WebBrowser/Network/NetworkManager.py Sun Mar 06 16:59:26 2016 +0100 @@ -9,6 +9,9 @@ from __future__ import unicode_literals +import json + +from PyQt5.QtCore import pyqtSignal from PyQt5.QtWidgets import QDialog from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkProxy @@ -23,13 +26,18 @@ from WebBrowser.WebBrowserWindow import WebBrowserWindow +from Utilities.AutoSaver import AutoSaver import Preferences class NetworkManager(QNetworkAccessManager): """ Class implementing a network manager. + + @signal changed() emitted to indicate a change """ + changed = pyqtSignal() + def __init__(self, parent=None): """ Constructor @@ -52,13 +60,60 @@ self.__sslErrorHandler = E5SslErrorHandler(self) self.sslErrors.connect(self.__sslErrorHandler.sslErrorsReplySlot) - self.__ignoredSslErrors = {} - # dictionary of temporarily ignored SSL errors + self.__temporarilyIgnoredSslErrors = {} + self.__permanentlyIgnoredSslErrors = {} + # dictionaries of permanently and temporarily ignored SSL errors + self.__loaded = False + self.__saveTimer = AutoSaver(self, self.__save) + + self.changed.connect(self.__saveTimer.changeOccurred) self.proxyAuthenticationRequired.connect(proxyAuthenticationRequired) self.authenticationRequired.connect( lambda reply, auth: self.authentication(reply.url(), auth)) + def __save(self): + """ + Private slot to save the permanent SSL error exceptions. + """ + if not self.__loaded: + return + + from WebBrowser.WebBrowserWindow import WebBrowserWindow + if not WebBrowserWindow.isPrivate(): + dbString = json.dumps(self.__permanentlyIgnoredSslErrors) + Preferences.setWebBrowser("SslExceptionsDB", dbString) + + def __load(self): + """ + Private method to load the permanent SSL error exceptions. + """ + if self.__loaded: + return + + dbString = Preferences.getWebBrowser("SslExceptionsDB") + if dbString: + try: + db = json.loads(dbString) + self.__permanentlyIgnoredSslErrors = db + except ValueError: + # ignore silently + pass + + self.__loaded = True + + def showSslErrorExceptionsDialog(self): + """ + Public method to show the SSL error exceptions dialog. + """ + self.__load() + + from .SslErrorExceptionsDialog import SslErrorExceptionsDialog + dlg = SslErrorExceptionsDialog(self.__permanentlyIgnoredSslErrors) + if dlg.exec_() == QDialog.Accepted: + self.__permanentlyIgnoredSslErrors = dlg.getSslErrorExceptions() + self.changed.emit() + def certificateError(self, error, view): """ Public method to handle SSL certificate errors. @@ -70,16 +125,21 @@ @return flag indicating to ignore this error @rtype bool """ - # TODO: permanent SSL certificate error exceptions + self.__load() + host = error.url().host() - if host in self.__ignoredSslErrors and \ - self.__ignoredSslErrors[host] == error.error(): + if host in self.__temporarilyIgnoredSslErrors and \ + error.error() in self.__temporarilyIgnoredSslErrors[host]: + return True + + if host in self.__permanentlyIgnoredSslErrors and \ + error.error() in self.__permanentlyIgnoredSslErrors[host]: return True title = self.tr("SSL Certificate Error") - accept = E5MessageBox.yesNo( - view, + msgBox = E5MessageBox.E5MessageBox( + E5MessageBox.Warning, title, self.tr("""<b>{0}</b>""" """<p>The page you are trying to access has errors""" @@ -87,12 +147,26 @@ """<ul><li>{1}</li></ul>""" """<p>Would you like to make an exception?</p>""") .format(title, error.errorDescription()), - icon=E5MessageBox.Warning) - if accept: - self.__ignoredSslErrors[error.url().host()] = error.error() + modal=True, parent=view) + permButton = msgBox.addButton(self.tr("&Permanent accept"), + E5MessageBox.AcceptRole) + tempButton = msgBox.addButton(self.tr("&Temporary accept"), + E5MessageBox.AcceptRole) + msgBox.addButton(self.tr("&Reject"), E5MessageBox.RejectRole) + msgBox.exec_() + if msgBox.clickedButton() == permButton: + if host not in self.__permanentlyIgnoredSslErrors: + self.__permanentlyIgnoredSslErrors[host] = [] + self.__permanentlyIgnoredSslErrors[host].append(error.error()) + self.changed.emit() return True - - return False + elif msgBox.clickedButton() == tempButton: + if host not in self.__temporarilyIgnoredSslErrors: + self.__temporarilyIgnoredSslErrors[host] = [] + self.__temporarilyIgnoredSslErrors[host].append(error.error()) + return True + else: + return False def authentication(self, url, auth): """
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Network/SslErrorExceptionsDialog.py Sun Mar 06 16:59:26 2016 +0100 @@ -0,0 +1,167 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a dialog to edit the SSL error exceptions. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import pyqtSlot, QPoint +from PyQt5.QtWidgets import QDialog, QTreeWidgetItem, QMenu +from PyQt5.QtWebEngineWidgets import QWebEngineCertificateError + +from .Ui_SslErrorExceptionsDialog import Ui_SslErrorExceptionsDialog + + +class SslErrorExceptionsDialog(QDialog, Ui_SslErrorExceptionsDialog): + """ + Class implementing a dialog to edit the SSL error exceptions. + """ + def __init__(self, errorsDict, parent=None): + """ + Constructor + + @param errorsDict error exceptions + @type dict of list of int + @param parent reference to the parent widget + @type QWidget + """ + super(SslErrorExceptionsDialog, self).__init__(parent) + self.setupUi(self) + + self.__errorDescriptions = { + QWebEngineCertificateError.SslPinnedKeyNotInCertificateChain: + self.tr("The certificate did not match the built-in public" + " keys pinned for the host name."), + QWebEngineCertificateError.CertificateCommonNameInvalid: + self.tr("The certificate's common name did not match the" + " host name."), + QWebEngineCertificateError.CertificateDateInvalid: + self.tr("The certificate is not valid at the current date" + " and time."), + QWebEngineCertificateError.CertificateAuthorityInvalid: + self.tr("The certificate is not signed by a trusted" + " authority."), + QWebEngineCertificateError.CertificateContainsErrors: + self.tr("The certificate contains errors."), + QWebEngineCertificateError.CertificateNoRevocationMechanism: + self.tr("The certificate has no mechanism for determining if" + " it has been revoked."), + QWebEngineCertificateError.CertificateUnableToCheckRevocation: + self.tr("Revocation information for the certificate is" + " not available."), + QWebEngineCertificateError.CertificateRevoked: + self.tr("The certificate has been revoked."), + QWebEngineCertificateError.CertificateInvalid: + self.tr("The certificate is invalid."), + QWebEngineCertificateError.CertificateWeakSignatureAlgorithm: + self.tr("The certificate is signed using a weak signature" + " algorithm."), + QWebEngineCertificateError.CertificateNonUniqueName: + self.tr("The host name specified in the certificate is" + " not unique."), + QWebEngineCertificateError.CertificateWeakKey: + self.tr("The certificate contains a weak key."), + QWebEngineCertificateError.CertificateNameConstraintViolation: + self.tr("The certificate claimed DNS names that are in" + " violation of name constraints."), + } + + for host, errors in errorsDict.items(): + itm = QTreeWidgetItem(self.errorsTree, [host]) + self.errorsTree.setFirstItemColumnSpanned(itm, True) + for error in errors: + try: + errorDesc = self.__errorDescriptions[error] + except KeyError: + errorDesc = self.tr("No error description available.") + QTreeWidgetItem(itm, [str(error), errorDesc]) + + self.errorsTree.expandAll() + for i in range(self.errorsTree.columnCount()): + self.errorsTree.resizeColumnToContents(i) + + self.__setRemoveButtons() + + def __setRemoveButtons(self): + """ + Private method to set the state of the 'remove' buttons. + """ + if self.errorsTree.topLevelItemCount() == 0: + self.removeButton.setEnabled(False) + self.removeAllButton.setEnabled(False) + else: + self.removeAllButton.setEnabled(True) + self.removeButton.setEnabled( + len(self.errorsTree.selectedItems()) > 0) + + @pyqtSlot(QPoint) + def on_errorsTree_customContextMenuRequested(self, pos): + """ + Private slot to show the context menu. + + @param pos cursor position + @type QPoint + """ + menu = QMenu() + menu.addAction( + self.tr("Remove Selected"), + self.on_removeButton_clicked).setEnabled( + self.errorsTree.topLevelItemCount() > 0 and + len(self.errorsTree.selectedItems()) > 0) + menu.addAction( + self.tr("Remove All"), + self.on_removeAllButton_clicked).setEnabled( + self.errorsTree.topLevelItemCount() > 0) + + menu.exec_(self.errorsTree.mapToGlobal(pos)) + + @pyqtSlot() + def on_errorsTree_itemSelectionChanged(self): + """ + Private slot handling the selection of entries. + """ + self.__setRemoveButtons() + + @pyqtSlot() + def on_removeButton_clicked(self): + """ + Private slot to remove the selected items. + """ + for itm in self.errorsTree.selectedItems(): + pitm = itm.parent() + if pitm: + pitm.removeChild(itm) + else: + index = self.errorsTree.indexOfTopLevelItem(itm) + self.errorsTree.takeTopLevelItem(index) + del itm + + @pyqtSlot() + def on_removeAllButton_clicked(self): + """ + Private slot to remove all entries. + """ + self.errorsTree.clear() + + def getSslErrorExceptions(self): + """ + Public method to retrieve the list of SSL error exceptions. + + @return error exceptions + @rtype dict of list of int + """ + errors = {} + + for index in range(self.errorsTree.topLevelItemCount()): + itm = self.errorsTree.topLevelItem(index) + host = itm.text(0) + errors[host] = [] + for cindex in range(itm.childCount()): + citm = itm.child(cindex) + errors[host].append(int(citm.text(0))) + + return errors
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Network/SslErrorExceptionsDialog.ui Sun Mar 06 16:59:26 2016 +0100 @@ -0,0 +1,169 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>SslErrorExceptionsDialog</class> + <widget class="QDialog" name="SslErrorExceptionsDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>751</width> + <height>513</height> + </rect> + </property> + <property name="windowTitle"> + <string>SSL Error Exceptions</string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0" rowspan="3"> + <widget class="QTreeWidget" name="errorsTree"> + <property name="contextMenuPolicy"> + <enum>Qt::CustomContextMenu</enum> + </property> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="selectionMode"> + <enum>QAbstractItemView::ExtendedSelection</enum> + </property> + <property name="sortingEnabled"> + <bool>true</bool> + </property> + <property name="allColumnsShowFocus"> + <bool>true</bool> + </property> + <column> + <property name="text"> + <string>Code</string> + </property> + </column> + <column> + <property name="text"> + <string>Error Description</string> + </property> + </column> + </widget> + </item> + <item row="0" column="1"> + <widget class="QPushButton" name="removeButton"> + <property name="toolTip"> + <string>Press to remove the selected entries</string> + </property> + <property name="text"> + <string>&Remove</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QPushButton" name="removeAllButton"> + <property name="toolTip"> + <string>Press to remove all entries</string> + </property> + <property name="text"> + <string>Remove &All</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>128</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + <item> + <layout class="QHBoxLayout"> + <property name="spacing"> + <number>6</number> + </property> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <spacer> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>131</width> + <height>31</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="okButton"> + <property name="text"> + <string>&OK</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="cancelButton"> + <property name="text"> + <string>&Cancel</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + <resources/> + <connections> + <connection> + <sender>okButton</sender> + <signal>clicked()</signal> + <receiver>SslErrorExceptionsDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>278</x> + <y>253</y> + </hint> + <hint type="destinationlabel"> + <x>96</x> + <y>254</y> + </hint> + </hints> + </connection> + <connection> + <sender>cancelButton</sender> + <signal>clicked()</signal> + <receiver>SslErrorExceptionsDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>369</x> + <y>253</y> + </hint> + <hint type="destinationlabel"> + <x>179</x> + <y>282</y> + </hint> + </hints> + </connection> + </connections> +</ui>
--- a/WebBrowser/Passwords/PasswordManager.py Sun Mar 06 14:12:58 2016 +0100 +++ b/WebBrowser/Passwords/PasswordManager.py Sun Mar 06 16:59:26 2016 +0100 @@ -150,19 +150,22 @@ if not self.__loaded: return - from .PasswordWriter import PasswordWriter - loginFile = self.getFileName() - writer = PasswordWriter() - if not writer.write( - loginFile, self.__logins, self.__loginForms, self.__never): - E5MessageBox.critical( - None, - self.tr("Saving login data"), - self.tr( - """<p>Login data could not be saved to <b>{0}</b></p>""" - ).format(loginFile)) - else: - self.passwordsSaved.emit() + from WebBrowser.WebBrowserWindow import WebBrowserWindow + if not WebBrowserWindow.isPrivate(): + from .PasswordWriter import PasswordWriter + loginFile = self.getFileName() + writer = PasswordWriter() + if not writer.write( + loginFile, self.__logins, self.__loginForms, self.__never): + E5MessageBox.critical( + None, + self.tr("Saving login data"), + self.tr( + """<p>Login data could not be saved to""" + """ <b>{0}</b></p>""" + ).format(loginFile)) + else: + self.passwordsSaved.emit() def __load(self): """
--- a/WebBrowser/WebBrowserWindow.py Sun Mar 06 14:12:58 2016 +0100 +++ b/WebBrowser/WebBrowserWindow.py Sun Mar 06 16:59:26 2016 +0100 @@ -1603,6 +1603,24 @@ ## self.__showCertificatesDialog) ## self.__actions.append(self.certificatesAct) + self.certificateErrorsAct = E5Action( + self.tr('Manage SSL Certificate Errors'), + UI.PixmapCache.getIcon("certificates.png"), + self.tr('Manage SSL Certificate Errors...'), + 0, 0, + self, 'webbrowser_manage_certificate_errors') + self.certificateErrorsAct.setStatusTip(self.tr( + 'Manage the accepted SSL certificate Errors')) + self.certificateErrorsAct.setWhatsThis(self.tr( + """<b>Manage SSL Certificate Errors...</b>""" + """<p>Opens a dialog to manage the accepted SSL""" + """ certificate errors.</p>""" + )) + if not self.__initShortcutsOnly: + self.certificateErrorsAct.triggered.connect( + self.__showCertificateErrorsDialog) + self.__actions.append(self.certificateErrorsAct) + # TODO: Network Monitor (?) ## self.toolsMonitorAct = E5Action( ## self.tr('Network Monitor'), @@ -1850,7 +1868,8 @@ ## from .Network.NetworkManager import SSL_AVAILABLE ## if SSL_AVAILABLE: ## menu.addAction(self.certificatesAct) -## menu.addSeparator() + menu.addAction(self.certificateErrorsAct) + menu.addSeparator() menu.addAction(self.zoomValuesAct) menu.addSeparator() ## menu.addAction(self.adblockAct) @@ -3326,6 +3345,12 @@ ## dlg = E5SslCertificatesDialog(self) ## dlg.exec_() ## + def __showCertificateErrorsDialog(self): + """ + Private slot to show the certificate errors management dialog. + """ + self.networkManager().showSslErrorExceptionsDialog() + # TODO: AdBlock ## def __showAdBlockDialog(self): ## """
--- a/WebBrowser/ZoomManager/ZoomManager.py Sun Mar 06 14:12:58 2016 +0100 +++ b/WebBrowser/ZoomManager/ZoomManager.py Sun Mar 06 16:59:26 2016 +0100 @@ -70,8 +70,10 @@ if not self.__loaded: return - dbString = json.dumps(self.__zoomDB) - Preferences.setWebBrowser("ZoomValuesDB", dbString) + from WebBrowser.WebBrowserWindow import WebBrowserWindow + if not WebBrowserWindow.isPrivate(): + dbString = json.dumps(self.__zoomDB) + Preferences.setWebBrowser("ZoomValuesDB", dbString) def __keyFromUrl(self, url): """
--- a/eric6.e4p Sun Mar 06 14:12:58 2016 +0100 +++ b/eric6.e4p Sun Mar 06 16:59:26 2016 +0100 @@ -1337,6 +1337,7 @@ <Source>WebBrowser/Network/FollowRedirectReply.py</Source> <Source>WebBrowser/Network/LoadRequest.py</Source> <Source>WebBrowser/Network/NetworkManager.py</Source> + <Source>WebBrowser/Network/SslErrorExceptionsDialog.py</Source> <Source>WebBrowser/Network/UrlInterceptor.py</Source> <Source>WebBrowser/Network/__init__.py</Source> <Source>WebBrowser/OpenSearch/DefaultSearchEngines/DefaultSearchEngines_rc.py</Source> @@ -1826,6 +1827,7 @@ <Form>WebBrowser/GreaseMonkey/GreaseMonkeyConfiguration/GreaseMonkeyConfigurationDialog.ui</Form> <Form>WebBrowser/GreaseMonkey/GreaseMonkeyConfiguration/GreaseMonkeyConfigurationScriptInfoDialog.ui</Form> <Form>WebBrowser/History/HistoryDialog.ui</Form> + <Form>WebBrowser/Network/SslErrorExceptionsDialog.ui</Form> <Form>WebBrowser/OpenSearch/OpenSearchDialog.ui</Form> <Form>WebBrowser/OpenSearch/OpenSearchEditDialog.ui</Form> <Form>WebBrowser/PageScreenDialog.ui</Form>