Sun, 17 Feb 2019 15:42:05 +0100
WebBrowserPage: fixed a TypeError caused by code specific to Qt 5.12 and newer.
# -*- coding: utf-8 -*- # Copyright (c) 2008 - 2019 Detlev Offenbach <detlev@die-offenbachs.de> # """ Module implementing the helpbrowser using QWebView. """ from __future__ import unicode_literals, print_function try: str = unicode # __IGNORE_EXCEPTION__ except NameError: pass from PyQt5.QtCore import pyqtSlot, pyqtSignal, QUrl, QUrlQuery, QTimer, \ QEventLoop, QPoint, QPointF from PyQt5.QtGui import QDesktopServices from PyQt5.QtWebEngineWidgets import QWebEnginePage, QWebEngineSettings, \ QWebEngineScript from PyQt5.QtWebChannel import QWebChannel from E5Gui import E5MessageBox from WebBrowser.WebBrowserWindow import WebBrowserWindow from .JavaScript.ExternalJsObject import ExternalJsObject from .Tools.WebHitTestResult import WebHitTestResult from .Tools import Scripts import Preferences from Globals import qVersionTuple 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 malicious web site as determined by safe browsing @signal printPageRequested() emitted to indicate a print request of the shown web page @signal navigationRequestAccepted(url, navigation type, main frame) emitted to signal an accepted navigation request """ if qVersionTuple() >= (5, 7, 0): SafeJsWorld = QWebEngineScript.ApplicationWorld # SafeJsWorld = QWebEngineScript.MainWorld else: SafeJsWorld = QWebEngineScript.MainWorld UnsafeJsWorld = QWebEngineScript.MainWorld safeBrowsingAbort = pyqtSignal() safeBrowsingBad = pyqtSignal(str, str) printPageRequested = pyqtSignal() navigationRequestAccepted = pyqtSignal(QUrl, QWebEnginePage.NavigationType, bool) def __init__(self, parent=None): """ Constructor @param parent parent widget of this window (QWidget) """ super(WebBrowserPage, self).__init__( WebBrowserWindow.webProfile(), parent) self.__printer = None self.__badSite = False self.__registerProtocolHandlerRequest = None self.featurePermissionRequested.connect( self.__featurePermissionRequested) self.authenticationRequired.connect( lambda url, auth: WebBrowserWindow.networkManager().authentication( url, auth, self)) self.proxyAuthenticationRequired.connect( WebBrowserWindow.networkManager().proxyAuthentication) self.fullScreenRequested.connect(self.__fullScreenRequested) self.urlChanged.connect(self.__urlChanged) try: self.contentsSizeChanged.connect(self.__contentsSizeChanged) except AttributeError: # defined for Qt >= 5.7 pass try: self.registerProtocolHandlerRequested.connect( self.__registerProtocolHandlerRequested) except AttributeError: # defined for Qt >= 5.11 pass # Workaround for changing webchannel world inside # acceptNavigationRequest not working self.__channelUrl = QUrl() self.__channelWorldId = -1 self.__setupChannelTimer = QTimer(self) self.__setupChannelTimer.setSingleShot(True) self.__setupChannelTimer.setInterval(100) self.__setupChannelTimer.timeout.connect(self.__setupChannelTimeout) @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 @type QWebEnginePage.NavigationType @param isMainFrame flag indicating, that the request originated from the main frame @type bool @return flag indicating acceptance @rtype bool """ scheme = url.scheme() if scheme == "mailto": QDesktopServices.openUrl(url) return False # AdBlock if url.scheme() == "abp": if WebBrowserWindow.adBlockManager().addSubscriptionFromUrl(url): return False # GreaseMonkey if type_ == QWebEnginePage.NavigationTypeLinkClicked 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"))) return False elif url.path() == "PrintPage": self.printPageRequested.emit() return False # Safe Browsing self.__badSite = False from WebBrowser.SafeBrowsing.SafeBrowsingManager import \ SafeBrowsingManager if SafeBrowsingManager.isEnabled() and \ url.scheme() not in \ SafeBrowsingManager.getIgnoreSchemes(): threatLists = \ WebBrowserWindow.safeBrowsingManager().lookupUrl(url)[0] if threatLists: threatMessages = WebBrowserWindow.safeBrowsingManager()\ .getThreatMessages(threatLists) res = E5MessageBox.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)), E5MessageBox.StandardButtons( E5MessageBox.Abort | E5MessageBox.Ignore), E5MessageBox.Abort) if res == E5MessageBox.Abort: self.safeBrowsingAbort.emit() return False self.__badSite = True threatType = WebBrowserWindow.safeBrowsingManager()\ .getThreatType(threatLists[0]) self.safeBrowsingBad.emit(threatType, "".join(threatMessages)) result = QWebEnginePage.acceptNavigationRequest(self, url, type_, isMainFrame) if result: if isMainFrame: isWeb = url.scheme() in ("http", "https", "ftp", "ftps", "file") globalJsEnabled = WebBrowserWindow.webSettings().testAttribute( QWebEngineSettings.JavascriptEnabled) if isWeb: enable = globalJsEnabled else: enable = True self.settings().setAttribute( QWebEngineSettings.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(): self.settings().setAttribute(QWebEngineSettings.JavascriptEnabled, True) self.triggerAction(QWebEnginePage.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) """ agent = Preferences.getWebBrowser("UserAgent") 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) """ agent = WebBrowserWindow.userAgentsManager().userAgentForUrl(url) if agent == "": # no agent string specified for the given host -> use global one agent = Preferences.getWebBrowser("UserAgent") if agent == "": # 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 @type QWebEnginePage.Feature """ manager = WebBrowserWindow.featurePermissionManager() manager.requestFeaturePermission(self, url, feature) def execJavaScript(self, script, worldId=QWebEngineScript.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 @type int @param timeout max. time the script is given to execute @type int @return result of the script @rtype depending upon script result """ 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 @type int @param callback callback function to be executed when the script has ended @type function """ if qVersionTuple() >= (5, 7, 0) and worldId > -1: if callback is None: QWebEnginePage.runJavaScript(self, script, worldId) else: QWebEnginePage.runJavaScript(self, script, worldId, callback) else: if callback is None: 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.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 ) 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 ) 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(pos.x() // self.zoomFactor(), 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 """ channel = self.webChannel() if channel is None: channel = QWebChannel(self) ExternalJsObject.setupWebChannel(channel, self) worldId = -1 if url.scheme() in ("eric", "qthelp"): worldId = self.UnsafeJsWorld else: worldId = self.SafeJsWorld if worldId != self.__channelWorldId: self.__channelWorldId = worldId try: self.setWebChannel(channel, self.__channelWorldId) except TypeError: # pre Qt 5.7.0 self.setWebChannel(channel) 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()) 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 @type int @return flag indicating a successful print job @rtype bool """ 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 @type str @param lineNumber line number of an error @type int @param sourceId source URL causing the error @type str """ self.view().mainWindow().javascriptConsole().javaScriptConsoleMessage( 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 compatibility functions ################################################## if not hasattr(QWebEnginePage, "icon"): def icon(self): """ Public method to get the web site icon. @return web site icon @rtype QIcon """ return self.view().icon() if not hasattr(QWebEnginePage, "scrollPosition"): def scrollPosition(self): """ Public method to get the scroll position of the web page. @return scroll position @rtype QPointF """ pos = self.execJavaScript( "(function() {" "var res = {" " x: 0," " y: 0," "};" "res.x = window.scrollX;" "res.y = window.scrollY;" "return res;" "})()", WebBrowserPage.SafeJsWorld ) if pos is not None: pos = QPointF(pos["x"], pos["y"]) else: pos = QPointF(0.0, 0.0) return pos ############################################################# ## Methods below implement protocol handler related functions ############################################################# try: @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 PyQt5.QtWebEngineCore import \ QWebEngineRegisterProtocolHandlerRequest if self.__registerProtocolHandlerRequest: del self.__registerProtocolHandlerRequest self.__registerProtocolHandlerRequest = None self.__registerProtocolHandlerRequest = \ QWebEngineRegisterProtocolHandlerRequest(request) except TypeError: # this is supported with Qt 5.12 and later pass 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()): 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()): return self.__registerProtocolHandlerRequest.scheme() else: return ""