%PDF- %PDF-
| Direktori : /lib/calibre/calibre/gui2/ |
| Current File : //lib/calibre/calibre/gui2/webengine.py |
#!/usr/bin/env python3
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
import json
from qt.core import QObject, Qt, pyqtSignal
from qt.webengine import QWebEnginePage, QWebEngineScript, QWebEngineView, QWebEngineSettings
from calibre import prints
from calibre.utils.monotonic import monotonic
from calibre.utils.rapydscript import special_title
from polyglot.builtins import iteritems
def secure_webengine(view_or_page_or_settings, for_viewer=False):
s = view_or_page_or_settings.settings() if hasattr(
view_or_page_or_settings, 'settings') else view_or_page_or_settings
a = s.setAttribute
a(QWebEngineSettings.WebAttribute.PluginsEnabled, False)
if not for_viewer:
a(QWebEngineSettings.WebAttribute.JavascriptEnabled, False)
s.setUnknownUrlSchemePolicy(QWebEngineSettings.UnknownUrlSchemePolicy.DisallowUnknownUrlSchemes)
if hasattr(view_or_page_or_settings, 'setAudioMuted'):
view_or_page_or_settings.setAudioMuted(True)
a(QWebEngineSettings.WebAttribute.JavascriptCanOpenWindows, False)
a(QWebEngineSettings.WebAttribute.JavascriptCanAccessClipboard, False)
# ensure javascript cannot read from local files
a(QWebEngineSettings.WebAttribute.LocalContentCanAccessFileUrls, False)
a(QWebEngineSettings.WebAttribute.AllowWindowActivationFromJavaScript, False)
return s
def insert_scripts(profile, *scripts):
sc = profile.scripts()
for script in scripts:
for existing in sc.findScripts(script.name()):
sc.remove(existing)
for script in scripts:
sc.insert(script)
def create_script(
name, src, world=QWebEngineScript.ScriptWorldId.ApplicationWorld,
injection_point=QWebEngineScript.InjectionPoint.DocumentReady,
on_subframes=True
):
script = QWebEngineScript()
if isinstance(src, bytes):
src = src.decode('utf-8')
script.setSourceCode(src)
script.setName(name)
script.setWorldId(world)
script.setInjectionPoint(injection_point)
script.setRunsOnSubFrames(on_subframes)
return script
from_js = pyqtSignal
class to_js(str):
def __call__(self, *a):
prints(f'WARNING: Calling {self.name}() before the javascript bridge is ready')
emit = __call__
class to_js_bound(QObject):
def __init__(self, bridge, name):
QObject.__init__(self, bridge)
self.name = name
def __call__(self, *args):
self.parent().page.runJavaScript('if (window.python_comm) python_comm._from_python({}, {})'.format(
json.dumps(self.name), json.dumps(args)), QWebEngineScript.ScriptWorldId.ApplicationWorld)
emit = __call__
class Bridge(QObject):
bridge_ready = pyqtSignal()
def __init__(self, page):
QObject.__init__(self, page)
self._signals = json.dumps(tuple({k for k, v in iteritems(self.__class__.__dict__) if isinstance(v, pyqtSignal)}))
self._signals_registered = False
page.titleChanged.connect(self._title_changed)
for k, v in iteritems(self.__class__.__dict__):
if isinstance(v, to_js):
v.name = k
@property
def page(self):
return self.parent()
@property
def ready(self):
return self._signals_registered
def _title_changed(self, title):
if title.startswith(special_title):
self._poll_for_messages()
def _register_signals(self):
self._signals_registered = True
for k, v in iteritems(self.__class__.__dict__):
if isinstance(v, to_js):
setattr(self, k, to_js_bound(self, k))
self.page.runJavaScript('python_comm._register_signals(' + self._signals + ')', QWebEngineScript.ScriptWorldId.ApplicationWorld)
self.bridge_ready.emit()
def _poll_for_messages(self):
self.page.runJavaScript('python_comm._poll()', QWebEngineScript.ScriptWorldId.ApplicationWorld, self._dispatch_messages)
def _dispatch_messages(self, messages):
try:
for msg in messages:
if isinstance(msg, dict):
mt = msg.get('type')
if mt == 'signal':
signal = getattr(self, msg['name'], None)
if signal is None:
prints('WARNING: No js-to-python signal named: ' + msg['name'])
else:
args = msg['args']
if args:
signal.emit(*args)
else:
signal.emit()
elif mt == 'qt-ready':
self._register_signals()
except Exception:
if messages:
import traceback
traceback.print_exc()
class RestartingWebEngineView(QWebEngineView):
render_process_restarted = pyqtSignal()
render_process_failed = pyqtSignal()
def __init__(self, parent=None):
QWebEngineView.__init__(self, parent)
self._last_reload_at = None
self.renderProcessTerminated.connect(self.render_process_terminated)
self.render_process_restarted.connect(self.reload, type=Qt.ConnectionType.QueuedConnection)
def render_process_terminated(self, termination_type, exit_code):
if termination_type == QWebEnginePage.RenderProcessTerminationStatus.NormalTerminationStatus:
return
self.webengine_crash_message = 'The Qt WebEngine Render process crashed with termination type: {} and exit code: {}'.format(
termination_type, exit_code)
prints(self.webengine_crash_message)
if self._last_reload_at is not None and monotonic() - self._last_reload_at < 2:
self.render_process_failed.emit()
prints('The Qt WebEngine Render process crashed too often')
else:
self._last_reload_at = monotonic()
self.render_process_restarted.emit()
prints('Restarting Qt WebEngine')
if __name__ == '__main__':
from calibre.gui2 import Application
from calibre.gui2.tweak_book.preview import WebPage
from qt.core import QMainWindow
app = Application([])
view = QWebEngineView()
page = WebPage(view)
view.setPage(page)
w = QMainWindow()
w.setCentralWidget(view)
class Test(Bridge):
s1 = from_js(object)
j1 = to_js()
t = Test(view.page())
t.s1.connect(print)
w.show()
view.setHtml('''
<p>hello</p>
''')
app.exec()
del t
del page
del app