mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
ToC Editor: Fix a regression in 6.0 that broke styling/images in the preview panel
QtWebEngine doesnt load resources from the filesystem in Qt 6
This commit is contained in:
parent
fb5c47c17d
commit
6c79a163ba
@ -3,17 +3,134 @@
|
|||||||
|
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import weakref
|
||||||
|
from functools import lru_cache
|
||||||
from qt.core import (
|
from qt.core import (
|
||||||
QFrame, QGridLayout, QIcon, QLabel, QLineEdit, QListWidget, QPushButton, QSize,
|
QApplication, QByteArray, QFrame, QGridLayout, QIcon, QLabel, QLineEdit,
|
||||||
QSplitter, Qt, QUrl, QVBoxLayout, QWidget, pyqtSignal
|
QListWidget, QPushButton, QSize, QSplitter, Qt, QUrl, QVBoxLayout, QWidget,
|
||||||
|
pyqtSignal
|
||||||
|
)
|
||||||
|
from qt.webengine import (
|
||||||
|
QWebEnginePage, QWebEngineProfile, QWebEngineScript,
|
||||||
|
QWebEngineUrlRequestInterceptor, QWebEngineUrlRequestJob,
|
||||||
|
QWebEngineUrlSchemeHandler, QWebEngineView
|
||||||
)
|
)
|
||||||
from qt.webengine import QWebEnginePage, QWebEngineScript, QWebEngineView
|
|
||||||
|
|
||||||
|
from calibre.constants import FAKE_HOST, FAKE_PROTOCOL
|
||||||
|
from calibre.ebooks.oeb.polish.utils import guess_type
|
||||||
from calibre.gui2 import error_dialog, gprefs, is_dark_theme, question_dialog
|
from calibre.gui2 import error_dialog, gprefs, is_dark_theme, question_dialog
|
||||||
from calibre.gui2.palette import dark_color, dark_link_color, dark_text_color
|
from calibre.gui2.palette import dark_color, dark_link_color, dark_text_color
|
||||||
from calibre.utils.webengine import secure_webengine
|
|
||||||
from calibre.utils.logging import default_log
|
from calibre.utils.logging import default_log
|
||||||
from calibre.utils.short_uuid import uuid4
|
from calibre.utils.short_uuid import uuid4
|
||||||
|
from calibre.utils.webengine import secure_webengine, send_reply
|
||||||
|
from polyglot.builtins import as_bytes
|
||||||
|
|
||||||
|
|
||||||
|
class RequestInterceptor(QWebEngineUrlRequestInterceptor):
|
||||||
|
|
||||||
|
def interceptRequest(self, request_info):
|
||||||
|
method = bytes(request_info.requestMethod())
|
||||||
|
if method not in (b'GET', b'HEAD'):
|
||||||
|
default_log.warn(f'Blocking URL request with method: {method}')
|
||||||
|
request_info.block(True)
|
||||||
|
return
|
||||||
|
qurl = request_info.requestUrl()
|
||||||
|
if qurl.scheme() not in (FAKE_PROTOCOL,):
|
||||||
|
default_log.warn(f'Blocking URL request {qurl.toString()} as it is not for a resource in the book')
|
||||||
|
request_info.block(True)
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def current_container():
|
||||||
|
return getattr(current_container, 'ans', lambda: None)()
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=2)
|
||||||
|
def mathjax_dir():
|
||||||
|
return P('mathjax', allow_user_override=False)
|
||||||
|
|
||||||
|
|
||||||
|
class UrlSchemeHandler(QWebEngineUrlSchemeHandler):
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
QWebEngineUrlSchemeHandler.__init__(self, parent)
|
||||||
|
self.allowed_hosts = (FAKE_HOST,)
|
||||||
|
|
||||||
|
def requestStarted(self, rq):
|
||||||
|
if bytes(rq.requestMethod()) != b'GET':
|
||||||
|
return self.fail_request(rq, QWebEngineUrlRequestJob.Error.RequestDenied)
|
||||||
|
c = current_container()
|
||||||
|
if c is None:
|
||||||
|
return self.fail_request(rq, QWebEngineUrlRequestJob.Error.RequestDenied)
|
||||||
|
url = rq.requestUrl()
|
||||||
|
host = url.host()
|
||||||
|
if host not in self.allowed_hosts or url.scheme() != FAKE_PROTOCOL:
|
||||||
|
return self.fail_request(rq)
|
||||||
|
path = url.path()
|
||||||
|
if path.startswith('/book/'):
|
||||||
|
name = path[len('/book/'):]
|
||||||
|
try:
|
||||||
|
mime_type = c.mime_map.get(name) or guess_type(name)
|
||||||
|
try:
|
||||||
|
with c.open(name) as f:
|
||||||
|
q = os.path.abspath(f.name)
|
||||||
|
if not q.startswith(c.root):
|
||||||
|
raise FileNotFoundError('Attempt to leave sandbox')
|
||||||
|
data = f.read()
|
||||||
|
except FileNotFoundError:
|
||||||
|
print(f'Could not find file {name} in book', file=sys.stderr)
|
||||||
|
rq.fail(QWebEngineUrlRequestJob.Error.UrlNotFound)
|
||||||
|
return
|
||||||
|
data = as_bytes(data)
|
||||||
|
mime_type = {
|
||||||
|
# Prevent warning in console about mimetype of fonts
|
||||||
|
'application/vnd.ms-opentype':'application/x-font-ttf',
|
||||||
|
'application/x-font-truetype':'application/x-font-ttf',
|
||||||
|
'application/font-sfnt': 'application/x-font-ttf',
|
||||||
|
}.get(mime_type, mime_type)
|
||||||
|
send_reply(rq, mime_type, data)
|
||||||
|
except Exception:
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return self.fail_request(rq, QWebEngineUrlRequestJob.Error.RequestFailed)
|
||||||
|
elif path.startswith('/mathjax/'):
|
||||||
|
try:
|
||||||
|
ignore, ignore, base, rest = path.split('/', 3)
|
||||||
|
except ValueError:
|
||||||
|
print(f'Could not find file {path} in mathjax', file=sys.stderr)
|
||||||
|
rq.fail(QWebEngineUrlRequestJob.Error.UrlNotFound)
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
mime_type = guess_type(rest)
|
||||||
|
if base == 'loader' and '/' not in rest and '\\' not in rest:
|
||||||
|
data = P(rest, allow_user_override=False, data=True)
|
||||||
|
elif base == 'data':
|
||||||
|
q = os.path.abspath(os.path.join(mathjax_dir(), rest))
|
||||||
|
if not q.startswith(mathjax_dir()):
|
||||||
|
raise FileNotFoundError('')
|
||||||
|
with open(q, 'rb') as f:
|
||||||
|
data = f.read()
|
||||||
|
else:
|
||||||
|
raise FileNotFoundError('')
|
||||||
|
send_reply(rq, mime_type, data)
|
||||||
|
except FileNotFoundError:
|
||||||
|
print(f'Could not find file {path} in mathjax', file=sys.stderr)
|
||||||
|
rq.fail(QWebEngineUrlRequestJob.Error.UrlNotFound)
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return self.fail_request(rq, QWebEngineUrlRequestJob.Error.RequestFailed)
|
||||||
|
else:
|
||||||
|
return self.fail_request(rq)
|
||||||
|
|
||||||
|
def fail_request(self, rq, fail_code=None):
|
||||||
|
if fail_code is None:
|
||||||
|
fail_code = QWebEngineUrlRequestJob.Error.UrlNotFound
|
||||||
|
rq.fail(fail_code)
|
||||||
|
print(f"Blocking FAKE_PROTOCOL request: {rq.requestUrl().toString()} with code: {fail_code}", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
class Page(QWebEnginePage): # {{{
|
class Page(QWebEnginePage): # {{{
|
||||||
@ -21,12 +138,21 @@ class Page(QWebEnginePage): # {{{
|
|||||||
elem_clicked = pyqtSignal(object, object, object, object, object)
|
elem_clicked = pyqtSignal(object, object, object, object, object)
|
||||||
frag_shown = pyqtSignal(object)
|
frag_shown = pyqtSignal(object)
|
||||||
|
|
||||||
def __init__(self, prefs):
|
def __init__(self, parent, prefs):
|
||||||
self.log = default_log
|
self.log = default_log
|
||||||
self.current_frag = None
|
self.current_frag = None
|
||||||
self.com_id = str(uuid4())
|
self.com_id = str(uuid4())
|
||||||
QWebEnginePage.__init__(self)
|
profile = QWebEngineProfile(QApplication.instance())
|
||||||
secure_webengine(self.settings(), for_viewer=True)
|
# store these globally as they need to be destructed after the QWebEnginePage
|
||||||
|
current_container.url_handler = UrlSchemeHandler(parent=profile)
|
||||||
|
current_container.interceptor = RequestInterceptor(profile)
|
||||||
|
current_container.profile_memory = profile
|
||||||
|
profile.installUrlSchemeHandler(QByteArray(FAKE_PROTOCOL.encode('ascii')), current_container.url_handler)
|
||||||
|
s = profile.settings()
|
||||||
|
s.setDefaultTextEncoding('utf-8')
|
||||||
|
profile.setUrlRequestInterceptor(current_container.interceptor)
|
||||||
|
QWebEnginePage.__init__(self, profile, parent)
|
||||||
|
secure_webengine(self, for_viewer=True)
|
||||||
self.titleChanged.connect(self.title_changed)
|
self.titleChanged.connect(self.title_changed)
|
||||||
self.loadFinished.connect(self.show_frag)
|
self.loadFinished.connect(self.show_frag)
|
||||||
s = QWebEngineScript()
|
s = QWebEngineScript()
|
||||||
@ -92,14 +218,16 @@ class WebView(QWebEngineView): # {{{
|
|||||||
|
|
||||||
def __init__(self, parent, prefs):
|
def __init__(self, parent, prefs):
|
||||||
QWebEngineView.__init__(self, parent)
|
QWebEngineView.__init__(self, parent)
|
||||||
self._page = Page(prefs)
|
self._page = Page(self, prefs)
|
||||||
self._page.elem_clicked.connect(self.elem_clicked)
|
self._page.elem_clicked.connect(self.elem_clicked)
|
||||||
self._page.frag_shown.connect(self.frag_shown)
|
self._page.frag_shown.connect(self.frag_shown)
|
||||||
self.setPage(self._page)
|
self.setPage(self._page)
|
||||||
|
|
||||||
def load_path(self, path, frag=None):
|
def load_name(self, name, frag=None):
|
||||||
self._page.current_frag = frag
|
self._page.current_frag = frag
|
||||||
self.setUrl(QUrl.fromLocalFile(path))
|
url = QUrl(f'{FAKE_PROTOCOL}://{FAKE_HOST}/')
|
||||||
|
url.setPath(f'/book/{name}')
|
||||||
|
self.setUrl(url)
|
||||||
|
|
||||||
def sizeHint(self):
|
def sizeHint(self):
|
||||||
return QSize(300, 300)
|
return QSize(300, 300)
|
||||||
@ -239,6 +367,7 @@ class ItemEdit(QWidget):
|
|||||||
|
|
||||||
def load(self, container):
|
def load(self, container):
|
||||||
self.container = container
|
self.container = container
|
||||||
|
current_container.ans = weakref.ref(container)
|
||||||
spine_names = [container.abspath_to_name(p) for p in
|
spine_names = [container.abspath_to_name(p) for p in
|
||||||
container.spine_items]
|
container.spine_items]
|
||||||
spine_names = [n for n in spine_names if container.has_name(n)]
|
spine_names = [n for n in spine_names if container.has_name(n)]
|
||||||
@ -246,7 +375,6 @@ class ItemEdit(QWidget):
|
|||||||
|
|
||||||
def current_changed(self, item):
|
def current_changed(self, item):
|
||||||
name = self.current_name = str(item.data(Qt.ItemDataRole.DisplayRole) or '')
|
name = self.current_name = str(item.data(Qt.ItemDataRole.DisplayRole) or '')
|
||||||
path = self.container.name_to_abspath(name)
|
|
||||||
# Ensure encoding map is populated
|
# Ensure encoding map is populated
|
||||||
root = self.container.parsed(name)
|
root = self.container.parsed(name)
|
||||||
nasty = root.xpath('//*[local-name()="head"]/*[local-name()="p"]')
|
nasty = root.xpath('//*[local-name()="head"]/*[local-name()="p"]')
|
||||||
@ -258,7 +386,7 @@ class ItemEdit(QWidget):
|
|||||||
for x in reversed(nasty):
|
for x in reversed(nasty):
|
||||||
body[0].insert(0, x)
|
body[0].insert(0, x)
|
||||||
self.container.commit_item(name, keep_parsed=True)
|
self.container.commit_item(name, keep_parsed=True)
|
||||||
self.view.load_path(path, self.current_frag)
|
self.view.load_name(name, self.current_frag)
|
||||||
self.current_frag = None
|
self.current_frag = None
|
||||||
self.dest_label.setText(self.base_msg + '<br>' + _('File:') + ' ' +
|
self.dest_label.setText(self.base_msg + '<br>' + _('File:') + ' ' +
|
||||||
name + '<br>' + _('Top of the file'))
|
name + '<br>' + _('Top of the file'))
|
||||||
|
Loading…
x
Reference in New Issue
Block a user