UNPKG

@jetbrains/websandbox

Version:

A sandbox library for runnung javascript inside HTML5 sandboxed iframe

199 lines (171 loc) 6.45 kB
import Connection from './connection'; // @ts-expect-error loader-based input import CompiledFrameScript from 'compile-code-loader!./frame.ts'; import { API } from './types'; export interface SandboxOptions { // A selector or DOM node where iframe will be appended frameContainer: string | Element; // A class that <iframe/> element will has frameClassName?: string; /* A url of iframe content. If set, "frameContent", "codeToRunBeforeInit", "initialStyles", "baseUrl" won't take any effect. In order to work properly, html file by frameSrc should have ./frame.js code bundled */ frameSrc?: string | null; // A content of sandbox iFrame frameContent?: string; // A js code to run before any other iFrame code (will be injected in <head/>) codeToRunBeforeInit?: string | null, // A CSS markup to inject into iFrame <head/> initialStyles?: string | null, // A URL that will be used as base url for all relative pathes in tags like <script/>, <link/>. See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base baseUrl?: string | null, // Is sandboxed iFrame allowed to capture pointer. See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe allowPointerLock?: boolean, // Is iFrame allowed to go fullscreen. See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe allowFullScreen?: boolean, // Additional attributes to add into sandboxed iFrame sandboxAdditionalAttributes?: string, // Additional attributes to add into sandboxed iFrame allowAdditionalAttributes?: string } export const BaseOptions: SandboxOptions = { frameContainer: 'body', frameClassName: 'websandbox__frame', frameSrc: null, frameContent: ` <!DOCTYPE html> <html> <head><meta charset="UTF-8"></head> <body></body> </html> `, codeToRunBeforeInit: null, initialStyles: null, baseUrl: null, allowPointerLock: false, allowFullScreen: false, sandboxAdditionalAttributes: '' }; class Websandbox { options: SandboxOptions; iframe: HTMLIFrameElement; promise: Promise<unknown>; connection: Connection | null = null; removeMessageListener: () => void = () => {}; /** * Creates sandbox instancea * @param localApi Api of this side. Will be available for sandboxed code as remoteApi * @param options Options of created sandbox */ static create(localApi: API, options: Partial<SandboxOptions> = {}) { return new Websandbox(localApi, options); } /** * {Constructor} * @param localApi * @param options */ constructor(localApi: API, options: Partial<SandboxOptions>) { this.validateOptions(options); this.options = {...BaseOptions, ...options}; this.iframe = this.createIframe(); this.promise = new Promise(resolve => { this.connection = new Connection( this.iframe.contentWindow!.postMessage.bind(this.iframe.contentWindow), listener => { const sourceCheckListener = (event: MessageEvent) => { if (event.source !== this.iframe.contentWindow) { return; } return listener(event); }; window.addEventListener('message', sourceCheckListener); this.removeMessageListener = () => window.removeEventListener('message', sourceCheckListener); }, {allowedSenderOrigin: 'null'} ); this.connection.setServiceMethods({ iframeInitialized: () => { return this.connection! .setLocalApi(localApi) .then(() => resolve(this)); } }); }); } validateOptions(options: Partial<SandboxOptions>) { if (options.frameSrc && (options.frameContent || options.initialStyles || options.baseUrl || options.codeToRunBeforeInit)) { throw new Error('You can not set both "frameSrc" and any of frameContent,initialStyles,baseUrl,codeToRunBeforeInit options'); } if ('frameContent' in options && !options.frameContent?.includes('<head>')) { throw new Error('Websandbox: iFrame content must have "<head>" tag.'); } } _prepareFrameContent(options: SandboxOptions) { let frameContent = options.frameContent ?? ''; if (options.codeToRunBeforeInit) { frameContent = frameContent .replace('<head>', `<head>\n<script>${options.codeToRunBeforeInit}</script>`) ?? ''; } frameContent = frameContent .replace('<head>', `<head>\n<script>${CompiledFrameScript}</script>`) ?? ''; if (options.initialStyles) { frameContent = frameContent .replace('</head>', `<style>${options.initialStyles}</style>\n</head>`); } if (options.baseUrl) { frameContent = frameContent .replace('<head>', `<head>\n<base target="_parent" href="${options.baseUrl}"/>`); } return frameContent; } createIframe() { const containerSelector = this.options.frameContainer; const container = typeof containerSelector === 'string' ? document.querySelector(containerSelector) : containerSelector; if (!container) { throw new Error('Websandbox: Cannot find container for sandbox ' + container); } const frame = document.createElement('iframe'); frame.sandbox = `allow-scripts ${this.options.sandboxAdditionalAttributes}`; frame.allow = `${this.options.allowAdditionalAttributes}`; frame.className = this.options.frameClassName ?? ''; if (this.options.allowFullScreen) { frame.allowFullscreen = true; } if (this.options.frameSrc) { frame.src = this.options.frameSrc; container.appendChild(frame); return frame; } frame.setAttribute('srcdoc', this._prepareFrameContent(this.options)); container.appendChild(frame); return frame; } destroy() { this.iframe.remove(); this.removeMessageListener(); } _runCode(code: string) { return this.connection!.callRemoteServiceMethod('runCode', code); } _runFunction(fn: Function) { return this._runCode(`(${fn.toString()})()`); } run(codeOrFunction: string | Function) { if ((codeOrFunction as Function).name) { return this._runFunction(codeOrFunction as Function); } return this._runCode(codeOrFunction as string); } importScript(path: string) { return this.connection!.callRemoteServiceMethod('importScript', path); } injectStyle(style: string) { return this.connection!.callRemoteServiceMethod('injectStyle', style); } } export default Websandbox;