mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Refactor iframe control logic into its own class
Allows it to be re-used for the popup iframe
This commit is contained in:
parent
1abf0f35f4
commit
67f6cd6ce0
144
src/pyj/read_book/comm.pyj
Normal file
144
src/pyj/read_book/comm.pyj
Normal 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> </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()
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user