Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(web): sandbox plugin iframe - alpha #399

Merged
merged 12 commits into from
May 12, 2023
6 changes: 6 additions & 0 deletions web/src/components/molecules/PluginEditor/core/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,11 @@ export default () => {
[],
);

const useExperimentalSandbox = useMemo(
() => new URLSearchParams(window.location.search).has("useSandbox"),
[],
);

return {
sourceCode,
layers,
Expand All @@ -197,6 +202,7 @@ export default () => {
showAlignSystem,
showInfobox,
engineMeta,
useExperimentalSandbox,
setSourceCode,
setMode,
setInfoboxSize,
Expand Down
2 changes: 2 additions & 0 deletions web/src/components/molecules/PluginEditor/core/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const PluginEditor: React.FC = () => {
showInfobox,
layers,
engineMeta,
useExperimentalSandbox,
setSourceCode,
setMode,
setInfoboxSize,
Expand Down Expand Up @@ -48,6 +49,7 @@ const PluginEditor: React.FC = () => {
widgetAlignSystem={widgets.alignSystem}
layers={layers}
meta={engineMeta}
useExperimentalSandbox={useExperimentalSandbox}
/>
<div
style={{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,10 @@ export default (isBuilt?: boolean) => {
[],
);

const useExperimentalSandbox = useMemo(() => {
return !!sceneProperty?.experimental?.experimental_sandbox;
}, [sceneProperty]);

return {
sceneId,
rootLayerId,
Expand All @@ -319,6 +323,7 @@ export default (isBuilt?: boolean) => {
widgetAlignEditorActivated,
engineMeta,
layerSelectionReason,
useExperimentalSandbox,
selectLayer,
selectBlock,
onBlockChange,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const CanvasArea: React.FC<Props> = ({ isBuilt, inEditor }) => {
widgetAlignEditorActivated,
engineMeta,
layerSelectionReason,
useExperimentalSandbox,
selectLayer,
selectBlock,
onBlockChange,
Expand Down Expand Up @@ -83,6 +84,7 @@ const CanvasArea: React.FC<Props> = ({ isBuilt, inEditor }) => {
widgetAlignSystemEditing={widgetAlignEditorActivated}
meta={engineMeta}
layerSelectionReason={layerSelectionReason}
useExperimentalSandbox={useExperimentalSandbox}
onLayerSelect={selectLayer}
onCameraChange={onCameraChange}
onWidgetLayoutUpdate={onWidgetUpdate}
Expand Down
43 changes: 31 additions & 12 deletions web/src/core/Crust/Plugins/PluginFrame/PluginIFrame/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import { forwardRef, ForwardRefRenderFunction, IframeHTMLAttributes, ReactNode,
import type { RefObject } from "react";
import { createPortal } from "react-dom";

import useExperimentalSandbox from "../../useExperimentalSandbox";
import IFrame, { AutoResize } from "../IFrame";
import SafeIframe from "../SafeIFrame";

import useHooks, { Ref } from "./hooks";

Expand Down Expand Up @@ -50,21 +52,38 @@ const PluginIFrame: ForwardRefRenderFunction<Ref, Props> = (
handleLoad,
} = useHooks({ ready, ref, visible, type, enabled, onRender });

const experimentalSandbox = useExperimentalSandbox();

const children = (
<>
{html ? (
<IFrame
ref={iFrameRef}
className={className}
iFrameProps={iFrameProps}
html={html}
autoResize={autoResize}
externalRef={externalRef}
onMessage={onMessage}
onClick={onClick}
onLoad={handleLoad}
{...options}
/>
experimentalSandbox ? (
<SafeIframe
ref={iFrameRef}
className={className}
iFrameProps={iFrameProps}
html={html}
autoResize={autoResize}
externalRef={externalRef}
onMessage={onMessage}
onClick={onClick}
onLoad={handleLoad}
{...options}
/>
) : (
<IFrame
ref={iFrameRef}
className={className}
iFrameProps={iFrameProps}
html={html}
autoResize={autoResize}
externalRef={externalRef}
onMessage={onMessage}
onClick={onClick}
onLoad={handleLoad}
{...options}
/>
)
) : renderPlaceholder ? (
<>{renderPlaceholder}</>
) : null}
Expand Down
171 changes: 171 additions & 0 deletions web/src/core/Crust/Plugins/PluginFrame/SafeIFrame/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import {
IframeHTMLAttributes,
Ref,
RefObject,
useCallback,
useEffect,
useImperativeHandle,
useMemo,
useRef,
useState,
} from "react";

import { insertToBody } from "./utils";

export type RefType = {
postMessage: (message: any) => void;
resize: (width: string | number | undefined, height: string | number | undefined) => void;
};

export type AutoResize = "both" | "width-only" | "height-only";

export default function useHook({
autoResizeMessageKey = "___iframe_auto_resize___",
width,
height,
html,
ref,
autoResize,
visible,
iFrameProps,
onLoad,
onMessage,
onClick,
onAutoResized,
}: {
width?: number | string;
height?: number | string;
autoResizeMessageKey?: string;
html?: string;
ref?: Ref<RefType>;
autoResize?: AutoResize;
visible?: boolean;
iFrameProps?: IframeHTMLAttributes<HTMLIFrameElement>;
onLoad?: () => void;
onMessage?: (message: any) => void;
onClick?: () => void;
onAutoResized?: () => void;
} = {}): {
ref: RefObject<HTMLIFrameElement>;
props: IframeHTMLAttributes<HTMLIFrameElement>;
srcDoc: string;
onLoad?: () => void;
} {
const loaded = useRef(false);
const iFrameRef = useRef<HTMLIFrameElement>(null);
const [iFrameSize, setIFrameSize] = useState<[string | undefined, string | undefined]>();
const pendingMesages = useRef<any[]>([]);

useImperativeHandle(
ref,
(): RefType => ({
postMessage: message => {
if (!loaded.current || !iFrameRef.current?.contentWindow) {
pendingMesages.current.push(message);
return;
}
iFrameRef.current.contentWindow.postMessage(message, "*");
},
resize: (width, height) => {
const width2 = typeof width === "number" ? width + "px" : width ?? undefined;
const height2 = typeof height === "number" ? height + "px" : height ?? undefined;
setIFrameSize(width2 || height2 ? [width2, height2] : undefined);
},
}),
[],
);

useEffect(() => {
const cb = (ev: MessageEvent<any>) => {
if (!iFrameRef.current || ev.source !== iFrameRef.current.contentWindow) return;
if (ev.data?.[autoResizeMessageKey]) {
const { width, height } = ev.data[autoResizeMessageKey];
if (typeof width !== "number" || typeof height !== "number") return;
setIFrameSize([width + "px", height + "px"]);
onAutoResized?.();
} else {
onMessage?.(ev.data);
}
};
window.addEventListener("message", cb);
return () => {
window.removeEventListener("message", cb);
};
}, [autoResize, autoResizeMessageKey, onMessage, onAutoResized]);

const onIframeLoad = useCallback(() => {
loaded.current = true;
onLoad?.();
}, [onLoad]);

const props = useMemo<IframeHTMLAttributes<HTMLIFrameElement>>(
() => ({
...iFrameProps,
style: {
display: visible ? "block" : "none",
width: visible
? !autoResize || autoResize == "height-only"
? "100%"
: iFrameSize?.[0]
: "0px",
height: visible
? !autoResize || autoResize == "width-only"
? "100%"
: iFrameSize?.[1]
: "0px",
...iFrameProps?.style,
},
}),
[autoResize, iFrameProps, iFrameSize, visible],
);

useEffect(() => {
const handleBlur = () => {
if (iFrameRef.current && iFrameRef.current === document.activeElement) {
onClick?.();
}
};
window.addEventListener("blur", handleBlur);
return () => {
window.removeEventListener("blur", handleBlur);
};
}, [onClick]);

useEffect(() => {
const w = typeof width === "number" ? width + "px" : width;
const h = typeof height === "number" ? height + "px" : height;
setIFrameSize(w || h ? [w, h] : undefined);
}, [width, height]);

const autoResizeScript = useMemo(() => {
return `<script id="_reearth_resize">
if ("ResizeObserver" in window) {
new window.ResizeObserver(entries => {
const win = document.defaultView;
const html = document.body.parentElement;
const st = win.getComputedStyle(html, "");
horizontalMargin = parseInt(st.getPropertyValue("margin-left"), 10) + parseInt(st.getPropertyValue("margin-right"), 10);
verticalMargin = parseInt(st.getPropertyValue("margin-top"), 10) + parseInt(st.getPropertyValue("margin-bottom"), 10);
const width = html.offsetWidth + horizontalMargin;
const height = html.offsetHeight + verticalMargin;
if(parent){
parent.postMessage({
[${JSON.stringify(autoResizeMessageKey)}]: { width, height }
}, "*")
}
}).observe(document.body.parentElement);
}
</script>`;
}, [autoResizeMessageKey]);

const srcDoc = useMemo(() => {
return insertToBody(html, autoResizeScript);
}, [html, autoResizeScript]);

return {
ref: iFrameRef,
props,
srcDoc,
onLoad: onIframeLoad,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Meta, Story } from "@storybook/react";
import { useRef } from "react";

import Component, { Props, Ref } from ".";

export default {
title: "atoms/Plugin/SafeIFrame",
component: Component,
argTypes: {
onLoad: { action: "onLoad" },
onMessage: { action: "onMessage" },
},
// parameters: { actions: { argTypesRegex: "^on.*" } },
} as Meta;

export const Default: Story<Props> = args => {
const ref = useRef<Ref>(null);
const postMessage = () => {
ref.current?.postMessage({ foo: new Date().toISOString() });
};
return (
<div style={{ background: "#fff" }}>
<Component {...args} ref={ref} />
<p>
<button onClick={postMessage}>postMessage</button>
</p>
</div>
);
};

Default.args = {
visible: true,
iFrameProps: {
style: {
width: "400px",
height: "300px",
},
},
html: `<h1>iframe</h1><script>
window.addEventListener("message", ev => {
if (ev.source !== parent) return;
const p = document.createElement("p");
p.textContent = JSON.stringify(ev.data);
document.body.appendChild(p);
parent.postMessage(ev.data, "*");
});
parent.postMessage("loaded", "*");
</script>`,
};