Merge pull request #1071 from heygen-com/fix/studio-cross-origin-iframe-guards

This commit is contained in:
Miguel Ángel
2026-05-25 12:53:06 -04:00
committed by GitHub
4 changed files with 128 additions and 54 deletions
@@ -62,33 +62,46 @@ function parsePositiveNumber(value: string | null): number | null {
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
}
// fallow-ignore-next-line complexity
function resolveIframeDuration(iframe: HTMLIFrameElement | null): number | null {
const win = iframe?.contentWindow as PreviewWindow | null;
const playerDuration = win?.__player?.getDuration?.();
if (Number.isFinite(playerDuration) && playerDuration != null && playerDuration > 0) {
return playerDuration;
try {
const win = iframe?.contentWindow as PreviewWindow | null;
const playerDuration = win?.__player?.getDuration?.();
if (Number.isFinite(playerDuration) && playerDuration != null && playerDuration > 0) {
return playerDuration;
}
} catch {
/* cross-origin iframe */
}
const doc = iframe?.contentDocument;
const root = doc?.querySelector("[data-composition-id]") ?? doc?.documentElement ?? null;
return (
parsePositiveNumber(root?.getAttribute("data-composition-duration") ?? null) ??
parsePositiveNumber(root?.getAttribute("data-duration") ?? null)
);
try {
const doc = iframe?.contentDocument;
const root = doc?.querySelector("[data-composition-id]") ?? doc?.documentElement ?? null;
return (
parsePositiveNumber(root?.getAttribute("data-composition-duration") ?? null) ??
parsePositiveNumber(root?.getAttribute("data-duration") ?? null)
);
} catch {
return null;
}
}
function syncIframePlayback(iframe: HTMLIFrameElement | null, shouldPlay: boolean): boolean {
const player = (iframe?.contentWindow as PreviewWindow | null)?.__player;
if (!player) return false;
try {
const player = (iframe?.contentWindow as PreviewWindow | null)?.__player;
if (!player) return false;
if (shouldPlay) {
player.play?.();
if (shouldPlay) {
player.play?.();
return true;
}
player.pause?.();
player.seek?.(resolveThumbnailSeekTime(resolveIframeDuration(iframe)));
return true;
} catch {
return false;
}
player.pause?.();
player.seek?.(resolveThumbnailSeekTime(resolveIframeDuration(iframe)));
return true;
}
function CompCard({
+70 -29
View File
@@ -7,6 +7,35 @@ import { STUDIO_MOTION_PATH } from "../components/editor/studioMotion";
import { shouldHandleTimelineToggleHotkey, isEditableTarget } from "../utils/timelineDiscovery";
import { shouldIgnoreHistoryShortcut } from "../utils/studioHelpers";
/** Safely resolves contentWindow for a potentially cross-origin iframe. */
function iframeContentWindow(iframe: HTMLIFrameElement | null): Window | null {
try {
return iframe?.contentWindow ?? null;
} catch {
return null;
}
}
/**
* Handles Cmd/Ctrl+Z (undo) and Cmd/Ctrl+Shift+Z / Ctrl+Y (redo) key events.
* Returns true if the event was handled, false otherwise.
*/
// fallow-ignore-next-line complexity
function handleUndoRedoKey(event: KeyboardEvent, onUndo: () => void, onRedo: () => void): boolean {
const key = event.key.toLowerCase();
if (key === "z" && !event.shiftKey) {
event.preventDefault();
onUndo();
return true;
}
if ((key === "z" && event.shiftKey) || (event.ctrlKey && !event.metaKey && key === "y")) {
event.preventDefault();
onRedo();
return true;
}
return false;
}
// ── Types ──
interface EditHistoryHandle {
@@ -177,18 +206,15 @@ export function useAppHotkeys({
// Cmd/Ctrl+Z — undo, Cmd/Ctrl+Shift+Z or Ctrl+Y — redo
if (event.metaKey || event.ctrlKey) {
if (!shouldIgnoreHistoryShortcut(event.target)) {
const key = event.key.toLowerCase();
if (key === "z" && !event.shiftKey) {
event.preventDefault();
void handleUndoRef.current();
return;
}
if ((key === "z" && event.shiftKey) || (event.ctrlKey && !event.metaKey && key === "y")) {
event.preventDefault();
void handleRedoRef.current();
return;
}
if (
!shouldIgnoreHistoryShortcut(event.target) &&
handleUndoRedoKey(
event,
() => void handleUndoRef.current(),
() => void handleRedoRef.current(),
)
) {
return;
}
// Cmd/Ctrl+1 — sidebar: Compositions tab
@@ -310,13 +336,21 @@ export function useAppHotkeys({
const syncPreviewTimelineHotkey = useCallback(
(iframe: HTMLIFrameElement | null) => {
const nextWindow = iframe?.contentWindow ?? null;
const nextWindow = iframeContentWindow(iframe);
if (previewHotkeyWindowRef.current === nextWindow) return;
if (previewHotkeyWindowRef.current) {
previewHotkeyWindowRef.current.removeEventListener("keydown", previewAppKeyDownHandler);
try {
previewHotkeyWindowRef.current.removeEventListener("keydown", previewAppKeyDownHandler);
} catch {
/* cross-origin iframe */
}
}
previewHotkeyWindowRef.current = nextWindow;
nextWindow?.addEventListener("keydown", previewAppKeyDownHandler, true);
try {
nextWindow?.addEventListener("keydown", previewAppKeyDownHandler, true);
} catch {
/* cross-origin iframe */
}
},
[previewAppKeyDownHandler],
);
@@ -324,7 +358,11 @@ export function useAppHotkeys({
useEffect(
() => () => {
if (previewHotkeyWindowRef.current) {
previewHotkeyWindowRef.current.removeEventListener("keydown", previewAppKeyDownHandler);
try {
previewHotkeyWindowRef.current.removeEventListener("keydown", previewAppKeyDownHandler);
} catch {
/* cross-origin iframe */
}
previewHotkeyWindowRef.current = null;
}
},
@@ -336,16 +374,11 @@ export function useAppHotkeys({
const handleHistoryHotkey = useCallback((event: KeyboardEvent) => {
if (!(event.metaKey || event.ctrlKey)) return;
if (shouldIgnoreHistoryShortcut(event.target)) return;
const key = event.key.toLowerCase();
if (key === "z" && !event.shiftKey) {
event.preventDefault();
void handleUndoRef.current();
return;
}
if ((key === "z" && event.shiftKey) || (event.ctrlKey && !event.metaKey && key === "y")) {
event.preventDefault();
void handleRedoRef.current();
}
handleUndoRedoKey(
event,
() => void handleUndoRef.current(),
() => void handleRedoRef.current(),
);
}, []);
const syncPreviewHistoryHotkey = useCallback(
@@ -353,7 +386,7 @@ export function useAppHotkeys({
previewHistoryHotkeyCleanupRef.current?.();
previewHistoryHotkeyCleanupRef.current = null;
const win = iframe?.contentWindow ?? null;
const win = iframeContentWindow(iframe);
let doc: Document | null = null;
try {
doc = iframe?.contentDocument ?? null;
@@ -362,10 +395,18 @@ export function useAppHotkeys({
}
if (!win && !doc) return;
win?.addEventListener("keydown", handleHistoryHotkey, true);
try {
win?.addEventListener("keydown", handleHistoryHotkey, true);
} catch {
/* cross-origin */
}
doc?.addEventListener("keydown", handleHistoryHotkey, true);
previewHistoryHotkeyCleanupRef.current = () => {
win?.removeEventListener("keydown", handleHistoryHotkey, true);
try {
win?.removeEventListener("keydown", handleHistoryHotkey, true);
} catch {
/* cross-origin */
}
doc?.removeEventListener("keydown", handleHistoryHotkey, true);
};
},
+6 -1
View File
@@ -22,8 +22,13 @@ function errorProps(value: unknown): {
return { error_message: String(value), error_name: null, stack_trace: null };
}
// fallow-ignore-next-line complexity
function isCompositionAssetError(msg: string): boolean {
return msg.includes("Error fetching") && (msg.includes("404") || msg.includes("Not Found"));
if (msg.includes("Error fetching") && (msg.includes("404") || msg.includes("Not Found")))
return true;
if (msg.includes("unsupported or unrecognizable format")) return true;
if (msg.includes("MEDIA_ERR_SRC_NOT_SUPPORTED")) return true;
return false;
}
const ERROR_CAP = 50;
@@ -182,23 +182,38 @@ export function usePlaybackKeyboard({
playbackKeyDownRef.current = handlePlaybackKeyDown;
playbackKeyUpRef.current = handlePlaybackKeyUp;
// fallow-ignore-next-line complexity
const attachIframeShortcutListeners = useCallback(() => {
iframeShortcutCleanupRef.current?.();
iframeShortcutCleanupRef.current = null;
const iframeWin = iframeRef.current?.contentWindow;
const iframeDoc = iframeRef.current?.contentDocument;
let iframeWin: Window | null = null;
let iframeDoc: Document | null = null;
try {
iframeWin = iframeRef.current?.contentWindow ?? null;
iframeDoc = iframeRef.current?.contentDocument ?? null;
} catch {
return;
}
if (!iframeWin && !iframeDoc) return;
const handleIframeKeyDown = (e: KeyboardEvent) => playbackKeyDownRef.current(e);
const handleIframeKeyUp = (e: KeyboardEvent) => playbackKeyUpRef.current(e);
iframeWin?.addEventListener("keydown", handleIframeKeyDown, true);
iframeWin?.addEventListener("keyup", handleIframeKeyUp, true);
try {
iframeWin?.addEventListener("keydown", handleIframeKeyDown, true);
iframeWin?.addEventListener("keyup", handleIframeKeyUp, true);
} catch {
/* cross-origin iframe */
}
iframeDoc?.addEventListener("keydown", handleIframeKeyDown, true);
iframeDoc?.addEventListener("keyup", handleIframeKeyUp, true);
iframeShortcutCleanupRef.current = () => {
iframeWin?.removeEventListener("keydown", handleIframeKeyDown, true);
iframeWin?.removeEventListener("keyup", handleIframeKeyUp, true);
try {
iframeWin?.removeEventListener("keydown", handleIframeKeyDown, true);
iframeWin?.removeEventListener("keyup", handleIframeKeyUp, true);
} catch {
/* cross-origin iframe */
}
iframeDoc?.removeEventListener("keydown", handleIframeKeyDown, true);
iframeDoc?.removeEventListener("keyup", handleIframeKeyUp, true);
};