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; } });