--- a/src/eric7/WebBrowser/WebBrowserPage.py Wed Jul 13 11:16:20 2022 +0200 +++ b/src/eric7/WebBrowser/WebBrowserPage.py Wed Jul 13 14:55:47 2022 +0200 @@ -9,16 +9,21 @@ """ from PyQt6.QtCore import ( - pyqtSlot, pyqtSignal, QUrl, QUrlQuery, QTimer, QEventLoop, QPoint + pyqtSlot, + pyqtSignal, + QUrl, + QUrlQuery, + QTimer, + QEventLoop, + QPoint, ) from PyQt6.QtGui import QDesktopServices -from PyQt6.QtWebEngineCore import ( - QWebEnginePage, QWebEngineSettings, QWebEngineScript -) +from PyQt6.QtWebEngineCore import QWebEnginePage, QWebEngineSettings, QWebEngineScript from PyQt6.QtWebChannel import QWebChannel try: from PyQt6.QtNetwork import QSslConfiguration, QSslCertificate + SSL_AVAILABLE = True except ImportError: SSL_AVAILABLE = False @@ -39,7 +44,7 @@ class WebBrowserPage(QWebEnginePage): """ Class implementing an enhanced web page. - + @signal safeBrowsingAbort() emitted to indicate an abort due to a safe browsing event @signal safeBrowsingBad(threatType, threatMessages) emitted to indicate a @@ -51,51 +56,53 @@ @signal sslConfigurationChanged() emitted to indicate a change of the stored SSL configuration data """ + SafeJsWorld = QWebEngineScript.ScriptWorldId.ApplicationWorld UnsafeJsWorld = QWebEngineScript.ScriptWorldId.MainWorld - + safeBrowsingAbort = pyqtSignal() safeBrowsingBad = pyqtSignal(str, str) - + printPageRequested = pyqtSignal() - navigationRequestAccepted = pyqtSignal(QUrl, QWebEnginePage.NavigationType, - bool) - + navigationRequestAccepted = pyqtSignal(QUrl, QWebEnginePage.NavigationType, bool) + sslConfigurationChanged = pyqtSignal() - + def __init__(self, view, parent=None): """ Constructor - + @param view reference to the WebBrowserView associated with the page @type WebBrowserView @param parent reference to the parent widget (defaults to None) @type QWidget (optional) """ - super().__init__( - WebBrowserWindow.webProfile(), parent) - + super().__init__(WebBrowserWindow.webProfile(), parent) + self.__printer = None self.__badSite = False self.__registerProtocolHandlerRequest = None - + self.__view = view - - self.featurePermissionRequested.connect( - self.__featurePermissionRequested) + + self.featurePermissionRequested.connect(self.__featurePermissionRequested) self.authenticationRequired.connect( lambda url, auth: WebBrowserWindow.networkManager().authentication( - url, auth, self)) + url, auth, self + ) + ) self.proxyAuthenticationRequired.connect( - WebBrowserWindow.networkManager().proxyAuthentication) + WebBrowserWindow.networkManager().proxyAuthentication + ) self.fullScreenRequested.connect(self.__fullScreenRequested) self.urlChanged.connect(self.__urlChanged) self.contentsSizeChanged.connect(self.__contentsSizeChanged) self.registerProtocolHandlerRequested.connect( - self.__registerProtocolHandlerRequested) - + self.__registerProtocolHandlerRequested + ) + self.__sslConfiguration = None - + # Workaround for changing webchannel world inside # acceptNavigationRequest not working self.__channelUrl = QUrl() @@ -104,28 +111,28 @@ self.__setupChannelTimer.setSingleShot(True) self.__setupChannelTimer.setInterval(100) self.__setupChannelTimer.timeout.connect(self.__setupChannelTimeout) - + def view(self): """ Public method to get a reference to the WebBrowserView associated with the page. - + @return reference to the WebBrowserView associated with the page r@type WebBrowserView """ return self.__view - + @pyqtSlot() def __setupChannelTimeout(self): """ Private slot to initiate the setup of the web channel. """ self.__setupWebChannelForUrl(self.__channelUrl) - + def acceptNavigationRequest(self, url, type_, isMainFrame): """ Public method to determine, if a request may be accepted. - + @param url URL to navigate to @type QUrl @param type_ type of the navigation request @@ -140,113 +147,114 @@ if scheme == "mailto": QDesktopServices.openUrl(url) return False - + # AdBlock if ( - url.scheme() == "abp" and - WebBrowserWindow.adBlockManager().addSubscriptionFromUrl(url) + url.scheme() == "abp" + and WebBrowserWindow.adBlockManager().addSubscriptionFromUrl(url) ): return False - + # GreaseMonkey navigationType = type_ in ( QWebEnginePage.NavigationType.NavigationTypeLinkClicked, - QWebEnginePage.NavigationType.NavigationTypeRedirect + QWebEnginePage.NavigationType.NavigationTypeRedirect, ) if navigationType and url.toString().endswith(".user.js"): WebBrowserWindow.greaseMonkeyManager().downloadScript(url) return False - + if url.scheme() == "eric": if url.path() == "AddSearchProvider": query = QUrlQuery(url) self.__view.mainWindow().openSearchManager().addEngine( - QUrl(query.queryItemValue("url"))) + QUrl(query.queryItemValue("url")) + ) return False elif url.path() == "PrintPage": self.printPageRequested.emit() return False - + # Safe Browsing self.__badSite = False - from WebBrowser.SafeBrowsing.SafeBrowsingManager import ( - SafeBrowsingManager - ) + from WebBrowser.SafeBrowsing.SafeBrowsingManager import SafeBrowsingManager + if ( - SafeBrowsingManager.isEnabled() and - url.scheme() not in SafeBrowsingManager.getIgnoreSchemes() + SafeBrowsingManager.isEnabled() + and url.scheme() not in SafeBrowsingManager.getIgnoreSchemes() ): - threatLists = ( - WebBrowserWindow.safeBrowsingManager().lookupUrl(url)[0] - ) + threatLists = WebBrowserWindow.safeBrowsingManager().lookupUrl(url)[0] if threatLists: threatMessages = ( - WebBrowserWindow.safeBrowsingManager() - .getThreatMessages(threatLists) + WebBrowserWindow.safeBrowsingManager().getThreatMessages( + threatLists + ) ) res = EricMessageBox.warning( WebBrowserWindow.getWindow(), self.tr("Suspicuous URL detected"), - self.tr("<p>The URL <b>{0}</b> was found in the Safe" - " Browsing database.</p>{1}").format( - url.toString(), "".join(threatMessages)), + self.tr( + "<p>The URL <b>{0}</b> was found in the Safe" + " Browsing database.</p>{1}" + ).format(url.toString(), "".join(threatMessages)), EricMessageBox.Abort | EricMessageBox.Ignore, - EricMessageBox.Abort) + EricMessageBox.Abort, + ) if res == EricMessageBox.Abort: self.safeBrowsingAbort.emit() return False - + self.__badSite = True - threatType = ( - WebBrowserWindow.safeBrowsingManager() - .getThreatType(threatLists[0]) + threatType = WebBrowserWindow.safeBrowsingManager().getThreatType( + threatLists[0] ) self.safeBrowsingBad.emit(threatType, "".join(threatMessages)) - - result = QWebEnginePage.acceptNavigationRequest( - self, url, type_, isMainFrame) - + + result = QWebEnginePage.acceptNavigationRequest(self, url, type_, isMainFrame) + if result: if isMainFrame: - isWeb = url.scheme() in ("http", "https", "ftp", "ftps", - "file") + isWeb = url.scheme() in ("http", "https", "ftp", "ftps", "file") globalJsEnabled = WebBrowserWindow.webSettings().testAttribute( - QWebEngineSettings.WebAttribute.JavascriptEnabled) + QWebEngineSettings.WebAttribute.JavascriptEnabled + ) if isWeb: enable = globalJsEnabled else: enable = True self.settings().setAttribute( - QWebEngineSettings.WebAttribute.JavascriptEnabled, enable) - + QWebEngineSettings.WebAttribute.JavascriptEnabled, enable + ) + self.__channelUrl = url self.__setupChannelTimer.start() self.navigationRequestAccepted.emit(url, type_, isMainFrame) - + return result - + @pyqtSlot(QUrl) def __urlChanged(self, url): """ Private slot to handle changes of the URL. - + @param url new URL @type QUrl """ if ( - not url.isEmpty() and - url.scheme() == "eric" and - not self.isJavaScriptEnabled() + not url.isEmpty() + and url.scheme() == "eric" + and not self.isJavaScriptEnabled() ): self.settings().setAttribute( - QWebEngineSettings.WebAttribute.JavascriptEnabled, True) + QWebEngineSettings.WebAttribute.JavascriptEnabled, True + ) self.triggerAction(QWebEnginePage.WebAction.Reload) - + @classmethod def userAgent(cls, resolveEmpty=False): """ Class method to get the global user agent setting. - + @param resolveEmpty flag indicating to resolve an empty user agent (boolean) @return user agent string (string) @@ -255,21 +263,21 @@ if agent == "" and resolveEmpty: agent = cls.userAgentForUrl(QUrl()) return agent - + @classmethod def setUserAgent(cls, agent): """ Class method to set the global user agent string. - + @param agent new current user agent string (string) """ Preferences.setWebBrowser("UserAgent", agent) - + @classmethod def userAgentForUrl(cls, url): """ Class method to determine the user agent for the given URL. - + @param url URL to determine user agent for (QUrl) @return user agent string (string) """ @@ -281,11 +289,11 @@ # no global agent string specified -> use default one agent = WebBrowserWindow.webProfile().httpUserAgent() return agent - + def __featurePermissionRequested(self, url, feature): """ Private slot handling a feature permission request. - + @param url url requesting the feature @type QUrl @param feature requested feature @@ -293,13 +301,13 @@ """ manager = WebBrowserWindow.featurePermissionManager() manager.requestFeaturePermission(self, url, feature) - - def execJavaScript(self, script, - worldId=QWebEngineScript.ScriptWorldId.MainWorld, - timeout=500): + + def execJavaScript( + self, script, worldId=QWebEngineScript.ScriptWorldId.MainWorld, timeout=500 + ): """ Public method to execute a JavaScript function synchroneously. - + @param script JavaScript script source to be executed @type str @param worldId ID to run the script under @@ -312,21 +320,21 @@ loop = QEventLoop() resultDict = {"res": None} QTimer.singleShot(timeout, loop.quit) - + def resultCallback(res, resDict=resultDict): if loop and loop.isRunning(): resDict["res"] = res loop.quit() - + self.runJavaScript(script, worldId, resultCallback) - + loop.exec() return resultDict["res"] - + def runJavaScript(self, script, worldId=-1, callback=None): """ Public method to run a script in the context of the page. - + @param script JavaScript script source to be executed @type str @param worldId ID to run the script under @@ -345,71 +353,72 @@ QWebEnginePage.runJavaScript(self, script) else: QWebEnginePage.runJavaScript(self, script, callback) - + def isJavaScriptEnabled(self): """ Public method to test, if JavaScript is enabled. - + @return flag indicating the state of the JavaScript support @rtype bool """ return self.settings().testAttribute( - QWebEngineSettings.WebAttribute.JavascriptEnabled) - + QWebEngineSettings.WebAttribute.JavascriptEnabled + ) + def scroll(self, x, y): """ Public method to scroll by the given amount of pixels. - + @param x horizontal scroll value @type int @param y vertical scroll value @type int """ self.runJavaScript( - "window.scrollTo(window.scrollX + {0}, window.scrollY + {1})" - .format(x, y), - WebBrowserPage.SafeJsWorld + "window.scrollTo(window.scrollX + {0}, window.scrollY + {1})".format(x, y), + WebBrowserPage.SafeJsWorld, ) - + def scrollTo(self, pos): """ Public method to scroll to the given position. - + @param pos position to scroll to @type QPointF """ self.runJavaScript( "window.scrollTo({0}, {1});".format(pos.x(), pos.y()), - WebBrowserPage.SafeJsWorld + WebBrowserPage.SafeJsWorld, ) - + def mapToViewport(self, pos): """ Public method to map a position to the viewport. - + @param pos position to be mapped @type QPoint @return viewport position @rtype QPoint """ - return QPoint(int(pos.x() // self.zoomFactor()), - int(pos.y() // self.zoomFactor())) - + return QPoint( + int(pos.x() // self.zoomFactor()), int(pos.y() // self.zoomFactor()) + ) + def hitTestContent(self, pos): """ Public method to test the content at a specified position. - + @param pos position to execute the test at @type QPoint @return test result object @rtype WebHitTestResult """ return WebHitTestResult(self, pos) - + def __setupWebChannelForUrl(self, url): """ Private method to setup a web channel to our external object. - + @param url URL for which to setup the web channel @type QUrl """ @@ -417,49 +426,48 @@ if channel is None: channel = QWebChannel(self) ExternalJsObject.setupWebChannel(channel, self) - + worldId = -1 worldId = ( self.UnsafeJsWorld - if url.scheme() in ("eric", "qthelp") else - self.SafeJsWorld + if url.scheme() in ("eric", "qthelp") + else self.SafeJsWorld ) if worldId != self.__channelWorldId: self.__channelWorldId = worldId self.setWebChannel(channel, self.__channelWorldId) - + def certificateError(self, error): """ Public method to handle SSL certificate errors. - + @param error object containing the certificate error information @type QWebEngineCertificateError @return flag indicating to ignore this error @rtype bool """ - return WebBrowserWindow.networkManager().certificateError( - error, self.__view) - + return WebBrowserWindow.networkManager().certificateError(error, self.__view) + def __fullScreenRequested(self, request): """ Private slot handling a full screen request. - + @param request reference to the full screen request @type QWebEngineFullScreenRequest """ self.__view.requestFullScreen(request.toggleOn()) - + accepted = request.toggleOn() == self.__view.isFullScreen() - + if accepted: request.accept() else: request.reject() - + def execPrintPage(self, printer, timeout=1000): """ Public method to execute a synchronous print. - + @param printer reference to the printer object @type QPrinter @param timeout timeout value in milliseconds @@ -470,36 +478,36 @@ loop = QEventLoop() resultDict = {"res": None} QTimer.singleShot(timeout, loop.quit) - + def printCallback(res, resDict=resultDict): if loop and loop.isRunning(): resDict["res"] = res loop.quit() - + self.print(printer, printCallback) - + loop.exec() return resultDict["res"] - + def __contentsSizeChanged(self, size): """ Private slot to work around QWebEnginePage not scrolling to anchors when opened in a background tab. - + @param size changed contents size (unused) @type QSize """ fragment = self.url().fragment() self.runJavaScript(Scripts.scrollToAnchor(fragment)) - + ############################################## ## Methods below deal with JavaScript messages ############################################## - + def javaScriptConsoleMessage(self, level, message, lineNumber, sourceId): """ Public method to show a console message. - + @param level severity @type QWebEnginePage.JavaScriptConsoleMessageLevel @param message message to be shown @@ -510,172 +518,169 @@ @type str """ self.__view.mainWindow().javascriptConsole().javaScriptConsoleMessage( - level, message, lineNumber, sourceId) - + level, message, lineNumber, sourceId + ) + ########################################################################### ## Methods below implement safe browsing related functions ########################################################################### - + def getSafeBrowsingStatus(self): """ Public method to get the safe browsing status of the current page. - + @return flag indicating a safe site @rtype bool """ return not self.__badSite - + ############################################################# ## Methods below implement protocol handler related functions ############################################################# - + @pyqtSlot("QWebEngineRegisterProtocolHandlerRequest") def __registerProtocolHandlerRequested(self, request): """ Private slot to handle the registration of a custom protocol handler. - + @param request reference to the registration request @type QWebEngineRegisterProtocolHandlerRequest """ - from PyQt6.QtWebEngineCore import ( - QWebEngineRegisterProtocolHandlerRequest - ) - + from PyQt6.QtWebEngineCore import QWebEngineRegisterProtocolHandlerRequest + if self.__registerProtocolHandlerRequest: del self.__registerProtocolHandlerRequest self.__registerProtocolHandlerRequest = None self.__registerProtocolHandlerRequest = ( QWebEngineRegisterProtocolHandlerRequest(request) ) - + def registerProtocolHandlerRequestUrl(self): """ Public method to get the registered protocol handler request URL. - + @return registered protocol handler request URL @rtype QUrl """ - if ( - self.__registerProtocolHandlerRequest and - (self.url().host() == - self.__registerProtocolHandlerRequest.origin().host()) + if self.__registerProtocolHandlerRequest and ( + self.url().host() == self.__registerProtocolHandlerRequest.origin().host() ): return self.__registerProtocolHandlerRequest.origin() else: return QUrl() - + def registerProtocolHandlerRequestScheme(self): """ Public method to get the registered protocol handler request scheme. - + @return registered protocol handler request scheme @rtype str """ - if ( - self.__registerProtocolHandlerRequest and - (self.url().host() == - self.__registerProtocolHandlerRequest.origin().host()) + if self.__registerProtocolHandlerRequest and ( + self.url().host() == self.__registerProtocolHandlerRequest.origin().host() ): return self.__registerProtocolHandlerRequest.scheme() else: return "" - + ############################################################# ## SSL configuration handling below ############################################################# - + def setSslConfiguration(self, sslConfiguration): """ Public slot to set the SSL configuration data of the page. - + @param sslConfiguration SSL configuration to be set @type QSslConfiguration """ self.__sslConfiguration = QSslConfiguration(sslConfiguration) self.__sslConfiguration.url = self.url() self.sslConfigurationChanged.emit() - + def getSslConfiguration(self): """ Public method to return a reference to the current SSL configuration. - + @return reference to the SSL configuration in use @rtype QSslConfiguration """ return self.__sslConfiguration - + def clearSslConfiguration(self): """ Public slot to clear the stored SSL configuration data. """ self.__sslConfiguration = None self.sslConfigurationChanged.emit() - + def getSslCertificate(self): """ Public method to get a reference to the SSL certificate. - + @return amended SSL certificate @rtype QSslCertificate """ if self.__sslConfiguration is None: return None - + sslCertificate = self.__sslConfiguration.peerCertificate() sslCertificate.url = QUrl(self.__sslConfiguration.url) return sslCertificate - + def getSslCertificateChain(self): """ Public method to get a reference to the SSL certificate chain. - + @return SSL certificate chain @rtype list of QSslCertificate """ if self.__sslConfiguration is None: return [] - + chain = self.__sslConfiguration.peerCertificateChain() return chain - + def showSslInfo(self, pos): """ Public slot to show some SSL information for the loaded page. - + @param pos position to show the info at @type QPoint """ if SSL_AVAILABLE and self.__sslConfiguration is not None: from EricNetwork.EricSslInfoWidget import EricSslInfoWidget - widget = EricSslInfoWidget(self.url(), self.__sslConfiguration, - self.__view) + + widget = EricSslInfoWidget(self.url(), self.__sslConfiguration, self.__view) widget.showAt(pos) else: EricMessageBox.warning( self.__view, self.tr("SSL Info"), - self.tr("""This site does not contain SSL information.""")) - + self.tr("""This site does not contain SSL information."""), + ) + def hasValidSslInfo(self): """ Public method to check, if the page has a valid SSL certificate. - + @return flag indicating a valid SSL certificate @rtype bool """ if self.__sslConfiguration is None: return False - + certList = self.__sslConfiguration.peerCertificateChain() if not certList: return False - + certificateDict = Globals.toDict( - Preferences.getSettings().value("Ssl/CaCertificatesDict")) + Preferences.getSettings().value("Ssl/CaCertificatesDict") + ) for server in certificateDict: localCAList = QSslCertificate.fromData(certificateDict[server]) if any(cert in localCAList for cert in certList): return True - + return all(not cert.isBlacklisted() for cert in certList)