Theseus: only theseus.x can ask to install an extension, and the sheet is armed
bcnr.installExtension is in every page's main world, and install links were honoured from any page and any frame, with no user gesture. Any site could raise the install sheet timed so that a double-click landed on Install, whose two buttons are always in the same place; an installed community extension runs in the main process at once. The call and the links now work only from theseus.x's top frame, the call needs a real click (checked in the isolated world), and Theseus's own sheets ignore every choice except Cancel for 800 ms, like the approval overlay.
This commit is contained in:
parent
70b35e2520
commit
db1acc1417
3 changed files with 44 additions and 7 deletions
|
|
@ -35,7 +35,11 @@ contextBridge.exposeInMainWorld("bcnr", {
|
|||
// verifies the publisher signature against the name's owner and installs.
|
||||
// Resolves `{ ok, version, publisher }` or `{ ok:false, error }` (also
|
||||
// "cancelled"). Pages can feature-detect it: absent on older builds.
|
||||
installExtension: (id) => ipcRenderer.invoke("bcnr:installExtension", String(id || "")),
|
||||
// Needs a real click: the isolated world reads the page's user activation,
|
||||
// which page script cannot fake.
|
||||
installExtension: (id) => (navigator.userActivation && !navigator.userActivation.isActive
|
||||
? Promise.resolve({ ok: false, error: "installing needs a click on the page" })
|
||||
: ipcRenderer.invoke("bcnr:installExtension", String(id || ""))),
|
||||
});
|
||||
|
||||
// Install-as-app relay. Theseus fires a synthetic `beforeinstallprompt` in
|
||||
|
|
|
|||
|
|
@ -118,7 +118,20 @@
|
|||
${order.map((i) => `<button class="pbtn ${i === req.defaultId ? "primary" : "quiet"}" type="button" data-i="${i}">${esc(req.buttons[i])}</button>`).join("")}
|
||||
</div>
|
||||
</div></div>`;
|
||||
document.querySelectorAll("button[data-i]").forEach((b) => b.addEventListener("click", () => finishApp(Number(b.dataset.i))));
|
||||
// Armed like the approval overlay: for the first moments only the
|
||||
// cancel choice works, so a click aimed at whatever was under the
|
||||
// pointer when the sheet appeared (a page timing a double-click) cannot
|
||||
// land on Install or another affirmative button.
|
||||
const ARM_MS = 800;
|
||||
const armAt = Date.now() + ARM_MS;
|
||||
req.armAt = armAt;
|
||||
const acts = Array.from(document.querySelectorAll("button[data-i]")).filter((b) => Number(b.dataset.i) !== req.cancelId);
|
||||
acts.forEach((b) => { b.disabled = true; b.style.opacity = "0.45"; });
|
||||
setTimeout(() => { if (current === req) acts.forEach((b) => { b.disabled = false; b.style.opacity = ""; }); }, ARM_MS);
|
||||
document.querySelectorAll("button[data-i]").forEach((b) => b.addEventListener("click", () => {
|
||||
if (Number(b.dataset.i) !== req.cancelId && Date.now() < armAt) return;
|
||||
finishApp(Number(b.dataset.i));
|
||||
}));
|
||||
document.getElementById("close").addEventListener("click", () => finishApp(req.cancelId));
|
||||
const mask = document.querySelector(".promptmask");
|
||||
mask.addEventListener("mousedown", (e) => { if (e.target === mask) finishApp(req.cancelId); });
|
||||
|
|
@ -159,7 +172,11 @@
|
|||
if (!current) return;
|
||||
if (current.kind === "app") {
|
||||
if (e.key === "Escape") { e.preventDefault(); finishApp(current.cancelId); }
|
||||
else if (e.key === "Enter" && !(e.target && e.target.matches("button.quiet, #close"))) { e.preventDefault(); finishApp(current.defaultId); }
|
||||
else if (e.key === "Enter" && !(e.target && e.target.matches("button.quiet, #close"))) {
|
||||
e.preventDefault();
|
||||
if (current.defaultId !== current.cancelId && Date.now() < (current.armAt || 0)) return;
|
||||
finishApp(current.defaultId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (e.key === "Escape") { e.preventDefault(); finish(current.kind === "alert"); }
|
||||
|
|
|
|||
24
main.js
24
main.js
|
|
@ -4009,7 +4009,12 @@ function createTab(initial, opts = {}) {
|
|||
const installId = installLinkId(u);
|
||||
if (installId) {
|
||||
e.preventDefault();
|
||||
let requester = null; try { requester = new URL(wc.getURL()).host || null; } catch {}
|
||||
let requester = null; try { requester = new URL(String(wc.getURL()).replace(/^bns:\/\//i, "https://")).host || null; } catch {}
|
||||
const init = e.initiator;
|
||||
if (!installRequesterOk(requester) || (init && init.frameTreeNodeId !== wc.mainFrame.frameTreeNodeId)) {
|
||||
console.log("[addons] install link ignored: not from theseus.x's top frame");
|
||||
return;
|
||||
}
|
||||
installExtensionWithConsent(installId, requester);
|
||||
return;
|
||||
}
|
||||
|
|
@ -4123,8 +4128,11 @@ function createTab(initial, opts = {}) {
|
|||
}
|
||||
const installId = installLinkId(url);
|
||||
if (installId) {
|
||||
let requester = null; try { requester = new URL(wc.getURL()).host || null; } catch {}
|
||||
installExtensionWithConsent(installId, requester);
|
||||
let requester = null; try { requester = new URL(String(wc.getURL()).replace(/^bns:\/\//i, "https://")).host || null; } catch {}
|
||||
let refHost = null; try { refHost = details.referrer && details.referrer.url ? new URL(String(details.referrer.url).replace(/^bns:\/\//i, "https://")).host : null; } catch {}
|
||||
// Same rule as the bcnr call: the catalog site's own top frame only.
|
||||
if (installRequesterOk(requester) && refHost === requester) installExtensionWithConsent(installId, requester);
|
||||
else console.log("[addons] install window ignored: not from theseus.x's top frame");
|
||||
} else if (url && url !== "about:blank") {
|
||||
// Chromium won't let a web page navigate to file://, chrome:// or
|
||||
// theseus://, but createTab → loadURL runs from main and would. Web
|
||||
|
|
@ -5647,9 +5655,17 @@ ipcMain.handle("webapp-prompt", async (e) => {
|
|||
ipcMain.handle("webapps-list", () => webapps.list().map((a) => ({ key: a.key, name: a.name, host: a.host, startUrl: a.startUrl, installedAt: a.installedAt })));
|
||||
ipcMain.handle("webapps-open", (_e, key) => { const a = webapps.find(String(key || "")); if (a) webapps.open(a); return !!a; });
|
||||
ipcMain.handle("webapps-remove", (_e, key) => webapps.uninstall(String(key || ""), true).then((ok) => { emitTabs(); return ok; }));
|
||||
// Only the catalog site may ask, and only from its top frame. Any page used
|
||||
// to be able to raise the install sheet without a click (bcnr is in every
|
||||
// page's main world), timed so a double-click landed on Install — and an
|
||||
// installed extension runs in the main process at once.
|
||||
const INSTALL_REQUESTERS = new Set(["theseus.x"]);
|
||||
function installRequesterOk(host) { return !!host && INSTALL_REQUESTERS.has(String(host).toLowerCase()); }
|
||||
ipcMain.handle("bcnr:installExtension", (e, id) => {
|
||||
if (!e.senderFrame || e.senderFrame !== e.sender.mainFrame) return { ok: false, error: "only the top frame may install" };
|
||||
let requester = null;
|
||||
try { requester = new URL(e.sender.getURL()).host || null; } catch {}
|
||||
try { requester = new URL(String(e.sender.getURL()).replace(/^bns:\/\//i, "https://")).host || null; } catch {}
|
||||
if (!installRequesterOk(requester)) return { ok: false, error: "extensions can only be installed from theseus.x" };
|
||||
return installExtensionWithConsent(id, requester);
|
||||
});
|
||||
// Background / manual add-on update check. Whatever gets staged is pushed to
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue