106 lines
5.9 KiB
JavaScript
106 lines
5.9 KiB
JavaScript
// Preload for tab webContents that host the built-in home page. All non-
|
|
// settings tabs get this preload since we don't know in advance whether a
|
|
// tab will land on home.html; the corresponding IPC handlers in main.js
|
|
// validate the sender's URL is our own home.html file:// and reject any
|
|
// origin-mismatched call, so a third-party page can inspect the API's
|
|
// SHAPE but can't invoke it against local user data.
|
|
const { contextBridge, ipcRenderer } = require("electron");
|
|
// A click in the page closes the left panel (web app / add-on) unless it is
|
|
// pinned. Main ignores this unless a panel is open and the tab is active.
|
|
window.addEventListener("pointerdown", () => { ipcRenderer.send("tab-pointerdown"); }, true);
|
|
contextBridge.exposeInMainWorld("home", {
|
|
getCards: () => ipcRenderer.invoke("home-cards-get"),
|
|
setCards: (cards) => ipcRenderer.invoke("home-cards-set", cards),
|
|
resetCards: () => ipcRenderer.invoke("home-cards-reset"),
|
|
navigate: (url) => ipcRenderer.invoke("navigate", url),
|
|
// Current default search engine — used to populate the search box
|
|
// placeholder with the actual engine name (e.g. "Search the web with
|
|
// Startpage") instead of hardcoding DuckDuckGo.
|
|
getEngines: () => ipcRenderer.invoke("search-engines"),
|
|
// Main pushes an updated list here after a successful remote refresh
|
|
// (only when the user has not customised locally). home.html re-renders.
|
|
onCards: (cb) => ipcRenderer.on("home-cards", (_e, list) => cb(list)),
|
|
});
|
|
// Same preload also serves the branded error page (error.html). Handlers in
|
|
// main.js sender-check for error.html so a random page seeing the API shape
|
|
// can't drive navigation.
|
|
contextBridge.exposeInMainWorld("errorpage", {
|
|
retry: (url) => ipcRenderer.invoke("error-retry", url),
|
|
goHome: () => ipcRenderer.invoke("error-home"),
|
|
searchFor: (text) => ipcRenderer.invoke("error-search", text),
|
|
registerOnSirius: (host) => ipcRenderer.invoke("error-register", host),
|
|
openExternal: (url) => ipcRenderer.invoke("error-open-external", url),
|
|
});
|
|
|
|
// ---- Password manager hooks -------------------------------------------------
|
|
// Runs in this preload's isolated world on every web page in a tab; nothing
|
|
// is exposed to the page. Two reports go to main, which takes the site from
|
|
// the tab's committed URL, never from here:
|
|
// pw-form a login field got focus -> main may offer saved logins under it
|
|
// pw-capture a form carrying a password was sent -> main may offer to save it
|
|
(() => {
|
|
if (!/^(https?|bns):$/.test(location.protocol)) return;
|
|
const visible = (el) => {
|
|
const r = el.getBoundingClientRect();
|
|
if (r.width < 4 || r.height < 4) return false;
|
|
const cs = getComputedStyle(el);
|
|
return cs.visibility !== "hidden" && cs.display !== "none";
|
|
};
|
|
const TEXTY = /^(text|email|tel|)$/i;
|
|
const passwords = (scope) => [...(scope || document).querySelectorAll("input[type=password]")].filter((el) => !el.disabled && visible(el));
|
|
// The username is the closest text-like field before the password in the
|
|
// same form (or page), which is how almost every login form is laid out.
|
|
function usernameFor(pw) {
|
|
const scope = pw.form || document;
|
|
const inputs = [...scope.querySelectorAll("input")].filter((el) => !el.disabled && visible(el));
|
|
const at = inputs.indexOf(pw);
|
|
for (let i = at - 1; i >= 0; i--) if (TEXTY.test(inputs[i].type || "text")) return inputs[i];
|
|
return null;
|
|
}
|
|
function capture() {
|
|
try {
|
|
const pws = passwords().filter((el) => el.value);
|
|
if (!pws.length) return;
|
|
// Sign-up: password + confirmation (same value). Change-password:
|
|
// current, new[, confirm] -> the new one is second.
|
|
const signup = pws.length >= 2 || /new-password/i.test(pws[0].autocomplete || "");
|
|
const pw = pws.length >= 3 ? pws[1] : pws.length === 2 && pws[0].value !== pws[1].value ? pws[1] : pws[0];
|
|
const user = usernameFor(pws[0]);
|
|
ipcRenderer.send("pw-capture", { username: user ? user.value : "", password: pw.value, signup });
|
|
} catch {}
|
|
}
|
|
document.addEventListener("submit", capture, true);
|
|
// Script-driven logins never fire submit: catch the button press and Enter.
|
|
document.addEventListener("click", (e) => {
|
|
const b = e.target instanceof Element ? e.target.closest("button, input[type=submit], input[type=button], [role=button]") : null;
|
|
if (b && passwords().some((el) => el.value)) capture();
|
|
}, true);
|
|
document.addEventListener("keydown", (e) => {
|
|
if (e.key === "Enter" && e.target instanceof HTMLInputElement && (e.target.type === "password" || TEXTY.test(e.target.type))) {
|
|
if (passwords().some((el) => el.value)) capture();
|
|
}
|
|
}, true);
|
|
|
|
// A focused username or password field of a login form (one password
|
|
// field, empty) asks main to show saved logins under it.
|
|
function loginField(el) {
|
|
if (!(el instanceof HTMLInputElement) || el.disabled || el.readOnly) return null;
|
|
if (el.type === "password") return passwords(el.form || document).length === 1 ? el : null;
|
|
if (!TEXTY.test(el.type || "text")) return null;
|
|
const pws = passwords(el.form || document);
|
|
return pws.length === 1 && usernameFor(pws[0]) === el ? el : null;
|
|
}
|
|
function offer(el) {
|
|
if (!el || el.value) return;
|
|
const r = el.getBoundingClientRect();
|
|
ipcRenderer.send("pw-form", { x: r.left, y: r.top, h: r.height });
|
|
}
|
|
document.addEventListener("focusin", (e) => offer(loginField(e.target)), true);
|
|
// Typing means the user is not taking the offer.
|
|
document.addEventListener("input", (e) => { if (loginField(e.target)) ipcRenderer.send("pw-form-dismiss"); }, true);
|
|
document.addEventListener("keydown", (e) => { if (e.key === "Escape") ipcRenderer.send("pw-form-dismiss"); }, true);
|
|
// An autofocused field is already focused when this runs.
|
|
const early = () => offer(loginField(document.activeElement));
|
|
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", early, { once: true });
|
|
else early();
|
|
})();
|