mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 09:16:55 +00:00
fix(studio): authenticate preview message senders (#3812)
This commit is contained in:
@@ -333,7 +333,7 @@ export function StudioApp() {
|
||||
: null,
|
||||
effectiveTimelineDuration,
|
||||
});
|
||||
const compositionDimensions = useCompositionDimensions();
|
||||
const compositionDimensions = useCompositionDimensions(previewIframeRef);
|
||||
const { lintModal, linting, handleLint, closeLintModal, findingsByFile } = useLintModal(
|
||||
projectId,
|
||||
refreshKey,
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
// @vitest-environment jsdom
|
||||
import { act } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { expect, it, vi } from "vitest";
|
||||
import { CaptionOverlay } from "./CaptionOverlay";
|
||||
import { useCaptionStore } from "../store";
|
||||
|
||||
Reflect.set(globalThis, "IS_REACT_ACT_ENVIRONMENT", true);
|
||||
|
||||
it("only schedules caption layout for messages from its preview", () => {
|
||||
const host = document.createElement("div");
|
||||
const preview = document.createElement("iframe");
|
||||
const foreign = document.createElement("iframe");
|
||||
document.body.append(host, preview, foreign);
|
||||
const root = createRoot(host);
|
||||
const schedule = vi.fn(() => 1);
|
||||
vi.stubGlobal("requestAnimationFrame", schedule);
|
||||
vi.stubGlobal("cancelAnimationFrame", vi.fn());
|
||||
vi.stubGlobal(
|
||||
"ResizeObserver",
|
||||
class {
|
||||
observe() {}
|
||||
disconnect() {}
|
||||
},
|
||||
);
|
||||
useCaptionStore.getState().setEditMode(true);
|
||||
const send = (source: MessageEventSource | null) =>
|
||||
act(() => {
|
||||
window.dispatchEvent(new MessageEvent("message", { source, data: { source: "hf-preview" } }));
|
||||
});
|
||||
try {
|
||||
act(() => root.render(<CaptionOverlay iframeRef={{ current: preview }} />));
|
||||
schedule.mockClear();
|
||||
send(foreign.contentWindow);
|
||||
send(null);
|
||||
expect(schedule).not.toHaveBeenCalled();
|
||||
send(preview.contentWindow);
|
||||
expect(schedule).toHaveBeenCalledOnce();
|
||||
} finally {
|
||||
act(() => root.unmount());
|
||||
useCaptionStore.getState().reset();
|
||||
host.remove();
|
||||
preview.remove();
|
||||
foreign.remove();
|
||||
vi.unstubAllGlobals();
|
||||
}
|
||||
});
|
||||
@@ -116,6 +116,7 @@ export const CaptionOverlay = memo(function CaptionOverlay({ iframeRef }: Captio
|
||||
// messages, element resize, window resize.
|
||||
const unsubPlayer = usePlayerStore.subscribe(scheduleTick);
|
||||
const handleMessage = (e: MessageEvent) => {
|
||||
if (!e.source || e.source !== iframeRef.current?.contentWindow) return;
|
||||
const data = e.data;
|
||||
if (data?.source === "hf-preview") scheduleTick();
|
||||
};
|
||||
|
||||
@@ -139,7 +139,7 @@ export function PreviewOverlays({
|
||||
}: PreviewOverlaysProps) {
|
||||
const { activeCompPath, previewIframeRef } = useStudioShellContext();
|
||||
const { captionEditMode, compositionLoading, isPlaying } = useStudioPlaybackContext();
|
||||
const compositionDimensions = useCompositionDimensions();
|
||||
const compositionDimensions = useCompositionDimensions(previewIframeRef);
|
||||
|
||||
// Caption edit mode is entered automatically when captions are detected;
|
||||
// these give the author an explicit way OUT (and back in). Without them the
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
// @vitest-environment jsdom
|
||||
import { act } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { expect, it, vi } from "vitest";
|
||||
import { useCaptionDetection } from "./useCaptionDetection";
|
||||
|
||||
Reflect.set(globalThis, "IS_REACT_ACT_ENVIRONMENT", true);
|
||||
|
||||
it("does not let a foreign window trigger caption activation", () => {
|
||||
const host = document.createElement("div");
|
||||
const preview = document.createElement("iframe");
|
||||
const foreign = document.createElement("iframe");
|
||||
document.body.append(host, preview, foreign);
|
||||
const root = createRoot(host);
|
||||
const fetchMock = vi.fn(() => new Promise<Response>(() => {}));
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
const params = {
|
||||
projectId: "demo",
|
||||
activeCompPath: "captions.html",
|
||||
compIdToSrc: new Map<string, string>(),
|
||||
captionEditMode: false,
|
||||
captionHasSelection: false,
|
||||
previewIframeRef: { current: preview },
|
||||
captionSync: { save: vi.fn(), loadOverrides: vi.fn(async () => {}) },
|
||||
setRightCollapsed: vi.fn(),
|
||||
};
|
||||
function Harness() {
|
||||
useCaptionDetection(params);
|
||||
return null;
|
||||
}
|
||||
const send = (source: MessageEventSource | null) =>
|
||||
act(() => {
|
||||
window.dispatchEvent(
|
||||
new MessageEvent("message", { source, data: { source: "hf-preview", type: "state" } }),
|
||||
);
|
||||
});
|
||||
try {
|
||||
act(() => root.render(<Harness />));
|
||||
const group = preview.contentDocument!.createElement("div");
|
||||
group.className = "caption-group";
|
||||
preview.contentDocument!.body.append(group);
|
||||
send(foreign.contentWindow);
|
||||
send(null);
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
send(preview.contentWindow);
|
||||
expect(fetchMock).toHaveBeenCalledOnce();
|
||||
} finally {
|
||||
act(() => root.unmount());
|
||||
host.remove();
|
||||
preview.remove();
|
||||
foreign.remove();
|
||||
vi.unstubAllGlobals();
|
||||
}
|
||||
});
|
||||
@@ -131,6 +131,7 @@ export function useCaptionDetection({
|
||||
};
|
||||
|
||||
const handleMessage = (e: MessageEvent) => {
|
||||
if (!e.source || e.source !== previewIframeRef.current?.contentWindow) return;
|
||||
const data = e.data;
|
||||
if (data?.source === "hf-preview" && (data?.type === "state" || data?.type === "timeline")) {
|
||||
if (!acceptStudioRuntimeMessage(data)) return;
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
// @vitest-environment jsdom
|
||||
import { act } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { expect, it } from "vitest";
|
||||
import { useCompositionDimensions } from "./useCompositionDimensions";
|
||||
|
||||
Reflect.set(globalThis, "IS_REACT_ACT_ENVIRONMENT", true);
|
||||
|
||||
it("accepts only the current preview frame and rejects foreign or missing senders", () => {
|
||||
const host = document.createElement("div");
|
||||
const preview = document.createElement("iframe");
|
||||
const foreign = document.createElement("iframe");
|
||||
document.body.append(host, preview, foreign);
|
||||
const ref = { current: preview };
|
||||
const root = createRoot(host);
|
||||
function Harness() {
|
||||
return JSON.stringify(useCompositionDimensions(ref));
|
||||
}
|
||||
const send = (source: MessageEventSource | null, width: number) =>
|
||||
act(() => {
|
||||
window.dispatchEvent(
|
||||
new MessageEvent("message", {
|
||||
source,
|
||||
origin: window.location.origin,
|
||||
data: { source: "hf-preview", type: "stage-size", width, height: 100 },
|
||||
}),
|
||||
);
|
||||
});
|
||||
try {
|
||||
act(() => root.render(<Harness />));
|
||||
send(foreign.contentWindow, 300);
|
||||
send(null, 400);
|
||||
expect(host.textContent).toBe("null");
|
||||
send(preview.contentWindow, 200);
|
||||
expect(host.textContent).toBe('{"width":200,"height":100}');
|
||||
ref.current = foreign;
|
||||
send(preview.contentWindow, 500);
|
||||
expect(host.textContent).toBe('{"width":200,"height":100}');
|
||||
send(foreign.contentWindow, 600);
|
||||
expect(host.textContent).toBe('{"width":600,"height":100}');
|
||||
} finally {
|
||||
act(() => root.unmount());
|
||||
host.remove();
|
||||
preview.remove();
|
||||
foreign.remove();
|
||||
}
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { useState, type RefObject } from "react";
|
||||
import { useMountEffect } from "./useMountEffect";
|
||||
import type { CompositionDimensions } from "../components/renders/RenderQueue";
|
||||
import { acceptStudioRuntimeMessage } from "../player/lib/runtimeProtocol";
|
||||
@@ -29,13 +29,14 @@ function readPositiveDimensions(width: unknown, height: unknown): CompositionDim
|
||||
return { width: parsedWidth, height: parsedHeight };
|
||||
}
|
||||
|
||||
export function useCompositionDimensions() {
|
||||
export function useCompositionDimensions(iframeRef: RefObject<HTMLIFrameElement | null>) {
|
||||
const [compositionDimensions, setCompositionDimensions] = useState<CompositionDimensions | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
useMountEffect(() => {
|
||||
const handleMessage = (e: MessageEvent) => {
|
||||
if (!e.source || e.source !== iframeRef.current?.contentWindow) return;
|
||||
const dimensions = readCompositionSizeMessage(e.data);
|
||||
if (!dimensions) return;
|
||||
setCompositionDimensions((prev) =>
|
||||
|
||||
Reference in New Issue
Block a user