mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
fix(core,studio): escape user values in querySelector attribute selectors
Extract cssAttrSelector to packages/core/src/utils/cssSelector.ts and use it (or CSS.escape for browser-side code) at all 12 sites that previously interpolated raw user-authored values into querySelector attribute selectors. A " in a composition ID, script src, or data-start value would produce a malformed selector that throws. Node-side (core compiler/parser): uses the shared cssAttrSelector. Browser-side (runtime, studio): uses native CSS.escape(). Supersedes #1568 which fixed only the 3 bundler sites.
This commit is contained in:
@@ -126,7 +126,7 @@ export const LayersPanel = memo(function LayersPanel() {
|
||||
if (doc) {
|
||||
const found =
|
||||
(layer.id ? doc.getElementById(layer.id) : null) ??
|
||||
(layer.hfId ? doc.querySelector(`[data-hf-id="${layer.hfId}"]`) : null) ??
|
||||
(layer.hfId ? doc.querySelector(`[data-hf-id="${CSS.escape(layer.hfId)}"]`) : null) ??
|
||||
doc.getElementById(layer.key);
|
||||
if (found instanceof HTMLElement) el = found;
|
||||
}
|
||||
|
||||
@@ -242,7 +242,7 @@ export function findElementForSelection(
|
||||
activeCompositionPath: string | null = null,
|
||||
): HTMLElement | null {
|
||||
if (selection.hfId) {
|
||||
const byHfId = doc.querySelector(`[data-hf-id="${selection.hfId}"]`);
|
||||
const byHfId = doc.querySelector(`[data-hf-id="${CSS.escape(selection.hfId)}"]`);
|
||||
if (isHtmlElement(byHfId)) return byHfId;
|
||||
}
|
||||
|
||||
|
||||
@@ -189,7 +189,7 @@ export const NLELayout = memo(function NLELayout({
|
||||
const doc = iframeRef_.current?.contentDocument;
|
||||
if (doc) {
|
||||
const host = doc.querySelector(
|
||||
`[data-composition-id="${compId}"][data-composition-src]`,
|
||||
`[data-composition-id="${CSS.escape(compId)}"][data-composition-src]`,
|
||||
);
|
||||
if (host) {
|
||||
resolvedPath = host.getAttribute("data-composition-src") || undefined;
|
||||
|
||||
@@ -123,7 +123,8 @@ export function createTimelineElementFromManifestClip(params: {
|
||||
if (clip.kind === "composition" && clip.compositionId) {
|
||||
let resolvedSrc = clip.compositionSrc;
|
||||
if (!resolvedSrc) {
|
||||
hostEl = doc?.querySelector(`[data-composition-id="${clip.compositionId}"]`) ?? hostEl;
|
||||
hostEl =
|
||||
doc?.querySelector(`[data-composition-id="${CSS.escape(clip.compositionId)}"]`) ?? hostEl;
|
||||
resolvedSrc =
|
||||
hostEl?.getAttribute("data-composition-src") ??
|
||||
hostEl?.getAttribute("data-composition-file") ??
|
||||
|
||||
@@ -163,13 +163,13 @@ export function getImplicitTimelineLayerLabel(el: HTMLElement): string {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function getTimelineElementSelector(el: Element): string | undefined {
|
||||
if (isHtmlElement(el) && el.id) return `#${el.id}`;
|
||||
if (isHtmlElement(el) && el.id) return `#${CSS.escape(el.id)}`;
|
||||
const compId = el.getAttribute("data-composition-id");
|
||||
if (compId) return `[data-composition-id="${compId}"]`;
|
||||
if (compId) return `[data-composition-id="${CSS.escape(compId)}"]`;
|
||||
if (isHtmlElement(el)) {
|
||||
const classes = el.className.split(/\s+/).filter(Boolean);
|
||||
const firstClass = classes.find((className) => className !== "clip") ?? classes[0];
|
||||
if (firstClass) return `.${firstClass}`;
|
||||
if (firstClass) return `.${CSS.escape(firstClass)}`;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -283,8 +283,8 @@ function nodeMatchesManifestClip(node: Element, clip: ClipManifestClip): boolean
|
||||
function findTimelineDomNode(doc: Document, id: string): Element | null {
|
||||
return (
|
||||
doc.getElementById(id) ??
|
||||
doc.querySelector(`[data-composition-id="${id}"]`) ??
|
||||
doc.querySelector(`.${id}`) ??
|
||||
doc.querySelector(`[data-composition-id="${CSS.escape(id)}"]`) ??
|
||||
doc.querySelector(`.${CSS.escape(id)}`) ??
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
@@ -295,7 +295,8 @@ export function buildMissingCompositionElements(
|
||||
let start = parseFloat(startAttr);
|
||||
if (isNaN(start)) {
|
||||
const ref =
|
||||
doc.getElementById(startAttr) || doc.querySelector(`[data-composition-id="${startAttr}"]`);
|
||||
doc.getElementById(startAttr) ||
|
||||
doc.querySelector(`[data-composition-id="${CSS.escape(startAttr)}"]`);
|
||||
if (ref) {
|
||||
const refStartAttr = ref.getAttribute("data-start") ?? "0";
|
||||
let refStart = parseFloat(refStartAttr);
|
||||
@@ -303,7 +304,7 @@ export function buildMissingCompositionElements(
|
||||
if (isNaN(refStart)) {
|
||||
const refRef =
|
||||
doc.getElementById(refStartAttr) ||
|
||||
doc.querySelector(`[data-composition-id="${refStartAttr}"]`);
|
||||
doc.querySelector(`[data-composition-id="${CSS.escape(refStartAttr)}"]`);
|
||||
const rrStart = parseFloat(refRef?.getAttribute("data-start") ?? "0") || 0;
|
||||
const rrCompId = refRef?.getAttribute("data-composition-id");
|
||||
const rrDur =
|
||||
@@ -400,7 +401,7 @@ export function buildMissingCompositionElements(
|
||||
// Find the matching DOM host by element id or composition id
|
||||
const host =
|
||||
doc.getElementById(existing.id) ??
|
||||
doc.querySelector(`[data-composition-id="${existing.id}"]`);
|
||||
doc.querySelector(`[data-composition-id="${CSS.escape(existing.id)}"]`);
|
||||
if (!host) return existing;
|
||||
const compSrc =
|
||||
host.getAttribute("data-composition-src") || host.getAttribute("data-composition-file");
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
if (typeof globalThis.CSS === "undefined") {
|
||||
(globalThis as Record<string, unknown>).CSS = {};
|
||||
}
|
||||
if (typeof CSS.escape !== "function") {
|
||||
CSS.escape = (value: string) => value.replace(/([^\w-])/g, "\\$1");
|
||||
}
|
||||
Reference in New Issue
Block a user