套壳油猴的广告拦截脚本

将 ABP 中的元素隐藏规则转换为 CSS 使用

目前为 2022-10-04 提交的版本。查看 最新版本

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