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) { async function showFilePreview(file) {
const ext = file.extension.toLowerCase(); const ext = file.extension.toLowerCase();
try { 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 // Mirror the parent window's theme in the popup
const parentTheme = document.documentElement.getAttribute('data-theme') || ''; const parentTheme = document.documentElement.getAttribute('data-theme') || '';
@ -511,7 +530,7 @@
<button class="btn" onclick="downloadFile()">Download</button> <button class="btn" onclick="downloadFile()">Download</button>
</div> </div>
${(ext === 'pdf' || ext === 'html' || ext === 'htm') ${(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>'} : '<div id="previewContent"><div class="loading">Loading preview...</div></div>'}
<script> <script>
var blobUrl = "${url}"; var blobUrl = "${url}";

View file

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

View file

@ -126,15 +126,20 @@
// PDF and HTML preview natively in an iframe — for HTML this // PDF and HTML preview natively in an iframe — for HTML this
// means the page is RENDERED (not shown as literal source text); // means the page is RENDERED (not shown as literal source text);
// the blob's MIME type ('text/html', see getMimeType) tells the // the blob's MIME type ('text/html', see getMimeType) tells the
// browser to render. `sandbox=""` on the HTML iframe disables // browser to render. The HTML iframe is sandboxed:
// all dangerous capabilities (scripts, top-level navigation, // - allow-same-origin: needed so the iframe's resource loads
// forms, plugins, popups) since these are arbitrary archived // (img / link / etc.) work normally for same-origin paths.
// files we don't trust to run JS in the parent's origin. // - 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; var contentHtml;
if (ext === 'pdf') { if (ext === 'pdf') {
contentHtml = '<iframe src="' + safeHref + '"></iframe>'; contentHtml = '<iframe src="' + safeHref + '"></iframe>';
} else if (ext === 'html' || ext === 'htm') { } 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 { } else {
contentHtml = '<div id="previewContent"><div class="loading">Loading preview...</div></div>'; contentHtml = '<div id="previewContent"><div class="loading">Loading preview...</div></div>';
} }