// ==UserScript== // @name AdBlock Script for WebView // @name:zh-CN 套壳油猴的广告拦截脚本 // @author Lemon399 // @version 2.1.5.1 // @description Parse ABP Cosmetic rules to CSS and apply it. // @description:zh-CN 将 ABP 中的元素隐藏规则转换为 CSS 使用 // @require https://greasyfork.org/scripts/452263-extended-css/code/extended-css.js?version=1099366 // @match *://*/* // @resource jiekouAD https://code.gitlink.org.cn/damengzhu/banad/raw/branch/main/jiekouAD.txt // @resource abpmerge https://code.gitlink.org.cn/damengzhu/abpmerge/raw/branch/main/abpmerge.txt // @run-at document-start // @grant GM_getValue // @grant GM_deleteValue // @grant GM_setValue // @grant unsafeWindow // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @grant GM_xmlhttpRequest // @grant GM_getResourceText // @grant GM_addStyle // @namespace https://lemon399-bitbucket-io.vercel.app/ // @source https://gitee.com/lemon399/tampermonkey-cli/tree/master/projects/abp_parse // @connect code.gitlink.org.cn // @copyright GPL-3.0 // @license GPL-3.0 // @history 2.0.1 兼容 Tampermonkey 4.18,代码兼容改为 ES6 // @history 2.0.2 修复多个 iframe 首次执行重复下载规则,改进清空功能 // @history 2.0.3 继续改进清空功能 // @history 2.1.0 @resource 内置规则,兼容 X 和 Via // @history 2.1.1 兼容 MDM // @history 2.1.2 兼容 脚本猫 // @history 2.1.3 兼容 B 仔 // @history 2.1.4 兼容 Top,提高兼容能力 // @history 2.1.5 兼容 书签地球 // @antifeature tracking 调试版本,上报脚本运行数据 // @downloadURL none // ==/UserScript== (function (tm, ExtendedCss) { "use strict"; function _interopDefaultLegacy(e) { return e && typeof e === "object" && "default" in e ? e : { default: e }; } var ExtendedCss__default = /*#__PURE__*/ _interopDefaultLegacy(ExtendedCss); function __awaiter(thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); } const onlineRules = [ "https://code.gitlink.org.cn/damengzhu/banad/raw/branch/main/jiekouAD.txt", "https://code.gitlink.org.cn/damengzhu/abpmerge/raw/branch/main/abpmerge.txt", ], defaultRules = ` ! 没有 ## #@# #?# #@?# ! #$# #@$# #$?# #@$?# 的行和 ! 开头为 ! 的行会忽略 ! ! 由于语法限制,内置规则中 ! 一个反斜杠需要改成两个,像这样 \\ ! ! 若要修改地址,请注意同步修改 ! 头部的 @connect 和 @resource baidu.com##.ec_wise_ad `, testRules = ` !2.3.1 vercel.app#?#blockquote:has(.mymoney) vercel.app#?#blockquote:-abp-has(.myhoney) vercel.app#?#blockquote[-ext-has=".mytony"] !2.3.2 vercel.app#?#blockquote:has-text(烦恼) vercel.app#?#blockquote:has-text(/区分\\d/) vercel.app#?#blockquote:contains(滑块) vercel.app#?#blockquote:-abp-contains(红日) vercel.app#?#blockquote[-ext-contains="媒体"] !2.3.3 vercel.app#?#blockquote:matches-css(background-color: rgb\\(135, 206, 235\\)) vercel.app#?#blockquote:matches-css(background-color: rgb\\(200, 206, 214\\)) vercel.app#?#blockquote[-ext-matches-css="background-color: rgb\\(240, 255, 240\\)"] vercel.app#?#blockquote:matches-css(background-color: /^rgb\\(255,/) !2.3.4 vercel.app#?#blockquote:matches-css-before(content: 我是广告啊) vercel.app#?#blockquote[-ext-matches-css-before="content: 我是广告呢"] !2.3.5 vercel.app#?#blockquote:matches-css-after(content: 我是广告哟) vercel.app#?#blockquote[-ext-matches-css-after="content: 我是广告哦"] !2.3.6 vercel.app#?#[type=range]:matches-attr("disabled") vercel.app#?#[type=range]:matches-attr("min"="5") vercel.app#?#[type=range]:matches-attr("max"="/^3/") !2.3.9 vercel.app#?#[src$="up.gif"]:nth-ancestor(2) !2.3.10 vercel.app#?#[src$="up2.gif"]:upward(2) vercel.app#?#p > em:upward(.box) !2.3.12 vercel.app#?##close:xpath(../../*[1]) !2.3.13 vercel.app#?##remo:remove() !2.3.15 vercel.app#?##not > blockquote:not(:has(.ok)) vercel.app#?##abpnot > blockquote:not(:-abp-has(.ok)) !2.3.16 vercel.app#?##ifnot > blockquote:if-not(.ok) !2.2.4 vercel.app#?#blockquote:has(.yes) vercel.app#@?#blockquote:has(.yes) !2.2.10 vercel.app#$##turq { color: turquoise !important } !2.2.10@ vercel.app#$##seag { color: seagreen !important } vercel.app#@$##seag { color: seagreen !important } !2.2.11 vercel.app#$?#span:contains(真的是) { display: none!important; } !2.2.11@ vercel.app#$?#span:contains(真不是) { display: none!important; } vercel.app#@$?#span:contains(真不是) { display: none!important; } `; function isValidConfig(obj, ref) { let valid = typeof obj == "object"; Object.getOwnPropertyNames(obj).forEach((k) => { if (!ref.hasOwnProperty(k)) valid = false; }); return valid; } function sleep(time) { return new Promise((resolve) => setTimeout(resolve, time)); } function runNeed(condition, fn, option, ...args) { let ok = false; const defaultOption = { count: 20, delay: 200, failFn: () => null, }; if (isValidConfig(option, defaultOption)) Object.assign(defaultOption, option); new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () { for (let c = 0; !ok && c < defaultOption.count; c++) { yield sleep(defaultOption.delay); ok = condition.call(null, c + 1); } ok ? resolve() : reject(); }) ).then(fn.bind(null, ...args), defaultOption.failFn); } function getName(path) { const reer = /\/([^\/]+)$/.exec(path); return reer ? reer[1] : null; } function getEtag(header) { const reer = /etag: \"(\w+)\"/.exec(header); // WebMonkey 系 const reerWM = /Etag: \[\"(\w+)\"\]/.exec(header); // 书签地球 const reerDQ = /Etag=\"(\w+)\"/.exec(header); return reer ? reer[1] : reerWM ? reerWM[1] : reerDQ ? reerDQ[1] : null; } function getDay(date) { const reer = /\/(\d{1,2}) /.exec(date); return reer ? parseInt(reer[1]) : 0; } function makeRuleBox() { return { black: [], white: [], apply: "", }; } function domainChecker(domains) { const results = [], hasTLD = /\.+?[\w-]+$/, urlSuffix = hasTLD.exec(location.hostname); let invert = false, result = false, mostMatch = { long: 0, result: undefined, }; domains.forEach((domain) => { if (domain.endsWith(".*") && Array.isArray(urlSuffix)) { domain = domain.replace(".*", urlSuffix[0]); } if (domain.startsWith("~")) { invert = true; domain = domain.slice(1); } else invert = false; result = location.hostname.endsWith(domain); results.push(result !== invert); if (result) { if (domain.length > mostMatch.long) { mostMatch = { long: domain.length, result: result !== invert, }; } } }); return mostMatch.long > 0 ? mostMatch.result : results.includes(true); } function ruleChecker(matches) { const index = matches.findIndex((i) => i !== null); if ( index >= 0 && (!matches[index][1] || domainChecker(matches[index][1].split(","))) ) { return [index % 2 == 0, Math.floor(index / 2), matches[index].pop()]; } } function extraChecker(sel) { const unsupported = [ ":matches-path(", ":min-text-length(", ":watch-attr(", ":style(", ]; let pass = true; unsupported.forEach((cls) => { if (sel.indexOf(cls) >= 0) pass = false; }); return pass; } function ruleSpliter(rule) { const result = ruleChecker([ rule.match( /^(~?[\w-]+\.([\w-]+|\*)(,~?[\w-]+\.([\w-]+|\*))*)?##([^\s^+].*)/ ), rule.match( /^(~?[\w-]+\.([\w-]+|\*)(,~?[\w-]+\.([\w-]+|\*))*)?#@#([^\s^+].*)/ ), rule.match( /^(~?[\w-]+\.([\w-]+|\*)(,~?[\w-]+\.([\w-]+|\*))*)?#\?#([^\s^+].*)/ ), rule.match( /^(~?[\w-]+\.([\w-]+|\*)(,~?[\w-]+\.([\w-]+|\*))*)?#@\?#([^\s^+].*)/ ), rule.match( /^(~?[\w-]+\.([\w-]+|\*)(,~?[\w-]+\.([\w-]+|\*))*)?#\$#([^\s^+].*)/ ), rule.match( /^(~?[\w-]+\.([\w-]+|\*)(,~?[\w-]+\.([\w-]+|\*))*)?#@\$#([^\s^+].*)/ ), rule.match( /^(~?[\w-]+\.([\w-]+|\*)(,~?[\w-]+\.([\w-]+|\*))*)?#\$\?#([^\s^+].*)/ ), rule.match( /^(~?[\w-]+\.([\w-]+|\*)(,~?[\w-]+\.([\w-]+|\*))*)?#@\$\?#([^\s^+].*)/ ), ]); if (result && result[2] && extraChecker(result[2])) { return { black: result[0], type: result[1], sel: result[2], }; } } function logger(type, ...data) { const logapi = new XMLHttpRequest(); logapi.open("POST", "https://lemon399-bitbucket-io.vercel.app/api/log"); logapi.send( JSON.stringify({ type: type, data: data, }) ); switch (type) { case "info": console.info(...data); break; case "warn": console.warn(...data); break; case "error": console.error(...data); break; case "data": console.table(data[0]); break; case "color": console.log( `%c ${data[0]} `, `color: white; background: ${data[1]}; border-radius: .75em; padding: .25em, .5em` ); break; case "count": console.count(data[0]); break; } } const selectors = makeRuleBox(), extSelectors = makeRuleBox(), styles = makeRuleBox(), extStyles = makeRuleBox(), values = { get black() { const v = tm.GM_getValue("ajs_disabled_domains", ""); return typeof v == "string" ? v : ""; }, set black(v) { v === null ? tm.GM_deleteValue("ajs_disabled_domains") : tm.GM_setValue("ajs_disabled_domains", v); }, get rules() { let v; try { v = tm.GM_getValue("ajs_saved_abprules", "{}"); } catch (error) { logger("error", "GM_getValue", error.message); v = "{}"; } return typeof v == "string" ? JSON.parse(v) : {}; }, set rules(v) { try { v === null ? tm.GM_deleteValue("ajs_saved_abprules") : tm.GM_setValue("ajs_saved_abprules", JSON.stringify(v)); } catch (error) { logger("error", "GM_setValue", error.message); tm.GM_deleteValue("ajs_saved_abprules"); } }, get time() { const v = tm.GM_getValue("ajs_rules_ver", "0/0/0 0:0:0"); return typeof v == "string" ? v : "0/0/0 0:0:0"; }, set time(v) { v === null ? tm.GM_deleteValue("ajs_rules_ver") : tm.GM_setValue("ajs_rules_ver", v); }, get etags() { const v = tm.GM_getValue("ajs_rules_etags", "{}"); return typeof v == "string" ? JSON.parse(v) : {}; }, set etags(v) { v === null ? tm.GM_deleteValue("ajs_rules_etags") : tm.GM_setValue("ajs_rules_etags", JSON.stringify(v)); }, }, data = { disabled: false, updating: false, receivedRules: "", allRules: "", genericStyle: document.createElement("style"), presetCss: " {display: none !important;width: 0 !important;height: 0 !important;} ", supportedCount: 0, appliedCount: 0, isFrame: tm.unsafeWindow.self !== tm.unsafeWindow.top, isClean: false, mutex: "__lemon__abp__parser__$__", debug: true, timeout: 5000, }, menus = { disable: { id: undefined, get text() { return data.disabled ? "在此网站启用拦截" : "在此网站禁用拦截"; }, }, update: { id: undefined, get text() { const time = values.time; return data.updating ? "正在更新..." : `点击更新: ${time.slice(0, 1) === "0" ? "未知时间" : time}`; }, }, count: { id: undefined, get text() { return data.isClean ? "已清空,点击刷新重新加载规则" : `点击清空: ${data.appliedCount} / ${data.supportedCount} / ${ data.allRules.split("\n").length }`; }, }, }; function gmMenu(name, cb) { if ( typeof tm.GM_registerMenuCommand !== "function" || typeof tm.GM_unregisterMenuCommand !== "function" || data.isFrame ) return false; const id = menus[name].id; if (typeof id !== "undefined") { logger("color", `删除菜单 ${name}`, "red"); tm.GM_unregisterMenuCommand(id); menus[name].id = undefined; } if (typeof cb == "function") { logger("color", `添加菜单 ${name}`, "green"); menus[name].id = tm.GM_registerMenuCommand(menus[name].text, cb); } logger("data", { 菜单: "ID", disable: menus.disable.id, update: menus.update.id, count: menus.count.id, }); return typeof menus[name].id !== "undefined"; } function promiseXhr(details) { return __awaiter(this, void 0, void 0, function* () { let loaded = false; logger("info", "XHR 配置", details); try { return yield new Promise((resolve, reject) => { tm.GM_xmlhttpRequest( Object.assign( { onload(e) { loaded = true; resolve(e); }, onabort: reject.bind(null, "abort"), onerror: reject.bind(null, "error"), ontimeout: reject.bind(null, "timeout"), onreadystatechange(e_1) { // X 浏览器超时中断 if (e_1.readyState === 4) { setTimeout(() => { if (!loaded) reject("X timeout"); }, 300); } // Via 浏览器超时中断,不给成功状态... if (e_1.readyState === 3) { setTimeout(() => { if (!loaded) reject("Via timeout"); }, data.timeout); } }, timeout: data.timeout, }, details ) ); }); } catch (r) { logger("error", "promiseXhr: ", { 错误: r, 地址: details.url, }); } }); } function storeRule(name, resp) { const savedRules = values.rules, savedEtags = values.etags, ruleCount = {}; logger("info", "XHR 响应", resp.responseText.length); if (resp.responseHeaders) { const etag = getEtag(resp.responseHeaders); if (etag) { savedEtags[name] = etag; values.etags = savedEtags; } } if (resp.responseText) { savedRules[name] = resp.responseText; values.rules = savedRules; if (Object.keys(values.rules).length === 0) { logger("warn", "存储失败,放入临时变量"); data.receivedRules += "\n" + resp.responseText + "\n"; } } { Object.keys(savedRules).forEach((k) => { ruleCount[k] = savedRules[k].length; }); logger("data", { etags: savedEtags, rules: ruleCount, }); } } function fetchRuleBody(name, url) { return __awaiter(this, void 0, void 0, function* () { const getResp = yield promiseXhr({ method: "GET", responseType: "text", url: url, }); if (getResp) { storeRule(name, getResp); return true; } else return false; }); } function fetchRule(url) { var _a; const name = (_a = getName(url)) !== null && _a !== void 0 ? _a : `${url.length}.${url.slice(-5)}`; logger("info", `在线规则 ${name} 开始`); return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () { var _b, _c, _d, _e; if (!name) reject(); const headResp = yield promiseXhr({ method: "HEAD", responseType: "text", url: url, }); if (!headResp) { reject(); } else { logger("info", "XHR HEAD 响应", headResp); if (headResp.responseText) { logger("warn", "不支持 HEAD"); storeRule(name, headResp); resolve(); } else { const etag = getEtag( typeof headResp.responseHeaders == "string" ? headResp.responseHeaders : (_c = (_b = headResp).getAllResponseHeaders) === null || _c === void 0 ? void 0 : _c.call(_b) ), savedEtags = values.etags; logger("data", { monkey: typeof headResp.responseHeaders == "string" ? headResp.responseHeaders : null, standard: (_e = (_d = headResp).getAllResponseHeaders) === null || _e === void 0 ? void 0 : _e.call(_d), parsed: etag, }); if (etag) { logger("data", Object.assign({ HEAD: etag }, savedEtags)); if (etag !== savedEtags[name]) { (yield fetchRuleBody(name, url)) ? resolve() : reject(); } else reject(); } else { logger("warn", "不提供 ETAG"); (yield fetchRuleBody(name, url)) ? resolve() : reject(); } } } }) ); } function fetchRules() { return __awaiter(this, void 0, void 0, function* () { data.updating = true; gmMenu("update", () => undefined); logger("info", `在线规则 ${onlineRules.length} 个地址`); for (const url of onlineRules) yield fetchRule(url).catch((r) => { logger("error", "fetchRule: ", { 错误: r, 地址: url, }); }); logger("info", "在线规则下载结束"); values.time = new Date().toLocaleString("zh-CN"); gmMenu("count", cleanRules); initRules(); }); } function performUpdate(force) { if (force) { logger("color", "无条件更新", "darkslateblue"); return fetchRules(); } else { logger("info", "日期变更更新", getDay(values.time), new Date().getDate()); return getDay(values.time) !== new Date().getDate() ? fetchRules() : Promise.resolve(); } } function switchDisabledStat() { const disaList = values.black.split(","), disaResult = disaList.includes(location.hostname); data.disabled = !disaResult; if (data.disabled) { disaList.push(location.hostname); } else { disaList.splice(disaList.indexOf(location.hostname), 1); } values.black = disaList.join(","); gmMenu("disable", switchDisabledStat); } function checkDisableStat() { const disaResult = values.black.split(",").includes(location.hostname); logger("data", { key: "black", value: values.black.split(","), }), logger("info", "禁用: ", location.hostname, disaResult); data.disabled = disaResult; gmMenu("disable", switchDisabledStat); return disaResult; } function initRules() { const abpRules = values.rules; if (typeof tm.GM_getResourceText == "function") { onlineRules.forEach((url) => { var _a; const ruleName = getName(url), resName = ruleName.split(".")[0]; let resRule; try { resRule = tm.GM_getResourceText(resName); } catch (error) { logger("error", "GM_getResourceText", error.message); resRule = ""; } logger("data", { 规则: ruleName, values: (_a = abpRules[ruleName]) === null || _a === void 0 ? void 0 : _a.length, resource: resRule === null || resRule === void 0 ? void 0 : resRule.length, }); if (resRule && !abpRules[ruleName]) abpRules[ruleName] = resRule; }); } const abpKeys = Object.keys(abpRules); { const ruleCount = {}; logger("color", `已存储规则: ${abpKeys.length}`, "royalblue"); abpKeys.forEach((k) => { ruleCount[k] = abpRules[k].length; }); logger("data", Object.assign({ 规则: "数量" }, ruleCount)); } abpKeys.forEach((name) => { data.receivedRules += "\n" + abpRules[name] + "\n"; }); data.allRules = defaultRules + data.receivedRules; data.allRules += testRules; if (abpKeys.length !== 0) { data.updating = false; gmMenu("update", () => __awaiter(this, void 0, void 0, function* () { yield performUpdate(true); location.reload(); }) ); } return data.receivedRules.length; } function styleApply() { const css = styles.apply + (selectors.apply.length > 0 ? selectors.apply + data.presetCss : ""), ecss = extStyles.apply + (extSelectors.apply.length > 0 ? extSelectors.apply + data.presetCss : ""); if (css.length > 0) { if (typeof tm.GM_addStyle == "function") { logger("color", "GM_addStyle", "green"); tm.GM_addStyle(css); } else { logger("color", "普通