eric6/WebBrowser/WebBrowserPage.py

changeset 6942
2602857055c5
parent 6789
6bafe4f7d5f0
child 7192
a22eee00b052
equal deleted inserted replaced
6941:f99d60d6b59b 6942:2602857055c5
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2008 - 2019 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6
7 """
8 Module implementing the helpbrowser using QWebView.
9 """
10
11 from __future__ import unicode_literals, print_function
12 try:
13 str = unicode # __IGNORE_EXCEPTION__
14 except NameError:
15 pass
16
17 from PyQt5.QtCore import pyqtSlot, pyqtSignal, QUrl, QUrlQuery, QTimer, \
18 QEventLoop, QPoint, QPointF
19 from PyQt5.QtGui import QDesktopServices
20 from PyQt5.QtWebEngineWidgets import QWebEnginePage, QWebEngineSettings, \
21 QWebEngineScript
22 from PyQt5.QtWebChannel import QWebChannel
23
24 from E5Gui import E5MessageBox
25
26 from WebBrowser.WebBrowserWindow import WebBrowserWindow
27
28 from .JavaScript.ExternalJsObject import ExternalJsObject
29
30 from .Tools.WebHitTestResult import WebHitTestResult
31 from .Tools import Scripts
32
33 import Preferences
34 from Globals import qVersionTuple
35
36
37 class WebBrowserPage(QWebEnginePage):
38 """
39 Class implementing an enhanced web page.
40
41 @signal safeBrowsingAbort() emitted to indicate an abort due to a safe
42 browsing event
43 @signal safeBrowsingBad(threatType, threatMessages) emitted to indicate a
44 malicious web site as determined by safe browsing
45 @signal printPageRequested() emitted to indicate a print request of the
46 shown web page
47 @signal navigationRequestAccepted(url, navigation type, main frame) emitted
48 to signal an accepted navigation request
49 """
50 if qVersionTuple() >= (5, 7, 0):
51 SafeJsWorld = QWebEngineScript.ApplicationWorld
52 # SafeJsWorld = QWebEngineScript.MainWorld
53 else:
54 SafeJsWorld = QWebEngineScript.MainWorld
55 UnsafeJsWorld = QWebEngineScript.MainWorld
56
57 safeBrowsingAbort = pyqtSignal()
58 safeBrowsingBad = pyqtSignal(str, str)
59
60 printPageRequested = pyqtSignal()
61 navigationRequestAccepted = pyqtSignal(QUrl, QWebEnginePage.NavigationType,
62 bool)
63
64 def __init__(self, parent=None):
65 """
66 Constructor
67
68 @param parent parent widget of this window (QWidget)
69 """
70 super(WebBrowserPage, self).__init__(
71 WebBrowserWindow.webProfile(), parent)
72
73 self.__printer = None
74 self.__badSite = False
75 self.__registerProtocolHandlerRequest = None
76
77 self.featurePermissionRequested.connect(
78 self.__featurePermissionRequested)
79 self.authenticationRequired.connect(
80 lambda url, auth: WebBrowserWindow.networkManager().authentication(
81 url, auth, self))
82 self.proxyAuthenticationRequired.connect(
83 WebBrowserWindow.networkManager().proxyAuthentication)
84 self.fullScreenRequested.connect(self.__fullScreenRequested)
85 self.urlChanged.connect(self.__urlChanged)
86
87 try:
88 self.contentsSizeChanged.connect(self.__contentsSizeChanged)
89 except AttributeError:
90 # defined for Qt >= 5.7
91 pass
92
93 try:
94 self.registerProtocolHandlerRequested.connect(
95 self.__registerProtocolHandlerRequested)
96 except AttributeError:
97 # defined for Qt >= 5.11
98 pass
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 url.scheme() == "abp":
137 if WebBrowserWindow.adBlockManager().addSubscriptionFromUrl(url):
138 return False
139
140 # GreaseMonkey
141 if type_ == QWebEnginePage.NavigationTypeLinkClicked and \
142 url.toString().endswith(".user.js"):
143 WebBrowserWindow.greaseMonkeyManager().downloadScript(url)
144 return False
145
146 if url.scheme() == "eric":
147 if url.path() == "AddSearchProvider":
148 query = QUrlQuery(url)
149 self.view().mainWindow().openSearchManager().addEngine(
150 QUrl(query.queryItemValue("url")))
151 return False
152 elif url.path() == "PrintPage":
153 self.printPageRequested.emit()
154 return False
155
156 # Safe Browsing
157 self.__badSite = False
158 from WebBrowser.SafeBrowsing.SafeBrowsingManager import \
159 SafeBrowsingManager
160 if SafeBrowsingManager.isEnabled() and \
161 url.scheme() not in \
162 SafeBrowsingManager.getIgnoreSchemes():
163 threatLists = \
164 WebBrowserWindow.safeBrowsingManager().lookupUrl(url)[0]
165 if threatLists:
166 threatMessages = WebBrowserWindow.safeBrowsingManager()\
167 .getThreatMessages(threatLists)
168 res = E5MessageBox.warning(
169 WebBrowserWindow.getWindow(),
170 self.tr("Suspicuous URL detected"),
171 self.tr("<p>The URL <b>{0}</b> was found in the Safe"
172 " Browsing database.</p>{1}").format(
173 url.toString(), "".join(threatMessages)),
174 E5MessageBox.StandardButtons(
175 E5MessageBox.Abort |
176 E5MessageBox.Ignore),
177 E5MessageBox.Abort)
178 if res == E5MessageBox.Abort:
179 self.safeBrowsingAbort.emit()
180 return False
181
182 self.__badSite = True
183 threatType = WebBrowserWindow.safeBrowsingManager()\
184 .getThreatType(threatLists[0])
185 self.safeBrowsingBad.emit(threatType, "".join(threatMessages))
186
187 result = QWebEnginePage.acceptNavigationRequest(self, url, type_,
188 isMainFrame)
189
190 if result:
191 if isMainFrame:
192 isWeb = url.scheme() in ("http", "https", "ftp", "ftps",
193 "file")
194 globalJsEnabled = WebBrowserWindow.webSettings().testAttribute(
195 QWebEngineSettings.JavascriptEnabled)
196 if isWeb:
197 enable = globalJsEnabled
198 else:
199 enable = True
200 self.settings().setAttribute(
201 QWebEngineSettings.JavascriptEnabled, enable)
202
203 self.__channelUrl = url
204 self.__setupChannelTimer.start()
205 self.navigationRequestAccepted.emit(url, type_, isMainFrame)
206
207 return result
208
209 @pyqtSlot(QUrl)
210 def __urlChanged(self, url):
211 """
212 Private slot to handle changes of the URL.
213
214 @param url new URL
215 @type QUrl
216 """
217 if not url.isEmpty() and url.scheme() == "eric" and \
218 not self.isJavaScriptEnabled():
219 self.settings().setAttribute(QWebEngineSettings.JavascriptEnabled,
220 True)
221 self.triggerAction(QWebEnginePage.Reload)
222
223 @classmethod
224 def userAgent(cls, resolveEmpty=False):
225 """
226 Class method to get the global user agent setting.
227
228 @param resolveEmpty flag indicating to resolve an empty
229 user agent (boolean)
230 @return user agent string (string)
231 """
232 agent = Preferences.getWebBrowser("UserAgent")
233 if agent == "" and resolveEmpty:
234 agent = cls.userAgentForUrl(QUrl())
235 return agent
236
237 @classmethod
238 def setUserAgent(cls, agent):
239 """
240 Class method to set the global user agent string.
241
242 @param agent new current user agent string (string)
243 """
244 Preferences.setWebBrowser("UserAgent", agent)
245
246 @classmethod
247 def userAgentForUrl(cls, url):
248 """
249 Class method to determine the user agent for the given URL.
250
251 @param url URL to determine user agent for (QUrl)
252 @return user agent string (string)
253 """
254 agent = WebBrowserWindow.userAgentsManager().userAgentForUrl(url)
255 if agent == "":
256 # no agent string specified for the given host -> use global one
257 agent = Preferences.getWebBrowser("UserAgent")
258 if agent == "":
259 # no global agent string specified -> use default one
260 agent = WebBrowserWindow.webProfile().httpUserAgent()
261 return agent
262
263 def __featurePermissionRequested(self, url, feature):
264 """
265 Private slot handling a feature permission request.
266
267 @param url url requesting the feature
268 @type QUrl
269 @param feature requested feature
270 @type QWebEnginePage.Feature
271 """
272 manager = WebBrowserWindow.featurePermissionManager()
273 manager.requestFeaturePermission(self, url, feature)
274
275 def execJavaScript(self, script, worldId=QWebEngineScript.MainWorld,
276 timeout=500):
277 """
278 Public method to execute a JavaScript function synchroneously.
279
280 @param script JavaScript script source to be executed
281 @type str
282 @param worldId ID to run the script under
283 @type int
284 @param timeout max. time the script is given to execute
285 @type int
286 @return result of the script
287 @rtype depending upon script result
288 """
289 loop = QEventLoop()
290 resultDict = {"res": None}
291 QTimer.singleShot(timeout, loop.quit)
292
293 def resultCallback(res, resDict=resultDict):
294 if loop and loop.isRunning():
295 resDict["res"] = res
296 loop.quit()
297
298 self.runJavaScript(script, worldId, resultCallback)
299
300 loop.exec_()
301 return resultDict["res"]
302
303 def runJavaScript(self, script, worldId=-1, callback=None):
304 """
305 Public method to run a script in the context of the page.
306
307 @param script JavaScript script source to be executed
308 @type str
309 @param worldId ID to run the script under
310 @type int
311 @param callback callback function to be executed when the script has
312 ended
313 @type function
314 """
315 if qVersionTuple() >= (5, 7, 0) and worldId > -1:
316 if callback is None:
317 QWebEnginePage.runJavaScript(self, script, worldId)
318 else:
319 QWebEnginePage.runJavaScript(self, script, worldId, callback)
320 else:
321 if callback is None:
322 QWebEnginePage.runJavaScript(self, script)
323 else:
324 QWebEnginePage.runJavaScript(self, script, callback)
325
326 def isJavaScriptEnabled(self):
327 """
328 Public method to test, if JavaScript is enabled.
329
330 @return flag indicating the state of the JavaScript support
331 @rtype bool
332 """
333 return self.settings().testAttribute(
334 QWebEngineSettings.JavascriptEnabled)
335
336 def scroll(self, x, y):
337 """
338 Public method to scroll by the given amount of pixels.
339
340 @param x horizontal scroll value
341 @type int
342 @param y vertical scroll value
343 @type int
344 """
345 self.runJavaScript(
346 "window.scrollTo(window.scrollX + {0}, window.scrollY + {1})"
347 .format(x, y),
348 WebBrowserPage.SafeJsWorld
349 )
350
351 def scrollTo(self, pos):
352 """
353 Public method to scroll to the given position.
354
355 @param pos position to scroll to
356 @type QPointF
357 """
358 self.runJavaScript(
359 "window.scrollTo({0}, {1});".format(pos.x(), pos.y()),
360 WebBrowserPage.SafeJsWorld
361 )
362
363 def mapToViewport(self, pos):
364 """
365 Public method to map a position to the viewport.
366
367 @param pos position to be mapped
368 @type QPoint
369 @return viewport position
370 @rtype QPoint
371 """
372 return QPoint(pos.x() // self.zoomFactor(),
373 pos.y() // self.zoomFactor())
374
375 def hitTestContent(self, pos):
376 """
377 Public method to test the content at a specified position.
378
379 @param pos position to execute the test at
380 @type QPoint
381 @return test result object
382 @rtype WebHitTestResult
383 """
384 return WebHitTestResult(self, pos)
385
386 def __setupWebChannelForUrl(self, url):
387 """
388 Private method to setup a web channel to our external object.
389
390 @param url URL for which to setup the web channel
391 @type QUrl
392 """
393 channel = self.webChannel()
394 if channel is None:
395 channel = QWebChannel(self)
396 ExternalJsObject.setupWebChannel(channel, self)
397
398 worldId = -1
399 if url.scheme() in ("eric", "qthelp"):
400 worldId = self.UnsafeJsWorld
401 else:
402 worldId = self.SafeJsWorld
403 if worldId != self.__channelWorldId:
404 self.__channelWorldId = worldId
405 try:
406 self.setWebChannel(channel, self.__channelWorldId)
407 except TypeError:
408 # pre Qt 5.7.0
409 self.setWebChannel(channel)
410
411 def certificateError(self, error):
412 """
413 Public method to handle SSL certificate errors.
414
415 @param error object containing the certificate error information
416 @type QWebEngineCertificateError
417 @return flag indicating to ignore this error
418 @rtype bool
419 """
420 return WebBrowserWindow.networkManager().certificateError(
421 error, self.view())
422
423 def __fullScreenRequested(self, request):
424 """
425 Private slot handling a full screen request.
426
427 @param request reference to the full screen request
428 @type QWebEngineFullScreenRequest
429 """
430 self.view().requestFullScreen(request.toggleOn())
431
432 accepted = request.toggleOn() == self.view().isFullScreen()
433
434 if accepted:
435 request.accept()
436 else:
437 request.reject()
438
439 def execPrintPage(self, printer, timeout=1000):
440 """
441 Public method to execute a synchronous print.
442
443 @param printer reference to the printer object
444 @type QPrinter
445 @param timeout timeout value in milliseconds
446 @type int
447 @return flag indicating a successful print job
448 @rtype bool
449 """
450 loop = QEventLoop()
451 resultDict = {"res": None}
452 QTimer.singleShot(timeout, loop.quit)
453
454 def printCallback(res, resDict=resultDict):
455 if loop and loop.isRunning():
456 resDict["res"] = res
457 loop.quit()
458
459 self.print(printer, printCallback)
460
461 loop.exec_()
462 return resultDict["res"]
463
464 def __contentsSizeChanged(self, size):
465 """
466 Private slot to work around QWebEnginePage not scrolling to anchors
467 when opened in a background tab.
468
469 @param size changed contents size (unused)
470 @type QSize
471 """
472 fragment = self.url().fragment()
473 self.runJavaScript(Scripts.scrollToAnchor(fragment))
474
475 ##############################################
476 ## Methods below deal with JavaScript messages
477 ##############################################
478
479 def javaScriptConsoleMessage(self, level, message, lineNumber, sourceId):
480 """
481 Public method to show a console message.
482
483 @param level severity
484 @type QWebEnginePage.JavaScriptConsoleMessageLevel
485 @param message message to be shown
486 @type str
487 @param lineNumber line number of an error
488 @type int
489 @param sourceId source URL causing the error
490 @type str
491 """
492 self.view().mainWindow().javascriptConsole().javaScriptConsoleMessage(
493 level, message, lineNumber, sourceId)
494
495 ###########################################################################
496 ## Methods below implement safe browsing related functions
497 ###########################################################################
498
499 def getSafeBrowsingStatus(self):
500 """
501 Public method to get the safe browsing status of the current page.
502
503 @return flag indicating a safe site
504 @rtype bool
505 """
506 return not self.__badSite
507
508 ##################################################
509 ## Methods below implement compatibility functions
510 ##################################################
511
512 if not hasattr(QWebEnginePage, "icon"):
513 def icon(self):
514 """
515 Public method to get the web site icon.
516
517 @return web site icon
518 @rtype QIcon
519 """
520 return self.view().icon()
521
522 if not hasattr(QWebEnginePage, "scrollPosition"):
523 def scrollPosition(self):
524 """
525 Public method to get the scroll position of the web page.
526
527 @return scroll position
528 @rtype QPointF
529 """
530 pos = self.execJavaScript(
531 "(function() {"
532 "var res = {"
533 " x: 0,"
534 " y: 0,"
535 "};"
536 "res.x = window.scrollX;"
537 "res.y = window.scrollY;"
538 "return res;"
539 "})()",
540 WebBrowserPage.SafeJsWorld
541 )
542 if pos is not None:
543 pos = QPointF(pos["x"], pos["y"])
544 else:
545 pos = QPointF(0.0, 0.0)
546
547 return pos
548
549 #############################################################
550 ## Methods below implement protocol handler related functions
551 #############################################################
552
553 try:
554 @pyqtSlot("QWebEngineRegisterProtocolHandlerRequest")
555 def __registerProtocolHandlerRequested(self, request):
556 """
557 Private slot to handle the registration of a custom protocol
558 handler.
559
560 @param request reference to the registration request
561 @type QWebEngineRegisterProtocolHandlerRequest
562 """
563 from PyQt5.QtWebEngineCore import \
564 QWebEngineRegisterProtocolHandlerRequest
565
566 if self.__registerProtocolHandlerRequest:
567 del self.__registerProtocolHandlerRequest
568 self.__registerProtocolHandlerRequest = None
569 self.__registerProtocolHandlerRequest = \
570 QWebEngineRegisterProtocolHandlerRequest(request)
571 except TypeError:
572 # this is supported with Qt 5.12 and later
573 pass
574
575 def registerProtocolHandlerRequestUrl(self):
576 """
577 Public method to get the registered protocol handler request URL.
578
579 @return registered protocol handler request URL
580 @rtype QUrl
581 """
582 if self.__registerProtocolHandlerRequest and \
583 (self.url().host() ==
584 self.__registerProtocolHandlerRequest.origin().host()):
585 return self.__registerProtocolHandlerRequest.origin()
586 else:
587 return QUrl()
588
589 def registerProtocolHandlerRequestScheme(self):
590 """
591 Public method to get the registered protocol handler request scheme.
592
593 @return registered protocol handler request scheme
594 @rtype str
595 """
596 if self.__registerProtocolHandlerRequest and \
597 (self.url().host() ==
598 self.__registerProtocolHandlerRequest.origin().host()):
599 return self.__registerProtocolHandlerRequest.scheme()
600 else:
601 return ""

eric ide

mercurial