Send link navigation event inside iframe to parent (#2160)

This commit is contained in:
Jannik Becher 2023-08-13 12:40:48 +01:00 committed by GitHub
parent 2ae39f59fb
commit 60d9beda5a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 244 additions and 4 deletions

View file

@ -26,14 +26,14 @@ import { sha256Base64 } from "../../lib/utils";
// (2): https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#attr-sandbox
// (3): https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts
const IFRAME_SHA256 = "vd7g1B8fLBFZH6C6KNpG4H8B0SQ/oIuqKaTW6jD053A=";
const IFRAME_SHA256 = "48LZtKkFYMd+4gsmVvbhvw9mTpJPw+ItRdGxPPs+5xw=";
export function initializeIframeSource(iframe, iframePort, iframeUrl) {
const url = getIframeUrl(iframePort, iframeUrl);
return verifyIframeSource(url).then(() => {
iframe.sandbox =
"allow-scripts allow-same-origin allow-downloads allow-modals allow-popups";
"allow-scripts allow-same-origin allow-downloads allow-modals allow-popups allow-top-navigation";
iframe.allow =
"accelerometer; ambient-light-sensor; camera; display-capture; encrypted-media; fullscreen; geolocation; gyroscope; microphone; midi; usb; xr-spatial-tracking; clipboard-read; clipboard-write";
iframe.src = url;
@ -48,8 +48,8 @@ function getIframeUrl(iframePort, iframeUrl) {
}
return protocol === "https:"
? "https://livebookusercontent.com/iframe/v4.html"
: `http://${window.location.hostname}:${iframePort}/iframe/v4.html`;
? "https://livebookusercontent.com/iframe/v5.html"
: `http://${window.location.hostname}:${iframePort}/iframe/v5.html`;
}
let iframeVerificationPromise = null;

View file

@ -0,0 +1,240 @@
<!DOCTYPE html>
<html lang="en">
<head>
<base target="_parent">
<title>Output</title>
<style>
html,
body {
margin: 0;
padding: 0;
font-family: sans-serif;
overflow-y: hidden;
}
</style>
</head>
<body>
<div id="root"></div>
<script>
"use strict";
// Invoke the init function in a separate context for better isolation
function applyInit(init, ctx, data) {
init(ctx, data);
}
(() => {
const state = {
token: null,
importPromise: null,
eventHandlers: {},
eventQueue: [],
syncHandler: null,
secretHandler: null,
};
function postMessage(message) {
window.parent.postMessage({ token: state.token, ...message }, "*");
}
const ctx = {
root: document.getElementById("root"),
handleEvent(event, callback) {
if (state.eventHandlers[event]) {
throw new Error(
`Handler has already been defined for event "${event}"`
);
}
state.eventHandlers[event] = callback;
while (
state.eventQueue.length > 0 &&
state.eventHandlers[state.eventQueue[0].event]
) {
const { event, payload } = state.eventQueue.shift();
const handler = state.eventHandlers[event];
handler(payload);
}
},
pushEvent(event, payload = null) {
postMessage({ type: "event", event, payload });
},
importCSS(url) {
return new Promise((resolve, reject) => {
const linkEl = document.createElement("link");
linkEl.addEventListener(
"load",
(event) => {
resolve();
},
{ once: true }
);
linkEl.rel = "stylesheet";
linkEl.href = url;
document.head.appendChild(linkEl);
});
},
importJS(url) {
return new Promise((resolve, reject) => {
const scriptEl = document.createElement("script");
scriptEl.addEventListener(
"load",
(event) => {
resolve();
},
{ once: true }
);
scriptEl.src = url;
document.head.appendChild(scriptEl);
});
},
handleSync(callback) {
state.syncHandler = callback;
},
selectSecret(callback, preselectName, options = {}) {
state.secretHandler = callback;
postMessage({ type: "selectSecret", preselectName, options });
},
};
window.addEventListener("message", (event) => {
if (event.source === window.parent) {
handleParentMessage(event.data);
}
});
function handleParentMessage(message) {
if (message.type === "readyReply") {
state.token = message.token;
onReady();
// Set the base URL for relative URLs
const baseUrlEl = document.createElement("base");
baseUrlEl.href = message.baseUrl;
document.head.appendChild(baseUrlEl);
// We already entered the script and the base URL change
// doesn't impact this import call, so we use the absolute
// URL instead
state.importPromise = import(`${message.baseUrl}${message.jsPath}`);
} else if (message.type === "init") {
state.importPromise
.then((module) => {
const init = module.init;
if (!init) {
const fns = Object.keys(module);
throw new Error(
`Expected the module to export an init function, but found: ${fns.join(
", "
)}`
);
}
applyInit(init, ctx, message.data);
})
.catch((error) => {
renderErrorMessage(
`Failed to load the widget JS module, got the following error:\n\n ${error.message}\n\nSee the browser console for more details. If running behind an authentication proxy, make sure the /public/* routes are publicly accessible.`
);
throw error;
});
} else if (message.type === "event") {
const { event, payload } = message;
const handler = state.eventHandlers[event];
if (state.eventQueue.length === 0 && handler) {
handler(payload);
} else {
state.eventQueue.push({ event, payload });
}
} else if (message.type === "sync") {
Promise.resolve(state.syncHandler && state.syncHandler()).then(
() => {
postMessage({ type: "syncReply" });
}
);
} else if (message.type === "secretSelected") {
state.secretHandler && state.secretHandler(message.secretName);
}
}
postMessage({ type: "ready" });
function onReady() {
// Report height changes
const resizeObserver = new ResizeObserver((entries) => {
postMessage({ type: "resize", height: document.body.scrollHeight });
});
resizeObserver.observe(document.body);
// Forward relevant DOM events
window.addEventListener("mousedown", (event) => {
postMessage({ type: "domEvent", event: { type: "mousedown" } });
});
window.addEventListener("focus", (event) => {
postMessage({ type: "domEvent", event: { type: "focus" } });
});
window.addEventListener("keydown", (event) => {
postMessage({
type: "domEvent",
event: keyboardEventToPayload(event),
isTargetEditable: isEditableElement(event.target),
});
});
}
function isEditableElement(element) {
return element.matches("input, textarea, [contenteditable]");
}
function keyboardEventToPayload(event) {
const {
altKey,
code,
ctrlKey,
isComposing,
key,
location,
metaKey,
repeat,
shiftKey,
} = event;
return {
type: event.type,
props: {
altKey,
code,
ctrlKey,
isComposing,
key,
location,
metaKey,
repeat,
shiftKey,
},
};
}
function renderErrorMessage(message) {
ctx.root.innerHTML = `
<div style="color: #FF3E38; white-space: pre-wrap; word-break: break-word;">${message}</div>
`;
}
})();
</script>
</body>
</html>