AdBlock Script for WebView

Parse ABP Cosmetic rules to CSS and apply it.

目前為 2022-10-04 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name AdBlock Script for WebView
  3. // @name:zh-CN 套壳油猴的广告拦截脚本
  4. // @author Lemon399
  5. // @version 2.1.1
  6. // @description Parse ABP Cosmetic rules to CSS and apply it.
  7. // @description:zh-CN 将 ABP 中的元素隐藏规则转换为 CSS 使用
  8. // @require https://greasyfork.org/scripts/452263-extended-css/code/extended-css.js?version=1099366
  9. // @match *://*/*
  10. // @resource jiekouAD https://code.gitlink.org.cn/damengzhu/banad/raw/branch/main/jiekouAD.txt
  11. // @resource abpmerge https://code.gitlink.org.cn/damengzhu/abpmerge/raw/branch/main/abpmerge.txt
  12. // @run-at document-start
  13. // @grant GM_getValue
  14. // @grant GM_deleteValue
  15. // @grant GM_setValue
  16. // @grant unsafeWindow
  17. // @grant GM_registerMenuCommand
  18. // @grant GM_unregisterMenuCommand
  19. // @grant GM_xmlhttpRequest
  20. // @grant GM_getResourceText
  21. // @grant GM_addStyle
  22. // @namespace https://lemon399-bitbucket-io.vercel.app/
  23. // @source https://gitee.com/lemon399/tampermonkey-cli/tree/master/projects/abp_parse
  24. // @connect code.gitlink.org.cn
  25. // @copyright GPL-3.0
  26. // @license GPL-3.0
  27. // @history 2.0.1 兼容 Tampermonkey 4.18,代码兼容改为 ES6
  28. // @history 2.0.2 修复多个 iframe 首次执行重复下载规则,改进清空功能
  29. // @history 2.0.3 继续改进清空功能
  30. // @history 2.1.0 @resource 内置规则,兼容 X 和 Via
  31. // @history 2.1.1 兼容 MDM
  32. // ==/UserScript==
  33.  
  34. (function (tm, ExtendedCss) {
  35. "use strict";
  36.  
  37. function _interopDefaultLegacy(e) {
  38. return e && typeof e === "object" && "default" in e ? e : { default: e };
  39. }
  40.  
  41. var ExtendedCss__default = /*#__PURE__*/ _interopDefaultLegacy(ExtendedCss);
  42.  
  43. function __awaiter(thisArg, _arguments, P, generator) {
  44. function adopt(value) {
  45. return value instanceof P
  46. ? value
  47. : new P(function (resolve) {
  48. resolve(value);
  49. });
  50. }
  51. return new (P || (P = Promise))(function (resolve, reject) {
  52. function fulfilled(value) {
  53. try {
  54. step(generator.next(value));
  55. } catch (e) {
  56. reject(e);
  57. }
  58. }
  59. function rejected(value) {
  60. try {
  61. step(generator["throw"](value));
  62. } catch (e) {
  63. reject(e);
  64. }
  65. }
  66. function step(result) {
  67. result.done
  68. ? resolve(result.value)
  69. : adopt(result.value).then(fulfilled, rejected);
  70. }
  71. step((generator = generator.apply(thisArg, _arguments || [])).next());
  72. });
  73. }
  74.  
  75. const onlineRules = [
  76. "https://code.gitlink.org.cn/damengzhu/banad/raw/branch/main/jiekouAD.txt",
  77. "https://code.gitlink.org.cn/damengzhu/abpmerge/raw/branch/main/abpmerge.txt",
  78. ],
  79. defaultRules = `
  80. ! 没有 ## #@# #?# #@?#
  81. ! #$# #@$# #$?# #@$?# 的行和
  82. ! 开头为 ! 的行会忽略
  83. !
  84. ! 由于语法限制,内置规则中
  85. ! 一个反斜杠需要改成两个,像这样 \\
  86. !
  87. ! 若要修改地址,请注意同步修改
  88. ! 头部的 @connect @resource
  89.  
  90. baidu.com##.ec_wise_ad
  91.  
  92. `;
  93.  
  94. const id = "placeholder";
  95.  
  96. function runOnce(fn, key) {
  97. const uniqId = "BEXT_UNIQ_ID_" + id + (key ? key : "");
  98. if (uniqId in window) return;
  99. window[uniqId] = true;
  100. fn === null || fn === void 0 ? void 0 : fn.call(null);
  101. }
  102. function isObj(o) {
  103. return (
  104. typeof o == "object" &&
  105. (o === null || o === void 0 ? void 0 : o.toString()) === "[object Object]"
  106. );
  107. }
  108. function runNeed(condition, fn, option, ...args) {
  109. let ok = false,
  110. sleep = (time) => {
  111. return new Promise((r) => setTimeout(r, time));
  112. },
  113. defaultOption = {
  114. count: 20,
  115. delay: 200,
  116. failFn: () => null,
  117. };
  118. if (isObj(option)) Object.assign(defaultOption, option);
  119. new Promise(async (resolve, reject) => {
  120. for (let c = 0; !ok && c < defaultOption.count; c++) {
  121. await sleep(defaultOption.delay);
  122. ok = condition.call(null, c + 1);
  123. }
  124. ok ? resolve() : reject();
  125. }).then(fn.bind(null, ...args), defaultOption.failFn);
  126. }
  127. `BEXT_LAST_CHECK_KEY_${id}`;
  128.  
  129. function getName(path) {
  130. const reer = /\/([^\/]+)$/.exec(path);
  131. return reer ? reer[1] : null;
  132. }
  133. function getEtag(header) {
  134. const reer = /etag: \"(\w+)\"/.exec(header);
  135. const reerVia = /Etag: \[\"(\w+)\"\]/.exec(header);
  136. return reer ? reer[1] : reerVia ? reerVia[1] : null;
  137. }
  138. function getDay(date) {
  139. const reer = /\/(\d{1,2}) /.exec(date);
  140. return reer ? parseInt(reer[1]) : 0;
  141. }
  142. function makeRuleBox() {
  143. return {
  144. black: [],
  145. white: [],
  146. apply: "",
  147. };
  148. }
  149. function domainChecker(domains) {
  150. const results = [],
  151. hasTLD = /\.+?[\w-]+$/,
  152. urlSuffix = hasTLD.exec(location.hostname);
  153. let invert = false,
  154. result = false,
  155. mostMatch = {
  156. long: 0,
  157. result: undefined,
  158. };
  159. domains.forEach((domain) => {
  160. if (domain.endsWith(".*") && Array.isArray(urlSuffix)) {
  161. domain = domain.replace(".*", urlSuffix[0]);
  162. }
  163. if (domain.startsWith("~")) {
  164. invert = true;
  165. domain = domain.slice(1);
  166. } else invert = false;
  167. result = location.hostname.endsWith(domain);
  168. results.push(result !== invert);
  169. if (result) {
  170. if (domain.length > mostMatch.long) {
  171. mostMatch = {
  172. long: domain.length,
  173. result: result !== invert,
  174. };
  175. }
  176. }
  177. });
  178. return mostMatch.long > 0 ? mostMatch.result : results.includes(true);
  179. }
  180. function ruleChecker(matches) {
  181. const index = matches.findIndex((i) => i !== null);
  182. if (
  183. index >= 0 &&
  184. (!matches[index][1] || domainChecker(matches[index][1].split(",")))
  185. ) {
  186. return [index % 2 == 0, Math.floor(index / 2), matches[index].pop()];
  187. }
  188. }
  189. function extraChecker(sel) {
  190. const unsupported = [
  191. ":matches-path(",
  192. ":min-text-length(",
  193. ":watch-attr(",
  194. ":style(",
  195. ];
  196. let pass = true;
  197. unsupported.forEach((cls) => {
  198. if (sel.indexOf(cls) >= 0) pass = false;
  199. });
  200. return pass;
  201. }
  202. function ruleSpliter(rule) {
  203. const result = ruleChecker([
  204. rule.match(
  205. /^(~?[\w-]+\.([\w-]+|\*)(,~?[\w-]+\.([\w-]+|\*))*)?##([^\s^+].*)/
  206. ),
  207. rule.match(
  208. /^(~?[\w-]+\.([\w-]+|\*)(,~?[\w-]+\.([\w-]+|\*))*)?#@#([^\s^+].*)/
  209. ),
  210. rule.match(
  211. /^(~?[\w-]+\.([\w-]+|\*)(,~?[\w-]+\.([\w-]+|\*))*)?#\?#([^\s^+].*)/
  212. ),
  213. rule.match(
  214. /^(~?[\w-]+\.([\w-]+|\*)(,~?[\w-]+\.([\w-]+|\*))*)?#@\?#([^\s^+].*)/
  215. ),
  216. rule.match(
  217. /^(~?[\w-]+\.([\w-]+|\*)(,~?[\w-]+\.([\w-]+|\*))*)?#\$#([^\s^+].*)/
  218. ),
  219. rule.match(
  220. /^(~?[\w-]+\.([\w-]+|\*)(,~?[\w-]+\.([\w-]+|\*))*)?#@\$#([^\s^+].*)/
  221. ),
  222. rule.match(
  223. /^(~?[\w-]+\.([\w-]+|\*)(,~?[\w-]+\.([\w-]+|\*))*)?#\$\?#([^\s^+].*)/
  224. ),
  225. rule.match(
  226. /^(~?[\w-]+\.([\w-]+|\*)(,~?[\w-]+\.([\w-]+|\*))*)?#@\$\?#([^\s^+].*)/
  227. ),
  228. ]);
  229. if (result && result[2] && extraChecker(result[2])) {
  230. return {
  231. black: result[0],
  232. type: result[1],
  233. sel: result[2],
  234. };
  235. }
  236. }
  237.  
  238. const selectors = makeRuleBox(),
  239. extSelectors = makeRuleBox(),
  240. styles = makeRuleBox(),
  241. extStyles = makeRuleBox(),
  242. values = {
  243. get black() {
  244. const v = tm.GM_getValue("ajs_disabled_domains", "");
  245. return typeof v == "string" ? v : "";
  246. },
  247. set black(v) {
  248. v === null
  249. ? tm.GM_deleteValue("ajs_disabled_domains")
  250. : tm.GM_setValue("ajs_disabled_domains", v);
  251. },
  252. get rules() {
  253. const v = tm.GM_getValue("ajs_saved_abprules", "{}");
  254. return typeof v == "string" ? JSON.parse(v) : {};
  255. },
  256. set rules(v) {
  257. v === null
  258. ? tm.GM_deleteValue("ajs_saved_abprules")
  259. : tm.GM_setValue("ajs_saved_abprules", JSON.stringify(v));
  260. },
  261. get time() {
  262. const v = tm.GM_getValue("ajs_rules_ver", "0/0/0 0:0:0");
  263. return typeof v == "string" ? v : "0/0/0 0:0:0";
  264. },
  265. set time(v) {
  266. v === null
  267. ? tm.GM_deleteValue("ajs_rules_ver")
  268. : tm.GM_setValue("ajs_rules_ver", v);
  269. },
  270. get etags() {
  271. const v = tm.GM_getValue("ajs_rules_etags", "{}");
  272. return typeof v == "string" ? JSON.parse(v) : {};
  273. },
  274. set etags(v) {
  275. v === null
  276. ? tm.GM_deleteValue("ajs_rules_etags")
  277. : tm.GM_setValue("ajs_rules_etags", JSON.stringify(v));
  278. },
  279. },
  280. data = {
  281. disabled: false,
  282. updating: false,
  283. receivedRules: "",
  284. allRules: "",
  285. genericStyle: document.createElement("style"),
  286. presetCss:
  287. " {display: none !important;width: 0 !important;height: 0 !important;} ",
  288. supportedCount: 0,
  289. appliedCount: 0,
  290. isFrame: unsafeWindow.self !== unsafeWindow.top,
  291. isClean: false,
  292. mutex: "__lemon__abp__parser__$__",
  293. debug: false,
  294. },
  295. menus = {
  296. disable: {
  297. id: undefined,
  298. get text() {
  299. return data.disabled ? "在此网站启用拦截" : "在此网站禁用拦截";
  300. },
  301. },
  302. update: {
  303. id: undefined,
  304. get text() {
  305. const time = values.time;
  306. return data.updating
  307. ? "正在更新..."
  308. : `点击更新: ${time.slice(0, 1) === "0" ? "未知时间" : time}`;
  309. },
  310. },
  311. count: {
  312. id: undefined,
  313. get text() {
  314. return data.isClean
  315. ? "已清空,点击刷新重新加载规则"
  316. : `点击清空: ${data.appliedCount} / ${data.supportedCount} / ${
  317. data.allRules.split("\n").length
  318. }`;
  319. },
  320. },
  321. };
  322. function gmMenu(name, cb) {
  323. if (
  324. typeof tm.GM_registerMenuCommand !== "function" ||
  325. typeof tm.GM_unregisterMenuCommand !== "function" ||
  326. data.isFrame
  327. )
  328. return false;
  329. const id = menus[name].id;
  330. if (typeof id !== "undefined") {
  331. tm.GM_unregisterMenuCommand(id);
  332. menus[name].id = undefined;
  333. }
  334. if (typeof cb == "function") {
  335. menus[name].id = tm.GM_registerMenuCommand(menus[name].text, cb);
  336. }
  337. return typeof menus[name].id !== "undefined";
  338. }
  339. function promiseXhr(details) {
  340. let loaded = false;
  341. return new Promise((resolve, reject) => {
  342. tm.GM_xmlhttpRequest(
  343. Object.assign(
  344. {
  345. onload(e) {
  346. loaded = true;
  347. resolve(e);
  348. },
  349. onabort: reject.bind(null, "abort"),
  350. onerror: reject.bind(null, "error"),
  351. ontimeout: reject.bind(null, "timeout"),
  352. onreadystatechange(e) {
  353. // X 浏览器超时中断
  354. if (e.readyState === 4) {
  355. setTimeout(() => {
  356. if (!loaded) reject("X timeout");
  357. }, 300);
  358. }
  359. // Via 浏览器超时中断,不给成功状态,5 秒没反应就放弃...
  360. if (e.readyState === 3) {
  361. setTimeout(() => {
  362. if (!loaded) reject("Via timeout");
  363. }, 5000);
  364. }
  365. },
  366. },
  367. details
  368. )
  369. );
  370. }).catch((r) => {});
  371. }
  372. function storeRule(name, resp) {
  373. const savedRules = values.rules,
  374. savedEtags = values.etags;
  375. if (resp.responseHeaders) {
  376. const etag = getEtag(resp.responseHeaders);
  377. if (etag) {
  378. savedEtags[name] = etag;
  379. values.etags = savedEtags;
  380. }
  381. }
  382. if (resp.responseText) {
  383. savedRules[name] = resp.responseText;
  384. values.rules = savedRules;
  385. }
  386. }
  387. function fetchRuleBody(name, url) {
  388. return __awaiter(this, void 0, void 0, function* () {
  389. const getResp = yield promiseXhr({
  390. method: "GET",
  391. responseType: "text",
  392. url: url,
  393. });
  394. if (getResp) {
  395. storeRule(name, getResp);
  396. return true;
  397. } else return false;
  398. });
  399. }
  400. function fetchRule(url) {
  401. var _a;
  402. const name =
  403. (_a = getName(url)) !== null && _a !== void 0
  404. ? _a
  405. : `${url.length}.${url.slice(-5)}`;
  406. return new Promise((resolve, reject) =>
  407. __awaiter(this, void 0, void 0, function* () {
  408. if (!name) reject();
  409. const headResp = yield promiseXhr({
  410. method: "HEAD",
  411. responseType: "text",
  412. url: url,
  413. });
  414. if (!headResp) {
  415. reject();
  416. } else {
  417. if (headResp.responseText) {
  418. storeRule(name, headResp);
  419. resolve();
  420. } else {
  421. const etag = getEtag(
  422. headResp.responseHeaders
  423. ? headResp.responseHeaders
  424. : headResp.getAllResponseHeaders
  425. ? headResp.getAllResponseHeaders()
  426. : ""
  427. ),
  428. savedEtags = values.etags;
  429. if (etag) {
  430. if (etag !== savedEtags[name]) {
  431. (yield fetchRuleBody(name, url)) ? resolve() : reject();
  432. } else reject();
  433. } else {
  434. (yield fetchRuleBody(name, url)) ? resolve() : reject();
  435. }
  436. }
  437. }
  438. })
  439. );
  440. }
  441. function fetchRules() {
  442. return __awaiter(this, void 0, void 0, function* () {
  443. data.updating = true;
  444. gmMenu("update", () => undefined);
  445. for (const url of onlineRules) yield fetchRule(url).catch((r) => {});
  446. values.time = new Date().toLocaleString("zh-CN");
  447. gmMenu("count", cleanRules);
  448. initRules();
  449. });
  450. }
  451. function performUpdate(force) {
  452. if (force) {
  453. return fetchRules();
  454. } else {
  455. return getDay(values.time) !== new Date().getDate()
  456. ? fetchRules()
  457. : Promise.resolve();
  458. }
  459. }
  460. function switchDisabledStat() {
  461. const disaList = values.black.split(","),
  462. disaResult = disaList.includes(location.hostname);
  463. data.disabled = !disaResult;
  464. if (data.disabled) {
  465. disaList.push(location.hostname);
  466. } else {
  467. disaList.splice(disaList.indexOf(location.hostname), 1);
  468. }
  469. values.black = disaList.join(",");
  470. gmMenu("disable", switchDisabledStat);
  471. }
  472. function checkDisableStat() {
  473. const disaResult = values.black.split(",").includes(location.hostname);
  474. data.disabled = disaResult;
  475. gmMenu("disable", switchDisabledStat);
  476. return disaResult;
  477. }
  478. function initRules() {
  479. const abpRules = values.rules;
  480. if (typeof tm.GM_getResourceText == "function") {
  481. onlineRules.forEach((url) => {
  482. const ruleName = getName(url),
  483. resName = ruleName.split(".")[0],
  484. resRule = tm.GM_getResourceText(resName);
  485. if (resRule && !abpRules[ruleName]) abpRules[ruleName] = resRule;
  486. });
  487. }
  488. const abpKeys = Object.keys(abpRules);
  489. abpKeys.forEach((name) => {
  490. data.receivedRules += "\n" + abpRules[name] + "\n";
  491. });
  492. data.allRules = defaultRules + data.receivedRules;
  493. if (abpKeys.length !== 0) {
  494. data.updating = false;
  495. gmMenu("update", () =>
  496. __awaiter(this, void 0, void 0, function* () {
  497. yield performUpdate(true);
  498. location.reload();
  499. })
  500. );
  501. }
  502. return data.receivedRules.length;
  503. }
  504. function styleApply() {
  505. const css =
  506. styles.apply +
  507. (selectors.apply.length > 0 ? selectors.apply + data.presetCss : ""),
  508. ecss =
  509. extStyles.apply +
  510. (extSelectors.apply.length > 0
  511. ? extSelectors.apply + data.presetCss
  512. : "");
  513. if (css.length > 0) {
  514. if (typeof tm.GM_addStyle == "function") {
  515. tm.GM_addStyle(css);
  516. } else {
  517. runNeed(
  518. () => !!document.documentElement,
  519. () => {
  520. data.genericStyle.textContent = css;
  521. document.documentElement.appendChild(data.genericStyle);
  522. }
  523. );
  524. }
  525. }
  526. if (ecss.length > 0)
  527. new ExtendedCss__default.default({ styleSheet: ecss }).apply();
  528. }
  529. function cleanRules() {
  530. if (confirm("是否清空存储规则 ?")) {
  531. values.rules = {};
  532. values.time = "0/0/0 0:0:0";
  533. values.etags = {};
  534. data.appliedCount = 0;
  535. data.supportedCount = 0;
  536. data.allRules = "";
  537. data.isClean = true;
  538. gmMenu("update");
  539. gmMenu("count", () => location.reload());
  540. }
  541. }
  542. function parseRules() {
  543. [selectors, extSelectors].forEach((obj) => {
  544. obj.black
  545. .filter((v) => !obj.white.includes(v))
  546. .forEach((sel) => {
  547. obj.apply += `${obj.apply.length == 0 ? "" : ","}${sel}`;
  548. data.appliedCount++;
  549. });
  550. });
  551. [styles, extStyles].forEach((obj) => {
  552. obj.black
  553. .filter((v) => !obj.white.includes(v))
  554. .forEach((sel) => {
  555. obj.apply += ` ${sel}`;
  556. data.appliedCount++;
  557. });
  558. });
  559. gmMenu("count", cleanRules);
  560. styleApply();
  561. }
  562. function main() {
  563. return __awaiter(this, void 0, void 0, function* () {
  564. if (checkDisableStat() || (initRules() === 0 && data.isFrame)) return;
  565. if (data.receivedRules.length === 0) yield performUpdate(true);
  566. data.allRules.split("\n").forEach((rule) => {
  567. const ruleObj = ruleSpliter(rule);
  568. let arr = "";
  569. if (typeof ruleObj !== "undefined") {
  570. arr = ruleObj.black ? "black" : "white";
  571. switch (ruleObj.type) {
  572. case 0:
  573. selectors[arr].push(ruleObj.sel);
  574. break;
  575. case 1:
  576. extSelectors[arr].push(ruleObj.sel);
  577. break;
  578. case 2:
  579. styles[arr].push(ruleObj.sel);
  580. break;
  581. case 3:
  582. extStyles[arr].push(ruleObj.sel);
  583. break;
  584. }
  585. data.supportedCount++;
  586. }
  587. });
  588. parseRules();
  589. performUpdate(false);
  590. });
  591. }
  592. runOnce(main, data.mutex);
  593. })(
  594. {
  595. GM_getValue,
  596. GM_deleteValue,
  597. GM_setValue,
  598. unsafeWindow,
  599. GM_registerMenuCommand,
  600. GM_unregisterMenuCommand,
  601. GM_xmlhttpRequest,
  602. GM_getResourceText,
  603. GM_addStyle,
  604. },
  605. ExtendedCss
  606. );