diff --git a/main.js b/main.js index 8cfc14a0..2a85c416 100644 --- a/main.js +++ b/main.js @@ -3914,8 +3914,9 @@ function emitPwAvailability() { // Kept intentionally small — the whole autofill affordance is opt-in // (user clicks the chip; nothing runs on page load). // frameRef: the iframe the login was offered in (null = the tab's own page). -async function pwFillIntoActiveTab(entry, frameRef = null) { - const t = activeTab(); if (!t) return false; +// into: the tab (or the quick panel's stand-in) to fill; the active tab if not given. +async function pwFillIntoActiveTab(entry, frameRef = null, into = null) { + const t = into || activeTab(); if (!t) return false; // Re-check at fill time: the page (or frame) may have navigated since the // picker opened. A frame must still be that tab's, still on that site. let target = t.view.webContents; @@ -5061,7 +5062,9 @@ function createWindow() { // Third-party sites only — no preload (home-preload's navigate/cards API // has no business here), and its popups become ordinary tabs instead of // bare Electron windows outside every tab protection. - quickPanel = new WebContentsView(); + // Same preload as a web tab, iframes included, so a login typed in a quick + // link (Twitter, …) gets the password manager's offers like any tab. + quickPanel = new WebContentsView({ webPreferences: { preload: path.join(__dirname, "home-preload.js"), nodeIntegrationInSubFrames: true } }); quickPanel.webContents.setWindowOpenHandler(({ url }) => { if (url && /^(?:https?|bns):/i.test(url)) createTab(url); return { action: "deny" }; @@ -6483,6 +6486,22 @@ ipcMain.handle("addon-msg", async (e, msg, payload) => { // page-inject origins may talk to it, and only through messages the add-on // registered. Origin is "://" (bns:// shown as https://). function tabForSender(sender) { return tabs.find((t) => t.view.webContents === sender) || null; } +// The left sidebar's quick-link panel (Twitter, Telegram, …) is not a tab, +// but logins happen in it, so the password manager and Theseus ID treat it as +// one more tab-like place: this stand-in has the view and an id the tab code +// can carry around. It is never in `tabs`, so the tab strip never sees it. +const QUICK_TAB_ID = "quick-panel"; +const quickTab = { id: QUICK_TAB_ID, quick: true, get view() { return quickPanel; } }; +function pwTabById(id) { + if (id === QUICK_TAB_ID) return quickPanel && activeQuickLinkId ? quickTab : null; + return tabs.find((t) => t.id === id) || null; +} +function pwTabForSender(sender) { + return tabForSender(sender) || (quickPanel && sender === quickPanel.webContents ? quickTab : null); +} +// Approvals are queued per tab and shown only over the active one; one raised +// for the quick panel belongs to no tab, so it shows whatever tab is active. +const approvalTabOf = (tabId) => (tabId === QUICK_TAB_ID ? null : tabId); function pageOriginOf(url) { try { const u = new URL(String(url).replace(/^bns:\/\//i, "https://")); @@ -8164,9 +8183,10 @@ ipcMain.handle("pw-fill-resize", (_e, h) => { ipcMain.handle("pw-fill-pick", async (e, id) => { if (!pwFillPop || e.sender !== pwFillPop.webContents) return { ok: false, err: "picker only" }; showPwFill(false); - const t = activeTab(); - // The offer came from a login field (possibly in an iframe), or from the - // toolbar chip (the tab's own page). + // The offer came from a login field (possibly in an iframe, possibly in the + // quick panel), or from the toolbar chip (the active tab's own page). + const quickT = pwfOffer && pwfOffer.tabId === QUICK_TAB_ID ? pwTabById(QUICK_TAB_ID) : null; + const t = quickT || activeTab(); const offer = pwfOffer && t && pwfOffer.tabId === t.id ? pwfOffer : null; const host = offer ? offer.host : t ? liveHost(t) : ""; let justUnlocked = false; // a PIN / password typed for this very fill also answers the per-login check @@ -8191,7 +8211,7 @@ ipcMain.handle("pw-fill-pick", async (e, id) => { if (!c.ok) return { ok: false, err: "cancelled" }; } const password = await v.resolvePassword(vaultState, id); - return await pwFillIntoActiveTab({ domain: entry.domain, username: entry.username, password }, offer && offer.ref); + return await pwFillIntoActiveTab({ domain: entry.domain, username: entry.username, password }, offer && offer.ref, t); } catch (e) { return { ok: false, err: e?.message || String(e) }; } }); @@ -8210,7 +8230,7 @@ function frameHost(frame) { } catch { return ""; } } function pwSource(e) { - const t = tabForSender(e.sender); + const t = pwTabForSender(e.sender); if (!t || t.settings || t.addonId) return null; const frame = e.senderFrame; if (!frame || frame.detached) return null; @@ -8233,7 +8253,8 @@ ipcMain.on("pw-form", (e, rect) => { try { if (!settings.pwOfferFill) return; const src = pwSource(e); - if (!src || src.t.id !== activeId) return; + // The active tab, or the quick panel while it is open beside it. + if (!src || (src.t.quick ? !activeQuickLinkId : src.t.id !== activeId)) return; const { t, host } = src; if (!fs.existsSync(vaultFile())) return; let matches = [], locked = false; @@ -8305,7 +8326,7 @@ ipcMain.on("pw-capture", (e, data) => { async function pwLoginSettled(tabId, host, ref) { const CHECK = "[...document.querySelectorAll('input[type=password]')].some((e) => e.offsetWidth > 0 && e.offsetHeight > 0)"; for (let i = 0; i < 8; i++) { - const t = tabs.find((x) => x.id === tabId); + const t = pwTabById(tabId); if (!t) return false; let pwField = false; if (ref) { @@ -8324,8 +8345,9 @@ async function pwLoginSettled(tabId, host, ref) { return false; } async function offerSavePassword(tabId, host, username, password, { ref = null, topHost = "" } = {}) { - if (!tabs.some((x) => x.id === tabId)) return; + if (!pwTabById(tabId)) return; if (!(await pwLoginSettled(tabId, host, ref))) return; + const apTab = approvalTabOf(tabId); // A login in an iframe is saved for the iframe's site; say so. const where = ref && topHost && topHost !== host ? `${host} (in a frame on ${topHost})` : host; if (!fs.existsSync(vaultFile())) { @@ -8335,7 +8357,7 @@ async function offerSavePassword(tabId, host, username, password, { ref = null, body: "Theseus can remember this login and fill it next time. Set up the Theseus Vault first: it is encrypted with a master password only you know, and nothing leaves this computer.", origin: where, actions: [{ id: "setup", label: "Set up the vault", primary: true }, { id: "never", label: "Never for this site" }, { id: "cancel", label: "Not now" }], - }, null, tabId); + }, null, apTab); if (pick === "setup") openSettingsTab("passwords"); else if (pick === "never") pwNeverFor(host); return; @@ -8362,7 +8384,7 @@ async function offerSavePassword(tabId, host, username, password, { ref = null, actions: update ? [{ id: "save", label: "Update", primary: true }, { id: "cancel", label: "Not now" }] : [{ id: "save", label: "Save", primary: true }, { id: "never", label: "Never for this site" }, { id: "cancel", label: "Not now" }], - }, null, tabId); + }, null, apTab); if (pick === "never") return pwNeverFor(host); if (!pick.startsWith("save")) return; const confirm = pick.split("+").includes("confirm"); @@ -8489,10 +8511,10 @@ function tidErr(err) { if (code === "error") console.warn("[theseus-id]", err && err.message); return { ok: false, error: { code, message: code === "error" ? "Theseus ID failed" : String(err.message || code) } }; } -// The caller must be the top frame of a web tab; the origin and URL come -// from what that frame has committed, never from the payload. +// The caller must be the top frame of a web tab or of the quick panel; the +// origin and URL come from what that frame has committed, never from the payload. function tidCaller(e) { - const t = tabForSender(e.sender); + const t = pwTabForSender(e.sender); if (!t || t.settings || t.addonId) return { error: { code: "origin-not-listed", message: "this page cannot use Theseus ID" } }; if (e.senderFrame !== e.sender.mainFrame) return { error: { code: "not-top-frame", message: "Theseus ID works only in the top frame" } }; const url = e.sender.getURL(); @@ -8507,7 +8529,7 @@ ipcMain.handle("theseus-id:signIn", async (e, payload) => { const result = await theseusId().signIn({ projectId: p.projectId, nonce: p.nonce, statement: p.statement, requestId: p.requestId, resources: p.resources, expiresIn: p.expiresIn, gesture: !!p.gesture, - origin: c.origin, uri: c.uri, ctx: { tabId: c.t.id }, + origin: c.origin, uri: c.uri, ctx: { tabId: approvalTabOf(c.t.id) }, }); return { ok: true, result }; } catch (err) { return tidErr(err); }