calibre/src/pyj/iframe_comm.pyj

230 lines
7.8 KiB
Plaintext

# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net>
from __python__ import bound_methods, hash_literals
import traceback
from aes import GCM
from gettext import gettext as _, install
from book_list.globals import get_translations, main_js
from book_list.theme import get_font_family
from dom import ensure_id
LOADING_DOC = '''
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<script type="text/javascript" id="bootstrap">
window.iframe_entry_point = '__ENTRY_POINT__'; // different in different iframes
window.default_font_family = '__FONT__'; // from the theme
__SCRIPT__
end_script
</head>
<body>
<div style="font-size:larger; font-weight: bold; margin-top:48vh; text-align:center">
__BS__
</div>
</body>
</html>
'''.replace('end_script', '<' + '/script>') # cannot have a closing script tag as this is embedded inside a script tag in index.html
def iframe_js():
if not iframe_js.ans:
iframe_js.ans = main_js().replace(/is_running_in_iframe\s*=\s*false/, 'is_running_in_iframe = true')
main_js(None)
return iframe_js.ans
class Messenger:
def __init__(self):
self.secret = Uint8Array(64)
def reset(self):
window.crypto.getRandomValues(self.secret)
self.gcm_to_iframe = GCM(self.secret.subarray(0, 32))
self.gcm_from_iframe = GCM(self.secret.subarray(32))
def encrypt(self, data):
return self.gcm_to_iframe.encrypt(JSON.stringify(data))
def decrypt(self, data):
return JSON.parse(self.gcm_from_iframe.decrypt(data))
class IframeWrapper:
def __init__(self, handlers, iframe, entry_point, bootstrap_text, url):
self.messenger = Messenger()
self.iframe_id = ensure_id(iframe, 'content-iframe')
self.needs_init = True
self.ready = False
self.encrypted_communications = False
self.srcdoc_created = False
self.constructor_url = url
self.entry_point = entry_point
self.bootstrap_text = bootstrap_text
self.handlers = {k: handlers[k] for k in handlers}
self.on_ready_handler = self.handlers.ready
self.handlers.ready = self.on_iframe_ready
window.addEventListener('message', self.handle_message, False)
def destroy(self):
window.removeEventListener('message', self.handle_message, False)
@property
def iframe(self):
return document.getElementById(self.iframe_id)
def create_srcdoc(self):
r = /__([A-Z][A-Z_0-9]*[A-Z0-9])__/g
if self.entry_point:
data = {
'BS': self.bootstrap_text,
'SCRIPT': iframe_js(),
'FONT': get_font_family(),
'ENTRY_POINT': self.entry_point,
}
self.iframe.srcdoc = LOADING_DOC.replace(r, def(match, field): return data[field];)
else:
self.iframe.src = self.constructor_url
self.srcdoc_created = True
def init(self):
if not self.needs_init:
return
self.needs_init = False
iframe = self.iframe
if self.srcdoc_created:
if self.entry_point:
sdoc = iframe.srcdoc
iframe.srcdoc = '<p>&nbsp;</p>'
iframe.srcdoc = sdoc
else:
iframe.src = 'about:blank'
iframe.src = self.constructor_url
else:
self.create_srcdoc()
def reset(self):
self.ready = False
self.needs_init = True
self.encrypted_communications = False
def _send_message(self, action, encrypted, data):
data.action = action
msg = {'data':data, 'encrypted': encrypted}
if encrypted:
msg.data = self.messenger.encrypt(data)
self.iframe.contentWindow.postMessage(msg, '*')
def send_message(self, action, **data):
self._send_message(action, self.encrypted_communications, data)
def send_unencrypted_message(self, action, **data):
self._send_message(action, False, data)
def handle_message(self, event):
if event.source is not self.iframe?.contentWindow:
return
data = event.data
if self.encrypted_communications:
if data.tag is undefined:
print('Ignoring unencrypted message from iframe:', data)
return
try:
data = self.messenger.decrypt(data)
except Exception as e:
print('Could not decrypt message from iframe:')
console.log(e)
traceback.print_exc()
return
if not data.action:
return
func = self.handlers[data.action]
if func:
func(data)
else:
print('Unknown action in message from iframe to parent:', data.action)
def on_iframe_ready(self, data):
self.messenger.reset()
msg = {'secret': self.messenger.secret, 'translations': get_translations()}
self.ready = True
callback = None
if self.on_ready_handler:
callback = self.on_ready_handler(msg)
self._send_message('initialize', False, msg)
self.encrypted_communications = True
if callback:
callback()
class IframeClient:
def __init__(self, handlers):
self.encrypted_communications = False
self.handlers = {k: handlers[k] for k in handlers}
self.initialize_handler = handlers.initialize
self.handlers.initialize = self.initialize
self.ready_sent = False
window.addEventListener('message', self.handle_message, False)
window.addEventListener('load', def():
if not self.ready_sent:
self.send_message('ready', {})
self.ready_sent = True
)
def initialize(self, data):
nonlocal print
self.gcm_from_parent, self.gcm_to_parent = GCM(data.secret.subarray(0, 32)), GCM(data.secret.subarray(32))
self.encrypted_communications = True
if data.translations:
install(data.translations)
print = self.print_to_parent
if self.initialize_handler:
self.initialize_handler(data)
def print_to_parent(self, *args):
self.send_message('print', string=' '.join(map(str, args)))
def handle_message(self, event):
if event.source is not window.parent:
return
msg = event.data
data = msg.data
if msg.encrypted:
# We cannot use self.encrypted_communications as the 'display'
# message has to be unencrypted as it transports Blob objects
try:
data = JSON.parse(self.gcm_from_parent.decrypt(data))
except Exception as e:
print('Could not process message from parent:')
console.log(e)
return
if not data or not data.action:
console.log('Got a null message from parent in iframe, ignoring')
return
func = self.handlers[data.action]
if func:
try:
func(data)
except Exception as e:
console.log('Error in iframe message handler {}:'.format(data?.action))
console.log(e)
details = traceback.format_exc()
console.log(details)
self.send_message('error', title=_('Error in message handler'), details=details, msg=e.toString())
else:
print('Unknown action in message to iframe from parent: ' + data.action)
def send_message(self, action, data):
data.action = action
if self.encrypted_communications:
data = self.gcm_to_parent.encrypt(JSON.stringify(data))
window.parent.postMessage(data, '*')