// 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"); 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(); })();