The preview showed nothing but the bare word `forbidden` in DSH Desktop. The cause is not this plugin's routes but the host, one layer above them: `DesktopWebServer` wraps EVERY route with `permits()`, and in the default isolated/扩展 mode `ordinaryBrowserEnabled` is false, so a request is served only when it carries the Electron renderer capability (`x-dsh-desktop-renderer`). That header is attached solely to MAIN-frame and SAME-ORIGIN-subframe traffic (`requestComesFromRendererOrigin`), and a frame sandboxed WITHOUT allow-same-origin is an opaque origin at request time — so the preview document request could never be served, while every other sidebar route (all main-frame) worked fine. The fix keeps the sandbox boundary exactly where it was and moves the delivery: the document is fetched by the MAIN frame — which the host does trust — and handed to the frame through `srcdoc`. New shared component HtmlPreviewFrame owns that path plus the sandbox tokens for BOTH surfaces (the editor viewer and the changes tab's op render preview, which had no escape hatch at all), so the two can no longer drift; a failed fetch falls back to handing the frame the route URL, leaving the route's own answer visible instead of a blank pane. `withPreviewBase` injects `<base href="<the file's route directory>">`: a srcdoc document has no URL of its own, so without it `./style.css` would resolve against the GUI page. A document that declares its own base is left alone. Two consequences, both deliberate: - sandboxed: the frame stays opaque, so a self-contained document previews completely while a multi-file one shows its markup with the relative assets missing (those requests are opaque-origin too — inlining them is a separate job); - unlocked (the warned per-surface unlock, or the setting): the srcdoc frame inherits the GUI origin — which is what that escape hatch already meant — and there relative references resolve and load like any other same-origin request, so multi-file previews work in that mode. Verified: typecheck clean; build clean; 1364 passed / 9 skipped (130 files, +13: the pure base-rewrite spec, the delivery spec, and the updated sandbox-contract assertions, which now pin srcdoc + no route src instead of the old route-src shape).
61 lines
2.5 KiB
TypeScript
61 lines
2.5 KiB
TypeScript
/**
|
|
* Shared vocabulary of the sidebar's HTML preview surfaces (the editor's
|
|
* html viewer and the changes tab's op render preview): the sandbox tokens
|
|
* both preview iframes load with, plus the document-base rewrite the srcdoc
|
|
* delivery path needs.
|
|
*/
|
|
|
|
/**
|
|
* The sandbox tokens of an HTML preview iframe. NO allow-same-origin (the
|
|
* preview must stay in an opaque origin — with the route's own origin it
|
|
* could read session data) and NO allow-top-navigation (a previewed page
|
|
* must not hijack the GUI). The editor viewer can disable the sandbox
|
|
* per-feature in the side card settings (warned); the changes tab's op
|
|
* render preview always stays sandboxed.
|
|
*/
|
|
export const HTML_IFRAME_SANDBOX = 'allow-scripts allow-popups allow-downloads allow-modals'
|
|
|
|
/** Whether a document already declares its own base URL. */
|
|
const HAS_BASE = /<base[\s/>]/i
|
|
|
|
/** Index just past the element a `<base>` must precede to land in <head>. */
|
|
function insertionPoint(html: string): number {
|
|
const head = /<head\b[^>]*>/i.exec(html)
|
|
if (head !== null) return head.index + head[0].length
|
|
const htmlTag = /<html\b[^>]*>/i.exec(html)
|
|
if (htmlTag !== null) return htmlTag.index + htmlTag[0].length
|
|
return 0
|
|
}
|
|
|
|
/**
|
|
* The directory part of a preview route URL, with its trailing slash.
|
|
* @param url - the `/sidebar/html/<sessionId>/<path…>` route URL.
|
|
* @returns the URL up to and including the last `/`.
|
|
*/
|
|
export function previewBaseOf(url: string): string {
|
|
const cut = url.lastIndexOf('/')
|
|
return cut < 0 ? url : url.slice(0, cut + 1)
|
|
}
|
|
|
|
/**
|
|
* Point a preview document's relative references back at its own route.
|
|
*
|
|
* A `srcdoc` document has no URL of its own — the browser resolves its
|
|
* relative references against the EMBEDDING page's URL, so `./style.css` in a
|
|
* previewed file would resolve to the GUI's origin and hit whatever route
|
|
* happens to match. Injecting `<base href="<the file's route directory>">`
|
|
* keeps relative resolution inside `/sidebar/html/…`, where the file's own
|
|
* siblings live. A document that declares its own `<base>` is left alone: the
|
|
* author said what the base is.
|
|
*
|
|
* @param html - the previewed document's markup.
|
|
* @param url - the route URL the document was fetched from.
|
|
* @returns the markup, with a base element injected when it had none.
|
|
*/
|
|
export function withPreviewBase(html: string, url: string): string {
|
|
if (HAS_BASE.test(html)) return html
|
|
const href = previewBaseOf(url).replace(/"/g, '"')
|
|
const at = insertionPoint(html)
|
|
return `${html.slice(0, at)}<base href="${href}">${html.slice(at)}`
|
|
}
|