eric6/WebBrowser/WebBrowserPage.py

changeset 6942
2602857055c5
parent 6789
6bafe4f7d5f0
child 7192
a22eee00b052
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric6/WebBrowser/WebBrowserPage.py	Sun Apr 14 15:09:21 2019 +0200
@@ -0,0 +1,601 @@
+# -*- 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 ""

eric ide

mercurial