eric7/WebBrowser/WebBrowserPage.py

branch
eric7
changeset 8312
800c432b34c8
parent 8260
2161475d9639
child 8318
962bce857696
equal deleted inserted replaced
8311:4e8b98454baa 8312:800c432b34c8
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2008 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6
7 """
8 Module implementing the helpbrowser using QWebView.
9 """
10
11 from PyQt5.QtCore import (
12 pyqtSlot, pyqtSignal, QUrl, QUrlQuery, QTimer, QEventLoop, QPoint, QPointF,
13 QT_VERSION
14 )
15 from PyQt5.QtGui import QDesktopServices
16 from PyQt5.QtWebEngineWidgets import (
17 QWebEnginePage, QWebEngineSettings, QWebEngineScript
18 )
19 try:
20 from PyQt5.QtWebEngine import PYQT_WEBENGINE_VERSION
21 # __IGNORE_EXCEPTION__
22 except (AttributeError, ImportError):
23 PYQT_WEBENGINE_VERSION = QT_VERSION
24 from PyQt5.QtWebChannel import QWebChannel
25
26 try:
27 from PyQt5.QtNetwork import QSslConfiguration, QSslCertificate
28 SSL_AVAILABLE = True
29 except ImportError:
30 SSL_AVAILABLE = False
31
32 from E5Gui import E5MessageBox
33
34 from WebBrowser.WebBrowserWindow import WebBrowserWindow
35
36 from .JavaScript.ExternalJsObject import ExternalJsObject
37
38 from .Tools.WebHitTestResult import WebHitTestResult
39 from .Tools import Scripts
40
41 import Preferences
42 import Globals
43
44
45 class WebBrowserPage(QWebEnginePage):
46 """
47 Class implementing an enhanced web page.
48
49 @signal safeBrowsingAbort() emitted to indicate an abort due to a safe
50 browsing event
51 @signal safeBrowsingBad(threatType, threatMessages) emitted to indicate a
52 malicious web site as determined by safe browsing
53 @signal printPageRequested() emitted to indicate a print request of the
54 shown web page
55 @signal navigationRequestAccepted(url, navigation type, main frame) emitted
56 to signal an accepted navigation request
57 @signal sslConfigurationChanged() emitted to indicate a change of the
58 stored SSL configuration data
59 """
60 SafeJsWorld = QWebEngineScript.ScriptWorldId.ApplicationWorld
61 UnsafeJsWorld = QWebEngineScript.ScriptWorldId.MainWorld
62
63 safeBrowsingAbort = pyqtSignal()
64 safeBrowsingBad = pyqtSignal(str, str)
65
66 printPageRequested = pyqtSignal()
67 navigationRequestAccepted = pyqtSignal(QUrl, QWebEnginePage.NavigationType,
68 bool)
69
70 sslConfigurationChanged = pyqtSignal()
71
72 def __init__(self, parent=None):
73 """
74 Constructor
75
76 @param parent parent widget of this window (QWidget)
77 """
78 super().__init__(
79 WebBrowserWindow.webProfile(), parent)
80
81 self.__printer = None
82 self.__badSite = False
83 self.__registerProtocolHandlerRequest = None
84
85 self.featurePermissionRequested.connect(
86 self.__featurePermissionRequested)
87 self.authenticationRequired.connect(
88 lambda url, auth: WebBrowserWindow.networkManager().authentication(
89 url, auth, self))
90 self.proxyAuthenticationRequired.connect(
91 WebBrowserWindow.networkManager().proxyAuthentication)
92 self.fullScreenRequested.connect(self.__fullScreenRequested)
93 self.urlChanged.connect(self.__urlChanged)
94 self.contentsSizeChanged.connect(self.__contentsSizeChanged)
95 self.registerProtocolHandlerRequested.connect(
96 self.__registerProtocolHandlerRequested)
97
98 self.__sslConfiguration = None
99
100 # Workaround for changing webchannel world inside
101 # acceptNavigationRequest not working
102 self.__channelUrl = QUrl()
103 self.__channelWorldId = -1
104 self.__setupChannelTimer = QTimer(self)
105 self.__setupChannelTimer.setSingleShot(True)
106 self.__setupChannelTimer.setInterval(100)
107 self.__setupChannelTimer.timeout.connect(self.__setupChannelTimeout)
108
109 @pyqtSlot()
110 def __setupChannelTimeout(self):
111 """
112 Private slot to initiate the setup of the web channel.
113 """
114 self.__setupWebChannelForUrl(self.__channelUrl)
115
116 def acceptNavigationRequest(self, url, type_, isMainFrame):
117 """
118 Public method to determine, if a request may be accepted.
119
120 @param url URL to navigate to
121 @type QUrl
122 @param type_ type of the navigation request
123 @type QWebEnginePage.NavigationType
124 @param isMainFrame flag indicating, that the request originated from
125 the main frame
126 @type bool
127 @return flag indicating acceptance
128 @rtype bool
129 """
130 scheme = url.scheme()
131 if scheme == "mailto":
132 QDesktopServices.openUrl(url)
133 return False
134
135 # AdBlock
136 if (
137 url.scheme() == "abp" and
138 WebBrowserWindow.adBlockManager().addSubscriptionFromUrl(url)
139 ):
140 return False
141
142 # GreaseMonkey
143 try:
144 # PyQtWebEngine >= 5.14.0
145 navigationType = type_ in [
146 QWebEnginePage.NavigationType.NavigationTypeLinkClicked,
147 QWebEnginePage.NavigationType.NavigationTypeRedirect
148 ]
149 except AttributeError:
150 navigationType = (
151 type_ ==
152 QWebEnginePage.NavigationType.NavigationTypeLinkClicked
153 )
154 if navigationType and url.toString().endswith(".user.js"):
155 WebBrowserWindow.greaseMonkeyManager().downloadScript(url)
156 return False
157
158 if url.scheme() == "eric":
159 if url.path() == "AddSearchProvider":
160 query = QUrlQuery(url)
161 self.view().mainWindow().openSearchManager().addEngine(
162 QUrl(query.queryItemValue("url")))
163 return False
164 elif url.path() == "PrintPage":
165 self.printPageRequested.emit()
166 return False
167
168 # Safe Browsing
169 self.__badSite = False
170 from WebBrowser.SafeBrowsing.SafeBrowsingManager import (
171 SafeBrowsingManager
172 )
173 if (
174 SafeBrowsingManager.isEnabled() and
175 url.scheme() not in SafeBrowsingManager.getIgnoreSchemes()
176 ):
177 threatLists = (
178 WebBrowserWindow.safeBrowsingManager().lookupUrl(url)[0]
179 )
180 if threatLists:
181 threatMessages = (
182 WebBrowserWindow.safeBrowsingManager()
183 .getThreatMessages(threatLists)
184 )
185 res = E5MessageBox.warning(
186 WebBrowserWindow.getWindow(),
187 self.tr("Suspicuous URL detected"),
188 self.tr("<p>The URL <b>{0}</b> was found in the Safe"
189 " Browsing database.</p>{1}").format(
190 url.toString(), "".join(threatMessages)),
191 E5MessageBox.StandardButtons(
192 E5MessageBox.Abort |
193 E5MessageBox.Ignore),
194 E5MessageBox.Abort)
195 if res == E5MessageBox.Abort:
196 self.safeBrowsingAbort.emit()
197 return False
198
199 self.__badSite = True
200 threatType = (
201 WebBrowserWindow.safeBrowsingManager()
202 .getThreatType(threatLists[0])
203 )
204 self.safeBrowsingBad.emit(threatType, "".join(threatMessages))
205
206 result = QWebEnginePage.acceptNavigationRequest(
207 self, url, type_, isMainFrame)
208
209 if result:
210 if isMainFrame:
211 isWeb = url.scheme() in ("http", "https", "ftp", "ftps",
212 "file")
213 globalJsEnabled = WebBrowserWindow.webSettings().testAttribute(
214 QWebEngineSettings.WebAttribute.JavascriptEnabled)
215 if isWeb:
216 enable = globalJsEnabled
217 else:
218 enable = True
219 self.settings().setAttribute(
220 QWebEngineSettings.WebAttribute.JavascriptEnabled, enable)
221
222 self.__channelUrl = url
223 self.__setupChannelTimer.start()
224 self.navigationRequestAccepted.emit(url, type_, isMainFrame)
225
226 return result
227
228 @pyqtSlot(QUrl)
229 def __urlChanged(self, url):
230 """
231 Private slot to handle changes of the URL.
232
233 @param url new URL
234 @type QUrl
235 """
236 if (
237 not url.isEmpty() and
238 url.scheme() == "eric" and
239 not self.isJavaScriptEnabled()
240 ):
241 self.settings().setAttribute(
242 QWebEngineSettings.WebAttribute.JavascriptEnabled, True)
243 self.triggerAction(QWebEnginePage.WebAction.Reload)
244
245 @classmethod
246 def userAgent(cls, resolveEmpty=False):
247 """
248 Class method to get the global user agent setting.
249
250 @param resolveEmpty flag indicating to resolve an empty
251 user agent (boolean)
252 @return user agent string (string)
253 """
254 agent = Preferences.getWebBrowser("UserAgent")
255 if agent == "" and resolveEmpty:
256 agent = cls.userAgentForUrl(QUrl())
257 return agent
258
259 @classmethod
260 def setUserAgent(cls, agent):
261 """
262 Class method to set the global user agent string.
263
264 @param agent new current user agent string (string)
265 """
266 Preferences.setWebBrowser("UserAgent", agent)
267
268 @classmethod
269 def userAgentForUrl(cls, url):
270 """
271 Class method to determine the user agent for the given URL.
272
273 @param url URL to determine user agent for (QUrl)
274 @return user agent string (string)
275 """
276 agent = WebBrowserWindow.userAgentsManager().userAgentForUrl(url)
277 if agent == "":
278 # no agent string specified for the given host -> use global one
279 agent = Preferences.getWebBrowser("UserAgent")
280 if agent == "":
281 # no global agent string specified -> use default one
282 agent = WebBrowserWindow.webProfile().httpUserAgent()
283 return agent
284
285 def __featurePermissionRequested(self, url, feature):
286 """
287 Private slot handling a feature permission request.
288
289 @param url url requesting the feature
290 @type QUrl
291 @param feature requested feature
292 @type QWebEnginePage.Feature
293 """
294 manager = WebBrowserWindow.featurePermissionManager()
295 manager.requestFeaturePermission(self, url, feature)
296
297 def execJavaScript(self, script,
298 worldId=QWebEngineScript.ScriptWorldId.MainWorld,
299 timeout=500):
300 """
301 Public method to execute a JavaScript function synchroneously.
302
303 @param script JavaScript script source to be executed
304 @type str
305 @param worldId ID to run the script under
306 @type QWebEngineScript.ScriptWorldId
307 @param timeout max. time the script is given to execute
308 @type int
309 @return result of the script
310 @rtype depending upon script result
311 """
312 loop = QEventLoop()
313 resultDict = {"res": None}
314 QTimer.singleShot(timeout, loop.quit)
315
316 def resultCallback(res, resDict=resultDict):
317 if loop and loop.isRunning():
318 resDict["res"] = res
319 loop.quit()
320
321 self.runJavaScript(script, worldId, resultCallback)
322
323 loop.exec()
324 return resultDict["res"]
325
326 def runJavaScript(self, script, worldId=-1, callback=None):
327 """
328 Public method to run a script in the context of the page.
329
330 @param script JavaScript script source to be executed
331 @type str
332 @param worldId ID to run the script under
333 @type int
334 @param callback callback function to be executed when the script has
335 ended
336 @type function
337 """
338 if worldId > -1:
339 if callback is None:
340 QWebEnginePage.runJavaScript(self, script, worldId)
341 else:
342 QWebEnginePage.runJavaScript(self, script, worldId, callback)
343 else:
344 if callback is None:
345 QWebEnginePage.runJavaScript(self, script)
346 else:
347 QWebEnginePage.runJavaScript(self, script, callback)
348
349 def isJavaScriptEnabled(self):
350 """
351 Public method to test, if JavaScript is enabled.
352
353 @return flag indicating the state of the JavaScript support
354 @rtype bool
355 """
356 return self.settings().testAttribute(
357 QWebEngineSettings.WebAttribute.JavascriptEnabled)
358
359 def scroll(self, x, y):
360 """
361 Public method to scroll by the given amount of pixels.
362
363 @param x horizontal scroll value
364 @type int
365 @param y vertical scroll value
366 @type int
367 """
368 self.runJavaScript(
369 "window.scrollTo(window.scrollX + {0}, window.scrollY + {1})"
370 .format(x, y),
371 WebBrowserPage.SafeJsWorld
372 )
373
374 def scrollTo(self, pos):
375 """
376 Public method to scroll to the given position.
377
378 @param pos position to scroll to
379 @type QPointF
380 """
381 self.runJavaScript(
382 "window.scrollTo({0}, {1});".format(pos.x(), pos.y()),
383 WebBrowserPage.SafeJsWorld
384 )
385
386 def mapToViewport(self, pos):
387 """
388 Public method to map a position to the viewport.
389
390 @param pos position to be mapped
391 @type QPoint
392 @return viewport position
393 @rtype QPoint
394 """
395 return QPoint(pos.x() // self.zoomFactor(),
396 pos.y() // self.zoomFactor())
397
398 def hitTestContent(self, pos):
399 """
400 Public method to test the content at a specified position.
401
402 @param pos position to execute the test at
403 @type QPoint
404 @return test result object
405 @rtype WebHitTestResult
406 """
407 return WebHitTestResult(self, pos)
408
409 def __setupWebChannelForUrl(self, url):
410 """
411 Private method to setup a web channel to our external object.
412
413 @param url URL for which to setup the web channel
414 @type QUrl
415 """
416 channel = self.webChannel()
417 if channel is None:
418 channel = QWebChannel(self)
419 ExternalJsObject.setupWebChannel(channel, self)
420
421 worldId = -1
422 worldId = (
423 self.UnsafeJsWorld
424 if url.scheme() in ("eric", "qthelp") else
425 self.SafeJsWorld
426 )
427 if worldId != self.__channelWorldId:
428 self.__channelWorldId = worldId
429 try:
430 self.setWebChannel(channel, self.__channelWorldId)
431 except TypeError:
432 # pre Qt 5.7.0
433 self.setWebChannel(channel)
434
435 def certificateError(self, error):
436 """
437 Public method to handle SSL certificate errors.
438
439 @param error object containing the certificate error information
440 @type QWebEngineCertificateError
441 @return flag indicating to ignore this error
442 @rtype bool
443 """
444 return WebBrowserWindow.networkManager().certificateError(
445 error, self.view())
446
447 def __fullScreenRequested(self, request):
448 """
449 Private slot handling a full screen request.
450
451 @param request reference to the full screen request
452 @type QWebEngineFullScreenRequest
453 """
454 self.view().requestFullScreen(request.toggleOn())
455
456 accepted = request.toggleOn() == self.view().isFullScreen()
457
458 if accepted:
459 request.accept()
460 else:
461 request.reject()
462
463 def execPrintPage(self, printer, timeout=1000):
464 """
465 Public method to execute a synchronous print.
466
467 @param printer reference to the printer object
468 @type QPrinter
469 @param timeout timeout value in milliseconds
470 @type int
471 @return flag indicating a successful print job
472 @rtype bool
473 """
474 loop = QEventLoop()
475 resultDict = {"res": None}
476 QTimer.singleShot(timeout, loop.quit)
477
478 def printCallback(res, resDict=resultDict):
479 if loop and loop.isRunning():
480 resDict["res"] = res
481 loop.quit()
482
483 self.print(printer, printCallback)
484
485 loop.exec()
486 return resultDict["res"]
487
488 def __contentsSizeChanged(self, size):
489 """
490 Private slot to work around QWebEnginePage not scrolling to anchors
491 when opened in a background tab.
492
493 @param size changed contents size (unused)
494 @type QSize
495 """
496 fragment = self.url().fragment()
497 self.runJavaScript(Scripts.scrollToAnchor(fragment))
498
499 ##############################################
500 ## Methods below deal with JavaScript messages
501 ##############################################
502
503 def javaScriptConsoleMessage(self, level, message, lineNumber, sourceId):
504 """
505 Public method to show a console message.
506
507 @param level severity
508 @type QWebEnginePage.JavaScriptConsoleMessageLevel
509 @param message message to be shown
510 @type str
511 @param lineNumber line number of an error
512 @type int
513 @param sourceId source URL causing the error
514 @type str
515 """
516 self.view().mainWindow().javascriptConsole().javaScriptConsoleMessage(
517 level, message, lineNumber, sourceId)
518
519 ###########################################################################
520 ## Methods below implement safe browsing related functions
521 ###########################################################################
522
523 def getSafeBrowsingStatus(self):
524 """
525 Public method to get the safe browsing status of the current page.
526
527 @return flag indicating a safe site
528 @rtype bool
529 """
530 return not self.__badSite
531
532 ##################################################
533 ## Methods below implement compatibility functions
534 ##################################################
535
536 if not hasattr(QWebEnginePage, "icon"):
537 def icon(self):
538 """
539 Public method to get the web site icon.
540
541 @return web site icon
542 @rtype QIcon
543 """
544 return self.view().icon()
545
546 if not hasattr(QWebEnginePage, "scrollPosition"):
547 def scrollPosition(self):
548 """
549 Public method to get the scroll position of the web page.
550
551 @return scroll position
552 @rtype QPointF
553 """
554 pos = self.execJavaScript(
555 "(function() {"
556 "var res = {"
557 " x: 0,"
558 " y: 0,"
559 "};"
560 "res.x = window.scrollX;"
561 "res.y = window.scrollY;"
562 "return res;"
563 "})()",
564 WebBrowserPage.SafeJsWorld
565 )
566 pos = (
567 QPointF(0.0, 0.0) if pos is None
568 else QPointF(pos["x"], pos["y"])
569 )
570
571 return pos
572
573 #############################################################
574 ## Methods below implement protocol handler related functions
575 #############################################################
576
577 @pyqtSlot("QWebEngineRegisterProtocolHandlerRequest")
578 def __registerProtocolHandlerRequested(self, request):
579 """
580 Private slot to handle the registration of a custom protocol
581 handler.
582
583 @param request reference to the registration request
584 @type QWebEngineRegisterProtocolHandlerRequest
585 """
586 from PyQt5.QtWebEngineCore import (
587 QWebEngineRegisterProtocolHandlerRequest
588 )
589
590 if self.__registerProtocolHandlerRequest:
591 del self.__registerProtocolHandlerRequest
592 self.__registerProtocolHandlerRequest = None
593 self.__registerProtocolHandlerRequest = (
594 QWebEngineRegisterProtocolHandlerRequest(request)
595 )
596
597 def registerProtocolHandlerRequestUrl(self):
598 """
599 Public method to get the registered protocol handler request URL.
600
601 @return registered protocol handler request URL
602 @rtype QUrl
603 """
604 if (
605 self.__registerProtocolHandlerRequest and
606 (self.url().host() ==
607 self.__registerProtocolHandlerRequest.origin().host())
608 ):
609 return self.__registerProtocolHandlerRequest.origin()
610 else:
611 return QUrl()
612
613 def registerProtocolHandlerRequestScheme(self):
614 """
615 Public method to get the registered protocol handler request scheme.
616
617 @return registered protocol handler request scheme
618 @rtype str
619 """
620 if (
621 self.__registerProtocolHandlerRequest and
622 (self.url().host() ==
623 self.__registerProtocolHandlerRequest.origin().host())
624 ):
625 return self.__registerProtocolHandlerRequest.scheme()
626 else:
627 return ""
628
629 #############################################################
630 ## SSL configuration handling below
631 #############################################################
632
633 def setSslConfiguration(self, sslConfiguration):
634 """
635 Public slot to set the SSL configuration data of the page.
636
637 @param sslConfiguration SSL configuration to be set
638 @type QSslConfiguration
639 """
640 self.__sslConfiguration = QSslConfiguration(sslConfiguration)
641 self.__sslConfiguration.url = self.url()
642 self.sslConfigurationChanged.emit()
643
644 def getSslConfiguration(self):
645 """
646 Public method to return a reference to the current SSL configuration.
647
648 @return reference to the SSL configuration in use
649 @rtype QSslConfiguration
650 """
651 return self.__sslConfiguration
652
653 def clearSslConfiguration(self):
654 """
655 Public slot to clear the stored SSL configuration data.
656 """
657 self.__sslConfiguration = None
658 self.sslConfigurationChanged.emit()
659
660 def getSslCertificate(self):
661 """
662 Public method to get a reference to the SSL certificate.
663
664 @return amended SSL certificate
665 @rtype QSslCertificate
666 """
667 if self.__sslConfiguration is None:
668 return None
669
670 sslCertificate = self.__sslConfiguration.peerCertificate()
671 sslCertificate.url = QUrl(self.__sslConfiguration.url)
672 return sslCertificate
673
674 def getSslCertificateChain(self):
675 """
676 Public method to get a reference to the SSL certificate chain.
677
678 @return SSL certificate chain
679 @rtype list of QSslCertificate
680 """
681 if self.__sslConfiguration is None:
682 return []
683
684 chain = self.__sslConfiguration.peerCertificateChain()
685 return chain
686
687 def showSslInfo(self, pos):
688 """
689 Public slot to show some SSL information for the loaded page.
690
691 @param pos position to show the info at
692 @type QPoint
693 """
694 if SSL_AVAILABLE and self.__sslConfiguration is not None:
695 from E5Network.E5SslInfoWidget import E5SslInfoWidget
696 widget = E5SslInfoWidget(self.url(), self.__sslConfiguration,
697 self.view())
698 widget.showAt(pos)
699 else:
700 E5MessageBox.warning(
701 self.view(),
702 self.tr("SSL Info"),
703 self.tr("""This site does not contain SSL information."""))
704
705 def hasValidSslInfo(self):
706 """
707 Public method to check, if the page has a valid SSL certificate.
708
709 @return flag indicating a valid SSL certificate
710 @rtype bool
711 """
712 if self.__sslConfiguration is None:
713 return False
714
715 certList = self.__sslConfiguration.peerCertificateChain()
716 if not certList:
717 return False
718
719 certificateDict = Globals.toDict(
720 Preferences.Prefs.settings.value("Ssl/CaCertificatesDict"))
721 for server in certificateDict:
722 localCAList = QSslCertificate.fromData(certificateDict[server])
723 if any(cert in localCAList for cert in certList):
724 return True
725
726 return all(not cert.isBlacklisted() for cert in certList)

eric ide

mercurial