diff --git a/src/calibre/gui2/viewer/convert_book.py b/src/calibre/gui2/viewer/convert_book.py
index a0026ffb19..8a89499765 100644
--- a/src/calibre/gui2/viewer/convert_book.py
+++ b/src/calibre/gui2/viewer/convert_book.py
@@ -120,7 +120,7 @@ def prepare_convert(temp_path, key, st):
def do_convert(path, temp_path, key, instance):
tdir = os.path.join(temp_path, instance['path'])
fork_job('calibre.srv.render_book', 'render', args=(
- path, tdir, {'size': instance['file_size'], 'mtime': instance['file_mtime'], 'hash': key}, True, True,
+ path, tdir, {'size': instance['file_size'], 'mtime': instance['file_mtime'], 'hash': key}, True, True, False,
), timeout=3000, no_output=True
)
size = 0
diff --git a/src/calibre/gui2/viewer/web_view.py b/src/calibre/gui2/viewer/web_view.py
index 84db3d1bac..febf98dd3d 100644
--- a/src/calibre/gui2/viewer/web_view.py
+++ b/src/calibre/gui2/viewer/web_view.py
@@ -115,25 +115,31 @@ class UrlSchemeHandler(QWebEngineUrlSchemeHandler):
QWebEngineUrlSchemeHandler.__init__(self, parent)
self.mathjax_dir = P('mathjax', allow_user_override=False)
self.mathjax_manifest = None
+ self.allowed_hosts = (FAKE_HOST, FAKE_HOST.rpartition('.')[0] + '.sandbox')
def requestStarted(self, rq):
if bytes(rq.requestMethod()) != b'GET':
rq.fail(rq.RequestDenied)
return
url = rq.requestUrl()
- if url.host() != FAKE_HOST or url.scheme() != FAKE_PROTOCOL:
+ if url.host() not in self.allowed_hosts or url.scheme() != FAKE_PROTOCOL:
rq.fail(rq.UrlNotFound)
return
name = url.path()[1:]
if name.startswith('book/'):
name = name.partition('/')[2]
+ if name == '__index__':
+ send_reply(rq, 'text/html', b'
\xa0
')
+ return
+ elif name == '__popup__':
+ send_reply(rq, 'text/html', b'')
+ return
try:
data, mime_type = get_data(name)
if data is None:
rq.fail(rq.UrlNotFound)
return
- if isinstance(data, type('')):
- data = data.encode('utf-8')
+ data = as_bytes(data)
mime_type = {
# Prevent warning in console about mimetype of fonts
'application/vnd.ms-opentype':'application/x-font-ttf',
diff --git a/src/calibre/srv/render_book.py b/src/calibre/srv/render_book.py
index bf7e38943f..7670f70383 100644
--- a/src/calibre/srv/render_book.py
+++ b/src/calibre/srv/render_book.py
@@ -85,6 +85,39 @@ def convert_fontsize(length, unit, base_font_size=16.0, dpi=96.0):
return length * length_factors.get(unit, 1) * pt_to_rem
+def create_link_replacer(container, link_uid, changed):
+ resource_template = link_uid + '|{}|'
+
+ def link_replacer(base, url):
+ if url.startswith('#'):
+ frag = urlunquote(url[1:])
+ if not frag:
+ return url
+ changed.add(base)
+ return resource_template.format(encode_url(base, frag))
+ purl = urlparse(url)
+ if purl.netloc or purl.query:
+ return url
+ if purl.scheme and purl.scheme != 'file':
+ return url
+ if not purl.path or purl.path.startswith('/'):
+ return url
+ url, frag = purl.path, purl.fragment
+ name = container.href_to_name(url, base)
+ if name:
+ if container.has_name_and_is_not_empty(name):
+ frag = urlunquote(frag)
+ url = resource_template.format(encode_url(name, frag))
+ else:
+ if isinstance(name, unicode_type):
+ name = name.encode('utf-8')
+ url = 'missing:' + force_unicode(quote(name), 'utf-8')
+ changed.add(base)
+ return url
+
+ return link_replacer
+
+
page_break_properties = ('page-break-before', 'page-break-after', 'page-break-inside')
@@ -217,7 +250,10 @@ class Container(ContainerBase):
tweak_mode = True
- def __init__(self, path_to_ebook, tdir, log=None, book_hash=None, save_bookmark_data=False, book_metadata=None, allow_no_cover=True):
+ def __init__(
+ self, path_to_ebook, tdir, log=None, book_hash=None, save_bookmark_data=False,
+ book_metadata=None, allow_no_cover=True, virtualize_resources=True
+ ):
log = log or default_log
self.allow_no_cover = allow_no_cover
book_fmt, opfpath, input_fmt = extract_book(path_to_ebook, tdir, log=log)
@@ -265,9 +301,8 @@ class Container(ContainerBase):
# Mark the spine as dirty since we have to ensure it is normalized
for name in data['spine']:
self.parsed(name), self.dirty(name)
- self.transform_all()
self.virtualized_names = set()
- self.virtualize_resources()
+ self.transform_all(virtualize_resources)
def manifest_data(name):
mt = (self.mime_map.get(name) or 'application/octet-stream').lower()
@@ -363,8 +398,9 @@ class Container(ContainerBase):
self.dirty(self.opf_name)
return raster_cover_name, titlepage_name
- def transform_html(self, name):
+ def transform_html(self, name, virtualize_resources):
style_xpath = XPath('//h:style')
+ link_xpath = XPath('//h:a[@href]')
img_xpath = XPath('//h:img[@src]')
res_link_xpath = XPath('//h:link[@href]')
head = ensure_head(self.parsed(name))
@@ -407,6 +443,20 @@ class Container(ContainerBase):
if transform_inline_styles(self, name, transform_sheet=transform_sheet, transform_style=transform_declaration):
changed = True
+ if not virtualize_resources:
+ link_uid = self.book_render_data['link_uid']
+ link_replacer = create_link_replacer(self, link_uid, set())
+ ltm = self.book_render_data['link_to_map']
+ for a in link_xpath(root):
+ href = link_replacer(name, a.get('href'))
+ if href and href.startswith(link_uid):
+ a.set('href', 'javascript:void(0)')
+ parts = decode_url(href.split('|')[1])
+ lname, lfrag = parts[0], parts[1]
+ ltm.setdefault(lname, {}).setdefault(lfrag or '', set()).add(name)
+ a.set('data-' + link_uid, json.dumps({'name':lname, 'frag':lfrag}, ensure_ascii=False))
+ changed = True
+
if changed:
self.dirty(name)
@@ -415,48 +465,30 @@ class Container(ContainerBase):
if transform_sheet(sheet):
self.dirty(name)
- def transform_all(self):
+ def transform_all(self, virtualize_resources):
for name, mt in tuple(iteritems(self.mime_map)):
mt = mt.lower()
if mt in OEB_DOCS:
- self.transform_html(name)
- elif mt in OEB_STYLES:
+ self.transform_html(name, virtualize_resources)
+ for name, mt in tuple(iteritems(self.mime_map)):
+ mt = mt.lower()
+ if mt in OEB_STYLES:
self.transform_css(name)
+ if virtualize_resources:
+ self.virtualize_resources()
+
+ ltm = self.book_render_data['link_to_map']
+ for name, amap in iteritems(ltm):
+ for k, v in tuple(iteritems(amap)):
+ amap[k] = tuple(v) # needed for JSON serialization
def virtualize_resources(self):
changed = set()
link_uid = self.book_render_data['link_uid']
- resource_template = link_uid + '|{}|'
xlink_xpath = XPath('//*[@xl:href]')
link_xpath = XPath('//h:a[@href]')
-
- def link_replacer(base, url):
- if url.startswith('#'):
- frag = urlunquote(url[1:])
- if not frag:
- return url
- changed.add(base)
- return resource_template.format(encode_url(base, frag))
- purl = urlparse(url)
- if purl.netloc or purl.query:
- return url
- if purl.scheme and purl.scheme != 'file':
- return url
- if not purl.path or purl.path.startswith('/'):
- return url
- url, frag = purl.path, purl.fragment
- name = self.href_to_name(url, base)
- if name:
- if self.has_name_and_is_not_empty(name):
- frag = urlunquote(frag)
- url = resource_template.format(encode_url(name, frag))
- else:
- if isinstance(name, unicode_type):
- name = name.encode('utf-8')
- url = 'missing:' + force_unicode(quote(name), 'utf-8')
- changed.add(base)
- return url
+ link_replacer = create_link_replacer(self, link_uid, changed)
ltm = self.book_render_data['link_to_map']
@@ -492,10 +524,6 @@ class Container(ContainerBase):
if altered:
changed.add(name)
- for name, amap in iteritems(ltm):
- for k, v in tuple(iteritems(amap)):
- amap[k] = tuple(v) # needed for JSON serialization
-
tuple(map(self.dirty, changed))
def serialize_item(self, name):
@@ -712,14 +740,17 @@ def get_stored_annotations(container):
yield {'type': 'last-read', 'pos': epubcfi, 'pos_type': 'epubcfi', 'timestamp': EPOCH}
-def render(pathtoebook, output_dir, book_hash=None, serialize_metadata=False, extract_annotations=False):
+def render(pathtoebook, output_dir, book_hash=None, serialize_metadata=False, extract_annotations=False, virtualize_resources=True):
mi = None
if serialize_metadata:
from calibre.ebooks.metadata.meta import get_metadata
from calibre.customize.ui import quick_metadata
with lopen(pathtoebook, 'rb') as f, quick_metadata:
mi = get_metadata(f, os.path.splitext(pathtoebook)[1][1:].lower())
- container = Container(pathtoebook, output_dir, book_hash=book_hash, save_bookmark_data=extract_annotations, book_metadata=mi)
+ container = Container(
+ pathtoebook, output_dir, book_hash=book_hash, save_bookmark_data=extract_annotations,
+ book_metadata=mi, virtualize_resources=virtualize_resources
+ )
if serialize_metadata:
from calibre.utils.serialize import json_dumps
from calibre.ebooks.metadata.book.serialize import metadata_as_dict
diff --git a/src/pyj/iframe_comm.pyj b/src/pyj/iframe_comm.pyj
index 6d22b13d6f..501aa5f785 100644
--- a/src/pyj/iframe_comm.pyj
+++ b/src/pyj/iframe_comm.pyj
@@ -58,14 +58,14 @@ class Messenger:
class IframeWrapper:
- def __init__(self, handlers, iframe, entry_point, bootstrap_text, srcdoc):
+ 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_srcdoc = srcdoc
+ self.constructor_url = url
self.entry_point = entry_point
self.bootstrap_text = bootstrap_text
self.handlers = {k: handlers[k] for k in handlers}
@@ -91,7 +91,7 @@ class IframeWrapper:
}
self.iframe.srcdoc = LOADING_DOC.replace(r, def(match, field): return data[field];)
else:
- self.iframe.srcdoc = self.constructor_srcdoc or '\xa0
'
+ self.iframe.src = self.constructor_url
self.srcdoc_created = True
def init(self):
@@ -100,9 +100,13 @@ class IframeWrapper:
self.needs_init = False
iframe = self.iframe
if self.srcdoc_created:
- sdoc = iframe.srcdoc
- iframe.srcdoc = '
'
- iframe.srcdoc = sdoc
+ if self.entry_point:
+ sdoc = iframe.srcdoc
+ iframe.srcdoc = '
'
+ iframe.srcdoc = sdoc
+ else:
+ iframe.src = 'about:blank'
+ iframe.src = self.constructor_url
else:
self.create_srcdoc()
@@ -129,11 +133,15 @@ class IframeWrapper:
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 process message from iframe:')
+ print('Could not decrypt message from iframe:')
console.log(e)
+ traceback.print_exc()
return
if not data.action:
return
@@ -174,10 +182,10 @@ class IframeClient:
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
- self.encrypted_communications = True
if self.initialize_handler:
self.initialize_handler(data)
@@ -203,9 +211,11 @@ class IframeClient:
try:
func(data)
except Exception as e:
- console.log('Error in iframe message handler:')
+ console.log('Error in iframe message handler {}:'.format(data.action))
console.log(e)
- self.send_message('error', title=_('Error in message handler'), details=traceback.format_exc(), msg=e.toString())
+ 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)
diff --git a/src/pyj/read_book/content_popup.pyj b/src/pyj/read_book/content_popup.pyj
index 569bdfc2e3..bf13f7d384 100644
--- a/src/pyj/read_book/content_popup.pyj
+++ b/src/pyj/read_book/content_popup.pyj
@@ -42,7 +42,10 @@ class ContentPopupOverlay:
self.loaded_resources = {}
c = self.container
c.classList.add(CLASS_NAME)
- iframe = E.iframe(seamless=True, sandbox='allow-scripts', style='width: 100%; max-height: 70vh')
+ sandbox = 'allow-scripts'
+ if runtime.is_standalone_viewer:
+ sandbox += ' allow-same-origin'
+ iframe = E.iframe(seamless=True, sandbox=sandbox, style='width: 100%; max-height: 70vh')
c.appendChild(E.div(
E.div(),
@@ -60,7 +63,7 @@ class ContentPopupOverlay:
entry_point = None if runtime.is_standalone_viewer else 'read_book.footnotes'
self.iframe_wrapper = IframeWrapper(
handlers, iframe, entry_point, _('Loading data, please wait...'),
- '')
+ f'{runtime.FAKE_PROTOCOL}://{runtime.SANDBOX_HOST}/book/__popup__')
self.pending_load = None
@property
@@ -83,7 +86,7 @@ class ContentPopupOverlay:
c.style.display = TOP_LEVEL_DISPLAY
def on_iframe_ready(self, msg):
- return self.do_pending_load
+ return self.do_pending_load()
def apply_color_scheme(self, bg, fg):
c = self.container.firstChild
@@ -139,7 +142,7 @@ class ContentPopupOverlay:
def show_footnote_item_stage2(self, resource_data):
self.iframe_wrapper.send_unencrypted_message('display',
resource_data=resource_data, book=self.view.book, name=self.current_footnote_data.name,
- frag=self.current_footnote_data.frag, settings=self.view.iframe_settings())
+ frag=self.current_footnote_data.frag, settings=self.view.currently_showing.settings)
def on_content_loaded(self, data):
self.iframe.style.height = f'{data.height}px'
diff --git a/src/pyj/read_book/footnotes.pyj b/src/pyj/read_book/footnotes.pyj
index cfff5b7362..7acb60797b 100644
--- a/src/pyj/read_book/footnotes.pyj
+++ b/src/pyj/read_book/footnotes.pyj
@@ -196,12 +196,12 @@ class PopupIframeBoss:
self.book = data.book
self.name = data.name
self.frag = data.frag
+ update_settings(data.settings)
for name in self.blob_url_map:
window.URL.revokeObjectURL(self.blob_url_map[name])
document.body.style.removeProperty('font-family')
root_data, self.mathjax, self.blob_url_map = finalize_resources(self.book, data.name, data.resource_data)
self.resource_urls = unserialize_html(root_data, self.content_loaded, self.show_only_footnote)
- update_settings(data.settings)
def on_clear(self, data):
clear(document.head)
@@ -218,6 +218,9 @@ class PopupIframeBoss:
show_footnote(self.frag, known_anchors)
def content_loaded(self):
+ if not self.comm.encrypted_communications:
+ window.setTimeout(self.content_loaded, 2)
+ return
apply_settings()
self.comm.send_message('content_loaded', height=document.documentElement.scrollHeight + 25)
diff --git a/src/pyj/read_book/view.pyj b/src/pyj/read_book/view.pyj
index b6886292d5..ecc3edd4c3 100644
--- a/src/pyj/read_book/view.pyj
+++ b/src/pyj/read_book/view.pyj
@@ -170,6 +170,9 @@ class View:
oncontextmenu=self.margin_context_menu.bind(None, 'right'))
set_right_margin_handler(right_margin)
iframe_id = unique_id('read-book-iframe')
+ sandbox = 'allow-popups allow-scripts allow-popups-to-escape-sandbox'
+ if runtime.is_standalone_viewer:
+ sandbox += ' allow-same-origin'
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
@@ -177,7 +180,7 @@ class View:
left_margin,
E.div(style='flex-grow:2; display:flex; align-items:stretch; flex-direction: column', # container for top and bottom margins
margin_elem(sd, 'margin_top', 'book-top-margin', self.top_margin_clicked, self.margin_context_menu.bind(None, 'top')),
- E.iframe(id=iframe_id, seamless=True, sandbox='allow-popups allow-scripts allow-popups-to-escape-sandbox', style='flex-grow: 2', allowfullscreen='true'),
+ E.iframe(id=iframe_id, seamless=True, sandbox=sandbox, style='flex-grow: 2', allowfullscreen='true'),
margin_elem(sd, 'margin_bottom', 'book-bottom-margin', self.bottom_margin_clicked, self.margin_context_menu.bind(None, 'bottom')),
),
right_margin,
@@ -218,7 +221,9 @@ class View:
if runtime.is_standalone_viewer:
document.documentElement.addEventListener('keydown', self.handle_keypress, {'passive': False})
self.current_color_scheme = resolve_color_scheme()
- self.iframe_wrapper = IframeWrapper(handlers, document.getElementById(iframe_id), entry_point, _('Bootstrapping book reader...'), runtime.FAKE_PROTOCOL, runtime.FAKE_HOST)
+ self.iframe_wrapper = IframeWrapper(
+ handlers, document.getElementById(iframe_id), entry_point, _('Bootstrapping book reader...'),
+ f'{runtime.FAKE_PROTOCOL}://{runtime.SANDBOX_HOST}/book/__index__')
self.search_overlay = SearchOverlay(self)
self.content_popup_overlay = ContentPopupOverlay(self)
self.overlay = Overlay(self)
diff --git a/src/pyj/viewer-main.pyj b/src/pyj/viewer-main.pyj
index c7dafd6031..5f760f8cfd 100644
--- a/src/pyj/viewer-main.pyj
+++ b/src/pyj/viewer-main.pyj
@@ -26,6 +26,7 @@ from viewer.constants import FAKE_HOST, FAKE_PROTOCOL, READER_BACKGROUND_URL
runtime.is_standalone_viewer = True
runtime.FAKE_HOST = FAKE_HOST
+runtime.SANDBOX_HOST = FAKE_HOST.rpartition('.')[0] + '.sandbox'
runtime.FAKE_PROTOCOL = FAKE_PROTOCOL
add_standalone_viewer_shortcuts()
book = None