From d33c66e19510f011d2643dc54484eed02414ae26 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 7 Oct 2021 22:23:00 +1000 Subject: [PATCH] change setHtml() to serve content via media server - fixes https://forums.ankiweb.net/t/deck-list-is-blank/2241/2 - fixes the security warnings on Qt 6, by ensuring our pages and resources are coming from the same origin --- qt/aqt/mediasrv.py | 34 ++++++++++++++++++++++++++++++++++ qt/aqt/webview.py | 26 +++++++++++++++++++++++--- 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index 967f6a731..819669a22 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -78,6 +78,8 @@ class MediaServer(threading.Thread): def __init__(self, mw: aqt.main.AnkiQt) -> None: super().__init__() self.is_shutdown = False + # map of webview ids to pages + self._page_html: dict[int, str] = {} def run(self) -> None: try: @@ -119,6 +121,18 @@ class MediaServer(threading.Thread): self._ready.wait() return int(self.server.effective_port) # type: ignore + def set_page_html(self, id: int, html: str) -> None: + self._page_html[id] = html + + def get_page_html(self, id: int) -> str | None: + return self._page_html.get(id) + + def clear_page_html(self, id: int) -> None: + try: + del self._page_html[id] + except KeyError: + pass + def _handle_local_file_request(request: LocalFileRequest) -> Response: directory = request.root @@ -220,6 +234,8 @@ def _extract_internal_request( if dirname == "_anki": if flask.request.method == "POST": return _extract_collection_post_request(filename) + elif get_handler := _extract_dynamic_get_request(filename): + return get_handler # remap legacy top-level references base, ext = os.path.splitext(filename) if ext == ".css": @@ -395,6 +411,7 @@ def complete_tag() -> bytes: return aqt.mw.col.tags.complete_tag(request.data) +# these require a collection post_handlers = { "graphData": graph_data, "graphPreferences": graph_preferences, @@ -435,3 +452,20 @@ def _handle_dynamic_request(request: DynamicRequest) -> Response: return request() except Exception as e: return flask.make_response(str(e), HTTPStatus.INTERNAL_SERVER_ERROR) + + +def legacy_page_data() -> Response: + id = int(request.args["id"]) + if html := aqt.mw.mediaServer.get_page_html(id): + return Response(html, mimetype="text/html") + else: + return flask.make_response("page not found", HTTPStatus.NOT_FOUND) + + +# this currently only handles a single method; in the future, idempotent +# requests like i18nResources should probably be moved here +def _extract_dynamic_get_request(path: str) -> DynamicRequest | None: + if path == "legacyPageData": + return legacy_page_data + else: + return None diff --git a/qt/aqt/webview.py b/qt/aqt/webview.py index 4754ba928..0b63d96b8 100644 --- a/qt/aqt/webview.py +++ b/qt/aqt/webview.py @@ -120,7 +120,11 @@ class AnkiWebPage(QWebEnginePage): def acceptNavigationRequest( self, url: QUrl, navType: Any, isMainFrame: bool ) -> bool: - if not self.open_links_externally or "_anki/pages" in url.path(): + if ( + not self.open_links_externally + or "_anki/pages" in url.path() + or url.path() == "/_anki/legacyPageData" + ): return super().acceptNavigationRequest(url, navType, isMainFrame) if not isMainFrame: @@ -315,12 +319,23 @@ class AnkiWebView(QWebEngineView): self.show() def _setHtml(self, html: str) -> None: + """Send page data to media server, then surf to it. + + This function used to be implemented by QWebEngine's + .setHtml() call. It is no longer used, as it has a + maximum size limit, and due to security changes, it + will stop working in the future.""" from aqt import mw oldFocus = mw.app.focusWidget() self._domDone = False - self._page.setHtml(html) + + webview_id = id(self) + mw.mediaServer.set_page_html(webview_id, html) + self.load_url(QUrl(f"{mw.serverURL()}_anki/legacyPageData?id={webview_id}")) + # work around webengine stealing focus on setHtml() + # fixme: check which if any qt versions this is still required on if oldFocus: oldFocus.setFocus() @@ -646,4 +661,9 @@ document.head.appendChild(style); Must be done on Windows prior to changing current working directory.""" self.requiresCol = False self._domReady = False - self._page.setContent(bytes("", "ascii")) + self._page.setContent(cast(QByteArray, bytes("", "ascii"))) + + def __del__(self) -> None: + from aqt import mw + + mw.mediaServer.clear_page_html(id(self))