2026-09-27 19:55:43 +02:00
|
|
|
// 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());
|
2026-09-27 20:37:30 +02:00
|
|
|
api.onMessage("open-settings", () => { api.openSettings("performance"); return true; });
|
2026-09-27 19:55:43 +02:00
|
|
|
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 };
|