From b4974ad0bc4620b4ff0c33040e66337e1c3794d1 Mon Sep 17 00:00:00 2001 From: Justin Oros Date: Wed, 30 Sep 2026 12:00:20 -0700 Subject: [PATCH] Initial commit --- background.js | 503 +++++++++++++++++++++++++++++++++++++++ bridge.js | 17 ++ chrome-adblock.zip | Bin 0 -> 14946 bytes engine.js | 575 +++++++++++++++++++++++++++++++++++++++++++++ manifest.json | 25 ++ popup.html | 26 ++ popup.js | 44 ++++ 7 files changed, 1190 insertions(+) create mode 100644 background.js create mode 100644 bridge.js create mode 100644 chrome-adblock.zip create mode 100644 engine.js create mode 100644 manifest.json create mode 100644 popup.html create mode 100644 popup.js diff --git a/background.js b/background.js new file mode 100644 index 0000000..50a0a9d --- /dev/null +++ b/background.js @@ -0,0 +1,503 @@ +const LISTS = [ + "https://raw.githubusercontent.com/easylist/easylist/gh-pages/easylist.txt", + "https://raw.githubusercontent.com/easylist/easylist/gh-pages/easyprivacy.txt", + "https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/filters.txt", + "https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/quick-fixes.txt", + "https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/privacy.txt", + "https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/unbreak.txt", + "https://raw.githubusercontent.com/brave/adblock-lists/master/brave-lists/brave-specific.txt" +]; + +const ENV = new Set(["env_chromium", "ext_ublock", "env_mv3"]); + +const TYPE_MAP = { + script: "script", image: "image", img: "image", stylesheet: "stylesheet", css: "stylesheet", + object: "object", "object-subrequest": "object", xmlhttprequest: "xmlhttprequest", xhr: "xmlhttprequest", + subdocument: "sub_frame", frame: "sub_frame", ping: "ping", beacon: "ping", media: "media", + font: "font", websocket: "websocket", other: "other", document: "main_frame", doc: "main_frame" +}; + +const ALL_TYPES = ["main_frame", "sub_frame", "stylesheet", "script", "image", "font", "object", "xmlhttprequest", "ping", "csp_report", "media", "websocket", "webtransport", "webbundle", "other"]; + +const METHODS = new Set(["connect", "delete", "get", "head", "options", "patch", "post", "put", "other"]); + +const ALIAS = { + "abort-on-property-read": "aopr", aopr: "aopr", + "abort-on-property-write": "aopw", aopw: "aopw", + "abort-current-script": "acs", acs: "acs", "abort-current-inline-script": "acs", acis: "acs", + "set-constant": "set", set: "set", + "trusted-set-constant": "trusted-set", "trusted-set": "trusted-set", + "no-setTimeout-if": "nostif", nostif: "nostif", "prevent-setTimeout": "nostif", "setTimeout-defuser": "nostif", + "no-setInterval-if": "nosiif", nosiif: "nosiif", "prevent-setInterval": "nosiif", "setInterval-defuser": "nosiif", + "nano-setTimeout-booster": "nano-stb", "nano-stb": "nano-stb", "adjust-setTimeout": "nano-stb", + "nano-setInterval-booster": "nano-sib", "nano-sib": "nano-sib", "adjust-setInterval": "nano-sib", + "addEventListener-defuser": "aeld", aeld: "aeld", "prevent-addEventListener": "aeld", + "no-window-open-if": "nowoif", nowoif: "nowoif", "window.open-defuser": "nowoif", "prevent-window-open": "nowoif", + "no-fetch-if": "no-fetch-if", "prevent-fetch": "no-fetch-if", + "no-xhr-if": "no-xhr-if", "prevent-xhr": "no-xhr-if", + noeval: "noeval", "silent-noeval": "noeval", + "noeval-if": "noeval-if", "prevent-eval-if": "noeval-if", + "remove-attr": "ra", ra: "ra", + "remove-class": "rc", rc: "rc", + "set-local-storage-item": "set-local-storage-item", + "remove-cookie": "remove-cookie", "cookie-remover": "remove-cookie", + "json-prune": "json-prune", + "json-prune-fetch-response": "json-prune-fetch-response", + "json-prune-xhr-response": "json-prune-xhr-response", + "trusted-replace-fetch-response": "trusted-replace-fetch-response", + "trusted-replace-xhr-response": "trusted-replace-xhr-response" +}; + +const YT_CORE = "playerResponse.adPlacements playerResponse.playerAds playerResponse.adSlots adPlacements playerAds adSlots"; +const YT_DEFAULTS = [ + { t: "json-prune", a: [YT_CORE] }, + { t: "json-prune", a: ["entries.[-].command.reelWatchEndpoint.adClientParams.isAd"] }, + { t: "json-prune-fetch-response", a: [YT_CORE + " [].playerResponse.adPlacements [].playerResponse.adSlots", "", "propsToMatch", "/player?"] }, + { t: "json-prune-xhr-response", a: [YT_CORE, "", "propsToMatch", "/\\/player(?:\\?.+)?$/"] } +]; + +const PROCEDURAL = /:(has-text|-abp-[\w-]+|upward|xpath|matches-[\w-]+|style|remove|remove-attr|remove-class|min-text-length|watch-attr|others|if|if-not|nth-ancestor|contains|properties|spath|shadow)\(/; + +const ASCII = /^[\x21-\x7e]*$/; +const DOMAIN = /^[a-z0-9-]+(\.[a-z0-9-]+)+$/; + +function evalExpr(expr) { + const t = expr.match(/!|&&|\|\||\(|\)|[\w-]+/g) || []; + let i = 0; + const unary = () => { + if (t[i] === "!") { i++; return !unary(); } + if (t[i] === "(") { i++; const v = or(); i++; return v; } + return ENV.has(t[i++]); + }; + const and = () => { let v = unary(); while (t[i] === "&&") { i++; const r = unary(); v = v && r; } return v; }; + const or = () => { let v = and(); while (t[i] === "||") { i++; const r = and(); v = v || r; } return v; }; + return or(); +} + +function splitArgs(s) { + const out = []; + let cur = ""; + let q = null; + for (let i = 0; i < s.length; i++) { + const c = s[i]; + if (c === "\\" && s[i + 1] === ",") { cur += ","; i++; continue; } + if (q) { if (c === q) q = null; cur += c; continue; } + if ((c === "'" || c === '"' || c === "`") && cur.trim() === "") { q = c; cur += c; continue; } + if (c === ",") { out.push(cur); cur = ""; continue; } + cur += c; + } + out.push(cur); + return out.map(a => { + a = a.trim(); + if (a.length >= 2 && "'\"`".includes(a[0]) && a[a.length - 1] === a[0]) a = a.slice(1, -1); + return a; + }); +} + +function parseDomains(s) { + const inc = []; + const exc = []; + for (let d of s.split(",")) { + d = d.trim().toLowerCase(); + if (!d || d.startsWith("/")) continue; + const neg = d.startsWith("~"); + if (neg) d = d.slice(1); + if (!/^[a-z0-9.*-]+$/.test(d)) continue; + (neg ? exc : inc).push(d); + } + return { inc, exc }; +} + +function netRule(line) { + let allow = false; + if (line.startsWith("@@")) { allow = true; line = line.slice(2); } + let pattern = line; + let opts = []; + const di = line.lastIndexOf("$"); + if (di >= 0 && !(line.startsWith("/") && line.endsWith("/"))) { + pattern = line.slice(0, di); + opts = line.slice(di + 1).split(",").map(o => o.trim()).filter(Boolean); + } + if (pattern.length > 2 && pattern.startsWith("/") && pattern.endsWith("/")) return null; + const canon = (allow ? "@@" : "") + pattern + "$" + opts.filter(o => o !== "badfilter").sort().join(","); + const types = []; + const exTypes = []; + const init = []; + const exInit = []; + const req = []; + const exReq = []; + const methods = []; + const exMethods = []; + let domainType = null; + let important = false; + let caseSensitive = false; + let badfilter = false; + let ghide = false; + for (const o of opts) { + const eq = o.indexOf("="); + let name = eq >= 0 ? o.slice(0, eq) : o; + const value = eq >= 0 ? o.slice(eq + 1) : ""; + const neg = name.startsWith("~"); + if (neg) name = name.slice(1); + if (TYPE_MAP[name]) { (neg ? exTypes : types).push(TYPE_MAP[name]); continue; } + switch (name) { + case "all": types.push(...ALL_TYPES); break; + case "third-party": case "3p": case "strict3p": domainType = neg ? "firstParty" : "thirdParty"; break; + case "first-party": case "1p": case "strict1p": domainType = neg ? "thirdParty" : "firstParty"; break; + case "important": important = true; break; + case "match-case": caseSensitive = !neg; break; + case "badfilter": badfilter = true; break; + case "generichide": case "ghide": case "elemhide": case "ehide": + if (!allow) return null; + ghide = true; + break; + case "domain": case "from": case "to": { + const tInc = name === "to" ? req : init; + const tExc = name === "to" ? exReq : exInit; + for (let v of value.split("|")) { + v = v.trim().toLowerCase(); + const vneg = v.startsWith("~"); + if (vneg) v = v.slice(1); + if (!v || !DOMAIN.test(v)) { if (!vneg) return null; continue; } + (vneg ? tExc : tInc).push(v); + } + break; + } + case "method": + for (let v of value.split("|")) { + v = v.trim().toLowerCase(); + const vneg = v.startsWith("~"); + if (vneg) v = v.slice(1); + if (!METHODS.has(v)) return null; + (vneg ? exMethods : methods).push(v); + } + break; + default: + return null; + } + } + if (badfilter) return { kind: "badfilter", canon }; + if (ghide) { + const m = /^\|\|([a-z0-9.-]+)\^?\|?$/.exec(pattern); + return m ? { kind: "ghide", host: m[1] } : null; + } + if (pattern === "*" || pattern === "|" || pattern === "||") pattern = ""; + if (pattern.startsWith("||*")) pattern = pattern.slice(2); + while (pattern.startsWith("*")) pattern = pattern.slice(1); + while (pattern.endsWith("*")) pattern = pattern.slice(0, -1); + if (!ASCII.test(pattern)) return null; + if (/.\|./.test(pattern.replace(/^\|\|?/, "").replace(/\|$/, ""))) return null; + if (!pattern && !init.length && !req.length) return null; + if (pattern && pattern.replace(/[|^*]/g, "").length < 3 && !init.length && !req.length) return null; + let tlist = [...new Set(types)]; + const xlist = [...new Set(exTypes)]; + if (tlist.length && xlist.length) tlist = tlist.filter(t => !xlist.includes(t)); + if (types.length && !tlist.length) return null; + let action = allow ? "allow" : "block"; + if (allow && tlist.includes("main_frame")) { + action = "allowAllRequests"; + tlist = ["main_frame", "sub_frame"]; + } + const condition = {}; + if (pattern) condition.urlFilter = pattern; + if (caseSensitive && pattern) condition.isUrlFilterCaseSensitive = true; + if (tlist.length) condition.resourceTypes = tlist; + else if (xlist.length) condition.excludedResourceTypes = xlist; + if (domainType) condition.domainType = domainType; + if (init.length) condition.initiatorDomains = [...new Set(init)]; + if (exInit.length) condition.excludedInitiatorDomains = [...new Set(exInit)]; + if (req.length) condition.requestDomains = [...new Set(req)]; + if (exReq.length) condition.excludedRequestDomains = [...new Set(exReq)]; + if (methods.length) condition.requestMethods = [...new Set(methods)]; + if (exMethods.length) condition.excludedRequestMethods = [...new Set(exMethods)]; + const priority = action === "block" ? (important ? 3 : 1) : 2; + const keys = Object.keys(condition); + const dm = /^\|\|([a-z0-9-]+(?:\.[a-z0-9-]+)+)\^?$/.exec(pattern); + const mergeable = dm && action !== "allowAllRequests" && keys.every(k => k === "urlFilter" || k === "domainType"); + if (action === "allowAllRequests") { + const hm = /^\|\|([a-z0-9.-]+)\^?\|?$/.exec(pattern); + if (hm && !init.length) return { kind: "dnr", canon, action, priority, condition, docHost: hm[1] }; + } + return { kind: "dnr", canon, action, priority, condition, merge: mergeable ? dm[1] : null }; +} + +function compile(texts) { + const bad = new Set(); + const net = []; + const generic = new Set(); + const genericExcAll = new Set(); + const hosts = new Map(); + const rec = h => { + let r = hosts.get(h); + if (!r) { r = { s: new Set(), x: new Set(), j: new Map(), jx: new Set(), g: false, d: false }; hosts.set(h, r); } + return r; + }; + for (const text of texts) { + const stack = []; + for (const raw of text.split(/\r?\n/)) { + const line = raw.trim(); + if (line.startsWith("!#if ")) { stack.push(evalExpr(line.slice(5))); continue; } + if (line.startsWith("!#else")) { if (stack.length) stack[stack.length - 1] = !stack[stack.length - 1]; continue; } + if (line.startsWith("!#endif")) { stack.pop(); continue; } + if (stack.some(v => !v)) continue; + if (!line || line.startsWith("!") || line.startsWith("[")) continue; + const cm = /^([^#]*)(#@?#)(.+)$/.exec(line); + if (cm) { + const { inc, exc } = parseDomains(cm[1]); + const isExc = cm[2] === "#@#"; + const body = cm[3]; + if (body.startsWith("+js(") && body.endsWith(")")) { + const inner = body.slice(4, -1); + if (isExc) { + if (!inc.length) continue; + let key = ""; + if (inner.trim()) { + const a = splitArgs(inner); + const t = ALIAS[a.shift().replace(/\.js$/, "")]; + if (!t) continue; + key = t + "|" + a.join("|"); + } + for (const d of inc) rec(d).jx.add(key); + continue; + } + if (!inc.length) continue; + const a = splitArgs(inner); + const t = ALIAS[a.shift().replace(/\.js$/, "")]; + if (!t) continue; + const key = t + "|" + a.join("|"); + for (const d of inc) rec(d).j.set(key, { t, a }); + for (const d of exc) rec(d).jx.add(key); + continue; + } + if (/^[#@$%?^]/.test(body) || body.startsWith("^")) continue; + const sel = body.trim(); + if (!sel || PROCEDURAL.test(sel) || sel.includes("{") || sel.includes("}")) continue; + if (isExc) { + if (!inc.length) genericExcAll.add(sel); + else for (const d of inc) rec(d).x.add(sel); + continue; + } + if (!inc.length) generic.add(sel); + else for (const d of inc) rec(d).s.add(sel); + for (const d of exc) rec(d).x.add(sel); + continue; + } + if (/#[@?$%]{1,2}#/.test(line)) continue; + const r = netRule(line); + if (!r) continue; + if (r.kind === "badfilter") { bad.add(r.canon); continue; } + if (r.kind === "ghide") { rec(r.host).g = true; continue; } + net.push(r); + } + } + for (const s of genericExcAll) generic.delete(s); + for (const r of hosts.values()) for (const s of genericExcAll) r.s.delete(s); + const groups = new Map(); + const rest = []; + const seen = new Set(); + for (const r of net) { + if (bad.has(r.canon)) continue; + if (r.docHost) rec(r.docHost).d = true; + if (r.merge) { + const gk = r.action + "|" + r.priority + "|" + (r.condition.domainType || ""); + if (!groups.has(gk)) groups.set(gk, { action: r.action, priority: r.priority, domainType: r.condition.domainType, set: new Set() }); + groups.get(gk).set.add(r.merge); + continue; + } + const sig = r.action + "|" + r.priority + "|" + JSON.stringify(r.condition); + if (seen.has(sig)) continue; + seen.add(sig); + rest.push({ action: { type: r.action }, priority: r.priority, condition: r.condition }); + } + const merged = []; + for (const g of groups.values()) { + const list = [...g.set]; + for (let i = 0; i < list.length; i += 1000) { + const condition = { requestDomains: list.slice(i, i + 1000) }; + if (g.domainType) condition.domainType = g.domainType; + merged.push({ action: { type: g.action }, priority: g.priority, condition }); + } + } + const allowFirst = [...merged, ...rest].sort((a, b) => (a.action.type === "block") - (b.action.type === "block")); + const allows = allowFirst.filter(r => r.action.type !== "block"); + const blocksMerged = merged.filter(r => r.action.type === "block"); + const blocksRest = rest.filter(r => r.action.type === "block"); + const dnr = [...allows, ...blocksMerged, ...blocksRest]; + for (const h of ["youtube.com", "m.youtube.com", "music.youtube.com", "youtube-nocookie.com"]) { + const r = rec(h); + for (const d of YT_DEFAULTS) r.j.set(d.t + "|" + d.a.join("|"), d); + } + const out = {}; + let scriptletCount = 0; + let specificCount = 0; + for (const [h, r] of hosts) { + const o = {}; + if (r.s.size) { o.s = [...r.s]; specificCount += r.s.size; } + if (r.x.size) o.x = [...r.x]; + if (r.j.size) { o.j = [...r.j.values()]; scriptletCount += r.j.size; } + if (r.jx.size) o.jx = [...r.jx]; + if (r.g) o.g = 1; + if (r.d) o.d = 1; + out["c:" + h] = o; + } + return { dnr, generic: [...generic], hosts: out, stats: { generic: generic.size, specific: specificCount, scriptlets: scriptletCount } }; +} + +async function update() { + try { + const texts = await Promise.all(LISTS.map(async u => { + const r = await fetch(u, { cache: "no-store" }); + if (!r.ok) throw new Error(u + " returned " + r.status); + return r.text(); + })); + const c = compile(texts); + const applied = await applyDnr(c.dnr); + const keep = await chrome.storage.local.get("allowlist"); + await chrome.storage.local.clear(); + const meta = { + updated: Date.now(), + error: null, + network: applied, + networkTotal: c.dnr.length, + generic: c.stats.generic, + specific: c.stats.specific, + scriptlets: c.stats.scriptlets + }; + await chrome.storage.local.set({ ...c.hosts, generic: c.generic, meta, allowlist: keep.allowlist || [] }); + genericCache = null; + } catch (e) { + const d = await chrome.storage.local.get("meta"); + await chrome.storage.local.set({ meta: { ...(d.meta || {}), error: String(e), lastAttempt: Date.now() } }); + } +} + +async function addRules(rules) { + if (!rules.length) return 0; + try { + await chrome.declarativeNetRequest.updateDynamicRules({ addRules: rules }); + return rules.length; + } catch (e) { + if (rules.length === 1) return 0; + const m = rules.length >> 1; + return (await addRules(rules.slice(0, m))) + (await addRules(rules.slice(m))); + } +} + +async function applyDnr(list) { + const max = (chrome.declarativeNetRequest.MAX_NUMBER_OF_DYNAMIC_RULES || 30000) - 1; + const existing = await chrome.declarativeNetRequest.getDynamicRules(); + const removeRuleIds = existing.map(r => r.id).filter(id => id !== 1); + if (removeRuleIds.length) await chrome.declarativeNetRequest.updateDynamicRules({ removeRuleIds }); + const rules = list.slice(0, max).map((r, i) => ({ id: i + 2, ...r })); + let added = 0; + for (let i = 0; i < rules.length; i += 5000) added += await addRules(rules.slice(i, i + 5000)); + return added; +} + +async function syncAllowlist(list) { + const rule = { + id: 1, + priority: 100, + action: { type: "allowAllRequests" }, + condition: { requestDomains: list, resourceTypes: ["main_frame", "sub_frame"] } + }; + await chrome.declarativeNetRequest.updateDynamicRules({ removeRuleIds: [1], addRules: list.length ? [rule] : [] }); +} + +let genericCache = null; + +async function genericList() { + if (!genericCache) { + const d = await chrome.storage.local.get("generic"); + const list = d.generic || []; + genericCache = { list, css: list.map(s => s + "{display:none!important}").join("\n") }; + } + return genericCache; +} + +function candidates(host) { + const labels = host.toLowerCase().split("."); + const out = []; + for (let i = 0; i < labels.length - 1; i++) { + const c = labels.slice(i).join("."); + out.push(c); + const p = labels.slice(i); + if (p.length >= 2) out.push(p.slice(0, -1).join(".") + ".*"); + if (p.length >= 3) out.push(p.slice(0, -2).join(".") + ".*"); + } + return [...new Set(out)]; +} + +async function lookup(host, top) { + const cands = candidates(host); + const d = await chrome.storage.local.get([...cands.map(c => "c:" + c), "allowlist"]); + const allow = d.allowlist || []; + if (cands.some(c => allow.includes(c))) return { css: "", scriptlets: [] }; + const recs = cands.map(c => d["c:" + c]).filter(Boolean); + if (recs.some(r => r.d)) return { css: "", scriptlets: [] }; + const exc = new Set(); + const jexc = new Set(); + const spec = new Set(); + const js = new Map(); + let ghide = false; + for (const r of recs) { + if (r.x) r.x.forEach(s => exc.add(s)); + if (r.jx) r.jx.forEach(k => jexc.add(k)); + if (r.s) r.s.forEach(s => spec.add(s)); + if (r.j) r.j.forEach(x => js.set(x.t + "|" + x.a.join("|"), x)); + if (r.g) ghide = true; + } + const parts = []; + for (const s of spec) if (!exc.has(s)) parts.push(s + "{display:none!important}"); + if (top && !ghide) { + const g = await genericList(); + if (exc.size) { + for (const s of g.list) if (!exc.has(s)) parts.push(s + "{display:none!important}"); + } else if (g.css) parts.push(g.css); + } + const scriptlets = jexc.has("") ? [] : [...js].filter(([k]) => !jexc.has(k)).map(([, v]) => v); + return { css: parts.join("\n"), scriptlets }; +} + +async function handlePage(msg, sender) { + if (!msg.host || !sender.tab) return { scriptlets: [] }; + const res = await lookup(msg.host, msg.top); + if (res.css) { + const target = sender.documentId + ? { tabId: sender.tab.id, documentIds: [sender.documentId] } + : { tabId: sender.tab.id, frameIds: [sender.frameId] }; + chrome.scripting.insertCSS({ target, css: res.css, origin: "USER" }).catch(() => {}); + } + return { scriptlets: res.scriptlets }; +} + +async function toggle(host) { + const d = await chrome.storage.local.get("allowlist"); + const set = new Set(d.allowlist || []); + if (set.has(host)) set.delete(host); else set.add(host); + const list = [...set]; + await chrome.storage.local.set({ allowlist: list }); + await syncAllowlist(list); + return set.has(host); +} + +async function maybeUpdate() { + const d = await chrome.storage.local.get("meta"); + if (!d.meta || !d.meta.updated || Date.now() - d.meta.updated > 6 * 3600 * 1000) await update(); +} + +chrome.runtime.onInstalled.addListener(() => { + chrome.alarms.create("update", { periodInMinutes: 360 }); + update(); +}); +chrome.runtime.onStartup.addListener(() => { + chrome.alarms.create("update", { periodInMinutes: 360 }); + maybeUpdate(); +}); +chrome.alarms.onAlarm.addListener(a => { if (a.name === "update") update(); }); +chrome.runtime.onMessage.addListener((msg, sender, reply) => { + if (!msg) return; + if (msg.cmd === "page") { handlePage(msg, sender).then(reply, () => reply({ scriptlets: [] })); return true; } + if (msg.cmd === "update") { update().then(() => reply(true)); return true; } + if (msg.cmd === "toggle") { toggle(msg.host).then(reply); return true; } +}); diff --git a/bridge.js b/bridge.js new file mode 100644 index 0000000..897c4bf --- /dev/null +++ b/bridge.js @@ -0,0 +1,17 @@ +(() => { + const KEY = "__cab"; + const host = location.hostname; + if (!host) return; + chrome.runtime.sendMessage({ cmd: "page", host, top: window === window.top }).then(res => { + if (!res) return; + const s = res.scriptlets && res.scriptlets.length ? JSON.stringify(res.scriptlets) : ""; + let prev = null; + try { prev = localStorage.getItem(KEY); } catch (e) {} + if (s === (prev || "")) return; + try { + if (s) localStorage.setItem(KEY, s); + else localStorage.removeItem(KEY); + } catch (e) {} + if (s) window.dispatchEvent(new CustomEvent(KEY, { detail: s })); + }).catch(() => {}); +})(); diff --git a/chrome-adblock.zip b/chrome-adblock.zip new file mode 100644 index 0000000000000000000000000000000000000000..8296a3a7cf9eb51262d889c113693f129baddb13 GIT binary patch literal 14946 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 literal 0 HcmV?d00001 diff --git a/engine.js b/engine.js new file mode 100644 index 0000000..1b1a162 --- /dev/null +++ b/engine.js @@ -0,0 +1,575 @@ +(() => { + const KEY = "__cab"; + const MAGIC = "cab" + Math.random().toString(36).slice(2); + const origParse = JSON.parse; + const applied = new Set(); + const hooked = new Set(); + const S = { + jsonPrune: [], fetchPrune: [], xhrPrune: [], fetchReplace: [], xhrReplace: [], + nostif: [], nosiif: [], stb: [], sib: [], aeld: [], nowoif: [], nofetch: [], noxhr: [], noevalif: [] + }; + + const hook = (name, fn) => { + if (hooked.has(name)) return; + hooked.add(name); + try { fn(); } catch (e) {} + }; + + const regexOf = p => { + const m = /^\/(.+)\/([gimsuy]*)$/.exec(p); + return m ? new RegExp(m[1], m[2].replace("g", "")) : null; + }; + + const needle = p => { + if (!p) return () => true; + let neg = false; + if (p[0] === "!") { neg = true; p = p.slice(1); } + const re = regexOf(p); + return s => (re ? re.test(String(s)) : String(s).includes(p)) !== neg; + }; + + const delayMatch = p => { + if (!p) return () => true; + const neg = p[0] === "!"; + const n = parseInt(neg ? p.slice(1) : p, 10); + if (isNaN(n)) return () => true; + return d => (Number(d) === n) !== neg; + }; + + const urlMatcher = p => { + if (!p) return () => true; + if (p.startsWith("url:")) p = p.slice(4); + else if (/^\w+:/.test(p) && !/^https?:/.test(p)) return () => true; + const re = regexOf(p); + return u => (re ? re.test(u) : u.includes(p)); + }; + + const propsMatcher = s => { + const props = []; + for (const tok of s.split(/\s+/).filter(Boolean)) { + const m = /^(\w+):(.*)$/.exec(tok); + if (m && !/^https?$/.test(m[1])) props.push([m[1].toLowerCase(), needle(m[2])]); + else props.push(["url", needle(tok)]); + } + return info => props.every(([k, n]) => info[k] !== undefined && n(info[k])); + }; + + const words = s => (s || "").split(/\s+/).filter(Boolean).map(x => x.split(".")); + + const varargs = a => { + const o = {}; + for (let i = 0; i + 1 < a.length; i += 2) o[a[i]] = a[i + 1]; + return o; + }; + + const toRegex = p => { + const m = /^\/(.+)\/([gimsuy]*)$/.exec(p); + if (m) return new RegExp(m[1], m[2]); + return new RegExp(p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g"); + }; + + const has = (o, segs, i) => { + if (i >= segs.length) return true; + if (o === null || typeof o !== "object") return false; + const s = segs[i]; + if (s === "[]" || s === "[-]") return Object.keys(o).some(k => has(o[k], segs, i + 1)); + if (!(s in o)) return false; + return has(o[s], segs, i + 1); + }; + + const prunePath = (o, segs, i) => { + if (o === null || typeof o !== "object") return false; + const s = segs[i]; + const last = i === segs.length - 1; + if (s === "[]") { + let ch = false; + for (const k of Object.keys(o)) ch = prunePath(o[k], segs, i + 1) || ch; + return ch; + } + if (s === "[-]") { + if (last) return false; + let ch = false; + if (Array.isArray(o)) { + for (let k = o.length - 1; k >= 0; k--) if (has(o[k], segs, i + 1)) { o.splice(k, 1); ch = true; } + } else { + for (const k of Object.keys(o)) if (has(o[k], segs, i + 1)) { delete o[k]; ch = true; } + } + return ch; + } + if (!(s in o)) return false; + if (last) { delete o[s]; return true; } + return prunePath(o[s], segs, i + 1); + }; + + const applyPrune = (obj, r) => { + if (obj === null || typeof obj !== "object") return false; + if (!r.needs.every(n => has(obj, n, 0))) return false; + let ch = false; + for (const p of r.paths) ch = prunePath(obj, p, 0) || ch; + return ch; + }; + + const transform = (txt, url, prune, replace) => { + const pr = prune.filter(r => r.match(url)); + const rp = replace.filter(r => r.match(url)); + if (!pr.length && !rp.length) return txt; + let out = txt; + for (const r of rp) { r.re.lastIndex = 0; out = out.replace(r.re, r.rep); } + if (pr.length) { + try { + const o = origParse(out); + let ch = false; + for (const r of pr) ch = applyPrune(o, r) || ch; + if (ch) out = JSON.stringify(o); + } catch (e) {} + } + return out; + }; + + const SKIP = {}; + const constVal = (raw, trusted) => { + switch (raw) { + case "undefined": return undefined; + case "false": return false; + case "true": return true; + case "null": return null; + case "''": case "emptyStr": case "": return ""; + case "noopFunc": return function () {}; + case "trueFunc": return function () { return true; }; + case "falseFunc": return function () { return false; }; + case "throwFunc": return function () { throw new Error(); }; + case "emptyObj": return {}; + case "emptyArr": return []; + case "noopPromiseResolve": return function () { return Promise.resolve(); }; + case "noopPromiseReject": return function () { return Promise.reject(); }; + case "noopCallbackFunc": return function () { return function () {}; }; + case "yes": case "no": case "on": case "off": return raw; + } + if (/^-?\d+$/.test(raw) && Math.abs(parseInt(raw, 10)) <= 32767) return parseInt(raw, 10); + if (trusted) { try { return origParse(raw); } catch (e) { return raw; } } + return SKIP; + }; + + const isObj = v => v !== null && (typeof v === "object" || typeof v === "function"); + + const onChain = (path, leaf) => { + if (!path) return; + const parts = path.replace(/^window\./, "").split("."); + const walk = (owner, i) => { + const p = parts[i]; + if (i === parts.length - 1) { try { leaf(owner, p); } catch (e) {} return; } + let cur; + try { cur = owner[p]; } catch (e) { return; } + if (isObj(cur)) { walk(cur, i + 1); return; } + const d = Object.getOwnPropertyDescriptor(owner, p); + if (d && d.configurable === false) return; + let v = cur; + try { + Object.defineProperty(owner, p, { + configurable: true, + enumerable: true, + get() { return v; }, + set(nv) { v = nv; if (isObj(nv)) walk(nv, i + 1); } + }); + } catch (e) {} + }; + walk(window, 0); + }; + + const leafSet = (raw, trusted) => { + const value = constVal(raw, trusted); + return (o, p) => { + if (value === SKIP) return; + const d = Object.getOwnPropertyDescriptor(o, p); + if (d && d.configurable === false) return; + if (d && "value" in d) { + const cur = d.value; + if (cur !== undefined && cur !== null && value !== undefined && value !== null && typeof cur !== typeof value) return; + } + Object.defineProperty(o, p, { + configurable: true, + enumerable: d ? d.enumerable : true, + get() { return value; }, + set() {} + }); + }; + }; + + const installErr = () => { + window.addEventListener("error", e => { + if (String(e.message).includes(MAGIC)) { e.preventDefault(); e.stopImmediatePropagation(); } + }, true); + }; + + const leafAopr = (o, p) => { + const d = Object.getOwnPropertyDescriptor(o, p); + if (d && d.configurable === false) return; + let v = d && "value" in d ? d.value : undefined; + Object.defineProperty(o, p, { + configurable: true, + get() { throw new ReferenceError(MAGIC); }, + set(nv) { v = nv; } + }); + }; + + const leafAopw = (o, p) => { + const d = Object.getOwnPropertyDescriptor(o, p); + if (d && d.configurable === false) return; + const v = d && "value" in d ? d.value : undefined; + Object.defineProperty(o, p, { + configurable: true, + get() { return v; }, + set() { throw new ReferenceError(MAGIC); } + }); + }; + + const leafAcs = n => (o, p) => { + const d = Object.getOwnPropertyDescriptor(o, p); + if (d && d.configurable === false) return; + let v = d && "value" in d ? d.value : undefined; + const g = d && d.get; + const s = d && d.set; + const check = () => { + const cs = document.currentScript; + if (!cs || cs.tagName !== "SCRIPT") return; + const text = cs.src ? cs.src + "\n" + cs.textContent : cs.textContent; + if (n(text)) throw new ReferenceError(MAGIC); + }; + Object.defineProperty(o, p, { + configurable: true, + get() { check(); return g ? g.call(this) : v; }, + set(nv) { check(); if (s) s.call(this, nv); else v = nv; } + }); + }; + + const installTimers = () => { + const wrap = (orig, block, boost) => new Proxy(orig, { + apply(t, th, a) { + let s = ""; + try { s = typeof a[0] === "function" ? Function.prototype.toString.call(a[0]) : String(a[0]); } catch (e) {} + for (const r of block) { + if (r.cb(s) && r.dl(a[1])) { a = [function () {}, a[1]]; break; } + } + for (const r of boost) { + if (r.cb(s) && (r.dl === "*" || Number(a[1]) === Number(r.dl))) { a = Array.from(a); a[1] = Number(a[1]) * r.boost; break; } + } + return Reflect.apply(t, th, a); + } + }); + window.setTimeout = wrap(window.setTimeout, S.nostif, S.stb); + window.setInterval = wrap(window.setInterval, S.nosiif, S.sib); + }; + + const installAeld = () => { + const P = EventTarget.prototype; + P.addEventListener = new Proxy(P.addEventListener, { + apply(t, th, a) { + let hs = ""; + try { + const h = a[1]; + hs = typeof h === "function" ? Function.prototype.toString.call(h) : String(h && h.handleEvent); + } catch (e) {} + for (const r of S.aeld) if (r.type(String(a[0])) && r.h(hs)) return; + return Reflect.apply(t, th, a); + } + }); + }; + + const decoy = () => ({ + closed: false, + opener: null, + location: { href: "" }, + document: { write() {}, writeln() {}, open() {}, close() {} }, + close() { this.closed = true; }, + focus() {}, + blur() {}, + postMessage() {}, + addEventListener() {}, + removeEventListener() {} + }); + + const installWo = () => { + window.open = new Proxy(window.open, { + apply(t, th, a) { + const url = String(a[0] === undefined ? "" : a[0]); + for (const n of S.nowoif) if (n(url)) return decoy(); + return Reflect.apply(t, th, a); + } + }); + }; + + const installJson = () => { + JSON.parse = new Proxy(origParse, { + apply(t, th, a) { + const r = Reflect.apply(t, th, a); + try { for (const rule of S.jsonPrune) applyPrune(r, rule); } catch (e) {} + return r; + } + }); + const origJson = Response.prototype.json; + Response.prototype.json = new Proxy(origJson, { + apply(t, th, a) { + return Reflect.apply(t, th, a).then(r => { + try { for (const rule of S.jsonPrune) applyPrune(r, rule); } catch (e) {} + return r; + }); + } + }); + if (/(^|\.)youtube(-nocookie)?\.com$/.test(location.hostname)) { + let yipr = window.ytInitialPlayerResponse; + try { + Object.defineProperty(window, "ytInitialPlayerResponse", { + configurable: true, + get() { return yipr; }, + set(v) { + try { for (const rule of S.jsonPrune) applyPrune(v, rule); } catch (e) {} + yipr = v; + } + }); + } catch (e) {} + } + }; + + const installFetch = () => { + window.fetch = new Proxy(window.fetch, { + apply(t, th, a) { + let url = ""; + let method = "GET"; + try { + if (a[0] instanceof Request) { url = a[0].url; method = a[0].method; } else url = String(a[0]); + if (a[1] && a[1].method) method = String(a[1].method); + } catch (e) {} + const info = { url, method: method.toUpperCase() }; + for (const m of S.nofetch) { + if (m(info)) return Promise.resolve(new Response("", { status: 200, statusText: "OK" })); + } + const p = Reflect.apply(t, th, a); + const any = S.fetchPrune.some(r => r.match(url)) || S.fetchReplace.some(r => r.match(url)); + if (!any) return p; + return p.then(res => res.clone().text().then(txt => { + const out = transform(txt, url, S.fetchPrune, S.fetchReplace); + if (out === txt) return res; + const nr = new Response(out, { status: res.status, statusText: res.statusText, headers: res.headers }); + Object.defineProperties(nr, { + url: { value: res.url }, + redirected: { value: res.redirected }, + type: { value: res.type } + }); + return nr; + }).catch(() => res)); + } + }); + }; + + const installXhr = () => { + const XP = XMLHttpRequest.prototype; + const info = new WeakMap(); + const cache = new WeakMap(); + XP.open = new Proxy(XP.open, { + apply(t, th, a) { + try { info.set(th, { url: String(a[1]), method: String(a[0] || "GET").toUpperCase() }); cache.delete(th); } catch (e) {} + return Reflect.apply(t, th, a); + } + }); + XP.send = new Proxy(XP.send, { + apply(t, th, a) { + const i = info.get(th); + if (i && S.noxhr.some(m => m(i))) { + const props = { readyState: 4, status: 200, statusText: "OK", response: "", responseText: "", responseURL: i.url }; + for (const k in props) Object.defineProperty(th, k, { configurable: true, value: props[k] }); + setTimeout(() => { + for (const ev of ["readystatechange", "load", "loadend"]) { + try { th.dispatchEvent(new Event(ev)); } catch (e) {} + } + }, 1); + return; + } + return Reflect.apply(t, th, a); + } + }); + const dText = Object.getOwnPropertyDescriptor(XP, "responseText"); + const dResp = Object.getOwnPropertyDescriptor(XP, "response"); + const xform = xhr => { + if (xhr.readyState !== 4) return null; + if (cache.has(xhr)) return cache.get(xhr); + let result = null; + try { + const url = (info.get(xhr) || {}).url || ""; + const type = xhr.responseType; + if (type === "" || type === "text" || type === "json") { + const raw = dResp.get.call(xhr); + const txt = type === "json" ? JSON.stringify(raw) : raw; + if (typeof txt === "string") { + const out = transform(txt, url, S.xhrPrune, S.xhrReplace); + if (out !== txt) result = { v: type === "json" ? origParse(out) : out }; + } + } + } catch (e) {} + cache.set(xhr, result); + return result; + }; + Object.defineProperty(XP, "response", { + configurable: true, + enumerable: dResp.enumerable, + get() { const r = xform(this); return r ? r.v : dResp.get.call(this); } + }); + Object.defineProperty(XP, "responseText", { + configurable: true, + enumerable: dText.enumerable, + get() { const r = xform(this); return r && typeof r.v === "string" ? r.v : dText.get.call(this); } + }); + }; + + const onReady = fn => { + if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", fn, { once: true }); + else fn(); + }; + + const domRemove = (a, kind) => { + const tokens = (a[0] || "").split(/\s*\|\s*/).filter(Boolean); + if (!tokens.length) return; + const selector = a[1] || tokens.map(t => (kind === "attr" ? "[" + t + "]" : "." + t)).join(","); + const run = () => { + try { + document.querySelectorAll(selector).forEach(el => { + for (const t of tokens) { + if (kind === "attr") el.removeAttribute(t); + else el.classList.remove(t); + } + }); + } catch (e) {} + }; + onReady(run); + window.addEventListener("load", run, { once: true }); + if ((a[2] || "").includes("stay")) { + let pending = false; + const mo = new MutationObserver(() => { + if (pending) return; + pending = true; + requestAnimationFrame(() => { pending = false; run(); }); + }); + onReady(() => mo.observe(document.documentElement, { childList: true, subtree: true, attributes: true })); + } + }; + + const LS_OK = new Set(["undefined", "false", "true", "null", "yes", "no", "on", "off", "accept", "accepted", "reject", "rejected", "allow", "allowed", "deny", "denied"]); + const lsItem = a => { + const key = a[0]; + let raw = a[1] || ""; + if (!key) return; + try { + if (raw === "$remove$") { + const re = regexOf(key); + if (re) { for (const k of Object.keys(localStorage)) if (re.test(k)) localStorage.removeItem(k); } + else localStorage.removeItem(key); + return; + } + if (raw === "emptyObj") raw = "{}"; + else if (raw === "emptyArr") raw = "[]"; + else if (raw === "''" || raw === "emptyStr") raw = ""; + else if (!LS_OK.has(raw) && !(/^\d+$/.test(raw) && parseInt(raw, 10) <= 32767)) return; + localStorage.setItem(key, raw); + } catch (e) {} + }; + + const cookieRemove = a => { + const n = needle(a[0] || ""); + const rm = () => { + for (const c of document.cookie.split(";")) { + const name = c.split("=")[0].trim(); + if (!name || !n(name)) continue; + const exp = "=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/"; + const h = location.hostname; + document.cookie = name + exp; + document.cookie = name + exp + "; domain=" + h; + document.cookie = name + exp + "; domain=." + h.split(".").slice(-2).join("."); + } + }; + rm(); + window.addEventListener("beforeunload", rm); + onReady(rm); + }; + + const H = { + "json-prune": a => { + const paths = words(a[0]); + if (!paths.length) return; + S.jsonPrune.push({ paths, needs: words(a[1]) }); + hook("json", installJson); + }, + "json-prune-fetch-response": a => { + const paths = words(a[0]); + if (!paths.length) return; + S.fetchPrune.push({ paths, needs: words(a[1]), match: urlMatcher(varargs(a.slice(2)).propsToMatch) }); + hook("fetch", installFetch); + }, + "json-prune-xhr-response": a => { + const paths = words(a[0]); + if (!paths.length) return; + S.xhrPrune.push({ paths, needs: words(a[1]), match: urlMatcher(varargs(a.slice(2)).propsToMatch) }); + hook("xhr", installXhr); + }, + "trusted-replace-fetch-response": a => { + if (!a[0]) return; + S.fetchReplace.push({ re: toRegex(a[0]), rep: a[1] || "", match: urlMatcher(a[2]) }); + hook("fetch", installFetch); + }, + "trusted-replace-xhr-response": a => { + if (!a[0]) return; + S.xhrReplace.push({ re: toRegex(a[0]), rep: a[1] || "", match: urlMatcher(a[2]) }); + hook("xhr", installXhr); + }, + set: a => onChain(a[0], leafSet(a[1] || "undefined", false)), + "trusted-set": a => onChain(a[0], leafSet(a[1] || "undefined", true)), + aopr: a => { hook("err", installErr); onChain(a[0], leafAopr); }, + aopw: a => { hook("err", installErr); onChain(a[0], leafAopw); }, + acs: a => { hook("err", installErr); onChain(a[0], leafAcs(needle(a[1]))); }, + nostif: a => { S.nostif.push({ cb: needle(a[0]), dl: delayMatch(a[1]) }); hook("timers", installTimers); }, + nosiif: a => { S.nosiif.push({ cb: needle(a[0]), dl: delayMatch(a[1]) }); hook("timers", installTimers); }, + "nano-stb": a => { + S.stb.push({ cb: needle(a[0] === "*" ? "" : a[0]), dl: a[1] || "1000", boost: parseFloat(a[2] || "0.05") || 0.05 }); + hook("timers", installTimers); + }, + "nano-sib": a => { + S.sib.push({ cb: needle(a[0] === "*" ? "" : a[0]), dl: a[1] || "1000", boost: parseFloat(a[2] || "0.05") || 0.05 }); + hook("timers", installTimers); + }, + aeld: a => { S.aeld.push({ type: needle(a[0]), h: needle(a[1]) }); hook("aeld", installAeld); }, + nowoif: a => { S.nowoif.push(needle(a[0])); hook("wo", installWo); }, + "no-fetch-if": a => { if (!a[0]) return; S.nofetch.push(propsMatcher(a[0])); hook("fetch", installFetch); }, + "no-xhr-if": a => { if (!a[0]) return; S.noxhr.push(propsMatcher(a[0])); hook("xhr", installXhr); }, + noeval: () => hook("noeval", () => { window.eval = new Proxy(window.eval, { apply() { return undefined; } }); }), + "noeval-if": a => { + S.noevalif.push(needle(a[0])); + hook("evalif", () => { + window.eval = new Proxy(window.eval, { + apply(t, th, x) { + if (S.noevalif.some(n => n(String(x[0])))) return undefined; + return Reflect.apply(t, th, x); + } + }); + }); + }, + ra: a => domRemove(a, "attr"), + rc: a => domRemove(a, "class"), + "set-local-storage-item": lsItem, + "remove-cookie": cookieRemove + }; + + const run = rules => { + if (!Array.isArray(rules)) return; + for (const r of rules) { + if (!r || !H[r.t]) continue; + const k = r.t + "|" + JSON.stringify(r.a || []); + if (applied.has(k)) continue; + applied.add(k); + try { H[r.t](r.a || []); } catch (e) {} + } + }; + + try { + const cached = localStorage.getItem(KEY); + if (cached) run(origParse(cached)); + } catch (e) {} + window.addEventListener(KEY, e => { try { run(origParse(e.detail)); } catch (x) {} }); +})(); diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..3813b0c --- /dev/null +++ b/manifest.json @@ -0,0 +1,25 @@ +{ + "manifest_version": 3, + "name": "chrome-adblock", + "version": "2.0.0", + "description": "Blocks ads and trackers on all sites using EasyList, EasyPrivacy and uBlock Origin lists, auto-updated from GitHub.", + "permissions": ["declarativeNetRequest", "storage", "unlimitedStorage", "alarms", "scripting", "tabs"], + "host_permissions": [""], + "background": { "service_worker": "background.js" }, + "action": { "default_popup": "popup.html", "default_title": "chrome-adblock" }, + "content_scripts": [ + { + "matches": [""], + "js": ["engine.js"], + "run_at": "document_start", + "world": "MAIN", + "all_frames": true + }, + { + "matches": [""], + "js": ["bridge.js"], + "run_at": "document_start", + "all_frames": true + } + ] +} diff --git a/popup.html b/popup.html new file mode 100644 index 0000000..840e256 --- /dev/null +++ b/popup.html @@ -0,0 +1,26 @@ + + + + + + + +

chrome-adblock

+

+ +

+

+

+

+

+ + + + diff --git a/popup.js b/popup.js new file mode 100644 index 0000000..92ffef5 --- /dev/null +++ b/popup.js @@ -0,0 +1,44 @@ +const $ = id => document.getElementById(id); +let tab = null; +let host = ""; + +const render = async () => { + const d = await chrome.storage.local.get(["meta", "allowlist"]); + const m = d.meta || {}; + const allow = d.allowlist || []; + $("net").textContent = "Network rules: " + (m.network || 0) + (m.networkTotal > m.network ? " of " + m.networkTotal : ""); + $("cos").textContent = "Cosmetic: " + (m.generic || 0) + " generic, " + (m.specific || 0) + " site-specific"; + $("js").textContent = "Scriptlets: " + (m.scriptlets || 0); + $("updated").textContent = "Updated: " + (m.updated ? new Date(m.updated).toLocaleString() : "never"); + $("err").textContent = m.error || ""; + if (host) { + const off = allow.includes(host); + $("site").textContent = host + (off ? " (disabled)" : ""); + $("toggle").textContent = off ? "Enable on this site" : "Disable on this site"; + $("toggle").hidden = false; + } +}; + +$("toggle").addEventListener("click", async () => { + await chrome.runtime.sendMessage({ cmd: "toggle", host }); + if (tab) chrome.tabs.reload(tab.id); + await render(); +}); + +$("update").addEventListener("click", async e => { + e.target.disabled = true; + e.target.textContent = "Updating..."; + await chrome.runtime.sendMessage({ cmd: "update" }); + await render(); + e.target.disabled = false; + e.target.textContent = "Update lists now"; +}); + +(async () => { + [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); + try { + const u = new URL(tab.url); + if (/^https?:$/.test(u.protocol)) host = u.hostname.replace(/^www\./, ""); + } catch (e) {} + await render(); +})();