fix(preview): make HTML iframe links navigate (zddc-server-backed archive)

User report: opening an .html file with a '../.archive/' hyperlink in
a new tab works (zddc-server intercepts and serves the right file),
but clicking the same link inside the file previewer does nothing.

Two combined causes:

  1. The previewer's iframe was loaded from a blob: URL (built from
     the file's bytes). Relative URLs in the iframe resolve relative
     to the blob URL — '../.archive/X.html' becomes 'blob:.../.archive/
     X.html', which is gibberish. The browser never sends a request to
     the server, so the .archive interception never fires.

  2. sandbox="" disables every iframe capability including popups,
     so even <a target=_blank> is silently swallowed.

Fix per tool:

  - archive (table.js): for HTML preview, use file.url (the real
    server URL) directly when available; fall back to blob only for
    File-System-Access-API mode where there's no server to intercept
    anyway. Now relative links in archived HTMLs resolve against the
    actual server origin and the .archive interception fires as
    designed. Sandbox loosens to allow-same-origin + allow-popups +
    allow-popups-to-escape-sandbox so resources within the iframe
    load and link clicks (default target / target=_blank / middle-
    click) work normally. allow-scripts is intentionally NOT set —
    archived HTML still cannot run JS in the popup's origin.

  - transmittal (files-preview.js) + classifier (preview.js): same
    sandbox loosening for consistency. These tools' files are
    typically local (FileSystemAccessAPI), so the file.url branch
    doesn't apply — relative URLs that depend on a server still
    won't resolve in local mode (intrinsic limitation, no server).

Tested behavior preserved:
  - PDFs: unchanged (no sandbox, browser's PDF viewer handles).
  - Images / docx / xlsx / tiff / zip / text: unchanged.
  - HTML in zddc-server-backed archive: relative '../.archive/' links
    now navigate the iframe to the correct target file.
This commit is contained in:
ZDDC 2026-05-03 18:54:55 -05:00
parent 2820dffeaa
commit 915ab8a87a
3 changed files with 37 additions and 12 deletions

View file

@ -345,9 +345,28 @@
*/
async function showFilePreview(file) {
const ext = file.extension.toLowerCase();
try {
const url = await getFileBlobUrl(file);
// For HTML preview, prefer the file's real server URL over a
// blob URL when available (zddc-server-backed archives have
// file.url set; local FileSystemAccessAPI mode doesn't).
//
// Why it matters: HTML files in an archive often link to
// sibling/parent paths via relative URLs — e.g.
// ../.archive/<tracking>.html — which zddc-server intercepts
// and resolves. From a blob: URL the relative resolution
// produces blob:.../.archive/X.html, which never reaches the
// server. Loading the iframe from the actual https://zddc.../
// URL means relative links resolve back to the server and the
// .archive interception fires as designed.
//
// Other types (pdf, images rendered via canvas / iframe etc.)
// are content-only — they don't depend on relative URLs — so
// a blob URL is fine.
const isHtml = ext === 'html' || ext === 'htm';
const url = (isHtml && file.url)
? file.url
: await getFileBlobUrl(file);
// Mirror the parent window's theme in the popup
const parentTheme = document.documentElement.getAttribute('data-theme') || '';
@ -511,7 +530,7 @@
<button class="btn" onclick="downloadFile()">Download</button>
</div>
${(ext === 'pdf' || ext === 'html' || ext === 'htm')
? '<iframe src="' + url + '"' + (ext === 'pdf' ? '' : ' sandbox=""') + '></iframe>'
? '<iframe src="' + url + '"' + (ext === 'pdf' ? '' : ' sandbox="allow-same-origin allow-popups allow-popups-to-escape-sandbox"') + '></iframe>'
: '<div id="previewContent"><div class="loading">Loading preview...</div></div>'}
<script>
var blobUrl = "${url}";

View file

@ -277,10 +277,11 @@
case 'pdf':
return `<iframe src="${blobUrl}#view=FitV"></iframe>`;
case 'html':
// Render the HTML natively (not as literal text). sandbox=""
// disables scripts / forms / top-level nav / plugins so an
// archived HTML file can't run code in the popup's origin.
return `<iframe src="${blobUrl}" sandbox=""></iframe>`;
// Render the HTML natively (not as literal text). Sandbox
// flags allow same-origin resource loads + opening links
// in real new tabs (target=_blank / middle-click), but
// NOT allow-scripts — archived HTML cannot run JS.
return `<iframe src="${blobUrl}" sandbox="allow-same-origin allow-popups allow-popups-to-escape-sandbox"></iframe>`;
case 'image':
return `<img src="${blobUrl}" alt="${escapeHtml(file.originalFilename)}" />`;
case 'text':

View file

@ -126,15 +126,20 @@
// PDF and HTML preview natively in an iframe — for HTML this
// means the page is RENDERED (not shown as literal source text);
// the blob's MIME type ('text/html', see getMimeType) tells the
// browser to render. `sandbox=""` on the HTML iframe disables
// all dangerous capabilities (scripts, top-level navigation,
// forms, plugins, popups) since these are arbitrary archived
// files we don't trust to run JS in the parent's origin.
// browser to render. The HTML iframe is sandboxed:
// - allow-same-origin: needed so the iframe's resource loads
// (img / link / etc.) work normally for same-origin paths.
// - allow-popups + allow-popups-to-escape-sandbox: clicking
// <a target="_blank"> (or middle-click) opens a real new tab
// with full browser features. Without these, link clicks
// intended for new tabs silently no-op.
// - NO allow-scripts: archived HTML cannot run JS in this
// popup's origin.
var contentHtml;
if (ext === 'pdf') {
contentHtml = '<iframe src="' + safeHref + '"></iframe>';
} else if (ext === 'html' || ext === 'htm') {
contentHtml = '<iframe src="' + safeHref + '" sandbox=""></iframe>';
contentHtml = '<iframe src="' + safeHref + '" sandbox="allow-same-origin allow-popups allow-popups-to-escape-sandbox"></iframe>';
} else {
contentHtml = '<div id="previewContent"><div class="loading">Loading preview...</div></div>';
}