Files
webstudio/apps/builder/app/shared/canvas-api.ts
T
Ivan Starkov 5edbeda077 fix: Skip inaccessible iframes added by extensions to prevent cross-origin errors (#3586)
## Description

Already released as hotfix


https://cdn.discordapp.com/attachments/1252596836729094184/1252596837056118834/image.png?ex=6672cb36&is=667179b6&hm=fc5718dfac9bd0e5a430b22a09cf0889831a767f5c60e2ca1731e6f3fd43a779&

## Steps for reproduction
Install Zotero extension

Then open 

https://main.development.webstudio.is/builder/a1371dce-752c-4ccf-8ea4-88bab577fe50
See error
`Failed to read a named property '__webstudio__$__canvasApi' from
'Window': Blocked a frame with origin
"https://main.development.webstudio.is" from accessing a cross-origin
frame.`

Now open

https://fix-zotero.development.webstudio.is/builder/a1371dce-752c-4ccf-8ea4-88bab577fe50
See no Error

## Code Review

- [ ] hi @kof, I need you to do
  - conceptual review (architecture, feature-correctness)
  - detailed review (read every line)
  - test it on preview

## Before requesting a review

- [ ] made a self-review
- [ ] added inline comments where things may be not obvious (the "why",
not "what")

## Before merging

- [ ] tested locally and on preview environment (preview dev login:
5de6)
- [ ] updated [test
cases](https://github.com/webstudio-is/webstudio/blob/main/apps/builder/docs/test-cases.md)
document
- [ ] added tests
- [ ] if any new env variables are added, added them to `.env` file
2024-06-18 19:38:36 +03:00

116 lines
3.0 KiB
TypeScript

import { preventUnhandled } from "@atlaskit/pragmatic-drag-and-drop/prevent-unhandled";
import { setInert, resetInert } from "../canvas/shared/inert";
import { monitorForExternal } from "@atlaskit/pragmatic-drag-and-drop/external/adapter";
import { createRecursiveProxy } from "@trpc/server/shared";
import invariant from "tiny-invariant";
import { $canvasIframeState } from "./nano-states";
const apiWindowNamespace = "__webstudio__$__canvasApi";
const _canvasApi = {
isInitialized: () => true,
setInert,
resetInert,
preventUnhandled,
monitorForExternal,
};
declare global {
interface Window {
[apiWindowNamespace]: typeof _canvasApi;
}
}
const isInIframe = () => {
try {
return window.self !== window.top;
} catch (e) {
return true;
}
};
const getIframeApi = () => {
if (isInIframe()) {
// Inside the iframe, use the local window.api
return _canvasApi;
} else {
// Find first iframe with the API
for (let i = 0; i < window.frames.length; ++i) {
try {
const frame = window.frames[i];
if (frame && frame[apiWindowNamespace]) {
return frame[apiWindowNamespace];
}
} catch {
// Certain extensions, such as Zotero, inject iframes into the page
// These iframes can be inaccessible and may cause access errors
// Therefore, we should skip processing them
}
}
return;
}
};
const isKeyOf = <T>(key: unknown, obj: T): key is keyof T => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return key in obj;
};
/**
* Forwards the call from the builder to the iframe, invoking the original API in the iframe.
*/
export const canvasApi = createRecursiveProxy((options) => {
const api = getIframeApi();
if (api == null) {
if (
options.path.join(".") ===
("isInitialized" satisfies keyof typeof _canvasApi)
) {
return false;
}
// eslint-disable-next-line no-console
console.warn(
`API not found in the iframe, skipping ${options.path.join(".")} call, iframe probably not loaded yet`
);
return null;
}
let currentMethod = api as unknown;
for (const key of options.path) {
invariant(
isKeyOf(key, currentMethod),
`API method ${options.path.join(".")} not found`
);
invariant(typeof currentMethod === "object");
invariant(currentMethod != null);
currentMethod = currentMethod[key];
}
invariant(
typeof currentMethod === "function",
`API method ${options.path.join(".")} is not a function`
);
return currentMethod.call(null, ...options.args);
}) as typeof _canvasApi;
/**
* Initializes the canvas API in the iframe. Must be called in the iframe context.
*/
export const initCanvasApi = () => {
if (isInIframe()) {
$canvasIframeState.set("ready");
window[apiWindowNamespace] = _canvasApi;
}
return () => {
// Does not work as expected, because the iframe is detached from the builder
$canvasIframeState.set("idle");
};
};