From 334ca0295e91629c6352b14a9d861ebe8eaa84a2 Mon Sep 17 00:00:00 2001 From: Veloman Yunkan Date: Mon, 1 Sep 2025 12:31:41 +0400 Subject: [PATCH] Viewer chaperon mode Viewer now rewrites internal links so that opening them in a new tab/window keeps the viewer around. Thus the viewer acts as a chaperon for the users preventing them from finding themselves out of the viewer's supervision. Of course there are ways to circumvent such oversight, however it has always been the case with chaperons in all cultures in all epochs. --- static/skin/viewer.js | 28 +++++++++++++++++++++++----- test/server.cpp | 4 ++-- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/static/skin/viewer.js b/static/skin/viewer.js index 0b7a6787..89bbae47 100644 --- a/static/skin/viewer.js +++ b/static/skin/viewer.js @@ -10,6 +10,8 @@ let viewerState = { uiLanguage: 'en', }; +const FULL_ROOT_URL = `${window.location.origin}${root}`; + function dropUserLang(query) { const q = new URLSearchParams(query); q.delete('userlang'); @@ -347,6 +349,12 @@ function onClickEvent(e) { if (target !== null && "href" in target) { const target_href = getRealHref(target); const target_url = new URL(target_href, iframeDocument.location); + if ( target_url.href.startsWith(`${FULL_ROOT_URL}/viewer#`) && + !linkShouldBeOpenedInANewWindow(target, e) ) { + contentIframe.contentWindow.parent.location = target_url; + e.preventDefault(); + return; + } const isExternalAppUrl = urlMustBeHandledByAnExternalApp(target_url); if ( (isExternalAppUrl && !viewerSettings.linkBlockingEnabled) || goingToOpenALinkToAnUndisplayableResource(target_url) ) { @@ -398,14 +406,24 @@ this.Element && function(ElementPrototype) { } }(Element.prototype); -function setup_external_link_blocker() { - setupEventHandler(contentIframe.contentDocument, 'a', 'click', onClickEvent); -} - //////////////////////////////////////////////////////////////////////////////// // End of external link blocking //////////////////////////////////////////////////////////////////////////////// +const internalUrlPrefix = `${FULL_ROOT_URL}/content/`; + +function setup_chaperon_mode() { + setupEventHandler(contentIframe.contentDocument, 'a', 'click', onClickEvent); + const links = contentIframe.contentDocument.getElementsByTagName('a'); + for ( const a of links ) { + // XXX: wombat's possible involvement with href not taken into account + if ( a.hasAttribute('href') && a.href.startsWith(internalUrlPrefix) ) { + const userUrl = a.href.substr(internalUrlPrefix.length); + a.href = `${root}/viewer#${userUrl}`; + } + } +} + let viewerSetupComplete = false; function on_content_load() { @@ -416,7 +434,7 @@ function on_content_load() { if ( viewerSetupComplete ) { handle_content_url_change(); } - setup_external_link_blocker(); + setup_chaperon_mode(); } function htmlDecode(input) { diff --git a/test/server.cpp b/test/server.cpp index bf5a2a34..481ff31f 100644 --- a/test/server.cpp +++ b/test/server.cpp @@ -77,7 +77,7 @@ const ResourceCollection resources200Compressible{ { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/taskbar.css" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/taskbar.css?cacheid=42e90cb9" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/viewer.js" }, - { STATIC_CONTENT, "/ROOT%23%3F/skin/viewer.js?cacheid=7f05bf6c" }, + { STATIC_CONTENT, "/ROOT%23%3F/skin/viewer.js?cacheid=05ef466b" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/fonts/Poppins.ttf" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/fonts/Poppins.ttf?cacheid=af705837" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/fonts/Roboto.ttf" }, @@ -338,7 +338,7 @@ R"EXPECTEDRESULT( - + const blankPageUrl = root + "/skin/blank.html?cacheid=6b1fa032";