function runInFrame(origin) { window.addEventListener("message", (e) => { if(e.origin !== origin) { return; } const { id, command, type, text } = e.data; try { switch(command) { case "sanitize": const result = sanitizeDom(text, type); e.source.postMessage({ id, result }); break; } } catch (error) { console.error(error); e.source.postMessage({ id, error: error.stack ?? error}); } }); } export default async function sanitize(text: string, type: "image/svg+xml" | "text/html"): Promise { const frame = document.createElement("iframe"); frame.style.left = "-5px"; frame.style.top = "-5px"; frame.style.position = "fixed"; frame.style.width = "1px"; frame.style.height = "1px"; document.body.appendChild(frame); const contentLoadedPromise = new Promise((resolve) => { frame.onload = () => { frame.onload = null; resolve(); } }); frame.srcdoc = ` `; await contentLoadedPromise; const id = window?.crypto?.randomUUID?.() ?? Date.now(); return new Promise((resolve, reject) => { const eh = (e: MessageEvent) => { const { id: rID, result, error } = e.data; if (rID === id) { window.removeEventListener("message", eh); frame.remove(); if (error) { reject(error); return; } resolve(result); } }; window.addEventListener("message", eh); frame.contentWindow.postMessage({ id, command: "sanitize", text, type }, "*"); }); } function eventNames() { return (self as any).ignoreEventNames ??= new Set([ 'onabort', 'onafterprint', 'onauxclick', 'onbeforematch', 'onbeforeprint', 'onbeforetoggle', 'onbeforeunload', 'onblur', 'oncancel', 'oncanplay', 'oncanplaythrough', 'onchange', 'onclick', 'onclose', 'oncontextlost', 'oncontextmenu', 'oncontextrestored', 'oncopy', 'oncuechange', 'oncut', 'ondblclick', 'ondrag', 'ondragend', 'ondragenter', 'ondragleave', 'ondragover', 'ondragstart', 'ondrop', 'ondurationchange', 'onemptied', 'onended', 'onerror', 'onfocus', 'onformdata', 'onhashchange', 'oninput', 'oninvalid', 'onkeydown', 'onkeypress', 'onkeyup', 'onlanguagechange', 'onload', 'onloadeddata', 'onloadedmetadata', 'onloadstart', 'onmessage', 'onmessageerror', 'onmousedown', 'onmouseenter', 'onmouseleave', 'onmousemove', 'onmouseout', 'onmouseover', 'onmouseup', 'onoffline', 'ononline', 'onpagehide', 'onpageshow', 'onpaste', 'onpause', 'onplay', 'onplaying', 'onpopstate', 'onprogress', 'onratechange', 'onrejectionhandled', 'onreset', 'onresize', 'onscroll', 'onscrollend', 'onsecuritypolicyviolation', 'onseeked', 'onseeking', 'onselect', 'onslotchange', 'onstalled', 'onstorage', 'onsubmit', 'onsuspend', 'ontimeupdate', 'ontoggle', 'onunhandledrejection', 'onunload', 'onvolumechange', 'onwaiting', 'onwheel' ]); } // function * flattenRecursiveIterator(current:Iterator<{ value?: T, iterator?: any }>): Iterable { // const stack = []; // for(;;) { // const { done, value: currentValue } = current.next(); // if (done) { // if(!stack.length) { // return; // } // current = stack.pop(); // continue; // } // const { value, iterator } = currentValue; // if (value) { // yield value; // continue; // } // if (iterator) { // stack.push(current); // current = iterator; // continue; // } // throw new Error("Invalid state"); // } // } function * allElementIterator(e: HTMLElement) { const stack = []; yield e; let start = e.firstElementChild; while(start) { stack.push(start); start = start.nextElementSibling; } while(stack.length) { const child = stack.pop(); yield child; if (!child.parentElement) { continue; } let start = child.firstElementChild; while(start) { stack.push(start); start = start.nextElementSibling; } } } export function sanitizeDom(text: string, type: DOMParserSupportedType) { const parser = new DOMParser(); const dom = parser.parseFromString(text, type); const ignore = eventNames(); for (const element of allElementIterator(dom.documentElement)) { if (/^(script|iframe|embed|object)$/i.test(element.tagName)) { element.remove(); continue; } const attributes = element.attributes; const deleteNames = [] as string[]; for (let index = 0; index < attributes.length; index++) { const a = attributes.item(index); if( ignore.has(a.name.toLowerCase())) { deleteNames.push(a.name); } } for (const a of deleteNames) { attributes.removeNamedItem(a); } } return dom.documentElement.outerHTML; } export function sanitizeDomElement(dom: Element) { const ignore = eventNames(); for (const element of allElementIterator(dom as any)) { if (/^(script|iframe|embed|object)$/i.test(element.tagName)) { element.remove(); continue; } const attributes = element.attributes; const deleteNames = [] as string[]; for (let index = 0; index < attributes.length; index++) { const a = attributes.item(index); if( ignore.has(a.name.toLowerCase())) { deleteNames.push(a.name); } } for (const a of deleteNames) { attributes.removeNamedItem(a); } } } // function * recursiveElementIterator(e: Element): Iterator<{ value?: Element, iterator?: any}> { // let next = e.nextElementSibling; // const first = e.firstElementChild; // yield { value: e }; // if (first?.isConnected) { // yield { iterator: recursiveElementIterator(first) } // } // while (next) { // const current = next; // next = next.nextElementSibling; // yield { iterator: recursiveElementIterator(current) }; // } // } // function * recursiveNodeIterator(e: Node): Iterator<{ value?: Node, iterator?: any}> { // let next = e.nextSibling as Node; // const first = e.firstChild; // yield { value: e }; // if (first?.isConnected) { // yield { iterator: recursiveNodeIterator(first as Node) } // } // while (next) { // const current = next; // next = next.nextSibling as Node; // yield { iterator: recursiveNodeIterator(current) }; // } // }