mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-01 19:42:03 +00:00
Merge pull request #1071 from heygen-com/fix/studio-cross-origin-iframe-guards
This commit is contained in:
@@ -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({
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user