套壳油猴的广告拦截脚本

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

当前为 2022-10-02 提交的版本,查看 最新版本

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