mirror of
https://github.com/ps5-payload-dev/websrv.git
synced 2026-10-21 19:22:37 -05:00
248 lines
9.2 KiB
JavaScript
248 lines
9.2 KiB
JavaScript
/* Copyright (C) 2024 idlesauce
|
|
|
|
This program is free software; you can redistribute it and/or modify it
|
|
under the terms of the GNU General Public License as published by the
|
|
Free Software Foundation; either version 3, or (at your option) any
|
|
later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program; see the file COPYING. If not, see
|
|
<http://www.gnu.org/licenses/>. */
|
|
|
|
|
|
// @ts-check
|
|
|
|
const EXTENSION_SANDBOX_EXECUTE_ACTION = "EXTENSION_SANDBOX_EXECUTE_ACTION";
|
|
|
|
const EXTENSION_SANDBOX_EXECUTE_RESPONSE = "EXTENSION_SANDBOX_EXECUTE_RESPONSE";
|
|
// @ts-ignore (demo hb.js is in a diff context)
|
|
const EXTENSION_API_PARENT_SHOW_CAROUSEL = "EXTENSION_API_PARENT_SHOW_CAROUSEL";
|
|
// @ts-ignore (demo hb.js is in a diff context)
|
|
const EXTENSION_API_PARENT_FILE_BROWSER = "EXTENSION_API_PARENT_FILE_BROWSER";
|
|
|
|
const EXTENSION_MAIN_TIMEOUT_MS = 5000;
|
|
|
|
/**
|
|
* @typedef {Object} ExtensionInfo
|
|
* @property {string} uuid - reference the sandbox iframe by this id
|
|
* @property {CarouselItem} result
|
|
*/
|
|
|
|
/**
|
|
* @typedef {Object} ExtensionSandboxExecResult
|
|
* @property {string} uuid
|
|
* @property {boolean?} result
|
|
*/
|
|
|
|
|
|
/**
|
|
* @param {string} extensionPath
|
|
* @returns {Promise<ExtensionInfo?>} extensionId and result of main(), null if main() timed out, if null sandbox is deleted
|
|
*/
|
|
async function loadJsExtension(extensionPath) {
|
|
// load js as raw text
|
|
let hbJs = await ApiClient.fsGetFileText(extensionPath);
|
|
if (!hbJs.data) {
|
|
// throw new Error("Failed to load extension: " + extensionPath);
|
|
removeFromHomebrewList(extensionPath);
|
|
await Globals.Router.handleHome();
|
|
return null;
|
|
}
|
|
|
|
if (!Globals.hbApiJs) {
|
|
let request2 = await fetch("homebrewApi.js");
|
|
if (!request2.ok) {
|
|
throw new Error("Failed to load extension api js");
|
|
}
|
|
Globals.hbApiJs = await request2.text();
|
|
}
|
|
|
|
if (!Globals.ApiClientJs) {
|
|
let request3 = await fetch("apiClient.js");
|
|
if (!request3.ok) {
|
|
throw new Error("Failed to load api client js for extension");
|
|
}
|
|
Globals.ApiClientJs = await request3.text();
|
|
}
|
|
|
|
// create element to hold iframes if there isnt one already
|
|
let sandbox = document.getElementById("js-extension-sandbox");
|
|
|
|
if (!sandbox) {
|
|
sandbox = document.createElement("div");
|
|
sandbox.style.display = "none";
|
|
sandbox.id = "js-extension-sandbox";
|
|
document.body.appendChild(sandbox);
|
|
}
|
|
|
|
let extensionId = uuidv4();
|
|
|
|
const iframeContent = `
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
</head>
|
|
|
|
<body>
|
|
<script>
|
|
window.extensionId = "${extensionId}";
|
|
window.workingDir = "${extensionPath.substring(0, extensionPath.lastIndexOf("/"))}";
|
|
${Globals.hbApiJs}
|
|
${Globals.ApiClientJs}
|
|
${hbJs.data}
|
|
// register message listener to execute code here in the iframe sandbox
|
|
window.addEventListener("message", async (event) => {
|
|
if (event.data.action !== "${EXTENSION_SANDBOX_EXECUTE_ACTION}") {
|
|
return;
|
|
}
|
|
|
|
let res = await window.Funcs[event.data.funcId]();
|
|
window.parent.postMessage({ extensionId: window.extensionId, action: "${EXTENSION_SANDBOX_EXECUTE_RESPONSE}", result: encodeURIComponent(JSON.stringify(res)), resultId: event.data.resultId }, "*");
|
|
});
|
|
window.addEventListener("error", (event) => { alert(event.error); })
|
|
window.addEventListener("unhandledrejection", (event) => { alert(event.reason); });
|
|
// call main and return result
|
|
(async () => {
|
|
const result = await main();
|
|
window.parent.postMessage({ extensionId: "${extensionId}", result: encodeURIComponent(JSON.stringify(remapFunctionsToFunctionIds(result))) }, "*");
|
|
})();
|
|
</script>
|
|
</body>
|
|
|
|
</html>
|
|
`;
|
|
|
|
const blob = new Blob([iframeContent], { type: "text/html" });
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
const iframe = document.createElement("iframe");
|
|
iframe.id = extensionId;
|
|
// @ts-ignore
|
|
iframe.sandbox = "allow-scripts allow-modals";
|
|
iframe.style.display = "none";
|
|
|
|
let result = new Promise((resolve, reject) => {
|
|
let timeout = null;
|
|
let handler = function(event) {
|
|
if (event.data.extensionId === extensionId) {
|
|
clearTimeout(timeout);
|
|
window.removeEventListener("message", handler);
|
|
resolve({ uuid: extensionId, result: remapFunctionIdsToSandboxedProxyCalls(JSON.parse(decodeURIComponent(event.data.result)),extensionId) });
|
|
}
|
|
}
|
|
|
|
timeout = setTimeout(() => {
|
|
window.removeEventListener("message", handler);
|
|
sandbox.removeChild(iframe);
|
|
resolve(null);
|
|
}, EXTENSION_MAIN_TIMEOUT_MS);
|
|
|
|
window.addEventListener("message", handler);
|
|
});
|
|
|
|
iframe.src = url;
|
|
|
|
sandbox.appendChild(iframe);
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* @param {string} extensionId
|
|
* @param {string} funcId
|
|
* @returns {Promise<ExtensionSandboxExecResult>?}
|
|
*/
|
|
function executeInExtensionSandbox(extensionId, funcId) {
|
|
const iframe = /** @type {HTMLIFrameElement} */ (document.getElementById(extensionId));
|
|
if (!iframe || !iframe.contentWindow) {
|
|
throw new Error("extension sandbox iframe not found");
|
|
}
|
|
|
|
let resultId = uuidv4();
|
|
|
|
iframe.contentWindow.postMessage({ action: EXTENSION_SANDBOX_EXECUTE_ACTION, funcId: funcId, resultId: resultId }, "*");
|
|
|
|
return new Promise((resolve, reject) => {
|
|
window.addEventListener("message", function handler(event) {
|
|
if (event.data.extensionId === extensionId && event.data.action === EXTENSION_SANDBOX_EXECUTE_RESPONSE && event.data.resultId === resultId) {
|
|
window.removeEventListener("message", handler);
|
|
let decodedResult = decodeURIComponent(event.data.result);
|
|
if (decodedResult == "null" || decodedResult == "undefined") {
|
|
resolve({ uuid: extensionId, result: null });
|
|
return;
|
|
}
|
|
resolve({ uuid: extensionId, result: JSON.parse(decodedResult) });
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
/** Reverse what remapFunctionsToFunctionIds of the hb api does */
|
|
function remapFunctionIdsToSandboxedProxyCalls(obj, extensionId) {
|
|
if (typeof obj !== "object" || obj === null) {
|
|
return obj;
|
|
}
|
|
|
|
// if array, iterate over its elements recursively
|
|
if (Array.isArray(obj)) {
|
|
return obj.map(item => remapFunctionIdsToSandboxedProxyCalls(item, extensionId));
|
|
}
|
|
|
|
return Object.keys(obj).reduce((acc, key) => {
|
|
if (key === "onclick") {
|
|
acc[key] = async () => {
|
|
let res = await executeInExtensionSandbox(extensionId, obj[key]);
|
|
if (res) {
|
|
return res.result;
|
|
}
|
|
return null;
|
|
}
|
|
} else if (typeof obj[key] === "object" && obj[key] !== null) {
|
|
// Recursively serialize nested objects
|
|
acc[key] = remapFunctionIdsToSandboxedProxyCalls(obj[key], extensionId);
|
|
} else {
|
|
acc[key] = obj[key];
|
|
}
|
|
return acc;
|
|
}, {});
|
|
}
|
|
|
|
|
|
/**
|
|
* Only call once at load
|
|
*/
|
|
function registerExtensionMessagesListener() {
|
|
window.addEventListener("message", async function handler(event) {
|
|
// check if message has an extension uuid and extension is loaded
|
|
if (!event.data.extensionId || !this.document.getElementById(event.data.extensionId)) {
|
|
return;
|
|
}
|
|
|
|
if (event.data.action === EXTENSION_API_PARENT_FILE_BROWSER) {
|
|
let sandbox = /** @type {HTMLIFrameElement?} */ (document.getElementById(event.data.extensionId));
|
|
if (!sandbox || !sandbox.contentWindow) {
|
|
return;
|
|
}
|
|
let result = await Globals.Router.pickPath(decodeURIComponent(event.data.initialPath), decodeURIComponent(event.data.title), decodeURIComponent(event.data.allowNetworkLocations) === "true", decodeURIComponent(event.data.pathType));
|
|
|
|
sandbox.contentWindow.postMessage({ extensionId: event.data.extensionId, callback: event.data.callback, result: result }, "*");
|
|
} else if (event.data.action === EXTENSION_API_PARENT_SHOW_CAROUSEL) {
|
|
let temp_items = JSON.parse(decodeURIComponent(event.data.items));
|
|
// these items should already be almost the same as what the carousel expects
|
|
// the only difference is the the onclicks where replaced by function ids
|
|
/**
|
|
* @type {CarouselItem[]}
|
|
*/
|
|
let items = remapFunctionIdsToSandboxedProxyCalls(temp_items, event.data.extensionId);
|
|
await Globals.Router.handleCarousel(items);
|
|
}
|
|
});
|
|
}
|