mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 09:16:55 +00:00
fix(studio-server): bound preview variable insertion scans (#3715)
* fix(studio-server): bound preview variable insertion scans * docs(studio-server): update preview variables helper reference
This commit is contained in:
@@ -111,7 +111,7 @@ export async function injectMediaCodecMapIntoHtml(
|
||||
});
|
||||
}
|
||||
// <-escape prevents a src path containing "</script>" from breaking out of
|
||||
// the injected tag, mirroring injectPreviewVariables in routes/preview.ts.
|
||||
// the injected tag, mirroring injectPreviewVariables in helpers/previewVariables.ts.
|
||||
const json = JSON.stringify(map)
|
||||
.replace(/</g, "\\u003c")
|
||||
.replace(/\u2028/g, "\\u2028")
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { injectPreviewVariables } from "./previewVariables";
|
||||
|
||||
const SCRIPT = '<script data-hf-preview-variables>window.__hfVariables={"a":1};</script>';
|
||||
|
||||
describe("injectPreviewVariables", () => {
|
||||
it.each([
|
||||
[
|
||||
"<!doctype html><html><head>x</head></html>",
|
||||
"<!doctype html><html><head>",
|
||||
"x</head></html>",
|
||||
],
|
||||
[
|
||||
"<!DOCTYPE html><HTML lang='en'><HEAD id='h'>x",
|
||||
"<!DOCTYPE html><HTML lang='en'><HEAD id='h'>",
|
||||
"x",
|
||||
],
|
||||
["<!doctype html><html lang='en'>x", "<!doctype html><html lang='en'>", "x"],
|
||||
[" \n<!DOCTYPE html>x", " \n<!DOCTYPE html>", "x"],
|
||||
["fragment", "", "fragment"],
|
||||
["x<!doctype html>y", "", "x<!doctype html>y"],
|
||||
["<headless>x", "<headless>", "x"],
|
||||
["<head attr='>'>x", "<head attr='>", "'>x"],
|
||||
["<html><head", "<html>", "<head"],
|
||||
["<head<head>x", "<head<head>", "x"],
|
||||
])("preserves insertion boundaries (case %#)", (html, before, after) => {
|
||||
expect(injectPreviewVariables(html, { a: 1 })).toBe(before + SCRIPT + after);
|
||||
});
|
||||
|
||||
it.each(["<head", "<html"])("handles repeated unterminated %s prefixes", (prefix) => {
|
||||
const malformed = prefix.repeat(20_000);
|
||||
expect(injectPreviewVariables(malformed, { a: 1 })).toBe(SCRIPT + malformed);
|
||||
expect(injectPreviewVariables(malformed + ">tail", { a: 1 })).toBe(
|
||||
malformed + ">" + SCRIPT + "tail",
|
||||
);
|
||||
});
|
||||
|
||||
it("escapes script-breaking input before inserting it", () => {
|
||||
expect(injectPreviewVariables("<head>", { a: "</script>" })).toBe(
|
||||
'<head><script data-hf-preview-variables>window.__hfVariables={"a":"\\u003c/script>"};</script>',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Inject preview variable overrides: `?variables=<json>` becomes
|
||||
* `window.__hfVariables` set before any composition script runs — the exact
|
||||
* global the engine sets via evaluateOnNewDocument at render time
|
||||
* (engine/src/services/frameCapture.ts), so preview-with-values cannot
|
||||
* diverge from render behavior. The runtime's getVariables() merges these
|
||||
* overrides over the declared defaults.
|
||||
*/
|
||||
export function injectPreviewVariables(html: string, values: Record<string, unknown>): string {
|
||||
// <-escape prevents a string value containing "</script>" from
|
||||
// breaking out of the injected tag.
|
||||
const json = JSON.stringify(values).replace(/</g, "\\u003c");
|
||||
const tag = `<script data-hf-preview-variables>window.__hfVariables=${json};</script>`;
|
||||
// Insert as early as possible without ever landing before the doctype —
|
||||
// content before <!doctype> flips the document into quirks mode, so the
|
||||
// fallback chain is <head…> → <html…> → after the doctype → prepend.
|
||||
for (const pattern of [/<head/i, /<html/i, /^\s*<!doctype/i]) {
|
||||
const match = pattern.exec(html);
|
||||
if (match) {
|
||||
// If the first prefix has no closing >, no later prefix can close either.
|
||||
const end = html.indexOf(">", match.index + match[0].length);
|
||||
if (end < 0) continue;
|
||||
const at = end + 1;
|
||||
return html.slice(0, at) + tag + html.slice(at);
|
||||
}
|
||||
}
|
||||
return tag + html;
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
import { ensureHfIds } from "@hyperframes/parsers/hf-ids";
|
||||
import { persistHfIdsIfNeeded, stampFileHfIds } from "../helpers/hfIdPersist.js";
|
||||
import { isVariablesPayload, VARIABLES_PAYLOAD_ERROR } from "../helpers/variablesPayload.js";
|
||||
import { injectPreviewVariables } from "../helpers/previewVariables.js";
|
||||
import {
|
||||
resolveProxy,
|
||||
ProxyCapacityError,
|
||||
@@ -203,32 +204,6 @@ function injectGsapCdnFallback(html: string): string {
|
||||
return GSAP_CDN_FALLBACK_SCRIPT + html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject preview variable overrides: `?variables=<json>` becomes
|
||||
* `window.__hfVariables` set before any composition script runs — the exact
|
||||
* global the engine sets via evaluateOnNewDocument at render time
|
||||
* (engine/src/services/frameCapture.ts), so preview-with-values cannot
|
||||
* diverge from render behavior. The runtime's getVariables() merges these
|
||||
* overrides over the declared defaults.
|
||||
*/
|
||||
function injectPreviewVariables(html: string, values: Record<string, unknown>): string {
|
||||
// <-escape prevents a string value containing "</script>" from
|
||||
// breaking out of the injected tag.
|
||||
const json = JSON.stringify(values).replace(/</g, "\\u003c");
|
||||
const tag = `<script data-hf-preview-variables>window.__hfVariables=${json};</script>`;
|
||||
// Insert as early as possible without ever landing before the doctype —
|
||||
// content before <!doctype> flips the document into quirks mode, so the
|
||||
// fallback chain is <head…> → <html…> → after the doctype → prepend.
|
||||
for (const pattern of [/<head[^>]*>/i, /<html[^>]*>/i, /^\s*<!doctype[^>]*>/i]) {
|
||||
const match = pattern.exec(html);
|
||||
if (match) {
|
||||
const at = match.index + match[0].length;
|
||||
return html.slice(0, at) + tag + html.slice(at);
|
||||
}
|
||||
}
|
||||
return tag + html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the `?variables=` query param. Absent/empty → null (no injection).
|
||||
* Invalid JSON or a non-object payload is a caller error — surfaced as a 400
|
||||
|
||||
Reference in New Issue
Block a user