Refactor iframe control logic into its own class

Allows it to be re-used for the popup iframe
This commit is contained in:
Kovid Goyal 2017-10-15 17:07:17 +05:30
parent 1abf0f35f4
commit 67f6cd6ce0
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
5 changed files with 182 additions and 134 deletions

144
src/pyj/read_book/comm.pyj Normal file
View File

@ -0,0 +1,144 @@
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net>
from __python__ import bound_methods, hash_literals
from aes import GCM
from book_list.globals import main_js, get_translations
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_type = '__IFRAME_TYPE__'; // different in different iframes
__SCRIPT__
end_script
</head>
<body style="font-family: __FONT__">
<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, iframe_type, bootstrap_text):
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.iframe_type = iframe_type
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)
@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
data = {
'BS': self.bootstrap_text,
'SCRIPT': iframe_js(),
'FONT': get_font_family(),
'IFRAME_TYPE': self.iframe_type,
}
self.iframe.srcdoc = LOADING_DOC.replace(r, def(match, field): return data[field];)
self.srcdoc_created = True
def init(self):
if not self.needs_init:
return
self.needs_init = False
iframe = self.iframe
if self.srcdoc_created:
sdoc = iframe.srcdoc
iframe.srcdoc = '<p>&nbsp;</p>'
iframe.srcdoc = sdoc
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:
try:
data = self.messenger.decrypt(data)
except Exception as e:
print('Could not process message from iframe:')
console.log(e)
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()

View File

@ -52,6 +52,10 @@ class ContentPopupOverlay:
def container(self):
return document.getElementById('book-content-popup-overlay')
@property
def iframe(self):
return self.container.querySelector('iframe')
@property
def is_visible(self):
return self.container.style.display is not 'none'
@ -91,4 +95,3 @@ class ContentPopupOverlay:
c.style.width = f'{width}vw'
header = c.firstChild
self.create_footnote_header(header)
# iframe = c.lastChild

View File

@ -2,7 +2,7 @@
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
from __python__ import hash_literals
from aes import GCM, random_bytes
from aes import random_bytes
from encodings import hexlify
from gettext import gettext as _, register_callback, gettext as gt
@ -19,24 +19,6 @@ def current_book():
return current_book.book
current_book.book = None
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))
messenger = Messenger()
iframe_id = 'read-book-iframe'
uid = 'calibre-' + hexlify(random_bytes(12))
def viewport_mode_changer(val):

View File

@ -426,6 +426,7 @@ class IframeBoss:
self.send_message('find_in_spine', text=data.text, backwards=data.backwards, searched_in_spine=data.searched_in_spine)
def init():
script = document.getElementById('bootstrap')
script.parentNode.removeChild(script) # free up some memory
IframeBoss()
if window.iframe_type is 'main':
script = document.getElementById('bootstrap')
script.parentNode.removeChild(script) # free up some memory
IframeBoss()

View File

@ -6,15 +6,14 @@ from elementmaker import E
from gettext import gettext as _
from ajax import ajax_send
from book_list.globals import get_session_data, get_translations, main_js
from book_list.globals import get_session_data
from book_list.router import push_state, read_book_mode
from book_list.theme import get_color, get_font_family
from dom import add_extra_css, build_rule, set_css, svgicon
from book_list.theme import get_color
from dom import add_extra_css, build_rule, set_css, svgicon, unique_id
from modals import error_dialog, warning_dialog
from read_book.comm import IframeWrapper
from read_book.content_popup import ContentPopupOverlay
from read_book.globals import (
current_book, iframe_id, messenger, set_current_spine_item
)
from read_book.globals import current_book, set_current_spine_item
from read_book.goto import get_next_section
from read_book.overlay import Overlay
from read_book.prefs.colors import resolve_color_scheme
@ -26,26 +25,6 @@ from read_book.touch import set_left_margin_handler, set_right_margin_handler
from session import get_device_uuid, get_interface_data
from utils import html_escape, is_ios, parse_url_params, username_key
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_type = '__IFRAME_TYPE__'; // different in different iframes
__SCRIPT__
end_script
</head>
<body style="font-family: __FONT__">
<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
add_extra_css(def():
sel = '.book-side-margin'
ans = build_rule(sel, cursor='pointer', color='rgba(0, 0, 0, 0)', text_align='center', height='100vh', user_select='none', display='flex', align_items='center', justify_content='center')
@ -118,6 +97,7 @@ class View:
set_left_margin_handler(left_margin)
right_margin = E.div(svgicon('caret-right'), style='width:{}px;'.format(sd.get('margin_right', 20)), class_='book-side-margin', id='book-right-margin', onclick=self.right_margin_clicked)
set_right_margin_handler(right_margin)
iframe_id = unique_id('read-book-iframe')
container.appendChild(
E.div(style='max-height: 100vh; width: 100vw; height: 100vh; overflow: hidden; display: flex; align-items: stretch', # container for horizontally aligned panels
E.div(style='max-height: 100vh; display: flex; flex-direction: column; align-items: stretch; flex-grow:2', # container for iframe and any other panels in the same column
@ -137,15 +117,7 @@ class View:
)
)
)
self.search_overlay = SearchOverlay(self)
self.content_popup_overlay = ContentPopupOverlay(self)
self.overlay = Overlay(self)
self.processing_spine_item_display = False
self.iframe_ready = False
self.pending_load = None
self.encrypted_communications = False
window.addEventListener('message', self.handle_message, False)
self.handlers = {
handlers = {
'ready': self.on_iframe_ready,
'error': self.on_iframe_error,
'next_spine_item': self.on_next_spine_item,
@ -162,21 +134,27 @@ class View:
'show_footnote': self.on_show_footnote,
'print': self.on_print,
}
self.iframe_wrapper = IframeWrapper(handlers, document.getElementById(iframe_id), 'main', _('Bootstrapping book reader...'))
self.search_overlay = SearchOverlay(self)
self.content_popup_overlay = ContentPopupOverlay(self)
self.overlay = Overlay(self)
self.processing_spine_item_display = False
self.pending_load = None
self.currently_showing = {}
@property
def iframe(self):
return document.getElementById(iframe_id)
return self.iframe_wrapper.iframe
def left_margin_clicked(self, event):
if event.button is 0:
event.preventDefault(), event.stopPropagation()
self.send_message('next_screen', backwards=True)
self.iframe_wrapper.send_message('next_screen', backwards=True)
def right_margin_clicked(self, event):
if event.button is 0:
event.preventDefault(), event.stopPropagation()
self.send_message('next_screen', backwards=False)
self.iframe_wrapper.send_message('next_screen', backwards=False)
def top_margin_clicked(self, event):
if event.button is 0:
@ -184,7 +162,7 @@ class View:
self.show_chrome()
def forward_gesture(self, gesture):
self.send_message('gesture_from_margin', gesture=gesture)
self.iframe_wrapper.send_message('gesture_from_margin', gesture=gesture)
def iframe_size(self):
iframe = self.iframe
@ -198,13 +176,13 @@ class View:
# On iOS/Safari window.innerWidth/Height are incorrect inside an iframe
window.scrollTo(0, 0) # ensure the window is at 0 because otherwise it sometimes moves down a bit on mobile thanks to the disappearing nav bar
w, h = self.iframe_size()
self.send_message('window_size', width=w, height=h)
self.iframe_wrapper.send_message('window_size', width=w, height=h)
def on_print(self, data):
print(data.string)
def find(self, text, backwards):
self.send_message('find', text=text, backwards=backwards, searched_in_spine=False)
self.iframe_wrapper.send_message('find', text=text, backwards=backwards, searched_in_spine=False)
def on_find_in_spine(self, data):
if data.searched_in_spine:
@ -225,7 +203,7 @@ class View:
if found_in:
self.show_name(found_in, initial_position={'type':'search', 'search_data':data, 'replace_history':True})
else:
self.send_message('find', text=data.text, backwards=data.backwards, searched_in_spine=True)
self.iframe_wrapper.send_message('find', text=data.text, backwards=data.backwards, searched_in_spine=True)
)
def bump_font_size(self, data):
@ -287,67 +265,9 @@ class View:
m.firstChild.style.height = val + 'px'
side_margin('left', margin_left), side_margin('right', margin_right)
def create_src_doc(self):
iframe_script = main_js().replace(/is_running_in_iframe\s*=\s*false/, 'is_running_in_iframe = true')
main_js(None)
r = /__([A-Z][A-Z_0-9]*[A-Z0-9])__/g
self.main_srcdoc = def():
data = {
'BS': _('Bootstrapping book reader...'),
'SCRIPT': iframe_script,
'FONT': get_font_family(),
'IFRAME_TYPE': 'main'
}
return LOADING_DOC.replace(r, def(match, field): return data[field];)
def init_iframe(self):
if self.main_srcdoc is None:
return
if not self.main_srcdoc:
self.create_src_doc()
self.encrypted_communications = False
self.iframe.srcdoc = self.main_srcdoc()
self.main_srcdoc = None
def reset_iframe(self):
# Reset the iframe to ensure that all state from previous books is
# cleared
self.iframe_ready = False
if self.main_srcdoc is None:
self.main_srcdoc = def():
return self.iframe.srcdoc
def send_message(self, action, **data):
data.action = action
msg = {'data':data, 'encrypted':self.encrypted_communications}
if self.encrypted_communications:
msg.data = messenger.encrypt(data)
self.iframe.contentWindow.postMessage(msg, '*')
def handle_message(self, event):
if event.source is not self.iframe.contentWindow:
return
data = event.data
if self.encrypted_communications:
try:
data = messenger.decrypt(data)
except Exception as e:
print('Could not process message from iframe:')
console.log(e)
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):
messenger.reset()
w, h = self.iframe_size()
self.send_message('initialize', secret=messenger.secret, translations=get_translations(), width=w, height=h)
self.encrypted_communications = True
self.iframe_ready = True
self.do_pending_load()
data.width, data.height = self.iframe_size()
return self.do_pending_load
def do_pending_load(self):
if self.pending_load:
@ -404,7 +324,7 @@ class View:
self.hide_overlays()
is_current_book = self.book and self.book.key == book.key
if not is_current_book:
self.reset_iframe()
self.iframe_wrapper.reset()
self.book = current_book.book = book
self.ui.db.update_last_read_time(book)
self.loaded_resources = {}
@ -474,7 +394,7 @@ class View:
def goto_named_destination(self, name, frag):
if self.currently_showing.name is name:
self.send_message('scroll_to_anchor', frag=frag)
self.iframe_wrapper.send_message('scroll_to_anchor', frag=frag)
else:
spine = self.book.manifest.spine
idx = spine.indexOf(name)
@ -548,23 +468,21 @@ class View:
def show_spine_item(self, resource_data):
self.loaded_resources = resource_data
self.pending_load = resource_data
if self.iframe_ready:
if self.iframe_wrapper.ready:
self.do_pending_load()
else:
self.init_iframe()
self.iframe_wrapper.init()
def show_spine_item_stage2(self, resource_data):
self.currently_showing.loading = False
# We cannot encrypt this message because the resource data contains
# Blob objects which do not survive encryption
self.encrypted_communications = False
self.processing_spine_item_display = True
self.send_message('display',
self.iframe_wrapper.send_unencrypted_message('display',
resource_data=resource_data, book=self.book, name=self.currently_showing.name,
initial_position=self.currently_showing.initial_position,
settings=self.currently_showing.settings,
)
self.encrypted_communications = True
def on_content_loaded(self, data):
self.processing_spine_item_display = False
@ -574,8 +492,8 @@ class View:
window.scrollTo(0, 0) # ensure window is at 0 on mobile where the navbar causes issues
def update_font_size(self):
self.send_message('change_font_size', base_font_size=get_session_data().get('base_font_size'))
self.iframe_wrapper.send_message('change_font_size', base_font_size=get_session_data().get('base_font_size'))
def update_color_scheme(self):
cs = self.get_color_scheme(True)
self.send_message('change_color_scheme', color_scheme=cs)
self.iframe_wrapper.send_message('change_color_scheme', color_scheme=cs)