Initial commit
This commit is contained in:
+503
@@ -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; }
|
||||
});
|
||||
@@ -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(() => {});
|
||||
})();
|
||||
Binary file not shown.
@@ -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) {} });
|
||||
})();
|
||||
@@ -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": ["<all_urls>"],
|
||||
"background": { "service_worker": "background.js" },
|
||||
"action": { "default_popup": "popup.html", "default_title": "chrome-adblock" },
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["<all_urls>"],
|
||||
"js": ["engine.js"],
|
||||
"run_at": "document_start",
|
||||
"world": "MAIN",
|
||||
"all_frames": true
|
||||
},
|
||||
{
|
||||
"matches": ["<all_urls>"],
|
||||
"js": ["bridge.js"],
|
||||
"run_at": "document_start",
|
||||
"all_frames": true
|
||||
}
|
||||
]
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body { font: 13px system-ui, sans-serif; width: 260px; margin: 12px; }
|
||||
h1 { font-size: 15px; margin: 0 0 8px; }
|
||||
p { margin: 4px 0; }
|
||||
#site { font-weight: 600; word-break: break-all; }
|
||||
#err { color: #c00; word-break: break-word; }
|
||||
button { margin-top: 8px; width: 100%; padding: 6px; cursor: pointer; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>chrome-adblock</h1>
|
||||
<p id="site"></p>
|
||||
<button id="toggle" hidden></button>
|
||||
<p id="net"></p>
|
||||
<p id="cos"></p>
|
||||
<p id="js"></p>
|
||||
<p id="updated"></p>
|
||||
<p id="err"></p>
|
||||
<button id="update">Update lists now</button>
|
||||
<script src="popup.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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();
|
||||
})();
|
||||
Reference in New Issue
Block a user