|
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 "" |