theseus/bundled-addons/consent/index.js
Local Dev f1b1de5a9e feat(theseus): Settings pages have addresses and sub-pages; Privacy regrouped
Navigation base, the way Firefox does about:preferences#privacy: the
address bar follows the Settings page (theseus://settings/privacy) and
the hash mirrors it, so every page has a link; a page can have
sub-pages (theseus://settings/privacy/exceptions) with a breadcrumb and
a back arrow; open-settings and theseus:// links accept the two-level
slug.

Privacy now reads top-down: a "Theseus is on guard" card (Shield and
its running total, cookie pop-ups answered, Tor state, version), then
Tracking protection with the Shield and Cookie Pop-ups cards moved here
from Performance and a Manage exceptions sub-page listing the sites
each add-on was told to leave alone (remove to protect again), then
Device access, Anti-fingerprinting, Network (Tor switch and the VPN
panel) and Browsing data. Performance is about resources again.
2026-09-27 22:01:28 +02:00

107 lines
5.9 KiB
JavaScript

// Cookie Pop-ups — answers cookie consent dialogs automatically.
//
// The work happens in inject.js (DuckDuckGo's autoconsent library, MPL-2.0,
// running in every http(s) frame's isolated world). This side holds the
// user's settings, hands the rule bundle to each frame that asks, and keeps
// count of what was handled so the panel can show it per site.
const fs = require("node:fs");
const path = require("node:path");
let api = null;
let rules = null; // { autoconsent: [], compact: <compact-rules.json> }
let rulesVersion = "";
let enabled = true;
let mode = "optOut"; // "optOut" = reject all but essentials; "optIn" = accept (just make it go away)
let skip = new Set(); // hosts where the add-on stays out
let handledTotal = 0;
let perTab = new Map(); // tabId -> { host, handled, cmp, at, failed }
let unsubTabs = null, emitTimer = null, saveTimer = null;
const hostOf = (u) => { try { return new URL(u).host; } catch { return ""; } };
function loadRules() {
const dir = path.join(api.folder, "rules");
const compact = JSON.parse(fs.readFileSync(path.join(dir, "compact-rules.json"), "utf8"));
try { rulesVersion = fs.readFileSync(path.join(dir, "VERSION"), "utf8").trim(); } catch { rulesVersion = ""; }
rules = { autoconsent: [], compact };
api.log(`rules loaded: ${Array.isArray(compact.r) ? compact.r.length : "?"} CMP rules (autoconsent ${rulesVersion || "?"})`);
}
// The configuration every frame runs with. Mirrors DuckDuckGo's own
// defaults; heuristics catch banners no rule knows yet.
function frameConfig() {
return {
enabled: true, autoAction: mode, disabledCmps: [],
enablePrehide: true, enableCosmeticRules: true, enableGeneratedRules: true,
detectRetries: 20, isMainWorld: false, prehideTimeout: 2000,
enableHeuristicDetection: true, enablePopupMutationObserver: false,
visualTest: false, performanceLoggingEnabled: false,
// Unknown banners: in reject mode only press buttons that read as a
// rejection; in accept mode the library's first tier of accept buttons.
heuristicPopupSearchTimeout: 1000, heuristicMode: mode === "optOut" ? "reject" : "tier1",
logs: { lifecycle: false, rulesteps: false, detectionsteps: false, evals: false, errors: false, messages: false, waits: false },
};
}
function snapshot() {
let tab = null; try { tab = api.tabs.active(); } catch {}
const host = tab ? tab.host : "";
const httpPage = !!tab && /^https?:/i.test(tab.url || "");
const t = tab ? perTab.get(tab.id) : null;
const here = t && t.host === host ? t : null;
return {
enabled, mode, handledTotal, rulesVersion,
site: httpPage && host ? { host, skipped: skip.has(host), handled: here ? here.handled : 0, cmp: here ? here.cmp : "", failed: here ? here.failed : 0 } : null,
};
}
function pushState() { try { api.emit("state", snapshot()); } catch {} }
function pushStateSoon() { if (emitTimer) return; emitTimer = setTimeout(() => { emitTimer = null; pushState(); }, 200); }
function scheduleSave() { if (saveTimer) return; saveTimer = setTimeout(() => { saveTimer = null; api.storage.set("handledTotal", handledTotal); }, 2000); }
async function activate(a) {
api = a;
enabled = api.storage.get("enabled", true) !== false;
mode = api.storage.get("mode", "optOut") === "optIn" ? "optIn" : "optOut";
skip = new Set((api.storage.get("skip", []) || []).map(String));
handledTotal = Number(api.storage.get("handledTotal", 0)) || 0;
try { loadRules(); } catch (err) { api.log("rules unavailable:", err?.message); rules = null; }
api.registerSidebarPanel({ id: "main", title: "Cookie Pop-ups", page: "panel.html" });
// From frames: the library asking whether (and how) to run here.
api.onMessage("consent.init", (p, ctx) => {
if (!ctx || ctx.from !== "page" || !enabled || !rules) return { enabled: false };
const host = hostOf(ctx.origin);
if (host && skip.has(host)) return { enabled: false };
return { enabled: true, config: frameConfig(), rules };
});
// From frames: what happened. Count a handled pop-up per tab once the
// library reports it is done; a failed opt-out is remembered for the panel.
api.onMessage("consent.event", (p, ctx) => {
if (!ctx || ctx.from !== "page" || !p) return false;
const host = hostOf(ctx.origin);
let t = perTab.get(ctx.tabId);
if (!t || t.host !== host) { t = { host, handled: 0, cmp: "", at: 0, failed: 0 }; perTab.set(ctx.tabId, t); }
if (p.type === "autoconsentDone") { t.handled++; t.cmp = String(p.cmp || ""); t.at = Date.now(); handledTotal++; scheduleSave(); pushStateSoon(); }
else if ((p.type === "optOutResult" || p.type === "optInResult") && p.result === false) { t.failed++; t.cmp = String(p.cmp || ""); pushStateSoon(); }
else if (p.type === "autoconsentError") { t.failed++; pushStateSoon(); }
return true;
});
// From the panel.
api.onMessage("state", () => snapshot());
api.onMessage("open-settings", () => { api.openSettings("privacy"); return true; });
api.onMessage("list-exceptions", () => [...skip].sort());
api.onMessage("set-enabled", (p) => { enabled = !!(p && p.enabled); api.storage.set("enabled", enabled); pushState(); return snapshot(); });
api.onMessage("set-mode", (p) => { mode = p && p.mode === "optIn" ? "optIn" : "optOut"; api.storage.set("mode", mode); pushState(); return snapshot(); });
api.onMessage("skip-site", (p) => {
const host = String((p && p.host) || "").toLowerCase(); if (!host) return snapshot();
if (p.skipped) skip.add(host); else skip.delete(host);
api.storage.set("skip", [...skip]); pushState(); return snapshot();
});
unsubTabs = api.tabs.onChange(() => pushState());
api.log(`active — ${enabled ? "on" : "off"}, mode ${mode}, ${skip.size} site(s) excluded`);
}
function deactivate() {
try { unsubTabs && unsubTabs(); } catch {}
clearTimeout(emitTimer); if (saveTimer) { clearTimeout(saveTimer); api.storage.set("handledTotal", handledTotal); }
}
module.exports = { activate, deactivate };