src/eric7/WebBrowser/WebBrowserPage.py

branch
eric7
changeset 9209
b99e7fd55fd3
parent 9077
33827549f187
child 9221
bf71ee032bb4
equal deleted inserted replaced
9208:3fc8dfeb6ebe 9209:b99e7fd55fd3
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2008 - 2022 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6
7 """
8 Module implementing the helpbrowser using QWebView.
9 """
10
11 from PyQt6.QtCore import (
12 pyqtSlot, pyqtSignal, QUrl, QUrlQuery, QTimer, QEventLoop, QPoint
13 )
14 from PyQt6.QtGui import QDesktopServices
15 from PyQt6.QtWebEngineCore import (
16 QWebEnginePage, QWebEngineSettings, QWebEngineScript
17 )
18 from PyQt6.QtWebChannel import QWebChannel
19
20 try:
21 from PyQt6.QtNetwork import QSslConfiguration, QSslCertificate
22 SSL_AVAILABLE = True
23 except ImportError:
24 SSL_AVAILABLE = False
25
26 from EricWidgets import EricMessageBox
27
28 from WebBrowser.WebBrowserWindow import WebBrowserWindow
29
30 from .JavaScript.ExternalJsObject import ExternalJsObject
31
32 from .Tools.WebHitTestResult import WebHitTestResult
33 from .Tools import Scripts
34
35 import Preferences
36 import Globals
37
38
39 class WebBrowserPage(QWebEnginePage):
40 """
41 Class implementing an enhanced web page.
42
43 @signal safeBrowsingAbort() emitted to indicate an abort due to a safe
44 browsing event
45 @signal safeBrowsingBad(threatType, threatMessages) emitted to indicate a
46 malicious web site as determined by safe browsing
47 @signal printPageRequested() emitted to indicate a print request of the
48 shown web page
49 @signal navigationRequestAccepted(url, navigation type, main frame) emitted
50 to signal an accepted navigation request
51 @signal sslConfigurationChanged() emitted to indicate a change of the
52 stored SSL configuration data
53 """
54 SafeJsWorld = QWebEngineScript.ScriptWorldId.ApplicationWorld
55 UnsafeJsWorld = QWebEngineScript.ScriptWorldId.MainWorld
56
57 safeBrowsingAbort = pyqtSignal()
58 safeBrowsingBad = pyqtSignal(str, str)
59
60 printPageRequested = pyqtSignal()
61 navigationRequestAccepted = pyqtSignal(QUrl, QWebEnginePage.NavigationType,
62 bool)
63
64 sslConfigurationChanged = pyqtSignal()
65
66 def __init__(self, view, parent=None):
67 """
68 Constructor
69
70 @param view reference to the WebBrowserView associated with the page
71 @type WebBrowserView
72 @param parent reference to the parent widget (defaults to None)
73 @type QWidget (optional)
74 """
75 super().__init__(
76 WebBrowserWindow.webProfile(), parent)
77
78 self.__printer = None
79 self.__badSite = False
80 self.__registerProtocolHandlerRequest = None
81
82 self.__view = view
83
84 self.featurePermissionRequested.connect(
85 self.__featurePermissionRequested)
86 self.authenticationRequired.connect(
87 lambda url, auth: WebBrowserWindow.networkManager().authentication(
88 url, auth, self))
89 self.proxyAuthenticationRequired.connect(
90 WebBrowserWindow.networkManager().proxyAuthentication)
91 self.fullScreenRequested.connect(self.__fullScreenRequested)
92 self.urlChanged.connect(self.__urlChanged)
93 self.contentsSizeChanged.connect(self.__contentsSizeChanged)
94 self.registerProtocolHandlerRequested.connect(
95 self.__registerProtocolHandlerRequested)
96
97 self.__sslConfiguration = None
98
99 # Workaround for changing webchannel world inside
100 # acceptNavigationRequest not working
101 self.__channelUrl = QUrl()
102 self.__channelWorldId = -1
103 self.__setupChannelTimer = QTimer(self)
104 self.__setupChannelTimer.setSingleShot(True)
105 self.__setupChannelTimer.setInterval(100)
106 self.__setupChannelTimer.timeout.connect(self.__setupChannelTimeout)
107
108 def view(self):
109 """
110 Public method to get a reference to the WebBrowserView associated with
111 the page.
112
113 @return reference to the WebBrowserView associated with the page
114 r@type WebBrowserView
115 """
116 return self.__view
117
118 @pyqtSlot()
119 def __setupChannelTimeout(self):
120 """
121 Private slot to initiate the setup of the web channel.
122 """
123 self.__setupWebChannelForUrl(self.__channelUrl)
124
125 def acceptNavigationRequest(self, url, type_, isMainFrame):
126 """
127 Public method to determine, if a request may be accepted.
128
129 @param url URL to navigate to
130 @type QUrl
131 @param type_ type of the navigation request
132 @type QWebEnginePage.NavigationType
133 @param isMainFrame flag indicating, that the request originated from
134 the main frame
135 @type bool
136 @return flag indicating acceptance
137 @rtype bool
138 """
139 scheme = url.scheme()
140 if scheme == "mailto":
141 QDesktopServices.openUrl(url)
142 return False
143
144 # AdBlock
145 if (
146 url.scheme() == "abp" and
147 WebBrowserWindow.adBlockManager().addSubscriptionFromUrl(url)
148 ):
149 return False
150
151 # GreaseMonkey
152 navigationType = type_ in (
153 QWebEnginePage.NavigationType.NavigationTypeLinkClicked,
154 QWebEnginePage.NavigationType.NavigationTypeRedirect
155 )
156 if navigationType and url.toString().endswith(".user.js"):
157 WebBrowserWindow.greaseMonkeyManager().downloadScript(url)
158 return False
159
160 if url.scheme() == "eric":
161 if url.path() == "AddSearchProvider":
162 query = QUrlQuery(url)
163 self.__view.mainWindow().openSearchManager().addEngine(
164 QUrl(query.queryItemValue("url")))
165 return False
166 elif url.path() == "PrintPage":
167 self.printPageRequested.emit()
168 return False
169
170 # Safe Browsing
171 self.__badSite = False
172 from WebBrowser.SafeBrowsing.SafeBrowsingManager import (
173 SafeBrowsingManager
174 )
175 if (
176 SafeBrowsingManager.isEnabled() and
177 url.scheme() not in SafeBrowsingManager.getIgnoreSchemes()
178 ):
179 threatLists = (
180 WebBrowserWindow.safeBrowsingManager().lookupUrl(url)[0]
181 )
182 if threatLists:
183 threatMessages = (
184 WebBrowserWindow.safeBrowsingManager()
185 .getThreatMessages(threatLists)
186 )
187 res = EricMessageBox.warning(
188 WebBrowserWindow.getWindow(),
189 self.tr("Suspicuous URL detected"),
190 self.tr("<p>The URL <b>{0}</b> was found in the Safe"
191 " Browsing database.</p>{1}").format(
192 url.toString(), "".join(threatMessages)),
193 EricMessageBox.Abort | EricMessageBox.Ignore,
194 EricMessageBox.Abort)
195 if res == EricMessageBox.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(int(pos.x() // self.zoomFactor()),
396 int(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 self.setWebChannel(channel, self.__channelWorldId)
430
431 def certificateError(self, error):
432 """
433 Public method to handle SSL certificate errors.
434
435 @param error object containing the certificate error information
436 @type QWebEngineCertificateError
437 @return flag indicating to ignore this error
438 @rtype bool
439 """
440 return WebBrowserWindow.networkManager().certificateError(
441 error, self.__view)
442
443 def __fullScreenRequested(self, request):
444 """
445 Private slot handling a full screen request.
446
447 @param request reference to the full screen request
448 @type QWebEngineFullScreenRequest
449 """
450 self.__view.requestFullScreen(request.toggleOn())
451
452 accepted = request.toggleOn() == self.__view.isFullScreen()
453
454 if accepted:
455 request.accept()
456 else:
457 request.reject()
458
459 def execPrintPage(self, printer, timeout=1000):
460 """
461 Public method to execute a synchronous print.
462
463 @param printer reference to the printer object
464 @type QPrinter
465 @param timeout timeout value in milliseconds
466 @type int
467 @return flag indicating a successful print job
468 @rtype bool
469 """
470 loop = QEventLoop()
471 resultDict = {"res": None}
472 QTimer.singleShot(timeout, loop.quit)
473
474 def printCallback(res, resDict=resultDict):
475 if loop and loop.isRunning():
476 resDict["res"] = res
477 loop.quit()
478
479 self.print(printer, printCallback)
480
481 loop.exec()
482 return resultDict["res"]
483
484 def __contentsSizeChanged(self, size):
485 """
486 Private slot to work around QWebEnginePage not scrolling to anchors
487 when opened in a background tab.
488
489 @param size changed contents size (unused)
490 @type QSize
491 """
492 fragment = self.url().fragment()
493 self.runJavaScript(Scripts.scrollToAnchor(fragment))
494
495 ##############################################
496 ## Methods below deal with JavaScript messages
497 ##############################################
498
499 def javaScriptConsoleMessage(self, level, message, lineNumber, sourceId):
500 """
501 Public method to show a console message.
502
503 @param level severity
504 @type QWebEnginePage.JavaScriptConsoleMessageLevel
505 @param message message to be shown
506 @type str
507 @param lineNumber line number of an error
508 @type int
509 @param sourceId source URL causing the error
510 @type str
511 """
512 self.__view.mainWindow().javascriptConsole().javaScriptConsoleMessage(
513 level, message, lineNumber, sourceId)
514
515 ###########################################################################
516 ## Methods below implement safe browsing related functions
517 ###########################################################################
518
519 def getSafeBrowsingStatus(self):
520 """
521 Public method to get the safe browsing status of the current page.
522
523 @return flag indicating a safe site
524 @rtype bool
525 """
526 return not self.__badSite
527
528 #############################################################
529 ## Methods below implement protocol handler related functions
530 #############################################################
531
532 @pyqtSlot("QWebEngineRegisterProtocolHandlerRequest")
533 def __registerProtocolHandlerRequested(self, request):
534 """
535 Private slot to handle the registration of a custom protocol
536 handler.
537
538 @param request reference to the registration request
539 @type QWebEngineRegisterProtocolHandlerRequest
540 """
541 from PyQt6.QtWebEngineCore import (
542 QWebEngineRegisterProtocolHandlerRequest
543 )
544
545 if self.__registerProtocolHandlerRequest:
546 del self.__registerProtocolHandlerRequest
547 self.__registerProtocolHandlerRequest = None
548 self.__registerProtocolHandlerRequest = (
549 QWebEngineRegisterProtocolHandlerRequest(request)
550 )
551
552 def registerProtocolHandlerRequestUrl(self):
553 """
554 Public method to get the registered protocol handler request URL.
555
556 @return registered protocol handler request URL
557 @rtype QUrl
558 """
559 if (
560 self.__registerProtocolHandlerRequest and
561 (self.url().host() ==
562 self.__registerProtocolHandlerRequest.origin().host())
563 ):
564 return self.__registerProtocolHandlerRequest.origin()
565 else:
566 return QUrl()
567
568 def registerProtocolHandlerRequestScheme(self):
569 """
570 Public method to get the registered protocol handler request scheme.
571
572 @return registered protocol handler request scheme
573 @rtype str
574 """
575 if (
576 self.__registerProtocolHandlerRequest and
577 (self.url().host() ==
578 self.__registerProtocolHandlerRequest.origin().host())
579 ):
580 return self.__registerProtocolHandlerRequest.scheme()
581 else:
582 return ""
583
584 #############################################################
585 ## SSL configuration handling below
586 #############################################################
587
588 def setSslConfiguration(self, sslConfiguration):
589 """
590 Public slot to set the SSL configuration data of the page.
591
592 @param sslConfiguration SSL configuration to be set
593 @type QSslConfiguration
594 """
595 self.__sslConfiguration = QSslConfiguration(sslConfiguration)
596 self.__sslConfiguration.url = self.url()
597 self.sslConfigurationChanged.emit()
598
599 def getSslConfiguration(self):
600 """
601 Public method to return a reference to the current SSL configuration.
602
603 @return reference to the SSL configuration in use
604 @rtype QSslConfiguration
605 """
606 return self.__sslConfiguration
607
608 def clearSslConfiguration(self):
609 """
610 Public slot to clear the stored SSL configuration data.
611 """
612 self.__sslConfiguration = None
613 self.sslConfigurationChanged.emit()
614
615 def getSslCertificate(self):
616 """
617 Public method to get a reference to the SSL certificate.
618
619 @return amended SSL certificate
620 @rtype QSslCertificate
621 """
622 if self.__sslConfiguration is None:
623 return None
624
625 sslCertificate = self.__sslConfiguration.peerCertificate()
626 sslCertificate.url = QUrl(self.__sslConfiguration.url)
627 return sslCertificate
628
629 def getSslCertificateChain(self):
630 """
631 Public method to get a reference to the SSL certificate chain.
632
633 @return SSL certificate chain
634 @rtype list of QSslCertificate
635 """
636 if self.__sslConfiguration is None:
637 return []
638
639 chain = self.__sslConfiguration.peerCertificateChain()
640 return chain
641
642 def showSslInfo(self, pos):
643 """
644 Public slot to show some SSL information for the loaded page.
645
646 @param pos position to show the info at
647 @type QPoint
648 """
649 if SSL_AVAILABLE and self.__sslConfiguration is not None:
650 from EricNetwork.EricSslInfoWidget import EricSslInfoWidget
651 widget = EricSslInfoWidget(self.url(), self.__sslConfiguration,
652 self.__view)
653 widget.showAt(pos)
654 else:
655 EricMessageBox.warning(
656 self.__view,
657 self.tr("SSL Info"),
658 self.tr("""This site does not contain SSL information."""))
659
660 def hasValidSslInfo(self):
661 """
662 Public method to check, if the page has a valid SSL certificate.
663
664 @return flag indicating a valid SSL certificate
665 @rtype bool
666 """
667 if self.__sslConfiguration is None:
668 return False
669
670 certList = self.__sslConfiguration.peerCertificateChain()
671 if not certList:
672 return False
673
674 certificateDict = Globals.toDict(
675 Preferences.getSettings().value("Ssl/CaCertificatesDict"))
676 for server in certificateDict:
677 localCAList = QSslCertificate.fromData(certificateDict[server])
678 if any(cert in localCAList for cert in certList):
679 return True
680
681 return all(not cert.isBlacklisted() for cert in certList)

eric ide

mercurial