// ==UserScript== // @name 小说漫画网页广告拦截器 // @namespace http://tampermonkey.net/ // @version 4.7.0 // @author DeepSeek&Gemini // @description 一个手机端via浏览器能用的强大的广告拦截器 // @match *://*/* // @license MIT // @grant unsafeWindow // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @grant GM_notification // @grant GM_xmlhttpRequest // @run-at document-start // ==/UserScript== (function() { 'use strict'; let panelOpenCount = 0; let navigationBlockingEnabled = false; let originalLocationAssign = null; let originalLocationReplace = null; let originalLocationReload = null; let originalLocationHrefDescriptor = null; let originalWindowOpen = null; let originalAddEventListener = null; let originalRemoveEventListener = null; let originalFormSubmit = null; let originalClickListeners = new WeakMap(); let isScriptNavigation = false; function setupNavigationBlocking() { if (panelOpenCount === 1 && !navigationBlockingEnabled) { originalLocationAssign = unsafeWindow.Location.prototype.assign; originalLocationReplace = unsafeWindow.Location.prototype.replace; originalLocationReload = unsafeWindow.Location.prototype.reload; originalWindowOpen = unsafeWindow.open; originalFormSubmit = HTMLFormElement.prototype.submit; unsafeWindow.Location.prototype.assign = function(url) { if (isScriptNavigation) { return originalLocationAssign.call(this, url); } return undefined; }; unsafeWindow.Location.prototype.replace = function(url) { if (isScriptNavigation) { return originalLocationReplace.call(this, url); } return undefined; }; unsafeWindow.Location.prototype.reload = function(forceReload) { if (isScriptNavigation) { return originalLocationReload.call(this, forceReload); } return undefined; }; originalLocationHrefDescriptor = Object.getOwnPropertyDescriptor(unsafeWindow.Location.prototype, 'href'); Object.defineProperty(unsafeWindow.Location.prototype, 'href', { set: function(value) { if (isScriptNavigation) { if (originalLocationHrefDescriptor && originalLocationHrefDescriptor.set) { return originalLocationHrefDescriptor.set.call(this, value); } } return; }, get: function() { if (originalLocationHrefDescriptor && originalLocationHrefDescriptor.get) { return originalLocationHrefDescriptor.get.call(this); } return null; } }); unsafeWindow.open = function(url, target, features) { if (isScriptNavigation) { return originalWindowOpen.call(this, url, target, features); } return null; }; HTMLFormElement.prototype.submit = function() { return; }; document.addEventListener('click', navigationClickHandler, {capture: true, passive: false}); document.addEventListener('submit', navigationSubmitHandler, {capture: true, passive: false}); document.addEventListener('keydown', navigationKeydownHandler, {capture: true, passive: false}); document.addEventListener('mousedown', navigationMousedownHandler, {capture: true, passive: false}); document.addEventListener('touchstart', navigationTouchHandler, {capture: true, passive: false}); originalAddEventListener = EventTarget.prototype.addEventListener; EventTarget.prototype.addEventListener = function(type, listener, options) { if (type === 'submit' || type === 'click') { originalClickListeners.set(this, {type, listener, options}); return; } return originalAddEventListener.call(this, type, listener, options); }; originalRemoveEventListener = EventTarget.prototype.removeEventListener; EventTarget.prototype.removeEventListener = function(type, listener, options) { if (type === 'submit' || type === 'click') { originalClickListeners.delete(this); } return originalRemoveEventListener.call(this, type, listener, options); }; navigationBlockingEnabled = true; } } function teardownNavigationBlocking() { if (panelOpenCount === 0 && navigationBlockingEnabled) { if (originalLocationAssign) { unsafeWindow.Location.prototype.assign = originalLocationAssign; } if (originalLocationReplace) { unsafeWindow.Location.prototype.replace = originalLocationReplace; } if (originalLocationReload) { unsafeWindow.Location.prototype.reload = originalLocationReload; } if (originalLocationHrefDescriptor) { Object.defineProperty(unsafeWindow.Location.prototype, 'href', originalLocationHrefDescriptor); } if (originalWindowOpen) { unsafeWindow.open = originalWindowOpen; } if (originalFormSubmit) { HTMLFormElement.prototype.submit = originalFormSubmit; } if (originalAddEventListener) { EventTarget.prototype.addEventListener = originalAddEventListener; } if (originalRemoveEventListener) { EventTarget.prototype.removeEventListener = originalRemoveEventListener; } document.removeEventListener('click', navigationClickHandler, {capture: true}); document.removeEventListener('submit', navigationSubmitHandler, {capture: true}); document.removeEventListener('keydown', navigationKeydownHandler, {capture: true}); document.removeEventListener('mousedown', navigationMousedownHandler, {capture: true}); document.removeEventListener('touchstart', navigationTouchHandler, {capture: true}); navigationBlockingEnabled = false; isScriptNavigation = false; } } function navigationClickHandler(event) { const path = event.composedPath(); const isPanelClick = path.some(el => { return el && el.classList && (el.classList.contains('mask') || el.classList.contains('panel') || el.id === 'ad-blocker-settings-container'); }); if (!isPanelClick) { event.preventDefault(); event.stopPropagation(); event.stopImmediatePropagation(); let target = event.target; while (target && target !== document.documentElement) { if (target.tagName === 'A' && target.href) { target.href = 'javascript:void(0)'; } if (target.tagName === 'BUTTON' && target.type === 'submit') { target.type = 'button'; } if (target.tagName === 'INPUT' && target.type === 'submit') { target.type = 'button'; } if (target.tagName === 'FORM') { target.action = 'javascript:void(0)'; } target = target.parentElement; } } } function navigationMousedownHandler(event) { const path = event.composedPath(); const isPanelClick = path.some(el => { return el && el.classList && (el.classList.contains('mask') || el.classList.contains('panel') || el.id === 'ad-blocker-settings-container'); }); if (!isPanelClick) { event.preventDefault(); event.stopPropagation(); event.stopImmediatePropagation(); } } function navigationTouchHandler(event) { const path = event.composedPath(); const isPanelClick = path.some(el => { return el && el.classList && (el.classList.contains('mask') || el.classList.contains('panel') || el.id === 'ad-blocker-settings-container'); }); if (!isPanelClick) { event.preventDefault(); event.stopPropagation(); event.stopImmediatePropagation(); } } function navigationSubmitHandler(event) { const path = event.composedPath(); const isPanelClick = path.some(el => { return el && el.classList && (el.classList.contains('mask') || el.classList.contains('panel') || el.id === 'ad-blocker-settings-container'); }); if (!isPanelClick) { event.preventDefault(); event.stopPropagation(); event.stopImmediatePropagation(); } } function navigationKeydownHandler(event) { if (event.key === 'Enter' || event.key === ' ') { const path = event.composedPath(); const isPanelClick = path.some(el => { return el && el.classList && (el.classList.contains('mask') || el.classList.contains('panel') || el.id === 'ad-blocker-settings-container'); }); if (!isPanelClick) { event.preventDefault(); event.stopPropagation(); event.stopImmediatePropagation(); } } } const DEFAULT_MODULE_STATE = { smartInterception: false, removeInlineScripts: false, removeExternalScripts: false, interceptThirdParty: false, blockDynamicScripts: false, manageCSP: false, }; const MODULE_NAMES = { smartInterception: '智能拦截', removeInlineScripts: '移除内嵌脚本', removeExternalScripts: '移除外联脚本', interceptThirdParty: '拦截第三方资源', blockDynamicScripts: '拦截动态脚本', manageCSP: 'CSP策略管理', }; const DEFAULT_CSP_RULES_TEMPLATE = [ { id: 1, name: '仅允许同源外部脚本 (禁止内嵌)', rule: "script-src 'self'", enabled: false }, { id: 2, name: '允许同源脚本和内嵌脚本', rule: "script-src 'self' 'unsafe-inline'", enabled: false }, { id: 3, name: '禁止eval和Function执行', rule: "script-src 'unsafe-eval'", enabled: false }, { id: 4, name: '仅允许同源外部样式 (禁止内联)', rule: "style-src 'self'", enabled: false }, { id: 5, name: '允许同源样式和内联样式', rule: "style-src 'self' 'unsafe-inline'", enabled: false }, { id: 6, name: '仅允许同源图片', rule: "img-src 'self'", enabled: false }, { id: 7, name: '禁止所有框架加载', rule: "frame-src 'none'", enabled: false }, { id: 8, name: '禁止所有媒体资源加载', rule: "media-src 'none'", enabled: false }, { id: 9, name: '禁止所有对象嵌入', rule: "object-src 'none'", enabled: false } ]; const CONFIG_STORAGE_KEY_PREFIX = `customAdBlockerConfig_`; const UI_CSS = ` .mask { position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0); backdrop-filter:blur(0px); z-index:2147483647; display:flex; align-items:center; justify-content:center; transition: all 0.3s ease; pointer-events: auto; animation: fade-in 0.3s forwards; } .panel { background:#fff; border-radius:20px; box-shadow:0 10px 30px rgba(0,0,0,0.15); padding:24px 20px; display:flex; flex-direction:column; gap:14px; width:94vw; max-width:500px; font-family:system-ui,-apple-system,sans-serif; box-sizing:border-box; position:relative; transform: scale(0.9); opacity: 0; animation: scale-in 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; max-height:85vh; overflow-y:auto; } .title { margin:0 0 8px 0; font-size:18px; font-weight:700; color:#1a1a1a; text-align:center; word-break:break-all; line-height:1.4; padding:0 10px; } .btn-group { display:grid; grid-template-columns: 1fr 1fr; gap:10px; } button { border:none; border-radius:12px; padding:12px; cursor:pointer; font-size:14px; font-weight:600; transition:all 0.2s; background:#f0f2f5; color:#444; display:flex; align-items:center; justify-content:center; min-height:44px; } button:hover { background:#e4e6e9; transform: translateY(-1px); } button:active { transform:scale(0.95); } button.primary { background:#007AFF; color:#fff; } button.primary:hover { background:#0063cc; box-shadow: 0 4px 12px rgba(0,122,255,0.3); } button.danger { background:#ff4d4f; color:#fff; } button.danger:hover { background:#d9363e; box-shadow: 0 4px 12px rgba(255,77,79,0.3); } textarea { width:100%; height:160px; border:1px solid #ddd; border-radius:12px; padding:12px; font-family:monospace; font-size:13px; resize:none; box-sizing:border-box; outline:none; line-height:1.5; } textarea:focus { border-color:#007AFF; box-shadow:0 0 0 2px rgba(0,122,255,0.1); } select { width:100%; padding:10px; border-radius:12px; border:1px solid #ddd; outline:none; font-size:14px; } .footer { display:flex; flex-direction:column; gap:6px; margin-top:6px; } .module-switch { display:flex; align-items:center; justify-content:space-between; padding:14px; background:#f8f9fa; border-radius:12px; border:1px solid #eee; } .switch-label { font-size:15px; font-weight:600; color:#333; } .switch { position:relative; width:44px; height:26px; } .switch input { opacity:0; width:0; height:0; } .slider { position:absolute; cursor:pointer; top:0; left:0; right:0; bottom:0; background-color:#ccc; transition:.4s; border-radius:26px; } .slider:before { position:absolute; content:""; height:20px; width:20px; left:3px; bottom:3px; background-color:white; transition:.4s; border-radius:50%; } input:checked + .slider { background-color:#007AFF; } input:checked + .slider:before { transform:translateX(18px); } .sub-panel { max-height:50vh; overflow-y:auto; background:#f9f9f9; padding:15px; border-radius:12px; border:1px solid #eee; } .log-entry { margin-bottom:10px; padding:12px; background:#fff; border-radius:8px; border-left:4px solid #007AFF; font-size:13px; position:relative; min-height: 70px; } .log-module { color:#007AFF; font-weight:bold; margin-bottom:4px; font-size:14px; } .log-content { color:#666; word-break:break-word; font-size:12px; max-height:200px; overflow-y:auto; white-space:normal; line-height:1.4; padding-right: 50px; } .whitelist-btn { position:absolute; top:12px; right:12px; background:#34C759; color:#fff; border:none; border-radius:6px; padding:4px 10px; font-size:11px; cursor:pointer; z-index: 1; } .csp-rule { display:flex; align-items:center; justify-content:space-between; padding:12px; background:#fff; border-radius:8px; margin-bottom:8px; } .csp-name { font-size:13px; color:#333; max-width:70%; } .whitelist-item { display:flex; align-items:center; justify-content:space-between; padding:10px; background:#fff; border-radius:8px; margin-bottom:6px; } .whitelist-text { font-size:12px; color:#333; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; max-width:70%; } .panel::-webkit-scrollbar { width:8px; } .panel::-webkit-scrollbar-track { background:#f1f1f1; border-radius:4px; } .panel::-webkit-scrollbar-thumb { background:#c1c1c1; border-radius:4px; } .panel::-webkit-scrollbar-thumb:hover { background:#a8a8a8; } .site-header { font-size:13px; color:#666; text-align:center; padding:8px 12px; background:rgba(0,122,255,0.08); border-radius:10px; margin-bottom:8px; word-break:break-all; } @media (prefers-color-scheme: dark) { .panel { background:#1c1c1e; color:#fff; } .title { color:#fff; } button { background:#2c2c2e; color:#ccc; } button:hover { background:#3a3a3c; } textarea { background:#2c2c2e; border-color:#444; color:#eee; } select { background:#2c2c2e; border-color:#444; color:#eee; } .module-switch { background:#2c2c2e; border-color:#444; } .switch-label { color:#eee; } .sub-panel { background:#2c2c2e; border-color:#444; } .log-entry { background:#1c1c1e; } .csp-rule { background:#1c1c1e; } .whitelist-item { background:#1c1c1e; } .panel::-webkit-scrollbar-track { background:#2c2c2e; } .panel::-webkit-scrollbar-thumb { background:#555; } .panel::-webkit-scrollbar-thumb:hover { background:#666; } .site-header { background:rgba(0,122,255,0.15); color:#8ab4f8; } } @keyframes fade-in { to { background:rgba(0,0,0,0.3); backdrop-filter:blur(8px); } } @keyframes scale-in { to { transform: scale(1); opacity: 1; } }`; let currentConfig = { modules: { ...DEFAULT_MODULE_STATE }, cspRules: DEFAULT_CSP_RULES_TEMPLATE.map(rule => ({ ...rule })), whitelist: {}, keywordWhitelist: {} }; class LRUCache { constructor(capacity = 100, defaultTTL = 0) { this.capacity = capacity; this.defaultTTL = defaultTTL; this.cache = new Map(); } get(key) { if (!this.cache.has(key)) return null; const entry = this.cache.get(key); if (this.defaultTTL > 0 && (Date.now() - entry.timestamp) > this.defaultTTL) { this.cache.delete(key); return null; } this.cache.delete(key); this.cache.set(key, entry); return entry.value; } set(key, value, ttl) { const finalTTL = ttl !== undefined ? ttl : this.defaultTTL; const entry = { value: value, timestamp: Date.now(), ttl: finalTTL }; if (this.cache.has(key)) { this.cache.delete(key); } else if (this.cache.size >= this.capacity) { const firstKey = this.cache.keys().next().value; this.cache.delete(firstKey); } this.cache.set(key, entry); } has(key) { const entry = this.cache.get(key); if (!entry) return false; if (this.defaultTTL > 0 && (Date.now() - entry.timestamp) > this.defaultTTL) { this.cache.delete(key); return false; } return true; } delete(key) { return this.cache.delete(key); } clear() { this.cache.clear(); } get size() { return this.cache.size; } } class URLResolutionCache { constructor() { this.hostnameCache = new LRUCache(1000, 300000); this.domainCache = new LRUCache(1000, 300000); this.absoluteUrlCache = new LRUCache(1000, 300000); this.thirdPartyCache = new LRUCache(1000, 300000); this.whitelistCache = new LRUCache(500, 300000); this.urlCheckCache = new LRUCache(500, 300000); } isDynamicURL(url) { if (!url || typeof url !== 'string') return false; const cacheKey = `dynamic_${url}`; if (this.urlCheckCache.has(cacheKey)) { return this.urlCheckCache.get(cacheKey); } const dynamicPatterns = [ /\?.*[tT]=/, /\?.*timestamp/, /\?.*rand/, /\?.*rnd/, /\?.*[0-9]{13,}/, /\?.*\d{10,}/, /\?.*[a-zA-Z0-9]{32,}/, /\/\d{10,}\./, /\/[0-9a-f]{32,}\./ ]; const result = dynamicPatterns.some(pattern => pattern.test(url)); this.urlCheckCache.set(cacheKey, result, 300000); return result; } getHostname(url) { if (!url || typeof url !== 'string') return null; const cacheKey = `hostname_${url}`; if (this.hostnameCache.has(cacheKey)) { return this.hostnameCache.get(cacheKey); } if (url.startsWith('data:') || url.startsWith('blob:') || url.startsWith('about:blank')) { this.hostnameCache.set(cacheKey, null, 60000); return null; } try { const hostname = new URL(url, location.href).hostname; const ttl = this.isDynamicURL(url) ? 30000 : 300000; this.hostnameCache.set(cacheKey, hostname, ttl); return hostname; } catch (e) { this.hostnameCache.set(cacheKey, null, 30000); return null; } } getDomain(hostname) { if (!hostname) return null; const cacheKey = `domain_${hostname}`; if (this.domainCache.has(cacheKey)) { return this.domainCache.get(cacheKey); } const parts = hostname.split('.'); const domain = parts.length <= 2 ? hostname : parts.slice(-2).join('.'); this.domainCache.set(cacheKey, domain, 300000); return domain; } getAbsoluteURL(url) { if (!url) return ''; const cacheKey = `absolute_${url}_${location.href}`; if (this.absoluteUrlCache.has(cacheKey)) { return this.absoluteUrlCache.get(cacheKey); } try { const absoluteUrl = new URL(url, location.href).href; const ttl = this.isDynamicURL(url) ? 30000 : 300000; this.absoluteUrlCache.set(cacheKey, absoluteUrl, ttl); return absoluteUrl; } catch (e) { this.absoluteUrlCache.set(cacheKey, url, 30000); return url; } } isThirdPartyHost(resourceHostname, currentHost) { if (!resourceHostname) return false; const cacheKey = `thirdparty_${resourceHostname}_${currentHost}`; if (this.thirdPartyCache.has(cacheKey)) { return this.thirdPartyCache.get(cacheKey); } let isThirdParty = false; if (!currentHost || !resourceHostname) { isThirdParty = false; } else { isThirdParty = resourceHostname !== currentHost; } this.thirdPartyCache.set(cacheKey, isThirdParty, 300000); return isThirdParty; } isWhitelisted(url, thirdPartyWhitelist) { if (!url || !thirdPartyWhitelist) return false; const cacheKey = `whitelist_${url}_${location.hostname}`; if (this.whitelistCache.has(cacheKey)) { return this.whitelistCache.get(cacheKey); } let isWhitelisted = false; for (const pattern of thirdPartyWhitelist) { try { if (pattern.includes('://')) { if (url.includes(pattern)) { isWhitelisted = true; break; } } else { const urlHost = new URL(url, location.href).hostname; const patternHost = pattern.startsWith('*.') ? pattern.substring(2) : pattern; if (urlHost.includes(patternHost) || url.includes(patternHost)) { isWhitelisted = true; break; } } } catch (e) { if (url.includes(pattern)) { isWhitelisted = true; break; } } } this.whitelistCache.set(cacheKey, isWhitelisted, 300000); return isWhitelisted; } clear() { this.hostnameCache.clear(); this.domainCache.clear(); this.absoluteUrlCache.clear(); this.thirdPartyCache.clear(); this.whitelistCache.clear(); this.urlCheckCache.clear(); } } const urlCache = new URLResolutionCache(); const Utils = { truncateString(str, maxLength = 200) { if (typeof str !== 'string') return ''; if (str.length <= maxLength) return str; return str.slice(0, maxLength) + '...'; }, getCurrentHostname() { return location.hostname; }, isElement(el) { return el instanceof Element; }, getScriptContentPreview(scriptElement) { if (!scriptElement || scriptElement.tagName !== 'SCRIPT') return ''; const content = scriptElement.textContent; return Utils.truncateString(content, 200); }, getIframeSrcPreview(iframeElement) { if (!iframeElement || iframeElement.tagName !== 'IFRAME') return ''; return Utils.truncateString(iframeElement.src, 200); }, getResourceHostname(url) { return urlCache.getHostname(url); }, getDomain(hostname) { return urlCache.getDomain(hostname); }, isThirdPartyHost(resourceHostname, currentHost) { return urlCache.isThirdPartyHost(resourceHostname, currentHost); }, getAbsoluteURL(url) { return urlCache.getAbsoluteURL(url); }, getContentIdentifier(element, reasonType = null) { if (!element && !reasonType) return null; if (element && Utils.isElement(element)) { const tagName = element.tagName; const src = element.src || element.getAttribute('data-src') || element.href || element.action || ''; if (tagName === 'SCRIPT') { return element.src ? `SCRIPT_SRC: ${Utils.truncateString(element.src, 150)}` : `SCRIPT_CONTENT: ${Utils.getScriptContentPreview(element)}`; } else if (tagName === 'IFRAME') { return `IFRAME_SRC: ${Utils.truncateString(element.src, 150)}`; } else if (tagName === 'IMG') { return src ? `IMG_SRC: ${Utils.truncateString(src, 150)}` : null; } else if (tagName === 'A') { return src ? `A_HREF: ${Utils.truncateString(src, 150)}` : null; } else if (tagName === 'LINK' && element.rel === 'stylesheet' && element.href) { return `CSS_HREF: ${Utils.truncateString(element.href, 150)}`; } else if (['VIDEO', 'AUDIO', 'SOURCE'].includes(tagName) && src) { return `${tagName}_SRC: ${Utils.truncateString(src, 150)}`; } else if (tagName === 'STYLE') { return `STYLE_CONTENT: ${Utils.truncateString(element.textContent, 150)}`; } else if (tagName === 'FORM') { return `FORM_ACTION: ${Utils.truncateString(element.action, 150)}`; } else if (tagName === 'EMBED') { return src ? `EMBED_SRC: ${Utils.truncateString(src, 150)}` : null; } return null; } else if (reasonType && typeof reasonType.detail === 'string') { if (reasonType.detail.startsWith('SRC:')) { return `${reasonType.type || 'INTERCEPTED'}_SRC: ${Utils.truncateString(reasonType.detail.substring(4).trim(), 150)}`; } else if (reasonType.detail.startsWith('URL:')) { return `${reasonType.type || 'INTERCEPTED'}_URL: ${Utils.truncateString(reasonType.detail.substring(5).trim(), 150)}`; } else if (reasonType.type === 'EVAL') { return `EVAL_CODE: ${Utils.truncateString(reasonType.detail, 150)}`; } else if (reasonType.type === 'FUNCTION_CONSTRUCTOR') { return `FUNCTION_CODE: ${Utils.truncateString(reasonType.detail, 150)}`; } else if (reasonType.type === 'SETTIMEOUT') { return `SETTIMEOUT: ${Utils.truncateString(reasonType.detail, 150)}`; } else if (reasonType.type === 'SETINTERVAL') { return `SETINTERVAL: ${Utils.truncateString(reasonType.detail, 150)}`; } else if (reasonType.type === 'STYLE_ADS') { return `STYLE_ADS: ${Utils.truncateString(reasonType.detail, 150)}`; } else if (reasonType.type === 'THIRD_PARTY') { return `THIRD_PARTY: ${Utils.truncateString(reasonType.detail, 150)}`; } else if (reasonType.type === 'DOCUMENT_WRITE') { return `DOCUMENT_WRITE: ${Utils.truncateString(reasonType.detail, 150)}`; } return `LOG_DETAIL: ${Utils.truncateString(reasonType.detail, 150)}`; } return null; }, isParentProcessed(element) { let parent = element.parentElement; while (parent) { if (parent.dataset.adblockProcessed === 'true' || ProcessedElementsCache.isProcessed(parent)) { return true; } parent = parent.parentElement; } return false; }, debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; }, throttle(func, limit) { let inThrottle; return function(...args) { if (!inThrottle) { func.apply(this, args); inThrottle = true; setTimeout(() => inThrottle = false, limit); } }; }, isThirdParty(url) { if (!url) return false; const cacheKey = `isThirdParty_${url}_${location.hostname}`; if (urlCache.urlCheckCache.has(cacheKey)) { return urlCache.urlCheckCache.get(cacheKey); } try { const urlObj = new URL(url, location.href); const hostname = urlObj.hostname; if (!hostname || url.startsWith('data:') || url.startsWith('blob:')) { urlCache.urlCheckCache.set(cacheKey, false, 300000); return false; } const result = this.isThirdPartyHost(hostname, this.getCurrentHostname()); urlCache.urlCheckCache.set(cacheKey, result, 300000); return result; } catch (e) { const result = !url.includes(this.getCurrentHostname()) && url.includes('://') && !url.startsWith('/'); urlCache.urlCheckCache.set(cacheKey, result, 300000); return result; } }, isSameOrigin(hostname) { const currentDomain = this.getCurrentHostname(); return hostname === currentDomain; }, shouldInterceptByModule(element, moduleKey) { if (!currentConfig.modules[moduleKey]) return false; if (ProcessedElementsCache.isProcessed(element) || Utils.isParentProcessed(element)) return false; if (Whitelisting.isElementWhitelisted(element)) return false; return true; } }; const LogManager = { logs: [], maxLogs: 150, logEntryData: new LRUCache(150, 300000), loggedContentIdentifiers: new LRUCache(150, 300000), add(moduleKey, element, reason) { if (!currentConfig.modules.interceptThirdParty && !currentConfig.modules.removeInlineScripts && !currentConfig.modules.removeExternalScripts && !currentConfig.modules.smartInterception && !currentConfig.modules.blockDynamicScripts) { return; } if (!Utils.isElement(element) && element !== null && typeof reason !== 'object') return; const currentDomain = Utils.getCurrentHostname(); if (Whitelisting.isElementWhitelisted(element) || Whitelisting.isReasonWhitelisted(reason)) { return; } let elementIdentifier = '[未知元素]'; let interceptedContent = '[无法获取内容]'; let contentIdentifier = null; let resourceDomain = ''; if (Utils.isElement(element)) { const tagName = element.tagName; const id = element.id ? `#${element.id}` : ''; const className = element.className ? `.${element.className.split(/\s+/).join('.')}` : ''; elementIdentifier = `${tagName}${id}${className}`; contentIdentifier = Utils.getContentIdentifier(element); if (tagName === 'SCRIPT') { if (element.src) { interceptedContent = `外联脚本: ${Utils.truncateString(element.src, 200)}`; resourceDomain = Utils.getResourceHostname(element.src) || ''; } else { interceptedContent = `内嵌脚本: ${Utils.getScriptContentPreview(element)}`; } } else if (tagName === 'IFRAME') { interceptedContent = Utils.getIframeSrcPreview(element); if (element.src) { resourceDomain = Utils.getResourceHostname(element.src) || ''; } } else if (tagName === 'IMG') { const src = element.src || element.dataset.src; interceptedContent = Utils.truncateString(src || '', 200); if (src) { resourceDomain = Utils.getResourceHostname(src) || ''; } } else if (tagName === 'A') { interceptedContent = Utils.truncateString(element.href || '', 200); if (element.href) { resourceDomain = Utils.getResourceHostname(element.href) || ''; } } else if (tagName === 'LINK' && element.rel === 'stylesheet' && element.href) { interceptedContent = Utils.truncateString(element.href || '', 200); if (element.href) { resourceDomain = Utils.getResourceHostname(element.href) || ''; } } else if (['VIDEO', 'AUDIO', 'SOURCE'].includes(tagName)) { interceptedContent = Utils.truncateString(element.src || '', 200); if (element.src) { resourceDomain = Utils.getResourceHostname(element.src) || ''; } } else if (tagName === 'STYLE') { interceptedContent = Utils.truncateString(element.textContent, 200); } else if (tagName === 'EMBED') { interceptedContent = Utils.truncateString(element.src || '', 200); if (element.src) { resourceDomain = Utils.getResourceHostname(element.src) || ''; } } else { interceptedContent = Utils.truncateString(element.outerHTML, 200); } } else if (reason && typeof reason.detail === 'string') { interceptedContent = Utils.truncateString(reason.detail, 200); elementIdentifier = reason.type ? `[${reason.type}]` : '[未知类型]'; contentIdentifier = Utils.getContentIdentifier(null, reason); if (reason.detail.includes('://')) { try { const urlMatch = reason.detail.match(/https?:\/\/[^\s]+/); if (urlMatch) { resourceDomain = Utils.getResourceHostname(urlMatch[0]) || ''; } } catch (e) {} } } if (!contentIdentifier) { contentIdentifier = `${moduleKey}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } if (this.loggedContentIdentifiers.has(contentIdentifier)) { return; } const logId = this.logs.length + 1; const logEntry = { id: logId, module: MODULE_NAMES[moduleKey] || moduleKey, element: elementIdentifier, content: interceptedContent, domain: resourceDomain, timestamp: Date.now(), contentIdentifier: contentIdentifier, elementRef: element, reason: reason }; this.logs.push(logEntry); this.logEntryData.set(logId, { contentIdentifier: contentIdentifier, element: element, reason: reason, domain: resourceDomain }); this.loggedContentIdentifiers.set(contentIdentifier, true); if (this.logs.length > this.maxLogs) { const removedLogEntry = this.logs.shift(); this.logEntryData.delete(removedLogEntry.id); this.loggedContentIdentifiers.delete(removedLogEntry.contentIdentifier); } } }; const Whitelisting = { isElementWhitelisted(element) { if (!element || !Utils.isElement(element)) return false; const currentDomain = Utils.getCurrentHostname(); const contentIdentifier = Utils.getContentIdentifier(element); if (contentIdentifier && currentDomain && currentConfig.whitelist[currentDomain] && currentConfig.whitelist[currentDomain].has(contentIdentifier)) { return true; } if (currentConfig.modules.interceptThirdParty) { const hostname = Utils.getResourceHostname(element.src || element.href || ''); if (hostname && currentDomain) { const thirdPartyWhitelist = StorageManager.getThirdPartyWhitelist(currentDomain); if (urlCache.isWhitelisted(element.src || element.href || '', thirdPartyWhitelist)) { return true; } } } const keywordWhitelist = currentConfig.keywordWhitelist[currentDomain]; if (keywordWhitelist) { const scriptContent = element.textContent || ''; const src = element.src || ''; for (const keyword of keywordWhitelist) { if (keyword && (scriptContent.includes(keyword) || src.includes(keyword))) { return true; } } } return false; }, isReasonWhitelisted(reason) { if (!reason || typeof reason.detail !== 'string') return false; const currentDomain = Utils.getCurrentHostname(); const contentIdentifier = Utils.getContentIdentifier(null, reason); if (contentIdentifier && currentDomain && currentConfig.whitelist[currentDomain] && currentConfig.whitelist[currentDomain].has(contentIdentifier)) { return true; } if (currentConfig.modules.interceptThirdParty) { const urlMatch = reason.detail.match(/https?:\/\/[^\s]+/); if (urlMatch) { const url = urlMatch[0]; const thirdPartyWhitelist = StorageManager.getThirdPartyWhitelist(currentDomain); if (urlCache.isWhitelisted(url, thirdPartyWhitelist)) { return true; } } } const keywordWhitelist = currentConfig.keywordWhitelist[currentDomain]; if (keywordWhitelist) { for (const keyword of keywordWhitelist) { if (keyword && reason.detail.includes(keyword)) { return true; } } } return false; }, isContentWhitelisted(domain, contentIdentifier) { if (!domain || !contentIdentifier) return false; return !!(currentConfig.whitelist[domain] && currentConfig.whitelist[domain].has(contentIdentifier)); }, add(domain, contentIdentifier) { if (!domain || !contentIdentifier || contentIdentifier.trim() === '') return; if (!currentConfig.whitelist[domain]) { currentConfig.whitelist[domain] = new Set(); } currentConfig.whitelist[domain].add(contentIdentifier); }, addKeyword(domain, keyword) { if (!domain || !keyword || keyword.trim() === '') return; if (!currentConfig.keywordWhitelist[domain]) { currentConfig.keywordWhitelist[domain] = new Set(); } currentConfig.keywordWhitelist[domain].add(keyword.trim()); }, clearDomainWhitelist(domain) { if (currentConfig.whitelist[domain]) { currentConfig.whitelist[domain].clear(); } if (currentConfig.keywordWhitelist[domain]) { currentConfig.keywordWhitelist[domain].clear(); } }, clearAllWhitelists() { currentConfig.whitelist = {}; currentConfig.keywordWhitelist = {}; } }; const StorageManager = { getConfigKey(hostname) { return `${CONFIG_STORAGE_KEY_PREFIX}${hostname}`; }, getThirdPartyWhitelistKey(domain) { return `third_party_whitelist_${domain}`; }, getKeywordWhitelistKey(domain) { return `keyword_whitelist_${domain}`; }, loadConfig() { const hostname = Utils.getCurrentHostname(); const key = this.getConfigKey(hostname); try { const savedConfig = JSON.parse(GM_getValue(key, '{}')); if (savedConfig) { if (savedConfig.modules) { Object.assign(currentConfig.modules, savedConfig.modules); } if (savedConfig.cspRules) { currentConfig.cspRules = savedConfig.cspRules.map(rule => ({ ...rule })); } if (savedConfig.whitelist) { currentConfig.whitelist = {}; for (const domain in savedConfig.whitelist) { if (Array.isArray(savedConfig.whitelist[domain])) { currentConfig.whitelist[domain] = new Set(savedConfig.whitelist[domain]); } } } } } catch (e) { Object.assign(currentConfig.modules, DEFAULT_MODULE_STATE); currentConfig.cspRules = DEFAULT_CSP_RULES_TEMPLATE.map(rule => ({ ...rule })); currentConfig.whitelist = {}; } Object.keys(DEFAULT_MODULE_STATE).forEach(key => { if (currentConfig.modules[key] === undefined) { currentConfig.modules[key] = DEFAULT_MODULE_STATE[key]; } }); if (currentConfig.modules.manageCSP === undefined) currentConfig.modules.manageCSP = false; if (currentConfig.modules.smartInterception === undefined) currentConfig.modules.smartInterception = false; if (currentConfig.modules.interceptThirdParty === undefined) currentConfig.modules.interceptThirdParty = false; if (currentConfig.modules.blockDynamicScripts === undefined) currentConfig.modules.blockDynamicScripts = false; const keywordWhitelistKey = this.getKeywordWhitelistKey(hostname); try { const savedKeywordWhitelist = JSON.parse(GM_getValue(keywordWhitelistKey, '[]')); if (Array.isArray(savedKeywordWhitelist)) { currentConfig.keywordWhitelist[hostname] = new Set(savedKeywordWhitelist); } } catch (e) { currentConfig.keywordWhitelist[hostname] = new Set(); } }, getThirdPartyWhitelist(domain) { const key = this.getThirdPartyWhitelistKey(domain); try { return JSON.parse(GM_getValue(key, '[]')); } catch (e) { return []; } }, saveThirdPartyWhitelist(domain, whitelist) { const key = this.getThirdPartyWhitelistKey(domain); GM_setValue(key, JSON.stringify(whitelist)); }, addThirdPartyToWhitelist(domain, pattern) { if (!pattern) return false; let normalized = pattern.trim(); normalized = normalized.replace(/^https?:\/\//, ''); try { const urlObj = new URL('http://' + normalized); normalized = urlObj.hostname + (urlObj.pathname !== '/' ? urlObj.pathname : ''); } catch (e) {} const whitelist = this.getThirdPartyWhitelist(domain); if (!whitelist.includes(normalized)) { whitelist.push(normalized); this.saveThirdPartyWhitelist(domain, whitelist); urlCache.whitelistCache.clear(); return true; } else { return false; } }, saveKeywordWhitelist(domain, keywordSet) { const key = this.getKeywordWhitelistKey(domain); GM_setValue(key, JSON.stringify(Array.from(keywordSet))); }, clearDomainThirdPartyWhitelist(domain) { this.saveThirdPartyWhitelist(domain, []); urlCache.whitelistCache.clear(); }, clearAllForDomain(domain) { this.clearDomainThirdPartyWhitelist(domain); const keywordWhitelistKey = this.getKeywordWhitelistKey(domain); GM_setValue(keywordWhitelistKey, JSON.stringify([])); const configKey = this.getConfigKey(domain); GM_setValue(configKey, JSON.stringify({ modules: DEFAULT_MODULE_STATE, cspRules: DEFAULT_CSP_RULES_TEMPLATE.map(rule => ({ ...rule })), whitelist: {} })); }, saveConfig() { const hostname = Utils.getCurrentHostname(); const key = this.getConfigKey(hostname); try { const whitelistForStorage = {}; for (const domain in currentConfig.whitelist) { whitelistForStorage[domain] = Array.from(currentConfig.whitelist[domain]); } GM_setValue(key, JSON.stringify({ modules: currentConfig.modules, cspRules: currentConfig.cspRules, whitelist: whitelistForStorage })); if (currentConfig.keywordWhitelist[hostname]) { this.saveKeywordWhitelist(hostname, currentConfig.keywordWhitelist[hostname]); } } catch (e) {} }, resetAllSettings() { const hostname = Utils.getCurrentHostname(); currentConfig.modules = { ...DEFAULT_MODULE_STATE }; currentConfig.cspRules = DEFAULT_CSP_RULES_TEMPLATE.map(rule => ({ ...rule })); currentConfig.whitelist = {}; currentConfig.keywordWhitelist = {}; this.saveThirdPartyWhitelist(hostname, []); this.saveKeywordWhitelist(hostname, new Set()); const key = this.getConfigKey(hostname); GM_setValue(key, JSON.stringify({ modules: DEFAULT_MODULE_STATE, cspRules: DEFAULT_CSP_RULES_TEMPLATE.map(rule => ({ ...rule })), whitelist: {} })); urlCache.whitelistCache.clear(); } }; const ProcessedElementsCache = { _processedElements: new WeakSet(), isProcessed(element) { if (!Utils.isElement(element)) return false; return this._processedElements.has(element) || element.dataset.adblockProcessed === 'true'; }, markAsProcessed(element) { if (!Utils.isElement(element)) return; this._processedElements.add(element); element.dataset.adblockProcessed = 'true'; }, clear() { this._processedElements = new WeakSet(); } }; const ResourceCanceller = { cancelResourceLoading(element) { if (!Utils.isElement(element) || ProcessedElementsCache.isProcessed(element)) return; const tagName = element.tagName; if (['IMG', 'VIDEO', 'AUDIO', 'SOURCE'].includes(tagName) && element.src) { element.src = ''; element.srcset = ''; element.removeAttribute('srcset'); if (element.load) element.load(); } else if (tagName === 'IFRAME' && element.src) { element.src = 'about:blank'; element.style.display = 'none'; element.remove(); } else if (tagName === 'SCRIPT' && element.src) { element.src = ''; element.remove(); } else if (tagName === 'LINK' && element.rel === 'stylesheet' && element.href) { element.href = ''; element.remove(); } else if (tagName === 'STYLE') { element.textContent = ''; element.remove(); } else if (tagName === 'EMBED' && element.src) { element.src = ''; element.remove(); } if (element.parentNode) { element.parentNode.removeChild(element); } } }; class CentralScheduler { constructor() { this.modules = []; this.elementCheckCache = new WeakSet(); this.urlCheckCache = new LRUCache(500, 300000); this.setupModules(); } setupModules() { this.modules = [ { key: 'smartInterception', check: SmartInterceptionModule.check.bind(SmartInterceptionModule) }, { key: 'removeInlineScripts', check: RemoveInlineScriptsModule.check.bind(RemoveInlineScriptsModule) }, { key: 'removeExternalScripts', check: RemoveExternalScriptsModule.check.bind(RemoveExternalScriptsModule) }, { key: 'interceptThirdParty', check: ThirdPartyInterceptionModule.check.bind(ThirdPartyInterceptionModule) } ]; } shouldProcessElement(element) { if (!Utils.isElement(element)) return false; if (this.elementCheckCache.has(element)) return false; if (ProcessedElementsCache.isProcessed(element)) return false; if (Utils.isParentProcessed(element)) return false; if (element.getAttribute('data-adblock-safe') === 'true') return false; return true; } processElement(element) { if (!this.shouldProcessElement(element)) return false; this.elementCheckCache.add(element); for (const module of this.modules) { if (currentConfig.modules[module.key] && module.check(element)) { ProcessedElementsCache.markAsProcessed(element); return true; } } return false; } processUrl(url, type) { if (!url) return false; const cacheKey = `url_${url}_${type}`; if (this.urlCheckCache.has(cacheKey)) { return this.urlCheckCache.get(cacheKey); } let result = false; if (currentConfig.modules.interceptThirdParty) { const currentDomain = Utils.getCurrentHostname(); const thirdPartyWhitelist = StorageManager.getThirdPartyWhitelist(currentDomain); if (Utils.isThirdParty(url) && !urlCache.isWhitelisted(url, thirdPartyWhitelist)) { const keywordWhitelist = currentConfig.keywordWhitelist[currentDomain]; if (!keywordWhitelist || !Array.from(keywordWhitelist).some(keyword => url.includes(keyword))) { result = true; } } } this.urlCheckCache.set(cacheKey, result, 300000); return result; } clearCache() { this.elementCheckCache = new WeakSet(); this.urlCheckCache.clear(); urlCache.clear(); } } const DynamicScriptInterceptor = { originalEval: null, originalFunction: null, init() { if (currentConfig.modules.blockDynamicScripts) { this.enable(); } }, enable() { this.originalEval = window.eval; this.originalFunction = window.Function; const self = this; window.eval = function(code) { if (typeof code === 'string') { const contentIdentifier = Utils.getContentIdentifier(null, { type: 'EVAL', detail: `代码: ${Utils.truncateString(code, 200)}` }); if (contentIdentifier && !Whitelisting.isContentWhitelisted(Utils.getCurrentHostname(), contentIdentifier)) { LogManager.add('blockDynamicScripts', null, { type: 'EVAL', detail: `动态执行: ${Utils.truncateString(code, 200)}` }); return undefined; } } return self.originalEval.call(this, code); }; window.Function = function(...args) { const code = args.length > 0 ? args[args.length - 1] : ''; if (typeof code === 'string') { const contentIdentifier = Utils.getContentIdentifier(null, { type: 'FUNCTION_CONSTRUCTOR', detail: `代码: ${Utils.truncateString(code, 200)}` }); if (contentIdentifier && !Whitelisting.isContentWhitelisted(Utils.getCurrentHostname(), contentIdentifier)) { LogManager.add('blockDynamicScripts', null, { type: 'FUNCTION_CONSTRUCTOR', detail: `Function构造器: ${Utils.truncateString(code, 200)}` }); return function() {}; } } return self.originalFunction.apply(this, args); }; }, disable() { if (this.originalEval) window.eval = this.originalEval; if (this.originalFunction) window.Function = this.originalFunction; }, check() { return false; } }; const SmartInterceptionModule = { originalSetTimeout: null, originalSetInterval: null, originalClearTimeout: null, originalClearInterval: null, processedTimers: new WeakSet(), stylePatterns: [ /position\s*:\s*(fixed|sticky|absolute|relative)\s*(!?\s*important)?/i, /z-index\s*:\s*([1-9]\d{3,}|[2-9]\d{4,}|[1-9]\d{5,}|auto|inherit|static|relative|absolute|fixed|sticky|relative)\s*(!?\s*important)?/i, /(display|visibility|opacity)\s*:\s*(none|hidden|0)\s*(!?\s*important)?/i, /(width|height)\s*:\s*(\d{1,4}px|auto|inherit|%|vw|vh|em|rem|vmin|vmax)\s*(!?\s*important)?/i, /(top|bottom|left|right)\s*:\s*(0|auto|inherit|px|%|em|rem|vw|vh)\s*(!?\s*important)?/i, /background(?:\s*-\s*image)?\s*:\s*url\s*\(\s*['"]?(?!(data:|blob:))[^)'"]+['"]?\s*\)/i, /border\s*:\s*\d{1,2}px\s*(solid|dashed|dotted)\s*\S+/i, /overflow\s*:\s*(hidden|scroll|auto|visible)\s*(!?\s*important)?/i, /(margin|padding)\s*:\s*(\d+(\.\d+)?(px|%|em|rem)?|\s*auto|inherit)\s*(!?\s*important)?/i, /text-align\s*:\s*(center|right|justify)\s*(!?\s*important)?/i, /(font-size|line-height)\s*:\s*(\d+(\.\d+)?(px|%|em|rem)|normal)\s*(!?\s*important)?/i, /([\w\s\.\#\[\]\-\:]*?)\s*{.*(position\s*:\s*(fixed|sticky|absolute|relative)|z-index\s*:\s*([1-9]\d{3,}|[2-9]\d{4,}|[1-9]\d{5,})|display\s*:\s*(block|flex|grid|table|inline-block)|visibility\s*:\s*(visible)|width\s*:\s*(\d{1,4}px)|height\s*:\s*(\d{1,4}px)|top\s*:\s*(0|auto)|bottom\s*:\s*(0|auto)|left\s*:\s*(0|auto)|right\s*:\s*(0|auto)|overflow\s*:\s*(hidden|scroll|auto)).*\s*!\s*important/i ], init() { if (currentConfig.modules.smartInterception) { this.enable(); } }, enable() { this.setupTimerInterception(); this.setupFastStyleChecker(); }, disable() { this.disableTimerInterception(); }, setupTimerInterception() { this.originalSetTimeout = window.setTimeout; this.originalSetInterval = window.setInterval; this.originalClearTimeout = window.clearTimeout; this.originalClearInterval = window.clearInterval; const self = this; window.setTimeout = function(callback, delay, ...args) { if (typeof callback === 'function') { const callbackStr = callback.toString(); const contentIdentifier = Utils.getContentIdentifier(null, { type: 'SETTIMEOUT', detail: `代码: ${Utils.truncateString(callbackStr, 200)}` }); if (contentIdentifier && !Whitelisting.isContentWhitelisted(Utils.getCurrentHostname(), contentIdentifier)) { LogManager.add('smartInterception', null, { type: 'SETTIMEOUT', detail: `广告定时器: ${Utils.truncateString(callbackStr, 200)}` }); const timerId = self.originalSetTimeout(() => {}, delay); self.processedTimers.add(timerId); return timerId; } } return self.originalSetTimeout.call(this, callback, delay, ...args); }; window.setInterval = function(callback, delay, ...args) { if (typeof callback === 'function') { const callbackStr = callback.toString(); const contentIdentifier = Utils.getContentIdentifier(null, { type: 'SETINTERVAL', detail: `代码: ${Utils.truncateString(callbackStr, 200)}` }); if (contentIdentifier && !Whitelisting.isContentWhitelisted(Utils.getCurrentHostname(), contentIdentifier)) { LogManager.add('smartInterception', null, { type: 'SETINTERVAL', detail: `广告定时器: ${Utils.truncateString(callbackStr, 200)}` }); const timerId = self.originalSetInterval(() => {}, delay); self.processedTimers.add(timerId); return timerId; } } return self.originalSetInterval.call(this, callback, delay, ...args); }; window.clearTimeout = function(timerId) { if (self.processedTimers.has(timerId)) { self.processedTimers.delete(timerId); return; } self.originalClearTimeout.call(this, timerId); }; window.clearInterval = function(timerId) { if (self.processedTimers.has(timerId)) { self.processedTimers.delete(timerId); return; } self.originalClearInterval.call(this, timerId); }; }, disableTimerInterception() { if (this.originalSetTimeout) window.setTimeout = this.originalSetTimeout; if (this.originalSetInterval) window.setInterval = this.originalSetInterval; if (this.originalClearTimeout) window.clearTimeout = this.originalClearTimeout; if (this.originalClearInterval) window.clearInterval = this.originalClearInterval; }, setupFastStyleChecker() { const observer = new MutationObserver(Utils.throttle((mutations) => { for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (node.nodeType !== Node.ELEMENT_NODE) continue; if (ProcessedElementsCache.isProcessed(node) || Utils.isParentProcessed(node)) continue; this.fastCheckElement(node); } } }, 200)); observer.observe(document.documentElement, { childList: true, subtree: true }); }, fastCheckElement(element) { const tagName = element.tagName; if (tagName === 'STYLE') { this.checkStyleElement(element); } }, checkStyleElement(styleElement) { const styleContent = styleElement.textContent; if (this.stylePatterns.some(pattern => pattern.test(styleContent))) { const contentIdentifier = Utils.getContentIdentifier(styleElement); if (contentIdentifier && !Whitelisting.isContentWhitelisted(Utils.getCurrentHostname(), contentIdentifier)) { LogManager.add('smartInterception', styleElement, { type: 'STYLE_ADS', detail: `广告样式: ${Utils.truncateString(styleContent, 200)}` }); ResourceCanceller.cancelResourceLoading(styleElement); ProcessedElementsCache.markAsProcessed(styleElement); } } }, check(element) { if (!Utils.shouldInterceptByModule(element, 'smartInterception')) return false; const tagName = element.tagName; if (tagName === 'STYLE') { this.checkStyleElement(element); return ProcessedElementsCache.isProcessed(element); } return false; } }; const RemoveInlineScriptsModule = { mutationObserver: null, init() { if (currentConfig.modules.removeInlineScripts) { this.enable(); } }, enable() { this.mutationObserver = new MutationObserver(mutations => { for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'SCRIPT' && !node.src && !ProcessedElementsCache.isProcessed(node) && !Utils.isParentProcessed(node)) { const contentIdentifier = Utils.getContentIdentifier(node); if (contentIdentifier && !Whitelisting.isContentWhitelisted(Utils.getCurrentHostname(), contentIdentifier)) { const scriptContent = node.textContent; LogManager.add('removeInlineScripts', node, { type: '内嵌脚本移除', detail: `内容: ${Utils.truncateString(scriptContent, 300)}` }); ProcessedElementsCache.markAsProcessed(node); node.remove(); } } } } }); this.mutationObserver.observe(document.documentElement, { childList: true, subtree: true }); document.querySelectorAll('script:not([src])').forEach(script => { if (ProcessedElementsCache.isProcessed(script) || Utils.isParentProcessed(script)) return; const contentIdentifier = Utils.getContentIdentifier(script); if (contentIdentifier && !Whitelisting.isContentWhitelisted(Utils.getCurrentHostname(), contentIdentifier)) { const scriptContent = script.textContent; LogManager.add('removeInlineScripts', script, { type: '内嵌脚本移除', detail: `内容: ${Utils.truncateString(scriptContent, 300)}` }); ProcessedElementsCache.markAsProcessed(script); script.remove(); } }); }, disable() { if (this.mutationObserver) { this.mutationObserver.disconnect(); this.mutationObserver = null; } }, check(element) { if (!Utils.shouldInterceptByModule(element, 'removeInlineScripts')) return false; if (element.tagName === 'SCRIPT' && !element.src) { const contentIdentifier = Utils.getContentIdentifier(element); if (contentIdentifier && !Whitelisting.isContentWhitelisted(Utils.getCurrentHostname(), contentIdentifier)) { const scriptContent = element.textContent; LogManager.add('removeInlineScripts', element, { type: '内嵌脚本移除', detail: `内容: ${Utils.truncateString(scriptContent, 300)}` }); ProcessedElementsCache.markAsProcessed(element); return true; } } return false; } }; const RemoveExternalScriptsModule = { mutationObserver: null, init() { if (currentConfig.modules.removeExternalScripts) { this.enable(); } }, enable() { this.mutationObserver = new MutationObserver(mutations => { for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'SCRIPT' && node.src && !ProcessedElementsCache.isProcessed(node) && !Utils.isParentProcessed(node)) { const contentIdentifier = Utils.getContentIdentifier(node); if (contentIdentifier && !Whitelisting.isContentWhitelisted(Utils.getCurrentHostname(), contentIdentifier)) { LogManager.add('removeExternalScripts', node, { type: '外联脚本移除', detail: `SRC: ${Utils.truncateString(node.src, 200)}` }); ResourceCanceller.cancelResourceLoading(node); ProcessedElementsCache.markAsProcessed(node); } } } } }); this.mutationObserver.observe(document.documentElement, { childList: true, subtree: true }); document.querySelectorAll('script[src]').forEach(script => { if (ProcessedElementsCache.isProcessed(script) || Utils.isParentProcessed(script)) return; const contentIdentifier = Utils.getContentIdentifier(script); if (contentIdentifier && !Whitelisting.isContentWhitelisted(Utils.getCurrentHostname(), contentIdentifier)) { LogManager.add('removeExternalScripts', script, { type: '外联脚本移除', detail: `SRC: ${Utils.truncateString(script.src, 200)}` }); ResourceCanceller.cancelResourceLoading(script); ProcessedElementsCache.markAsProcessed(script); } }); }, disable() { if (this.mutationObserver) { this.mutationObserver.disconnect(); this.mutationObserver = null; } }, check(element) { if (!Utils.shouldInterceptByModule(element, 'removeExternalScripts')) return false; if (element.tagName === 'SCRIPT' && element.src) { const contentIdentifier = Utils.getContentIdentifier(element); if (contentIdentifier && !Whitelisting.isContentWhitelisted(Utils.getCurrentHostname(), contentIdentifier)) { LogManager.add('removeExternalScripts', element, { type: '外联脚本移除', detail: `SRC: ${Utils.truncateString(element.src, 200)}` }); ResourceCanceller.cancelResourceLoading(element); ProcessedElementsCache.markAsProcessed(element); return true; } } return false; } }; const ThirdPartyInterceptionModule = { originalCreateElement: null, originalAppendChild: null, originalInsertBefore: null, originalSetAttribute: null, originalFetch: null, originalXhrOpen: null, originalXhrSend: null, originalDocumentWrite: null, originalDocumentWriteln: null, interceptMethods: [], tagsToIntercept: { 'SCRIPT': ['src', 'data-src'], 'IMG': ['src', 'data-src', 'srcset'], 'IFRAME': ['src', 'data-src'], 'LINK': ['href'], 'VIDEO': ['src', 'poster'], 'AUDIO': ['src'], 'SOURCE': ['src', 'srcset'], 'EMBED': ['src'], 'OBJECT': ['data'], 'FORM': ['action'], 'INPUT': ['src', 'formaction'], 'BUTTON': ['formaction'], 'TRACK': ['src'], 'BGSOUND': ['src'], 'MARQUEE': ['behavior', 'direction'], 'META': ['content'], 'BASE': ['href'] }, init() { if (currentConfig.modules.interceptThirdParty) { this.enable(); } }, enable() { this.stopInterception(); this.interceptDOM(); this.interceptNetwork(); this.interceptDocumentWrite(); this.observeDOM(); }, disable() { this.stopInterception(); }, stopInterception() { this.interceptMethods.forEach(fn => { try { if (fn && typeof fn.restore === 'function') { fn.restore(); } } catch (e) {} }); this.interceptMethods = []; }, interceptDOM() { const self = this; const currentDomain = Utils.getCurrentHostname(); const thirdPartyWhitelist = StorageManager.getThirdPartyWhitelist(currentDomain); const whitelistedSelectors = [ '#floating-btn', '#ad-blocker-settings-container', '[data-adblock-safe="true"]' ]; const isWhitelistedElement = (element) => { if (!element || !element.tagName) return false; for (const selector of whitelistedSelectors) { if (element.matches && element.matches(selector)) return true; if (element.id && element.id === selector.replace('#', '')) return true; if (element.getAttribute && element.getAttribute('data-adblock-safe') === 'true') return true; } return false; }; this.originalCreateElement = document.createElement; const createElementOverride = function(tagName, options) { const element = self.originalCreateElement.call(this, tagName, options); if (!isWhitelistedElement(element)) { self.monitorElement(element, thirdPartyWhitelist); } return element; }; document.createElement = createElementOverride; createElementOverride.restore = () => { document.createElement = self.originalCreateElement; }; this.interceptMethods.push(createElementOverride); this.originalAppendChild = Node.prototype.appendChild; Node.prototype.appendChild = function(childNode) { if (childNode && childNode.nodeType === 1 && !isWhitelistedElement(childNode)) { self.checkElement(childNode, thirdPartyWhitelist); } return self.originalAppendChild.call(this, childNode); }; this.originalInsertBefore = Node.prototype.insertBefore; Node.prototype.insertBefore = function(newNode, referenceNode) { if (newNode && newNode.nodeType === 1 && !isWhitelistedElement(newNode)) { self.checkElement(newNode, thirdPartyWhitelist); } return self.originalInsertBefore.call(this, newNode, referenceNode); }; this.originalSetAttribute = Element.prototype.setAttribute; Element.prototype.setAttribute = function(name, value) { if (value && self.tagsToIntercept[this.tagName] && self.tagsToIntercept[this.tagName].includes(name) && !isWhitelistedElement(this)) { if (self.shouldBlockResource(value, this.tagName, thirdPartyWhitelist)) { LogManager.add('interceptThirdParty', this, { type: 'THIRD_PARTY', detail: `URL: ${Utils.truncateString(value, 200)}` }); return; } } return self.originalSetAttribute.call(this, name, value); }; const originalInnerHTML = Object.getOwnPropertyDescriptor(Element.prototype, 'innerHTML'); if (originalInnerHTML && originalInnerHTML.set) { const innerHTMLOverride = function(html) { if (html && html.includes('<') && !isWhitelistedElement(this)) { const tempDiv = document.createElement('div'); tempDiv.innerHTML = html; self.scanAndBlockElements(tempDiv, thirdPartyWhitelist); html = tempDiv.innerHTML; } return originalInnerHTML.set.call(this, html); }; Object.defineProperty(Element.prototype, 'innerHTML', { set: innerHTMLOverride, get: originalInnerHTML.get }); } }, interceptDocumentWrite() { const self = this; const currentDomain = Utils.getCurrentHostname(); const thirdPartyWhitelist = StorageManager.getThirdPartyWhitelist(currentDomain); this.originalDocumentWrite = document.write; this.originalDocumentWriteln = document.writeln; const documentWriteOverride = function(content) { if (typeof content === 'string' && content.includes('<')) { const processedContent = self.processDocumentWriteContent(content, thirdPartyWhitelist); return self.originalDocumentWrite.call(this, processedContent); } return self.originalDocumentWrite.call(this, content); }; const documentWritelnOverride = function(content) { if (typeof content === 'string' && content.includes('<')) { const processedContent = self.processDocumentWriteContent(content, thirdPartyWhitelist); return self.originalDocumentWriteln.call(this, processedContent); } return self.originalDocumentWriteln.call(this, content); }; document.write = documentWriteOverride; documentWriteOverride.restore = () => { document.write = self.originalDocumentWrite; }; this.interceptMethods.push(documentWriteOverride); document.writeln = documentWritelnOverride; documentWritelnOverride.restore = () => { document.writeln = self.originalDocumentWriteln; }; this.interceptMethods.push(documentWritelnOverride); }, processDocumentWriteContent(content, thirdPartyWhitelist) { const tempDiv = document.createElement('div'); tempDiv.innerHTML = content; const elementsToCheck = []; Object.keys(this.tagsToIntercept).forEach(tagName => { const elements = tempDiv.querySelectorAll(tagName); elements.forEach(element => { elementsToCheck.push(element); }); }); let hasThirdParty = false; elementsToCheck.forEach(element => { const tagName = element.tagName; if (!this.tagsToIntercept[tagName]) return; this.tagsToIntercept[tagName].forEach(attr => { const value = element[attr] || element.getAttribute(attr); if (value && this.shouldBlockResource(value, tagName, thirdPartyWhitelist)) { LogManager.add('interceptThirdParty', element, { type: 'DOCUMENT_WRITE', detail: `document.write写入的${tagName}标签: ${Utils.truncateString(value, 200)}` }); element.remove(); ProcessedElementsCache.markAsProcessed(element); hasThirdParty = true; } }); }); return tempDiv.innerHTML; }, monitorElement(element, thirdPartyWhitelist) { const self = this; const tagName = element.tagName; if (!self.tagsToIntercept[tagName]) return; const originalSetAttribute = element.setAttribute; element.setAttribute = function(name, value) { if (value && self.tagsToIntercept[tagName].includes(name)) { if (self.shouldBlockResource(value, tagName, thirdPartyWhitelist)) { LogManager.add('interceptThirdParty', this, { type: 'THIRD_PARTY', detail: `URL: ${Utils.truncateString(value, 200)}` }); return; } } return originalSetAttribute.call(this, name, value); }; self.tagsToIntercept[tagName].forEach(attr => { if (attr in element) { const descriptor = Object.getOwnPropertyDescriptor(element, attr); if (descriptor && descriptor.set) { Object.defineProperty(element, attr, { set: function(value) { if (value && self.shouldBlockResource(value, tagName, thirdPartyWhitelist)) { LogManager.add('interceptThirdParty', this, { type: 'THIRD_PARTY', detail: `URL: ${Utils.truncateString(value, 200)}` }); return; } descriptor.set.call(this, value); }, get: descriptor.get }); } } }); }, checkElement(element, thirdPartyWhitelist) { if (ProcessedElementsCache.isProcessed(element) || Utils.isParentProcessed(element)) return; const tagName = element.tagName; if (!this.tagsToIntercept[tagName]) return; this.tagsToIntercept[tagName].forEach(attr => { const value = element[attr] || element.getAttribute(attr); if (value && this.shouldBlockResource(value, tagName, thirdPartyWhitelist)) { LogManager.add('interceptThirdParty', element, { type: 'THIRD_PARTY', detail: `URL: ${Utils.truncateString(value, 200)}` }); this.blockElement(element, attr); } }); }, scanAndBlockElements(container, thirdPartyWhitelist) { const self = this; Object.keys(this.tagsToIntercept).forEach(tagName => { const elements = container.querySelectorAll(tagName); elements.forEach(element => { self.tagsToIntercept[tagName].forEach(attr => { const value = element[attr] || element.getAttribute(attr); if (value && self.shouldBlockResource(value, tagName, thirdPartyWhitelist)) { LogManager.add('interceptThirdParty', element, { type: 'THIRD_PARTY', detail: `URL: ${Utils.truncateString(value, 200)}` }); self.blockElement(element, attr); } }); }); }); }, interceptNetwork() { const self = this; const currentDomain = Utils.getCurrentHostname(); const thirdPartyWhitelist = StorageManager.getThirdPartyWhitelist(currentDomain); this.originalFetch = window.fetch; window.fetch = function(input, init) { const url = typeof input === 'string' ? input : input.url; if (url && self.shouldBlockResource(url, 'fetch', thirdPartyWhitelist)) { LogManager.add('interceptThirdParty', null, { type: 'THIRD_PARTY', detail: `FETCH: ${Utils.truncateString(url, 200)}` }); return Promise.reject(new Error('请求被拦截')); } return self.originalFetch.call(this, input, init); }; this.originalXhrOpen = XMLHttpRequest.prototype.open; this.originalXhrSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function(method, url, async, user, password) { this._interceptorUrl = url; return self.originalXhrOpen.call(this, method, url, async || true, user, password); }; XMLHttpRequest.prototype.send = function(body) { if (this._interceptorUrl && self.shouldBlockResource(this._interceptorUrl, 'xhr', thirdPartyWhitelist)) { LogManager.add('interceptThirdParty', null, { type: 'THIRD_PARTY', detail: `XHR: ${Utils.truncateString(this._interceptorUrl, 200)}` }); this._interceptorUrl = null; return; } return self.originalXhrSend.call(this, body); }; }, observeDOM() { const self = this; const currentDomain = Utils.getCurrentHostname(); const thirdPartyWhitelist = StorageManager.getThirdPartyWhitelist(currentDomain); const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { mutation.addedNodes.forEach((node) => { if (node.nodeType === 1) { self.checkElement(node, thirdPartyWhitelist); } }); }); }); observer.observe(document.documentElement, { childList: true, subtree: true }); this.interceptMethods.push({ restore: () => observer.disconnect() }); }, shouldBlockResource(url, resourceType, thirdPartyWhitelist) { if (!url || !Utils.isThirdParty(url)) return false; if (urlCache.isWhitelisted(url, thirdPartyWhitelist)) { return false; } const currentDomain = Utils.getCurrentHostname(); const keywordWhitelist = currentConfig.keywordWhitelist[currentDomain]; if (keywordWhitelist) { for (const keyword of keywordWhitelist) { if (keyword && url.includes(keyword)) { return false; } } } return true; }, blockElement(element, attr) { ResourceCanceller.cancelResourceLoading(element); }, check(element) { if (!Utils.shouldInterceptByModule(element, 'interceptThirdParty')) return false; const currentDomain = Utils.getCurrentHostname(); const thirdPartyWhitelist = StorageManager.getThirdPartyWhitelist(currentDomain); const tagName = element.tagName; if (tagName === 'EMBED') { const src = element.src || element.getAttribute('src'); if (src && this.shouldBlockResource(src, 'EMBED', thirdPartyWhitelist)) { LogManager.add('interceptThirdParty', element, { type: 'THIRD_PARTY', detail: `EMBED: ${Utils.truncateString(src, 200)}` }); this.blockElement(element, 'src'); return true; } return false; } if (!this.tagsToIntercept[tagName]) return false; let shouldBlock = false; this.tagsToIntercept[tagName].forEach(attr => { const value = element[attr] || element.getAttribute(attr); if (value && this.shouldBlockResource(value, tagName, thirdPartyWhitelist)) { shouldBlock = true; } }); if (shouldBlock) { this.blockElement(element, 'src'); return true; } return false; } }; const CSPModule = { init() {}, applyCSP() { if (!currentConfig.modules.manageCSP) return; const existingMeta = document.querySelector('meta[http-equiv="Content-Security-Policy"]'); if (existingMeta) { existingMeta.remove(); } const enabledRules = currentConfig.cspRules.filter(rule => rule.enabled); if (enabledRules.length === 0) { return; } const directives = {}; enabledRules.forEach(rule => { const [directive, ...values] = rule.rule.split(' '); if (!directives[directive]) { directives[directive] = new Set(); } values.forEach(value => directives[directive].add(value)); }); let policyString = ''; for (const directive in directives) { if (directives.hasOwnProperty(directive)) { const values = Array.from(directives[directive]).join(' '); policyString += `${directive} ${values}; `; } } policyString = policyString.trim(); if (policyString) { const meta = document.createElement('meta'); meta.httpEquiv = "Content-Security-Policy"; meta.content = policyString; if (document.head) { document.head.appendChild(meta); } else { document.documentElement.prepend(meta); } } }, removeCSP() { const existingMeta = document.querySelector('meta[http-equiv="Content-Security-Policy"]'); if (existingMeta) { existingMeta.remove(); } }, updateRule(ruleId, enabled) { const rule = currentConfig.cspRules.find(r => r.id === ruleId); if (rule) { rule.enabled = enabled; } } }; let shadowRoot = null; let settingsContainer = null; let centralScheduler = null; const ensureShadow = () => { if (shadowRoot) return; settingsContainer = document.createElement('div'); settingsContainer.id = 'ad-blocker-settings-container'; settingsContainer.style.cssText = 'position:absolute;top:0;left:0;z-index:2147483647;pointer-events:none;'; settingsContainer.setAttribute('data-adblock-safe', 'true'); document.documentElement.appendChild(settingsContainer); shadowRoot = settingsContainer.attachShadow({ mode: 'closed' }); const style = document.createElement('style'); style.textContent = UI_CSS; shadowRoot.appendChild(style); ProcessedElementsCache.markAsProcessed(settingsContainer); }; const showDiaryWhitelistPanel = (parentMask) => { parentMask.remove(); ensureShadow(); panelOpenCount++; setupNavigationBlocking(); const currentDomain = Utils.getCurrentHostname(); const diaryWhitelist = currentConfig.whitelist[currentDomain] ? Array.from(currentConfig.whitelist[currentDomain]) : []; const mask = document.createElement('div'); mask.className = 'mask'; mask.setAttribute('data-adblock-safe', 'true'); const whitelistHtml = diaryWhitelist.length > 0 ? diaryWhitelist.map((item, index) => `