// ==UserScript== // @name 网页广告拦截器 // @namespace http://tampermonkey.net/ // @version 5.2.2 // @author DeepSeek&Gemini // @description 一个手机端浏览器能用的强大的广告拦截器 // @match *://*/* // @license MIT // @grant unsafeWindow // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @grant GM_notification // @run-at document-start // ==/UserScript== (function() { 'use strict'; const CONFIG = { Z_INDEX: 2147483640, CACHE_TTL: 300000, BATCH_SIZE: 20, LOG_MAX: 50, LOG_IDENTIFIER_TTL: 300000, STRONG_BLOCK_TIMEOUT: 600000, DEBOUNCE_WAIT: 100, THROTTLE_LIMIT: 100, RESIDUAL_MIN_WIDTH: 30, RESIDUAL_MIN_HEIGHT: 30, RESIDUAL_AD_SELECTORS: 'div, span, embed', URL_DYNAMIC_PATTERNS: [ /\?.*[tT]=/, /\?.*timestamp/, /\?.*rand/, /\?.*rnd/, /\?.*[0-9]{13,}/, /\?.*\d{10,}/, /\/\d{10,}\./, /\/[0-9a-f]{32,}\./ ] }; const _globals = (typeof unsafeWindow !== 'undefined' ? unsafeWindow : window); const _document = _globals.document; const _location = _globals.location; const _MutationObserver = _globals.MutationObserver; const _Element = _globals.Element; const _Node = _globals.Node; const _setTimeout = _globals.setTimeout; const _clearTimeout = _globals.clearTimeout; const _requestAnimationFrame = _globals.requestAnimationFrame; const _cancelAnimationFrame = _globals.cancelAnimationFrame; const _XMLHttpRequest = _globals.XMLHttpRequest; const _fetch = _globals.fetch; const _Proxy = _globals.Proxy; const _Set = _globals.Set; const _Map = _globals.Map; const _IntersectionObserver = _globals.IntersectionObserver; const _requestIdleCallback = _globals.requestIdleCallback || function(cb) { return _setTimeout(cb, 1); }; const _cancelIdleCallback = _globals.cancelIdleCallback || _clearTimeout; function escapeHtml(unsafe) { if (unsafe == null) return ''; return String(unsafe) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function generateContentHash(str, maxLength = 500) { if (typeof str !== 'string') return ''; const content = str.slice(0, maxLength); let hash = 0; for (let i = 0; i < content.length; i++) { const char = content.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; } return 'hash_' + Math.abs(hash).toString(36); } if (_document.designMode === 'on' || _document.documentElement.style.pointerEvents === 'none') { _document.designMode = 'off'; _document.documentElement.style.pointerEvents = ''; } let activePanels = new _Set(); let strongBlockingEnabled = false; let blockingTimer = null; const _beforeunloadHandler = function(e) { e.preventDefault(); e.returnValue = '系统可能不会保存您所做的更改。'; return '系统可能不会保存您所做的更改。'; }; function enableStrongBlocking() { if (!currentConfig.navigationBlockEnabled) return; if (strongBlockingEnabled) return; _globals.addEventListener('beforeunload', _beforeunloadHandler); strongBlockingEnabled = true; if (blockingTimer) _clearTimeout(blockingTimer); blockingTimer = _setTimeout(() => { if (activePanels.size > 0) { activePanels.clear(); disableStrongBlocking(); } }, CONFIG.STRONG_BLOCK_TIMEOUT); } function disableStrongBlocking() { if (!strongBlockingEnabled) return; _globals.removeEventListener('beforeunload', _beforeunloadHandler); strongBlockingEnabled = false; if (blockingTimer) { _clearTimeout(blockingTimer); blockingTimer = null; } } function setupNavigationBlocking(panelId) { if (!currentConfig.navigationBlockEnabled) return; activePanels.add(panelId); if (activePanels.size === 1) { enableStrongBlocking(); if (currentConfig.modules.blockDynamicScripts) { DynamicScriptInterceptor.disable(); } } } function teardownNavigationBlocking(panelId) { if (!currentConfig.navigationBlockEnabled) return; activePanels.delete(panelId); if (activePanels.size === 0) { disableStrongBlocking(); if (currentConfig.modules.blockDynamicScripts) { DynamicScriptInterceptor.enable(); } } } const DEFAULT_MODULE_STATE = { removeInlineScripts: false, removeExternalScripts: false, interceptThirdParty: false, blockDynamicScripts: false, manageCSP: false, scriptBlacklistMode: false }; const MODULE_NAMES = { removeInlineScripts: '移除内嵌脚本', removeExternalScripts: '移除外联脚本', blockDynamicScripts: '拦截动态脚本', interceptThirdParty: '拦截第三方资源', manageCSP: 'CSP策略管理', scriptBlacklistMode: '脚本黑名单模式' }; const DEFAULT_CSP_RULES_TEMPLATE = [ { id: 1, name: '只允许同源外部脚本', rule: "script-src 'self'", enabled: false }, { id: 2, name: '只允许同源外部样式', rule: "style-src 'self'", enabled: false }, { id: 3, name: '只允许同源图片', rule: "img-src 'self'", enabled: false }, { id: 4, name: '禁止所有框架', rule: "frame-src 'none'", enabled: false }, { id: 5, name: '禁止所有媒体', rule: "media-src 'none'", enabled: false }, { id: 6, name: '禁止所有对象与嵌入', rule: "object-src 'none'", enabled: false }, { id: 7, name: '禁止所有第三方资源', rule: "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self'; font-src 'self'; connect-src 'self'; frame-src 'self'; media-src 'self'; object-src 'none';", enabled: false } ]; let currentConfig = { modules: { ...DEFAULT_MODULE_STATE }, cspRules: DEFAULT_CSP_RULES_TEMPLATE.map(rule => ({ ...rule })), whitelist: new Set(), keywordWhitelist: new Set(), thirdPartySettings: {}, scriptBlacklist: new Set(), thirdPartyWhitelist: [], inlineScriptStrictMode: false, thirdPartyStrictMode: false, thirdPartyStrictMethod: false, spoofUAEnabled: false, residualCleanupEnabled: false, cssUniversalHideEnabled: false, iframeUIFix: false, navigationBlockEnabled: true, redirectBlockerEnabled: false }; const StorageManager = { getConfigKey(domain) { return `adblock_unified_config_${domain}`; }, loadConfig() { const hostname = _location.hostname; const key = this.getConfigKey(hostname); try { const saved = GM_getValue(key, null); if (saved) { const data = JSON.parse(saved); if (data.modules) Object.assign(currentConfig.modules, data.modules); if (data.cspRules) currentConfig.cspRules = data.cspRules.map(r => ({ ...r })); if (Array.isArray(data.whitelist)) currentConfig.whitelist = new Set(data.whitelist); if (Array.isArray(data.keywordWhitelist)) currentConfig.keywordWhitelist = new Set(data.keywordWhitelist); if (data.thirdPartySettings && typeof data.thirdPartySettings === 'object') currentConfig.thirdPartySettings = data.thirdPartySettings; if (Array.isArray(data.scriptBlacklist)) currentConfig.scriptBlacklist = new Set(data.scriptBlacklist); if (Array.isArray(data.thirdPartyWhitelist)) currentConfig.thirdPartyWhitelist = data.thirdPartyWhitelist; if (data.inlineScriptStrictMode !== undefined) currentConfig.inlineScriptStrictMode = data.inlineScriptStrictMode; if (data.thirdPartyStrictMode !== undefined) currentConfig.thirdPartyStrictMode = data.thirdPartyStrictMode; if (data.thirdPartyStrictMethod !== undefined) currentConfig.thirdPartyStrictMethod = data.thirdPartyStrictMethod; if (data.spoofUAEnabled !== undefined) currentConfig.spoofUAEnabled = data.spoofUAEnabled; if (data.residualCleanupEnabled !== undefined) currentConfig.residualCleanupEnabled = data.residualCleanupEnabled; if (data.cssUniversalHideEnabled !== undefined) currentConfig.cssUniversalHideEnabled = data.cssUniversalHideEnabled; if (data.iframeUIFix !== undefined) currentConfig.iframeUIFix = data.iframeUIFix; if (data.navigationBlockEnabled !== undefined) currentConfig.navigationBlockEnabled = data.navigationBlockEnabled; if (data.redirectBlockerEnabled !== undefined) currentConfig.redirectBlockerEnabled = data.redirectBlockerEnabled; } } catch (e) {} if (!currentConfig.thirdPartySettings) currentConfig.thirdPartySettings = {}; if (!currentConfig.thirdPartyWhitelist) currentConfig.thirdPartyWhitelist = []; if (currentConfig.thirdPartySettings.blockParentSubDomains === undefined) currentConfig.thirdPartySettings.blockParentSubDomains = true; if (currentConfig.inlineScriptStrictMode === undefined) currentConfig.inlineScriptStrictMode = false; if (currentConfig.thirdPartyStrictMode === undefined) currentConfig.thirdPartyStrictMode = false; if (currentConfig.thirdPartyStrictMethod === undefined) currentConfig.thirdPartyStrictMethod = false; if (currentConfig.residualCleanupEnabled === undefined) currentConfig.residualCleanupEnabled = false; if (currentConfig.cssUniversalHideEnabled === undefined) currentConfig.cssUniversalHideEnabled = false; if (currentConfig.iframeUIFix === undefined) currentConfig.iframeUIFix = false; if (currentConfig.navigationBlockEnabled === undefined) currentConfig.navigationBlockEnabled = true; if (currentConfig.redirectBlockerEnabled === undefined) currentConfig.redirectBlockerEnabled = false; const legacySpoof = GM_getValue('spoofUAEnabled', null); if (legacySpoof !== null) { currentConfig.spoofUAEnabled = legacySpoof; GM_setValue('spoofUAEnabled', null); this.saveConfig(); } }, saveConfig() { const hostname = _location.hostname; const key = this.getConfigKey(hostname); const toStore = { modules: currentConfig.modules, cspRules: currentConfig.cspRules, whitelist: Array.from(currentConfig.whitelist), keywordWhitelist: Array.from(currentConfig.keywordWhitelist), thirdPartySettings: currentConfig.thirdPartySettings, scriptBlacklist: Array.from(currentConfig.scriptBlacklist), thirdPartyWhitelist: currentConfig.thirdPartyWhitelist, inlineScriptStrictMode: currentConfig.inlineScriptStrictMode, thirdPartyStrictMode: currentConfig.thirdPartyStrictMode, thirdPartyStrictMethod: currentConfig.thirdPartyStrictMethod, spoofUAEnabled: currentConfig.spoofUAEnabled, residualCleanupEnabled: currentConfig.residualCleanupEnabled, cssUniversalHideEnabled: currentConfig.cssUniversalHideEnabled, iframeUIFix: currentConfig.iframeUIFix, navigationBlockEnabled: currentConfig.navigationBlockEnabled, redirectBlockerEnabled: currentConfig.redirectBlockerEnabled }; GM_setValue(key, JSON.stringify(toStore)); }, resetAllSettings() { currentConfig.modules = { ...DEFAULT_MODULE_STATE }; currentConfig.cspRules = DEFAULT_CSP_RULES_TEMPLATE.map(rule => ({ ...rule })); currentConfig.whitelist.clear(); currentConfig.keywordWhitelist.clear(); currentConfig.thirdPartySettings = {}; currentConfig.scriptBlacklist.clear(); currentConfig.thirdPartyWhitelist = []; currentConfig.inlineScriptStrictMode = false; currentConfig.thirdPartyStrictMode = false; currentConfig.thirdPartyStrictMethod = false; currentConfig.spoofUAEnabled = false; currentConfig.residualCleanupEnabled = false; currentConfig.cssUniversalHideEnabled = false; currentConfig.iframeUIFix = false; currentConfig.navigationBlockEnabled = true; currentConfig.redirectBlockerEnabled = false; this.saveConfig(); GM_setValue(`residual_cleanup_prompt_shown_${_location.hostname}`, null); } }; 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); const ttl = entry.ttl !== undefined ? entry.ttl : this.defaultTTL; if (ttl > 0 && (Date.now() - entry.timestamp) > ttl) { 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, 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; const ttl = entry.ttl !== undefined ? entry.ttl : this.defaultTTL; if (ttl > 0 && (Date.now() - entry.timestamp) > ttl) { 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; } } const PUBLIC_SUFFIX_LIST = new Set([ 'co.uk', 'org.uk', 'me.uk', 'ltd.uk', 'plc.uk', 'net.uk', 'sch.uk', 'ac.uk', 'gov.uk', 'com.cn', 'net.cn', 'org.cn', 'gov.cn', 'edu.cn', 'co.jp', 'ne.jp', 'or.jp', 'go.jp', 'ac.jp', 'ad.jp', 'com.au', 'net.au', 'org.au', 'edu.au', 'gov.au', 'asn.au', 'id.au', 'co.nz', 'org.nz', 'net.nz', 'edu.nz', 'govt.nz', 'com.br', 'net.br', 'org.br', 'gov.br', 'edu.br', 'co.in', 'net.in', 'org.in', 'gov.in', 'ac.in', 'res.in', 'co.kr', 'ne.kr', 'or.kr', 'go.kr', 'ac.kr', 'com.tw', 'net.tw', 'org.tw', 'gov.tw', 'edu.tw', 'idv.tw' ]); class URLResolutionCache { constructor() { this.hostnameCache = new LRUCache(1000, CONFIG.CACHE_TTL); this.domainCache = new LRUCache(1000, CONFIG.CACHE_TTL); this.absoluteUrlCache = new LRUCache(1000, CONFIG.CACHE_TTL); this.thirdPartyCache = new LRUCache(1000, CONFIG.CACHE_TTL); this.whitelistCache = new LRUCache(500, CONFIG.CACHE_TTL); this.urlCheckCache = new LRUCache(500, CONFIG.CACHE_TTL); } isDynamicURL(url) { if (!url || typeof url !== 'string') return false; const cacheKey = `dynamic_${url}`; if (this.urlCheckCache.has(cacheKey)) return this.urlCheckCache.get(cacheKey); const result = CONFIG.URL_DYNAMIC_PATTERNS.some(pattern => pattern.test(url)); this.urlCheckCache.set(cacheKey, result, CONFIG.CACHE_TTL); 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; this.hostnameCache.set(cacheKey, hostname, CONFIG.CACHE_TTL); return hostname; } catch (e) { this.hostnameCache.set(cacheKey, null, 30000); return null; } } isIPv4(hostname) { return /^(\d{1,3}\.){3}\d{1,3}$/.test(hostname); } getDomain(hostname) { if (!hostname) return null; const cacheKey = `domain_${hostname}`; if (this.domainCache.has(cacheKey)) return this.domainCache.get(cacheKey); if (this.isIPv4(hostname)) { this.domainCache.set(cacheKey, hostname, CONFIG.CACHE_TTL); return hostname; } const parts = hostname.split('.'); let domain = hostname; for (let i = 1; i <= parts.length; i++) { const candidate = parts.slice(-i).join('.'); if (PUBLIC_SUFFIX_LIST.has(candidate)) { if (i + 1 <= parts.length) { domain = parts.slice(-(i + 1)).join('.'); } else { domain = candidate; } break; } } if (domain === hostname && parts.length > 2) { domain = parts.slice(-2).join('.'); } this.domainCache.set(cacheKey, domain, CONFIG.CACHE_TTL); 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 : CONFIG.CACHE_TTL; this.absoluteUrlCache.set(cacheKey, absoluteUrl, ttl); return absoluteUrl; } catch (e) { this.absoluteUrlCache.set(cacheKey, url, 30000); return url; } } isThirdPartyHost(resourceHostname, currentHost, blockParentSubDomains = true) { if (!resourceHostname) return false; const cacheKey = `thirdparty_${resourceHostname}_${currentHost}_${blockParentSubDomains}`; if (this.thirdPartyCache.has(cacheKey)) return this.thirdPartyCache.get(cacheKey); let isThirdParty = false; if (!currentHost || !resourceHostname) { isThirdParty = false; } else if (resourceHostname === currentHost) { isThirdParty = false; } else if (blockParentSubDomains) { isThirdParty = true; } else { const currentDomain = this.getDomain(currentHost); const resourceDomain = this.getDomain(resourceHostname); isThirdParty = currentDomain !== resourceDomain; } this.thirdPartyCache.set(cacheKey, isThirdParty, CONFIG.CACHE_TTL); 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) { if (!pattern) continue; try { if (pattern.includes('://')) { if (url.includes(pattern)) { isWhitelisted = true; break; } } else { const urlHost = new URL(url, _location.href).hostname; let patternHost = pattern.startsWith('*.') ? pattern.substring(2) : pattern; const escapeRegExp = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); patternHost = escapeRegExp(patternHost); const regex = new RegExp(`(^|\\.)${patternHost}$`); if (regex.test(urlHost)) { isWhitelisted = true; break; } } } catch (e) { if (url.includes(pattern)) { isWhitelisted = true; break; } } } this.whitelistCache.set(cacheKey, isWhitelisted, CONFIG.CACHE_TTL); 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 ''; return this.truncateString(scriptElement.textContent, 200); }, getIframeSrcPreview(iframeElement) { if (!iframeElement || iframeElement.tagName !== 'IFRAME') return ''; return this.truncateString(iframeElement.src, 200); }, getResourceHostname(url) { return urlCache.getHostname(url); }, getDomain(hostname) { return urlCache.getDomain(hostname); }, isThirdPartyHost(resourceHostname, currentHost, blockParentSubDomains = true) { return urlCache.isThirdPartyHost(resourceHostname, currentHost, blockParentSubDomains); }, getAbsoluteURL(url) { return urlCache.getAbsoluteURL(url); }, isThirdParty(url, blockParentSubDomains = true) { if (!url) return false; const cacheKey = `isThirdParty_${url}_${_location.hostname}_${blockParentSubDomains}`; 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, CONFIG.CACHE_TTL); return false; } const result = urlCache.isThirdPartyHost(hostname, this.getCurrentHostname(), blockParentSubDomains); urlCache.urlCheckCache.set(cacheKey, result, CONFIG.CACHE_TTL); return result; } catch (e) { urlCache.urlCheckCache.set(cacheKey, false, CONFIG.CACHE_TTL); return false; } }, isSameOrigin(hostname) { return hostname === this.getCurrentHostname(); }, shouldInterceptByModule(element, moduleKey) { if (!currentConfig.modules[moduleKey]) return false; if (ProcessedElementsCache.isProcessed(element) || this.isParentProcessed(element)) return false; if (Whitelisting.isElementWhitelisted(element)) return false; return true; }, getBlockParentSubDomainsSetting() { return !!currentConfig.thirdPartyStrictMode; }, isUIElement(el) { return el?.classList && (el.classList.contains('mask') || el.classList.contains('panel') || el.id === 'ad-blocker-settings-container'); }, isPanelElement(el) { return el?.classList && el.classList.contains('panel'); }, isPanelClick(event) { const path = event.composedPath(); return path.some(el => this.isUIElement(el)); }, isContainerEmpty(container, ignoreProcessedFlag = true) { if (!this.isElement(container)) return false; if (container.children.length === 0 && container.textContent.trim() === '') { return this.isSuspiciousAdContainer(container); } let hasVisibleContent = false; for (const child of container.childNodes) { if (child.nodeType === Node.TEXT_NODE && child.textContent.trim().length > 0) { hasVisibleContent = true; break; } if (child.nodeType === Node.ELEMENT_NODE) { const el = child; if (ignoreProcessedFlag && el.dataset && el.dataset.adblockProcessed === 'true') continue; const style = getComputedStyle(el); if (style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0') { if (el.tagName === 'IMG' || el.tagName === 'VIDEO' || el.tagName === 'CANVAS' || (el.textContent && el.textContent.trim().length > 0)) { hasVisibleContent = true; break; } if ( style.opacity !== '0' && style.visibility !== 'hidden' && el.offsetWidth > 0 && el.offsetHeight > 0 && el.tagName !== 'BR' && el.tagName !== 'HR' ) { hasVisibleContent = true; break; } } } } return !hasVisibleContent; }, isSuspiciousAdContainer(container) { if (!this.isElement(container)) return false; const style = getComputedStyle(container); const hasAdStyles = ( (style.backgroundColor !== 'rgba(0, 0, 0, 0)' && style.backgroundColor !== 'transparent') || style.backgroundImage !== 'none' || style.borderWidth !== '0px' || style.boxShadow !== 'none' || style.paddingTop !== '0px' || style.paddingBottom !== '0px' || style.marginTop !== '0px' || style.marginBottom !== '0px' || (parseFloat(style.width) > CONFIG.RESIDUAL_MIN_WIDTH && parseFloat(style.height) > CONFIG.RESIDUAL_MIN_HEIGHT) || style.position === 'relative' || style.position === 'absolute' || style.position === 'fixed' || style.position === 'sticky' ); return hasAdStyles; }, quickAdFilter(container) { if (!Utils.isElement(container)) return false; if (ProcessedElementsCache.isProcessed(container)) return false; if (Utils.isUIElement(container)) return false; if (!currentConfig.residualCleanupEnabled) return false; const isEmpty = Utils.isContainerEmpty(container, true); if (!isEmpty) return false; return Utils.isSuspiciousAdContainer(container); }, debounce(func, wait = CONFIG.DEBOUNCE_WAIT) { let timeout; return function(...args) { _clearTimeout(timeout); timeout = _setTimeout(() => func.apply(this, args), wait); }; }, throttle(func, limit = CONFIG.THROTTLE_LIMIT) { let inThrottle; return function(...args) { if (!inThrottle) { func.apply(this, args); inThrottle = true; _setTimeout(() => inThrottle = false, limit); } }; }, isParentProcessed(element) { let parent = element.parentElement; while (parent) { if (parent.dataset.adblockProcessed === 'true' || ProcessedElementsCache.isProcessed(parent)) return true; parent = parent.parentElement; } return false; }, getContentIdentifier(element, reasonType = null) { if (!element && !reasonType) return null; if (element && this.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: ${this.truncateString(element.src, 500)}` : `SCRIPT_CONTENT: ${this.getScriptContentPreview(element)}`; } else if (tagName === 'IFRAME') { return `IFRAME_SRC: ${this.truncateString(element.src, 500)}`; } else if (tagName === 'IMG') { return src ? `IMG_SRC: ${this.truncateString(src, 500)}` : null; } else if (tagName === 'A') { return src ? `A_HREF: ${this.truncateString(src, 500)}` : null; } else if (tagName === 'LINK' && element.rel === 'stylesheet' && element.href) { return `CSS_HREF: ${this.truncateString(element.href, 500)}`; } else if (tagName === 'STYLE') { return `STYLE_CONTENT: ${this.truncateString(element.textContent, 500)}`; } else if (tagName === 'EMBED') { return src ? `EMBED_SRC: ${this.truncateString(src, 500)}` : null; } else if (tagName === 'OBJECT') { return src ? `OBJECT_DATA: ${this.truncateString(src, 500)}` : null; } return null; } else if (reasonType && typeof reasonType.detail === 'string') { if (reasonType.detail.startsWith('SRC:')) { return `${reasonType.type || 'INTERCEPTED'}_SRC: ${this.truncateString(reasonType.detail.substring(4).trim(), 500)}`; } else if (reasonType.detail.startsWith('URL:')) { return `${reasonType.type || 'INTERCEPTED'}_URL: ${this.truncateString(reasonType.detail.substring(5).trim(), 500)}`; } else if (reasonType.type === 'EVAL') { return `EVAL_HASH: ${generateContentHash(reasonType.detail, 500)}`; } else if (reasonType.type === 'FUNCTION_CONSTRUCTOR') { return `FUNCTION_HASH: ${generateContentHash(reasonType.detail, 500)}`; } else if (reasonType.type === 'DOCUMENT_WRITE') { return `DOCUMENT_WRITE_HASH: ${generateContentHash(reasonType.detail, 500)}`; } else if (reasonType.type === 'SETTIMEOUT') { return `SETTIMEOUT_HASH: ${generateContentHash(reasonType.detail, 500)}`; } else if (reasonType.type === 'SETINTERVAL') { return `SETINTERVAL_HASH: ${generateContentHash(reasonType.detail, 500)}`; } else if (reasonType.type === 'REQUESTANIMATIONFRAME') { return `REQUESTANIMATIONFRAME_HASH: ${generateContentHash(reasonType.detail, 500)}`; } else if (reasonType.type === 'THIRD_PARTY') { const urlMatch = reasonType.detail.match(/(https?:\/\/[^\s]+)/); if (urlMatch) return `THIRD_PARTY_URL: ${this.truncateString(urlMatch[1], 500)}`; return `THIRD_PARTY_DETAIL: ${this.truncateString(reasonType.detail, 500)}`; } else if (reasonType.type === 'SCRIPT_BLACKLIST') { return `BLACKLIST: ${this.truncateString(reasonType.detail, 500)}`; } else if (reasonType.type === '内联事件') { return `INLINE_EVENT: ${reasonType.detail}`; } else if (reasonType.type === 'javascript URL') { return `JAVASCRIPT_URL: ${reasonType.detail}`; } return `LOG_DETAIL: ${this.truncateString(reasonType.detail, 500)}`; } return null; } }; function shouldBlockResource(url) { if (!url) return false; if (urlCache.isWhitelisted(url, currentConfig.thirdPartyWhitelist)) return false; for (const keyword of currentConfig.keywordWhitelist) { if (url.includes(keyword)) return false; } return Utils.isThirdParty(url, Utils.getBlockParentSubDomainsSetting()); } const LogManager = { logs: [], maxLogs: CONFIG.LOG_MAX, loggedContentIdentifiers: new LRUCache(CONFIG.LOG_MAX, CONFIG.LOG_IDENTIFIER_TTL), add(moduleKey, element, reason) { const anyModuleEnabled = Object.values(currentConfig.modules).some(v => v === true); if (!anyModuleEnabled) return; if (!Utils.isElement(element) && element !== null && typeof reason !== 'object') return; if (Whitelisting.isElementWhitelisted(element) || Whitelisting.isReasonWhitelisted(reason)) return; let elementIdentifier = '[未知元素]', interceptedContent = '[无法获取内容]', contentIdentifier = null, resourceDomain = ''; if (reason && typeof reason.detail === 'string' && ( reason.type === '内联事件' || reason.type === 'javascript URL' || reason.type === 'EVAL' || reason.type === 'FUNCTION_CONSTRUCTOR' || reason.type === 'DOCUMENT_WRITE' || reason.type === 'SETTIMEOUT' || reason.type === 'SETINTERVAL' || reason.type === 'REQUESTANIMATIONFRAME' || reason.type === 'THIRD_PARTY' || reason.type === 'SCRIPT_BLACKLIST' || reason.type === 'THIRD_PARTY_SCAN' )) { interceptedContent = Utils.truncateString(reason.detail, 500); elementIdentifier = reason.type ? `[${reason.type}]` : '[未知类型]'; if (reason.type === 'EVAL' || reason.type === 'FUNCTION_CONSTRUCTOR' || reason.type === 'DOCUMENT_WRITE' || reason.type === 'SETTIMEOUT' || reason.type === 'SETINTERVAL' || reason.type === 'REQUESTANIMATIONFRAME') { contentIdentifier = Utils.getContentIdentifier(null, { type: reason.type, detail: reason.rawDetail || reason.detail }); } else { 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) {} } } else 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, 500)}`; 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 || element.getAttribute('data-src') || ''; interceptedContent = Utils.truncateString(src, 500); if (src) resourceDomain = Utils.getResourceHostname(src) || ''; } else if (tagName === 'A') { interceptedContent = Utils.truncateString(element.href || '', 500); if (element.href) resourceDomain = Utils.getResourceHostname(element.href) || ''; } else if (tagName === 'LINK') { interceptedContent = Utils.truncateString(element.href || '', 500); if (element.href) resourceDomain = Utils.getResourceHostname(element.href) || ''; } else if (tagName === 'STYLE') { interceptedContent = Utils.truncateString(element.textContent, 500); } else if (tagName === 'EMBED') { interceptedContent = Utils.truncateString(element.src || '', 500); if (element.src) resourceDomain = Utils.getResourceHostname(element.src) || ''; } else if (tagName === 'OBJECT') { interceptedContent = Utils.truncateString(element.data || '', 500); if (element.data) resourceDomain = Utils.getResourceHostname(element.data) || ''; } else { interceptedContent = Utils.truncateString(element.outerHTML, 500); } } else if (reason && typeof reason.detail === 'string') { interceptedContent = Utils.truncateString(reason.detail, 500); elementIdentifier = reason.type ? `[${reason.type}]` : '[未知类型]'; if (reason.type === 'EVAL' || reason.type === 'FUNCTION_CONSTRUCTOR' || reason.type === 'DOCUMENT_WRITE' || reason.type === 'SETTIMEOUT' || reason.type === 'SETINTERVAL' || reason.type === 'REQUESTANIMATIONFRAME') { contentIdentifier = Utils.getContentIdentifier(null, { type: reason.type, detail: reason.rawDetail || reason.detail }); } else { 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) return; const existingIndex = this.logs.findIndex(l => l.contentIdentifier === contentIdentifier); if (existingIndex !== -1) { this.logs.splice(existingIndex, 1); } else if (this.loggedContentIdentifiers.has(contentIdentifier)) { return; } const logId = this.logs.length + 1; const logEntry = { id: logId, moduleKey: moduleKey, module: MODULE_NAMES[moduleKey] || moduleKey, element: elementIdentifier, content: interceptedContent, domain: resourceDomain, timestamp: Date.now(), contentIdentifier: contentIdentifier }; this.logs.push(logEntry); this.loggedContentIdentifiers.set(contentIdentifier, true); if (this.logs.length > this.maxLogs) { const removed = this.logs.shift(); this.loggedContentIdentifiers.delete(removed.contentIdentifier); } }, clearLoggedIdentifiers() { this.loggedContentIdentifiers.clear(); } }; const Whitelisting = { isElementWhitelisted(element) { if (!element || !Utils.isElement(element)) return false; const contentIdentifier = Utils.getContentIdentifier(element); if (contentIdentifier && currentConfig.whitelist.has(contentIdentifier)) return true; if (currentConfig.modules.interceptThirdParty) { const hostname = Utils.getResourceHostname(element.src || element.href || element.action || element.data || ''); if (hostname) { const resourceUrl = element.src || element.href || element.action || element.data || ''; if (urlCache.isWhitelisted(resourceUrl, currentConfig.thirdPartyWhitelist)) return true; } } const keywordWhitelist = currentConfig.keywordWhitelist; if (keywordWhitelist.size > 0) { const scriptContent = element.textContent || ''; const src = element.src || element.href || element.action || element.data || ''; for (const keyword of keywordWhitelist) { if (!keyword) continue; if (scriptContent.includes(keyword) || src.includes(keyword)) return true; } } return false; }, isReasonWhitelisted(reason) { if (!reason || typeof reason.detail !== 'string') return false; let contentIdentifier = null; if (reason.type === 'EVAL' || reason.type === 'FUNCTION_CONSTRUCTOR' || reason.type === 'DOCUMENT_WRITE' || reason.type === 'SETTIMEOUT' || reason.type === 'SETINTERVAL' || reason.type === 'REQUESTANIMATIONFRAME') { contentIdentifier = Utils.getContentIdentifier(null, { type: reason.type, detail: reason.rawDetail || reason.detail }); } else { contentIdentifier = Utils.getContentIdentifier(null, reason); } if (contentIdentifier && currentConfig.whitelist.has(contentIdentifier)) return true; if (currentConfig.modules.interceptThirdParty) { const urlMatch = reason.detail.match(/https?:\/\/[^\s]+/); if (urlMatch) { const url = urlMatch[0]; if (urlCache.isWhitelisted(url, currentConfig.thirdPartyWhitelist)) return true; } } const keywordWhitelist = currentConfig.keywordWhitelist; if (keywordWhitelist.size > 0) { for (const keyword of keywordWhitelist) { if (!keyword) continue; if (reason.detail.includes(keyword)) return true; } } return false; }, isCodeWhitelisted(code, type) { if (typeof code !== 'string' || code.trim() === '') return false; if (currentConfig.keywordWhitelist.size > 0) { for (const keyword of currentConfig.keywordWhitelist) { if (!keyword) continue; if (code.includes(keyword)) return true; } } const contentIdentifier = Utils.getContentIdentifier(null, { type: type, detail: code }); if (contentIdentifier && currentConfig.whitelist.has(contentIdentifier)) return true; return false; }, add(contentIdentifier) { if (!contentIdentifier || contentIdentifier.trim() === '') return; currentConfig.whitelist.add(contentIdentifier); StorageManager.saveConfig(); }, addKeyword(keyword) { if (!keyword || keyword.trim() === '') return; currentConfig.keywordWhitelist.add(keyword.trim()); StorageManager.saveConfig(); }, removeKeywordsMatchingDomain(domain) { let changed = false; const keywordsToRemove = []; for (const keyword of currentConfig.keywordWhitelist) { if (domain.includes(keyword)) keywordsToRemove.push(keyword); } keywordsToRemove.forEach(k => { currentConfig.keywordWhitelist.delete(k); changed = true; }); if (changed) StorageManager.saveConfig(); }, clearAllWhitelists() { currentConfig.whitelist.clear(); currentConfig.keywordWhitelist.clear(); currentConfig.thirdPartyWhitelist = []; StorageManager.saveConfig(); } }; 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 ResidualCleaner = { _scanTimer: null, _dedupSet: new WeakSet(), observer: null, init() { const anyModuleEnabled = Object.values(currentConfig.modules).some(v => v === true); if (!anyModuleEnabled) return; if (!currentConfig.residualCleanupEnabled) { this.stop(); return; } this.setupMutationObserver(); this.initialScan(); this.startPeriodicScan(); }, stop() { if (this.observer) { this.observer.disconnect(); this.observer = null; } if (this._scanTimer !== null) { _cancelIdleCallback(this._scanTimer); this._scanTimer = null; } this._dedupSet = new WeakSet(); }, setupMutationObserver() { this.observer = new _MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.type === 'childList') { if (mutation.addedNodes.length > 0) { for (const node of mutation.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE && Utils.isElement(node)) { this.checkAndCleanup(node); } } } if (mutation.removedNodes.length > 0 && mutation.target && Utils.isElement(mutation.target)) { this.checkAndCleanup(mutation.target); } } } }); this.observer.observe(_document.documentElement, { childList: true, subtree: true, attributes: false }); }, checkAndCleanup(element) { if (!element || !Utils.isElement(element) || this._dedupSet.has(element)) return; if (!Utils.quickAdFilter(element)) return; this._dedupSet.add(element); this.cleanupContainer(element); }, cleanupContainer(container) { if (!container || !Utils.isElement(container) || !container.isConnected) return; if (container === _document.body || container === _document.documentElement) return; const style = getComputedStyle(container); if (style.position === 'absolute' || style.position === 'fixed' || style.position === 'sticky') { container.style.setProperty('display', 'none', 'important'); container.style.setProperty('visibility', 'hidden', 'important'); container.style.setProperty('pointer-events', 'none', 'important'); if (container.style.height !== '0px') container.style.setProperty('height', '0px', 'important'); if (container.style.width !== '0px') container.style.setProperty('width', '0px', 'important'); container.style.setProperty('opacity', '0', 'important'); ProcessedElementsCache.markAsProcessed(container); } else { container.remove(); ProcessedElementsCache.markAsProcessed(container); } }, initialScan() { const elementsToScan = Array.from(_document.querySelectorAll(CONFIG.RESIDUAL_AD_SELECTORS)); for (let i = 0; i < elementsToScan.length; i++) { this.checkAndCleanup(elementsToScan[i]); } }, startPeriodicScan() { const scanLoop = () => { if (!currentConfig.residualCleanupEnabled) return; const elementsToScan = Array.from(_document.querySelectorAll(CONFIG.RESIDUAL_AD_SELECTORS)); for (let i = 0; i < elementsToScan.length; i++) { this.checkAndCleanup(elementsToScan[i]); } this._scanTimer = _requestIdleCallback(scanLoop); }; this._scanTimer = _requestIdleCallback(scanLoop); } }; const CSSUniversalHider = { _enabled: false, _observer: null, _scanTimer: null, _processedSet: new WeakSet(), _hideClass: 'adblock-universal-hidden', _styleElement: null, _universalSelector: `[style^='left'],[style^='visibility:visible;padding:0;margin:0;-webkit-appearance:none;position:absolute !important;left:0px;top:-'],[style*='background-size: 400px 127px !important;'],[style*='background-size: 470px 149px !important;'],[style*='position: fixed; left: 0px; transform: none; top: 0px; z-index: '],[style$='width:100vw;display:block;'],[style*='width: 100vw; position: fixed;'],[style$='important;'],p[class],ul *,UL,H2,body.clearfix *,[style^='display: block; z-index: '],[style^='background-image: url('],body#read.read *,.adsbygoogle[referrerpolicy],[style='height: 125px;'],[style^='display: block;'],[style='height: 125.002px;'],[style$='display: block;'],[class*='_'][id*='_'],[style='height:0px;'],[style*='background: url'],[style*='width: 100vw; top:']`, init() { if (currentConfig.cssUniversalHideEnabled) { this.enable(); } else { this.disable(); } }, enable() { if (this._enabled) return; this._enabled = true; this.injectStyle(); this.startObserver(); this.startPeriodicScan(); this.scanAndHide(); }, disable() { if (!this._enabled) return; this._enabled = false; if (this._observer) { this._observer.disconnect(); this._observer = null; } if (this._scanTimer) { _clearTimeout(this._scanTimer); this._scanTimer = null; } if (this._styleElement && this._styleElement.parentNode) { this._styleElement.parentNode.removeChild(this._styleElement); this._styleElement = null; } const elements = _document.querySelectorAll(`.${this._hideClass}`); elements.forEach(el => { el.classList.remove(this._hideClass); el.style.display = ''; el.style.visibility = ''; el.style.opacity = ''; el.style.position = ''; el.style.left = ''; el.style.top = ''; el.style.width = ''; el.style.height = ''; el.style.pointerEvents = ''; }); this._processedSet = new WeakSet(); }, injectStyle() { if (this._styleElement) return; const style = _document.createElement('style'); style.textContent = `.${this._hideClass} { display: none !important; visibility: hidden !important; opacity: 0 !important; pointer-events: none !important; position: absolute !important; left: -9999px !important; top: -9999px !important; width: 0 !important; height: 0 !important; overflow: hidden !important; z-index: -999 !important; }`; _document.head.appendChild(style); this._styleElement = style; }, shouldHideElement(el) { if (!el || el.nodeType !== 1) return false; if (el.getAttribute && el.getAttribute('data-adblock-safe') === 'true') return false; if (Utils.isUIElement(el)) return false; if (this._processedSet.has(el)) return false; let zIndex = parseInt(window.getComputedStyle(el).zIndex); if (isNaN(zIndex)) zIndex = 0; if (zIndex > 600) return true; return false; }, hideElement(el) { if (!this.shouldHideElement(el)) return; el.classList.add(this._hideClass); this._processedSet.add(el); }, scanAndHide() { if (!this._enabled) return; let elements; try { elements = _document.querySelectorAll(this._universalSelector); } catch(e) { elements = []; } for (let i = 0; i < elements.length; i++) { const el = elements[i]; if (this.shouldHideElement(el)) { this.hideElement(el); } } }, startObserver() { if (this._observer) this._observer.disconnect(); this._observer = new _MutationObserver((mutations) => { let needsScan = false; for (const mutation of mutations) { if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { needsScan = true; break; } if (mutation.type === 'attributes' && (mutation.attributeName === 'style' || mutation.attributeName === 'class')) { needsScan = true; break; } } if (needsScan) { this.scanAndHide(); } }); this._observer.observe(_document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); }, startPeriodicScan() { const scanLoop = () => { try { if (!this._enabled) return; this.scanAndHide(); } catch(e) {} this._scanTimer = _setTimeout(scanLoop, 5000); }; this._scanTimer = _setTimeout(scanLoop, 5000); } }; const ResourceCanceller = { cancelResourceLoading(element) { if (!Utils.isElement(element) || ProcessedElementsCache.isProcessed(element)) return; const tagName = element.tagName; if (tagName === 'IMG') { element.removeAttribute('src'); element.removeAttribute('srcset'); element.removeAttribute('data-src'); } else if (tagName === 'IFRAME') { element.removeAttribute('src'); element.style.display = 'none'; } else if (tagName === 'SCRIPT') { element.removeAttribute('src'); } else if (tagName === 'LINK' && element.rel === 'stylesheet') { element.removeAttribute('href'); } else if (tagName === 'STYLE') { element.textContent = ''; } else if (tagName === 'EMBED') { element.removeAttribute('src'); } else if (tagName === 'OBJECT') { element.removeAttribute('data'); } const parent = element.parentElement; if (parent && parent !== _document.body && parent !== _document.documentElement) { if (Utils.isContainerEmpty(parent, true) && Utils.isSuspiciousAdContainer(parent)) { ResidualCleaner.checkAndCleanup(parent); } } if (element.parentNode) element.parentNode.removeChild(element); ProcessedElementsCache.markAsProcessed(element); } }; const TAG_HANDLERS = { SCRIPT: { srcAttr: 'src', inlineContent: true, check: function(element, moduleKey, reason) { const contentIdentifier = Utils.getContentIdentifier(element); if (contentIdentifier && !currentConfig.whitelist.has(contentIdentifier)) { LogManager.add(moduleKey, element, reason); if (moduleKey === 'removeExternalScripts' || moduleKey === 'scriptBlacklistMode') { ResourceCanceller.cancelResourceLoading(element); } else if (moduleKey === 'removeInlineScripts' && !element.src) { element.remove(); } ProcessedElementsCache.markAsProcessed(element); return true; } return false; } }, IFRAME: { srcAttr: 'src', check: function(element, moduleKey) { if (moduleKey !== 'interceptThirdParty') return false; const url = element.src; if (url && shouldBlockResource(url)) { LogManager.add(moduleKey, element, { type: 'THIRD_PARTY', detail: `IFRAME: ${Utils.truncateString(url,500)}` }); ResourceCanceller.cancelResourceLoading(element); ProcessedElementsCache.markAsProcessed(element); return true; } return false; } }, IMG: { srcAttr: 'src', dataSrcAttr: 'data-src', check: function(element, moduleKey) { if (moduleKey !== 'interceptThirdParty') return false; const url = element.src || element.getAttribute('data-src'); if (url && shouldBlockResource(url)) { LogManager.add(moduleKey, element, { type: 'THIRD_PARTY', detail: `IMG: ${Utils.truncateString(url,500)}` }); ResourceCanceller.cancelResourceLoading(element); ProcessedElementsCache.markAsProcessed(element); return true; } return false; } }, LINK: { srcAttr: 'href', check: function(element, moduleKey) { if (moduleKey !== 'interceptThirdParty') return false; const url = element.href; if (url && element.rel === 'stylesheet' && shouldBlockResource(url)) { LogManager.add(moduleKey, element, { type: 'THIRD_PARTY', detail: `LINK: ${Utils.truncateString(url,500)}` }); ResourceCanceller.cancelResourceLoading(element); ProcessedElementsCache.markAsProcessed(element); return true; } return false; } }, EMBED: { srcAttr: 'src', check: function(element, moduleKey) { if (moduleKey !== 'interceptThirdParty') return false; const url = element.src; if (url && shouldBlockResource(url)) { LogManager.add(moduleKey, element, { type: 'THIRD_PARTY', detail: `EMBED: ${Utils.truncateString(url,500)}` }); ResourceCanceller.cancelResourceLoading(element); ProcessedElementsCache.markAsProcessed(element); return true; } return false; } }, OBJECT: { srcAttr: 'data', check: function(element, moduleKey) { if (moduleKey !== 'interceptThirdParty') return false; const url = element.data; if (url && shouldBlockResource(url)) { LogManager.add(moduleKey, element, { type: 'THIRD_PARTY', detail: `OBJECT: ${Utils.truncateString(url,500)}` }); ResourceCanceller.cancelResourceLoading(element); ProcessedElementsCache.markAsProcessed(element); return true; } return false; } }, A: { srcAttr: 'href', check: function(element, moduleKey) { if (moduleKey !== 'interceptThirdParty') return false; const url = element.href; if (url && shouldBlockResource(url)) { LogManager.add(moduleKey, element, { type: 'THIRD_PARTY', detail: `A: ${Utils.truncateString(url,500)}` }); element.href = 'javascript:void(0)'; element.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation(); return false; }, true); ProcessedElementsCache.markAsProcessed(element); return true; } return false; } } }; class BaseModule { constructor(moduleKey) { this.moduleKey = moduleKey; this.enabled = false; this.observer = null; } init() { if (currentConfig.modules[this.moduleKey]) this.enable(); else this.disable(); } enable() { if (this.enabled) return; this.enabled = true; this.onEnable(); } disable() { if (!this.enabled) return; this.enabled = false; this.onDisable(); } onEnable() {} onDisable() { if (this.observer) { this.observer.disconnect(); this.observer = null; } } checkElement(element) { if (!Utils.shouldInterceptByModule(element, this.moduleKey)) return false; return this._checkElement(element); } _checkElement(element) { throw new Error('_checkElement must be implemented by subclass'); } } class RemoveInlineScriptsModule extends BaseModule { constructor() { super('removeInlineScripts'); this.attributeObserver = null; } onEnable() { _document.querySelectorAll('script:not([src])').forEach(script => this.checkElement(script)); if (currentConfig.inlineScriptStrictMode) { _document.querySelectorAll('*').forEach(el => this.sanitizeInlineEventAttributes(el)); } this.observer = new _MutationObserver(mutations => { for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (node.nodeType === 1) { if (currentConfig.inlineScriptStrictMode) this.sanitizeInlineEventAttributes(node); if (node.tagName === 'SCRIPT' && !node.src) this.checkElement(node); } } } }); this.observer.observe(_document.documentElement, { childList: true, subtree: true }); if (currentConfig.inlineScriptStrictMode) { this.attributeObserver = new _MutationObserver(mutations => { for (const mutation of mutations) { if (mutation.type === 'attributes' && mutation.target.nodeType === 1) { const attrName = mutation.attributeName; const target = mutation.target; if (attrName && attrName.toLowerCase().startsWith('on')) { const value = target.getAttribute(attrName); if (value && typeof value === 'string' && value.trim() !== '') { const reason = { type: '内联事件', detail: `属性: ${attrName}="${value}"` }; if (!Whitelisting.isReasonWhitelisted(reason)) { LogManager.add(this.moduleKey, target, reason); target.removeAttribute(attrName); ProcessedElementsCache.markAsProcessed(target); } } } else if (attrName === 'href' || attrName === 'src' || attrName === 'action' || attrName === 'data' || attrName === 'formaction') { const value = target.getAttribute(attrName); if (value && typeof value === 'string' && value.trim().toLowerCase().startsWith('javascript:')) { const reason = { type: 'javascript URL', detail: `${attrName}="${value}"` }; if (!Whitelisting.isReasonWhitelisted(reason)) { LogManager.add(this.moduleKey, target, reason); target.removeAttribute(attrName); ProcessedElementsCache.markAsProcessed(target); } } } } } }); this.attributeObserver.observe(_document.documentElement, { attributes: true, subtree: true, attributeFilter: ['onclick', 'onload', 'onerror', 'onmouseover', 'onfocus', 'onblur', 'onsubmit', 'onchange', 'onkeydown', 'onkeyup', 'ontouchstart', 'ontouchend', 'ontouchmove', 'oncontextmenu', 'href', 'src', 'action', 'data', 'formaction'] }); } } onDisable() { super.onDisable(); if (this.attributeObserver) { this.attributeObserver.disconnect(); this.attributeObserver = null; } } _checkElement(element) { if (element.tagName === 'SCRIPT' && !element.src) { return TAG_HANDLERS.SCRIPT.check(element, this.moduleKey, { type: '内嵌脚本移除', detail: `内容: ${Utils.truncateString(element.textContent,500)}` }); } return false; } sanitizeInlineEventAttributes(element) { if (!Utils.isElement(element) || ProcessedElementsCache.isProcessed(element)) return false; let modified = false; const attrs = element.attributes; if (attrs) { for (let i = attrs.length - 1; i >= 0; i--) { const attr = attrs[i]; if (attr.name.toLowerCase().startsWith('on') && typeof attr.value === 'string' && attr.value.trim() !== '') { const reason = { type: '内联事件', detail: `属性: ${attr.name}="${attr.value}"` }; if (!Whitelisting.isReasonWhitelisted(reason)) { LogManager.add(this.moduleKey, element, reason); element.removeAttribute(attr.name); modified = true; } } } } const dangerousAttrs = ['href', 'src', 'action', 'data', 'formaction']; dangerousAttrs.forEach(attr => { const val = element.getAttribute(attr); if (val && typeof val === 'string' && val.trim().toLowerCase().startsWith('javascript:')) { const reason = { type: 'javascript URL', detail: `${attr}="${val}"` }; if (!Whitelisting.isReasonWhitelisted(reason)) { LogManager.add(this.moduleKey, element, reason); element.removeAttribute(attr); modified = true; } } }); if (modified) ProcessedElementsCache.markAsProcessed(element); return modified; } updateStrictMode() { if (this.enabled) { this.onDisable(); this.onEnable(); } } } class RemoveExternalScriptsModule extends BaseModule { constructor() { super('removeExternalScripts'); } onEnable() { _document.querySelectorAll('script[src]').forEach(script => this.checkElement(script)); this.observer = new _MutationObserver(mutations => { for (const mutation of mutations) { if (mutation.type === 'childList') { for (const node of mutation.addedNodes) { if (node.nodeType === 1) { if (node.tagName === 'SCRIPT' && node.src) { this.checkElement(node); } if (node.querySelectorAll) { node.querySelectorAll('script[src]').forEach(s => this.checkElement(s)); } } } } else if (mutation.type === 'attributes' && mutation.attributeName === 'src') { const el = mutation.target; if (el.tagName === 'SCRIPT' && el.src) { this.checkElement(el); } } } }); this.observer.observe(_document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ['src'] }); } _checkElement(element) { if (element.tagName === 'SCRIPT' && element.src) { return TAG_HANDLERS.SCRIPT.check(element, this.moduleKey, { type: '外联脚本移除', detail: `SRC: ${Utils.truncateString(element.src,500)}` }); } return false; } } class ScriptBlacklistModeModule extends BaseModule { constructor() { super('scriptBlacklistMode'); } onEnable() { _document.querySelectorAll('script').forEach(script => this.checkElement(script)); this.observer = new _MutationObserver(mutations => { for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (node.nodeType === 1 && node.tagName === 'SCRIPT') this.checkElement(node); } } }); this.observer.observe(_document.documentElement, { childList: true, subtree: true }); } _checkElement(element) { if (element.tagName !== 'SCRIPT') return false; const blacklist = currentConfig.scriptBlacklist; if (!blacklist || blacklist.size === 0) return false; const scriptContent = element.textContent; const scriptSrc = element.src; let matched = false, matchedKeyword = ''; for (const keyword of blacklist) { if (!keyword) continue; if (scriptSrc && scriptSrc.includes(keyword)) { matched = true; matchedKeyword = keyword; break; } if (!scriptSrc && scriptContent && scriptContent.includes(keyword)) { matched = true; matchedKeyword = keyword; break; } } if (matched) { LogManager.add(this.moduleKey, element, { type: 'SCRIPT_BLACKLIST', detail: `命中关键词: ${matchedKeyword} - ${scriptSrc ? `SRC: ${Utils.truncateString(scriptSrc,500)}` : `内嵌: ${Utils.truncateString(scriptContent,500)}`}` }); ResourceCanceller.cancelResourceLoading(element); ProcessedElementsCache.markAsProcessed(element); return true; } return false; } } class ThirdPartyInterceptionModule extends BaseModule { constructor() { super('interceptThirdParty'); this.originalSetAttribute = null; this.originalFetch = null; this.originalXhrOpen = null; this.originalXhrSend = null; this.originalInnerHTMLSetter = null; this.originalInsertAdjacentHTML = null; this.originalCreateElement = null; this.originalAppendChild = null; this.originalInsertBefore = null; this.originalAttachShadow = null; this.restoredFns = []; } onEnable() { this.stopInterception(); this.setupProxyInterception(); this.setupNetworkInterception(); this.setupMutationFallback(); this.setupHTMLInterception(); this.patchAttachShadow(); if (currentConfig.thirdPartyStrictMethod) { this.setupStrictDOMInterception(); } } onDisable() { this.stopInterception(); } stopInterception() { if (this.originalSetAttribute) { _Element.prototype.setAttribute = this.originalSetAttribute; this.originalSetAttribute = null; } if (this.originalFetch) { _globals.fetch = this.originalFetch; this.originalFetch = null; } if (this.originalXhrOpen) { _XMLHttpRequest.prototype.open = this.originalXhrOpen; _XMLHttpRequest.prototype.send = this.originalXhrSend; this.originalXhrOpen = null; this.originalXhrSend = null; } if (this.originalInnerHTMLSetter) { const desc = Object.getOwnPropertyDescriptor(_Element.prototype, 'innerHTML'); if (desc && desc.set) { Object.defineProperty(_Element.prototype, 'innerHTML', { set: this.originalInnerHTMLSetter, configurable: true }); } this.originalInnerHTMLSetter = null; } if (this.originalInsertAdjacentHTML) { _Element.prototype.insertAdjacentHTML = this.originalInsertAdjacentHTML; this.originalInsertAdjacentHTML = null; } if (this.originalCreateElement) { _document.createElement = this.originalCreateElement; this.originalCreateElement = null; } if (this.originalAppendChild) { _Node.prototype.appendChild = this.originalAppendChild; this.originalAppendChild = null; } if (this.originalInsertBefore) { _Node.prototype.insertBefore = this.originalInsertBefore; this.originalInsertBefore = null; } if (this.originalAttachShadow) { _Element.prototype.attachShadow = this.originalAttachShadow; this.originalAttachShadow = null; } this.restoredFns.forEach(fn => { try { fn(); } catch(e) {} }); this.restoredFns = []; if (this.observer) { this.observer.disconnect(); this.observer = null; } } setupProxyInterception() { const self = this; const logAndCancel = (element, attr, url, tagName) => { LogManager.add(this.moduleKey, element, { type: 'THIRD_PARTY', detail: `${tagName}: ${Utils.truncateString(url,500)}` }); ResourceCanceller.cancelResourceLoading(element); ProcessedElementsCache.markAsProcessed(element); }; this.originalSetAttribute = _Element.prototype.setAttribute; const setAttributeProxy = new _Proxy(_Element.prototype.setAttribute, { apply(target, thisArg, args) { const [name, value] = args; const tagName = thisArg.tagName; const shouldIntercept = TAG_HANDLERS[tagName] && TAG_HANDLERS[tagName].srcAttr === name && shouldBlockResource(value); let result; try { result = Reflect.apply(target, thisArg, args); } catch (e) { throw e; } if (shouldIntercept) { logAndCancel(thisArg, name, value, tagName); } return result; } }); _Element.prototype.setAttribute = setAttributeProxy; this.restoredFns.push(() => { _Element.prototype.setAttribute = this.originalSetAttribute; }); for (const tagName in TAG_HANDLERS) { const proto = _globals[`HTML${tagName}Element`]?.prototype; if (!proto) continue; const srcAttr = TAG_HANDLERS[tagName].srcAttr; try { const desc = Object.getOwnPropertyDescriptor(proto, srcAttr); if (desc && desc.set && desc.configurable !== false) { const originalSetter = desc.set; const setterProxy = new _Proxy(originalSetter, { apply(target, thisArg, args) { const [value] = args; const shouldIntercept = shouldBlockResource(value); let result; try { result = Reflect.apply(target, thisArg, args); } catch (e) { throw e; } if (shouldIntercept) { logAndCancel(thisArg, srcAttr, value, tagName); } return result; } }); Object.defineProperty(proto, srcAttr, { set: setterProxy, get: desc.get, configurable: true, enumerable: true }); this.restoredFns.push(() => { Object.defineProperty(proto, srcAttr, desc); }); } } catch (e) {} } } setupNetworkInterception() { const self = this; this.originalFetch = _fetch; const fetchProxy = new _Proxy(_fetch, { apply(target, thisArg, args) { const input = args[0]; const url = typeof input === 'string' ? input : input?.url; if (url && shouldBlockResource(url)) { LogManager.add(self.moduleKey, null, { type: 'THIRD_PARTY', detail: `FETCH: ${Utils.truncateString(url,500)}` }); return Promise.reject(new Error('广告拦截器:拦截第三方请求')); } return Reflect.apply(target, thisArg, args); } }); _globals.fetch = fetchProxy; this.restoredFns.push(() => { _globals.fetch = this.originalFetch; }); this.originalXhrOpen = _XMLHttpRequest.prototype.open; this.originalXhrSend = _XMLHttpRequest.prototype.send; const openProxy = new _Proxy(_XMLHttpRequest.prototype.open, { apply(target, thisArg, args) { const method = args[0]; const url = args[1]; if (url && shouldBlockResource(url)) { LogManager.add(self.moduleKey, null, { type: 'THIRD_PARTY', detail: `XHR: ${Utils.truncateString(url,500)}` }); try { thisArg.open(method, 'about:blank', ...args.slice(2)); thisArg.abort(); } catch (e) {} throw new Error('广告拦截器:拦截第三方XHR请求'); } return Reflect.apply(target, thisArg, args); } }); _XMLHttpRequest.prototype.open = openProxy; this.restoredFns.push(() => { _XMLHttpRequest.prototype.open = this.originalXhrOpen; _XMLHttpRequest.prototype.send = this.originalXhrSend; }); } setupMutationFallback() { const self = this; this.observer = new _MutationObserver((mutations) => { for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (node.nodeType !== 1) continue; const tagName = node.tagName; if (TAG_HANDLERS[tagName]) { const srcAttr = TAG_HANDLERS[tagName].srcAttr; const dataSrcAttr = TAG_HANDLERS[tagName].dataSrcAttr; const value = node[srcAttr] || node.getAttribute(srcAttr) || (dataSrcAttr && node.getAttribute(dataSrcAttr)); if (value && shouldBlockResource(value)) { LogManager.add(self.moduleKey, node, { type: 'THIRD_PARTY', detail: `${tagName}: ${Utils.truncateString(value,500)}` }); ResourceCanceller.cancelResourceLoading(node); ProcessedElementsCache.markAsProcessed(node); } } if (node.shadowRoot) { self.observeShadowRoot(node.shadowRoot); } } } }); this.observer.observe(_document.documentElement, { childList: true, subtree: true }); } observeShadowRoot(shadowRoot) { if (!shadowRoot || shadowRoot._adblockObserved) return; shadowRoot._adblockObserved = true; const self = this; const observer = new _MutationObserver((mutations) => { for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (node.nodeType !== 1) continue; const tagName = node.tagName; if (TAG_HANDLERS[tagName]) { const srcAttr = TAG_HANDLERS[tagName].srcAttr; const dataSrcAttr = TAG_HANDLERS[tagName].dataSrcAttr; const value = node[srcAttr] || node.getAttribute(srcAttr) || (dataSrcAttr && node.getAttribute(dataSrcAttr)); if (value && shouldBlockResource(value)) { LogManager.add(self.moduleKey, node, { type: 'THIRD_PARTY', detail: `Shadow DOM ${tagName}: ${Utils.truncateString(value,500)}` }); ResourceCanceller.cancelResourceLoading(node); ProcessedElementsCache.markAsProcessed(node); } } if (node.shadowRoot) this.observeShadowRoot(node.shadowRoot); } } }); observer.observe(shadowRoot, { childList: true, subtree: true }); this.restoredFns.push(() => { observer.disconnect(); delete shadowRoot._adblockObserved; }); } patchAttachShadow() { const self = this; const proto = _Element.prototype; if (proto.attachShadow && !proto._adblockOriginalAttachShadow) { proto._adblockOriginalAttachShadow = proto.attachShadow; this.originalAttachShadow = proto.attachShadow; proto.attachShadow = function(init) { const shadow = proto._adblockOriginalAttachShadow.call(this, init); self.observeShadowRoot(shadow); if (init && init.mode === 'open') { const originalInnerHTMLDescriptor = Object.getOwnPropertyDescriptor(shadow, 'innerHTML'); if (!originalInnerHTMLDescriptor || originalInnerHTMLDescriptor.configurable) { Object.defineProperty(shadow, 'innerHTML', { get: function() { return originalInnerHTMLDescriptor ? originalInnerHTMLDescriptor.get.call(this) : ''; }, set: function(value) { const filtered = self.filterHTMLString(value); if (originalInnerHTMLDescriptor) { originalInnerHTMLDescriptor.set.call(this, filtered); } else { const range = _document.createRange(); range.selectNodeContents(this); range.deleteContents(); this.appendChild(range.createContextualFragment(filtered)); } }, configurable: true }); } } return shadow; }; this.restoredFns.push(() => { if (proto._adblockOriginalAttachShadow) { proto.attachShadow = proto._adblockOriginalAttachShadow; delete proto._adblockOriginalAttachShadow; } }); } } filterHTMLString(html) { if (typeof html !== 'string' || !html.includes('<')) return html; const tagRegex = /<(script|link|img|iframe|embed|object|a|form|base)\s+([^>]*?)\s*\/?>/gi; let result = html; result = result.replace(tagRegex, (matchStr, tagName, attrsStr) => { const lowerTagName = tagName.toLowerCase(); let urlAttr = ''; if (lowerTagName === 'script' || lowerTagName === 'img' || lowerTagName === 'iframe' || lowerTagName === 'embed') { urlAttr = 'src'; } else if (lowerTagName === 'link' || lowerTagName === 'a' || lowerTagName === 'base') { urlAttr = 'href'; } else if (lowerTagName === 'object') { urlAttr = 'data'; } else if (lowerTagName === 'form') { urlAttr = 'action'; } if (!urlAttr) return matchStr; const urlRegex = new RegExp(`${urlAttr}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s>]*))`, 'i'); const urlMatch = attrsStr.match(urlRegex); const url = urlMatch ? (urlMatch[1] || urlMatch[2] || urlMatch[3]) : null; if (!url) return matchStr; if (lowerTagName === 'link') { const relRegex = /rel\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\\s>]*))/i; const relMatch = attrsStr.match(relRegex); const rel = relMatch ? (relMatch[1] || relMatch[2] || relMatch[3]) : ''; if (!rel.includes('stylesheet')) return matchStr; } if (shouldBlockResource(url)) { const detail = `阻止: ${lowerTagName.toUpperCase()} ${Utils.truncateString(url, 500)}`; LogManager.add(this.moduleKey, null, { type: 'THIRD_PARTY_HTML_INJECTION', detail: detail }); if (lowerTagName === 'img') { const replaceSrcRegex = new RegExp(`${urlAttr}\\s*=\\s*(?:"[^"]*"|'[^']*'|[^\\s>]*)`, 'i'); let newTag = matchStr.replace(replaceSrcRegex, `${urlAttr}="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"`); newTag = newTag.replace(/srcset\s*=\s*(?:"[^"]*"|'[^']*'|[^>\s]*)/gi, ''); newTag = newTag.replace(/data-src\s*=\s*(?:"[^"]*"|'[^']*'|[^>\s]*)/gi, ''); return newTag; } else if (lowerTagName === 'a' || lowerTagName === 'form') { const replaceSrcRegex = new RegExp(`${urlAttr}\\s*=\\s*(?:"[^"]*"|'[^']*'|[^\\s>]*)`, 'i'); return matchStr.replace(replaceSrcRegex, ''); } else { return ''; } } return matchStr; }); return result; } setupHTMLInterception() { const self = this; const innerHTMLDesc = Object.getOwnPropertyDescriptor(_Element.prototype, 'innerHTML'); if (innerHTMLDesc && innerHTMLDesc.set) { this.originalInnerHTMLSetter = innerHTMLDesc.set; const newSetter = function(value) { const filtered = self.filterHTMLString(value); return self.originalInnerHTMLSetter.call(this, filtered); }; try { Object.defineProperty(_Element.prototype, 'innerHTML', { set: newSetter, get: innerHTMLDesc.get, configurable: true }); this.restoredFns.push(() => { Object.defineProperty(_Element.prototype, 'innerHTML', innerHTMLDesc); }); } catch(e) {} } this.originalInsertAdjacentHTML = _Element.prototype.insertAdjacentHTML; _Element.prototype.insertAdjacentHTML = function(position, html) { const filtered = self.filterHTMLString(html); return self.originalInsertAdjacentHTML.call(this, position, filtered); }; this.restoredFns.push(() => { _Element.prototype.insertAdjacentHTML = this.originalInsertAdjacentHTML; }); } setupStrictDOMInterception() { const self = this; const checkAndBlockElement = (element) => { if (!element || !Utils.isElement(element)) return false; const tagName = element.tagName; if (TAG_HANDLERS[tagName]) { const handler = TAG_HANDLERS[tagName]; const srcAttr = handler.srcAttr; const dataSrcAttr = handler.dataSrcAttr; const url = element[srcAttr] || element.getAttribute(srcAttr) || (dataSrcAttr && element.getAttribute(dataSrcAttr)); if (url && shouldBlockResource(url)) { LogManager.add(self.moduleKey, element, { type: 'THIRD_PARTY', detail: `严格拦截: ${tagName}: ${Utils.truncateString(url,500)}` }); ResourceCanceller.cancelResourceLoading(element); ProcessedElementsCache.markAsProcessed(element); return true; } } return false; }; this.originalAppendChild = _Node.prototype.appendChild; _Node.prototype.appendChild = function(child) { if (child && Utils.isElement(child)) { if (checkAndBlockElement(child)) { return child; } } return self.originalAppendChild.call(this, child); }; this.restoredFns.push(() => { _Node.prototype.appendChild = this.originalAppendChild; }); this.originalInsertBefore = _Node.prototype.insertBefore; _Node.prototype.insertBefore = function(newNode, referenceNode) { if (newNode && Utils.isElement(newNode)) { if (checkAndBlockElement(newNode)) { return newNode; } } return self.originalInsertBefore.call(this, newNode, referenceNode); }; this.restoredFns.push(() => { _Node.prototype.insertBefore = this.originalInsertBefore; }); } _checkElement(element) { const tagName = element.tagName; if (!TAG_HANDLERS[tagName]) return false; const handler = TAG_HANDLERS[tagName]; const srcAttr = handler.srcAttr; const dataSrcAttr = handler.dataSrcAttr; const value = element[srcAttr] || element.getAttribute(srcAttr) || (dataSrcAttr && element.getAttribute(dataSrcAttr)); if (value && shouldBlockResource(value)) { LogManager.add(this.moduleKey, element, { type: 'THIRD_PARTY', detail: `${tagName}: ${Utils.truncateString(value,500)}` }); ResourceCanceller.cancelResourceLoading(element); ProcessedElementsCache.markAsProcessed(element); return true; } return false; } updateStrictMode() { if (this.enabled) { this.onDisable(); this.onEnable(); } } updateStrictMethod() { if (this.enabled) { this.onDisable(); this.onEnable(); } } } const DynamicScriptInterceptor = { _enabled: false, originalEval: null, originalFunction: null, originalSetTimeout: null, originalSetInterval: null, originalClearTimeout: null, originalClearInterval: null, originalRequestAnimationFrame: null, originalCancelAnimationFrame: null, originalDocumentWrite: null, originalDocumentWriteln: null, timerMap: new _Map(), rafMap: new _Map(), timerIdCounter: 1, rafIdCounter: 1, init() { if (currentConfig.modules.blockDynamicScripts) this.enable(); }, enable() { if (this._enabled) return; this._enabled = true; this.originalEval = _globals.eval; this.originalFunction = _globals.Function; this.originalSetTimeout = _globals.setTimeout; this.originalSetInterval = _globals.setInterval; this.originalClearTimeout = _globals.clearTimeout; this.originalClearInterval = _globals.clearInterval; this.originalRequestAnimationFrame = _globals.requestAnimationFrame; this.originalCancelAnimationFrame = _globals.cancelAnimationFrame; this.originalDocumentWrite = _document.write; this.originalDocumentWriteln = _document.writeln; const self = this; _globals.eval = function(code) { if (typeof code === 'string') { if (Whitelisting.isCodeWhitelisted(code, 'EVAL')) { return self.originalEval.call(this, code); } const contentIdentifier = Utils.getContentIdentifier(null, { type: 'EVAL', detail: code }); if (contentIdentifier && !currentConfig.whitelist.has(contentIdentifier)) { LogManager.add('blockDynamicScripts', null, { type: 'EVAL', detail: Utils.truncateString(code, 500), rawDetail: code }); return undefined; } } return self.originalEval.call(this, code); }; _globals.Function = new _Proxy(this.originalFunction, { construct(target, args, newTarget) { const code = (args.length > 0) ? String(args[args.length-1]) : ''; if (typeof code === 'string') { if (Whitelisting.isCodeWhitelisted(code, 'FUNCTION_CONSTRUCTOR')) { return Reflect.construct(target, args, newTarget || target); } const contentIdentifier = Utils.getContentIdentifier(null, { type: 'FUNCTION_CONSTRUCTOR', detail: code }); if (contentIdentifier && !currentConfig.whitelist.has(contentIdentifier)) { LogManager.add('blockDynamicScripts', null, { type: 'FUNCTION_CONSTRUCTOR', detail: Utils.truncateString(code, 500), rawDetail: code }); return Reflect.construct(target, ['return;'], newTarget || target); } } return Reflect.construct(target, args, newTarget || target); }, apply(target, thisArg, args) { const code = (args.length > 0) ? String(args[args.length-1]) : ''; if (typeof code === 'string') { if (Whitelisting.isCodeWhitelisted(code, 'FUNCTION_CONSTRUCTOR')) { return Reflect.apply(target, thisArg, args); } const contentIdentifier = Utils.getContentIdentifier(null, { type: 'FUNCTION_CONSTRUCTOR', detail: code }); if (contentIdentifier && !currentConfig.whitelist.has(contentIdentifier)) { LogManager.add('blockDynamicScripts', null, { type: 'FUNCTION_CONSTRUCTOR', detail: Utils.truncateString(code, 500), rawDetail: code }); return Reflect.apply(target, thisArg, ['return;']); } } return Reflect.apply(target, thisArg, args); } }); const checkCallback = (callback, type) => { if (typeof callback === 'string') { if (Whitelisting.isCodeWhitelisted(callback, type)) { return { blocked: false }; } const contentIdentifier = Utils.getContentIdentifier(null, { type: type, detail: callback }); if (contentIdentifier && !currentConfig.whitelist.has(contentIdentifier)) { LogManager.add('blockDynamicScripts', null, { type: type, detail: Utils.truncateString(callback, 500), rawDetail: callback }); return { blocked: true }; } return { blocked: false }; } else if (typeof callback === 'function') { const funcStr = callback.toString(); if (funcStr.includes('eval') || funcStr.includes('Function')) { const contentIdentifier = Utils.getContentIdentifier(null, { type: type, detail: funcStr }); if (contentIdentifier && !currentConfig.whitelist.has(contentIdentifier)) { LogManager.add('blockDynamicScripts', null, { type: type, detail: Utils.truncateString(funcStr, 500), rawDetail: funcStr }); return { blocked: true }; } } return { blocked: false }; } return { blocked: false }; }; _globals.setTimeout = function(callback, delay, ...args) { const result = checkCallback(callback, 'SETTIMEOUT'); if (result.blocked) { const fakeId = -Math.abs(self.timerIdCounter++); self.timerMap.set(fakeId, { type: 'timeout', originalCallback: callback }); return fakeId; } return self.originalSetTimeout.call(this, callback, delay, ...args); }; _globals.setInterval = function(callback, delay, ...args) { const result = checkCallback(callback, 'SETINTERVAL'); if (result.blocked) { const fakeId = -Math.abs(self.timerIdCounter++); self.timerMap.set(fakeId, { type: 'interval', originalCallback: callback }); return fakeId; } return self.originalSetInterval.call(this, callback, delay, ...args); }; _globals.clearTimeout = function(id) { if (typeof id === 'number' && id < 0) { self.timerMap.delete(id); return; } return self.originalClearTimeout.call(this, id); }; _globals.clearInterval = function(id) { if (typeof id === 'number' && id < 0) { self.timerMap.delete(id); return; } return self.originalClearInterval.call(this, id); }; _globals.requestAnimationFrame = function(callback) { const result = checkCallback(callback, 'REQUESTANIMATIONFRAME'); if (result.blocked) { const fakeId = -Math.abs(self.rafIdCounter++); self.rafMap.set(fakeId, { originalCallback: callback }); return fakeId; } return self.originalRequestAnimationFrame.call(this, callback); }; _globals.cancelAnimationFrame = function(id) { if (typeof id === 'number' && id < 0) { self.rafMap.delete(id); return; } return self.originalCancelAnimationFrame.call(this, id); }; _document.write = function(...args) { const content = args.join(''); if (typeof content === 'string') { if (Whitelisting.isCodeWhitelisted(content, 'DOCUMENT_WRITE')) { return self.originalDocumentWrite.apply(this, args); } const contentIdentifier = Utils.getContentIdentifier(null, { type: 'DOCUMENT_WRITE', detail: content }); if (contentIdentifier && !currentConfig.whitelist.has(contentIdentifier)) { LogManager.add('blockDynamicScripts', null, { type: 'DOCUMENT_WRITE', detail: Utils.truncateString(content, 500), rawDetail: content }); return; } } return self.originalDocumentWrite.apply(this, args); }; _document.writeln = function(...args) { const content = args.join(''); if (typeof content === 'string') { if (Whitelisting.isCodeWhitelisted(content, 'DOCUMENT_WRITELN')) { return self.originalDocumentWriteln.apply(this, args); } const contentIdentifier = Utils.getContentIdentifier(null, { type: 'DOCUMENT_WRITE', detail: content }); if (contentIdentifier && !currentConfig.whitelist.has(contentIdentifier)) { LogManager.add('blockDynamicScripts', null, { type: 'DOCUMENT_WRITE', detail: Utils.truncateString(content, 500), rawDetail: content }); return; } } return self.originalDocumentWriteln.apply(this, args); }; }, disable() { if (!this._enabled) return; this._enabled = false; _globals.eval = this.originalEval; _globals.Function = this.originalFunction; _globals.setTimeout = this.originalSetTimeout; _globals.setInterval = this.originalSetInterval; _globals.clearTimeout = this.originalClearTimeout; _globals.clearInterval = this.originalClearInterval; _globals.requestAnimationFrame = this.originalRequestAnimationFrame; _globals.cancelAnimationFrame = this.originalCancelAnimationFrame; _document.write = this.originalDocumentWrite; _document.writeln = this.originalDocumentWriteln; this.originalEval = this.originalFunction = this.originalSetTimeout = this.originalSetInterval = null; this.originalClearTimeout = this.originalClearInterval = this.originalRequestAnimationFrame = this.originalCancelAnimationFrame = null; this.originalDocumentWrite = this.originalDocumentWriteln = null; this.timerMap.clear(); this.rafMap.clear(); }, check() { return false; } }; const CSPModule = { init() { if (currentConfig.modules.manageCSP) this.applyCSP(); }, 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 = {}; for (const rule of enabledRules) { const parts = rule.rule.split(/\s+/); const directive = parts[0]; const values = parts.slice(1); 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) return; const injectCSP = () => { if (_document.head) { if (_document.querySelector('meta[http-equiv="Content-Security-Policy"]')) return; const meta = _document.createElement('meta'); meta.httpEquiv = "Content-Security-Policy"; meta.content = policyString; _document.head.appendChild(meta); } else { const observer = new _MutationObserver(() => { if (_document.head) { observer.disconnect(); if (_document.querySelector('meta[http-equiv="Content-Security-Policy"]')) return; const meta = _document.createElement('meta'); meta.httpEquiv = "Content-Security-Policy"; meta.content = policyString; _document.head.appendChild(meta); } }); observer.observe(_document.documentElement, { childList: true, subtree: true }); } }; injectCSP(); }, 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; } }; function installRedirectBlocker() { let hasNotifiedUser = false; function showNotification(message) { if (window.via && typeof window.via.toast === 'function') { try { window.via.toast(message); return; } catch (e) {} } if (typeof GM_notification === 'function') { try { GM_notification({ text: message, title: '网页重定向', timeout: 3000 }); return; } catch (e) {} } } function showBlockedNotification(silent = false) { if (silent) return; if (!hasNotifiedUser) { hasNotifiedUser = true; showNotification('已阻止跳转到其他网站'); _setTimeout(() => { hasNotifiedUser = false; }, 3000); } } function shouldBlockRedirect(href) { try { if (!href || href.startsWith('#') || href.startsWith('javascript:')) return false; const normalizedHref = href.startsWith('/') ? _location.origin + href : href.startsWith('//') ? _location.protocol + href : href; const targetUrl = new URL(normalizedHref, _location.href); const currentUrl = new URL(_location.href); if (targetUrl.hostname === currentUrl.hostname) return false; if (!currentConfig.thirdPartyStrictMode) { const targetDomain = urlCache.getDomain(targetUrl.hostname); const currentDomain = urlCache.getDomain(currentUrl.hostname); if (targetDomain && currentDomain && targetDomain === currentDomain) return false; } return true; } catch (e) { return false; } } const original = { windowOpen: _globals.open, assign: _location.assign, replace: _location.replace, pushState: history.pushState, replaceState: history.replaceState }; try { _globals.open = function(...args) { if (!currentConfig.redirectBlockerEnabled) return original.windowOpen.apply(this, args); if (shouldBlockRedirect(args[0])) { showBlockedNotification(); return null; } return original.windowOpen.apply(this, args); }; } catch (e) {} try { if (typeof original.assign === 'function') { _location.assign = function(url) { if (!currentConfig.redirectBlockerEnabled) return original.assign.call(_location, url); if (shouldBlockRedirect(url)) { showBlockedNotification(); return; } return original.assign.call(_location, url); }; } if (typeof original.replace === 'function') { _location.replace = function(url) { if (!currentConfig.redirectBlockerEnabled) return original.replace.call(_location, url); if (shouldBlockRedirect(url)) { showBlockedNotification(); return; } return original.replace.call(_location, url); }; } } catch (e) {} try { history.pushState = function(state, title, url) { if (!currentConfig.redirectBlockerEnabled) return original.pushState.apply(this, arguments); if (shouldBlockRedirect(url)) { showBlockedNotification(); return; } return original.pushState.apply(this, arguments); }; history.replaceState = function(state, title, url) { if (!currentConfig.redirectBlockerEnabled) return original.replaceState.apply(this, arguments); if (shouldBlockRedirect(url)) { showBlockedNotification(); return; } return original.replaceState.apply(this, arguments); }; } catch (e) {} try { const anchorProto = HTMLAnchorElement.prototype; const origAnchorHrefDesc = Object.getOwnPropertyDescriptor(anchorProto, 'href'); if (origAnchorHrefDesc && origAnchorHrefDesc.set && origAnchorHrefDesc.configurable) { Object.defineProperty(anchorProto, 'href', { configurable: true, enumerable: true, get() { return origAnchorHrefDesc.get.call(this); }, set(v) { if (!currentConfig.redirectBlockerEnabled) { return origAnchorHrefDesc.set.call(this, v); } if (shouldBlockRedirect(v)) { const origSetAttr = _Element.prototype.setAttribute; origSetAttr.call(this, 'data-blocked-redirect', v); return; } return origAnchorHrefDesc.set.call(this, v); } }); } } catch (e) {} try { const formProto = HTMLFormElement.prototype; const origSubmit = formProto.submit; formProto.submit = function() { if (!currentConfig.redirectBlockerEnabled) return origSubmit.apply(this, arguments); const action = this.getAttribute('action'); if (!action || action.trim() === '') { if (this.hasAttribute('data-blocked-redirect-target') || this.hasAttribute('data-blocked-action')) { showBlockedNotification(); return; } return origSubmit.apply(this, arguments); } if (shouldBlockRedirect(action)) { showBlockedNotification(); return; } return origSubmit.apply(this, arguments); }; const actionDesc = Object.getOwnPropertyDescriptor(formProto, 'action'); if (actionDesc && actionDesc.set && actionDesc.configurable) { Object.defineProperty(formProto, 'action', { configurable: true, enumerable: true, get() { return actionDesc.get.call(this); }, set(val) { if (!currentConfig.redirectBlockerEnabled) { return actionDesc.set.call(this, val); } if (!val || val.trim() === '') { return actionDesc.set.call(this, val); } if (shouldBlockRedirect(val)) { const origSetAttr2 = _Element.prototype.setAttribute; origSetAttr2.call(this, 'data-blocked-redirect-target', val); } return actionDesc.set.call(this, val); } }); } _document.addEventListener('submit', function(e) { if (!currentConfig.redirectBlockerEnabled) return; const form = e.target; if (form && form.tagName === 'FORM') { const action = form.getAttribute('action'); if (!action || action.trim() === '') { if (form.hasAttribute('data-blocked-redirect-target') || form.hasAttribute('data-blocked-action')) { showBlockedNotification(); e.preventDefault(); e.stopImmediatePropagation(); return false; } return; } if (shouldBlockRedirect(action)) { showBlockedNotification(); e.preventDefault(); e.stopImmediatePropagation(); return false; } } }, true); } catch (e) {} try { const mo = new _MutationObserver((mutations) => { for (const m of mutations) { if (!m.addedNodes) continue; m.addedNodes.forEach((node) => { if (!(node instanceof _Element)) return; if (node.tagName === 'META' && /refresh/i.test(node.getAttribute('http-equiv'))) { if (/url\s*=/i.test(node.getAttribute('content') || '')) node.remove(); } if (node.tagName === 'A') { const href = node.getAttribute('href'); if (href && currentConfig.redirectBlockerEnabled && shouldBlockRedirect(href)) { const origSetAttr2 = _Element.prototype.setAttribute; origSetAttr2.call(node, 'data-blocked-redirect', href); node.removeAttribute('href'); } } if (node.querySelectorAll) { node.querySelectorAll('meta[http-equiv="refresh"]').forEach(m => m.remove()); } }); } }); mo.observe(_document.documentElement || _document, { childList: true, subtree: true }); } catch (e) {} try { _globals.addEventListener('click', (e) => { if (!currentConfig.redirectBlockerEnabled) return; const link = e.target && e.target.closest ? e.target.closest('a') : null; if (link) { const href = link.getAttribute('href') || link.href; if (shouldBlockRedirect(href)) { showBlockedNotification(); e.preventDefault(); e.stopImmediatePropagation(); return false; } } }, true); } catch (e) {} try { const locationDesc = Object.getOwnPropertyDescriptor(_globals, 'location'); if (locationDesc && locationDesc.configurable) { Object.defineProperty(_globals, 'location', { configurable: true, enumerable: true, get: () => locationDesc.get.call(_globals), set: (value) => { if (!currentConfig.redirectBlockerEnabled) return locationDesc.set.call(_globals, value); if (shouldBlockRedirect(value)) { showBlockedNotification(); return; } locationDesc.set.call(_globals, value); } }); } else { const protoDesc = Object.getOwnPropertyDescriptor(Window.prototype, 'location'); if (protoDesc && protoDesc.configurable) { Object.defineProperty(Window.prototype, 'location', { configurable: true, enumerable: true, get: () => protoDesc.get.call(_globals), set: (value) => { if (!currentConfig.redirectBlockerEnabled) return protoDesc.set.call(_globals, value); if (shouldBlockRedirect(value)) { showBlockedNotification(); return; } protoDesc.set.call(_globals, value); } }); } } const hrefDesc = Object.getOwnPropertyDescriptor(_location, 'href'); if (hrefDesc && hrefDesc.set && hrefDesc.configurable) { Object.defineProperty(_location, 'href', { configurable: true, enumerable: true, get: () => hrefDesc.get.call(_location), set: (value) => { if (!currentConfig.redirectBlockerEnabled) return hrefDesc.set.call(_location, value); if (shouldBlockRedirect(value)) { showBlockedNotification(); return; } hrefDesc.set.call(_location, value); } }); } } catch (e) {} try { const iframeProto = HTMLIFrameElement.prototype; const origIframeSrcDesc = Object.getOwnPropertyDescriptor(iframeProto, 'src'); if (origIframeSrcDesc && origIframeSrcDesc.set && origIframeSrcDesc.configurable) { try { Object.defineProperty(iframeProto, 'src', { configurable: true, enumerable: true, get() { return origIframeSrcDesc.get.call(this); }, set(value) { if (!currentConfig.redirectBlockerEnabled) return origIframeSrcDesc.set.call(this, value); if (shouldBlockRedirect(value)) { showBlockedNotification(); return; } origIframeSrcDesc.set.call(this, value); } }); } catch (e) {} } const frameProto = HTMLFrameElement.prototype; const origFrameSrcDesc = Object.getOwnPropertyDescriptor(frameProto, 'src'); if (origFrameSrcDesc && origFrameSrcDesc.set && origFrameSrcDesc.configurable) { try { Object.defineProperty(frameProto, 'src', { configurable: true, enumerable: true, get() { return origFrameSrcDesc.get.call(this); }, set(value) { if (!currentConfig.redirectBlockerEnabled) return origFrameSrcDesc.set.call(this, value); if (shouldBlockRedirect(value)) { showBlockedNotification(); return; } origFrameSrcDesc.set.call(this, value); } }); } catch (e) {} } } catch (e) {} try { const baseObserver = new _MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.type === 'childList') { mutation.addedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'BASE') { const href = node.getAttribute('href'); if (href && currentConfig.redirectBlockerEnabled && shouldBlockRedirect(href)) { node.removeAttribute('href'); showBlockedNotification(true); } } }); } else if (mutation.type === 'attributes' && mutation.attributeName === 'href') { const target = mutation.target; if (target.tagName === 'BASE') { const href = target.getAttribute('href'); if (href && currentConfig.redirectBlockerEnabled && shouldBlockRedirect(href)) { target.removeAttribute('href'); showBlockedNotification(true); } } } } }); baseObserver.observe(_document.documentElement || _document, { childList: true, subtree: true, attributes: true, attributeFilter: ['href'] }); } catch (e) {} try { const areaProto = HTMLAreaElement.prototype; const origAreaHrefDesc = Object.getOwnPropertyDescriptor(areaProto, 'href'); if (origAreaHrefDesc && origAreaHrefDesc.set && origAreaHrefDesc.configurable) { try { Object.defineProperty(areaProto, 'href', { configurable: true, enumerable: true, get() { return origAreaHrefDesc.get.call(this); }, set(value) { if (!currentConfig.redirectBlockerEnabled) return origAreaHrefDesc.set.call(this, value); if (shouldBlockRedirect(value)) { showBlockedNotification(); return; } origAreaHrefDesc.set.call(this, value); } }); } catch (e) {} } const embedProto = HTMLEmbedElement.prototype; const origEmbedSrcDesc = Object.getOwnPropertyDescriptor(embedProto, 'src'); if (origEmbedSrcDesc && origEmbedSrcDesc.set && origEmbedSrcDesc.configurable) { try { Object.defineProperty(embedProto, 'src', { configurable: true, enumerable: true, get() { return origEmbedSrcDesc.get.call(this); }, set(value) { if (!currentConfig.redirectBlockerEnabled) return origEmbedSrcDesc.set.call(this, value); if (shouldBlockRedirect(value)) { showBlockedNotification(); return; } origEmbedSrcDesc.set.call(this, value); } }); } catch (e) {} } const objectProto = HTMLObjectElement.prototype; const origObjectDataDesc = Object.getOwnPropertyDescriptor(objectProto, 'data'); if (origObjectDataDesc && origObjectDataDesc.set && origObjectDataDesc.configurable) { try { Object.defineProperty(objectProto, 'data', { configurable: true, enumerable: true, get() { return origObjectDataDesc.get.call(this); }, set(value) { if (!currentConfig.redirectBlockerEnabled) return origObjectDataDesc.set.call(this, value); if (shouldBlockRedirect(value)) { showBlockedNotification(); return; } origObjectDataDesc.set.call(this, value); } }); } catch (e) {} } } catch (e) {} } 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: ${CONFIG.Z_INDEX}; 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: 16px 12px; display: flex; flex-direction: column; gap: 10px; 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 6px 0; font-size: 16px; font-weight: 700; color: #1a1a1a; text-align: center; word-break: break-all; line-height: 1.3; padding: 0 8px; } .btn-group { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 4px; } .btn-group button { flex: 1 0 calc(50% - 8px); min-width: 100px; } button { border: none; border-radius: 10px; padding: 10px 8px; cursor: pointer; font-size: 13px; font-weight: 600; transition: all 0.2s; background: #f0f2f5; color: #444; display: flex; align-items: center; justify-content: center; min-height: 40px; } 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: 140px; border: 1px solid #ddd; border-radius: 10px; padding: 10px; font-family: monospace; font-size: 12px; resize: none; box-sizing: border-box; outline: none; line-height: 1.4; } textarea:focus { border-color: #007AFF; box-shadow: 0 0 0 2px rgba(0, 122, 255, 0.1); } select { width: 100%; padding: 8px; border-radius: 10px; border: 1px solid #ddd; outline: none; font-size: 13px; } .footer { display: flex; flex-direction: column; gap: 6px; margin-top: 6px; } .module-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; margin-bottom: 8px; } .module-switch { display: flex; align-items: center; justify-content: space-between; padding: 6px 10px; background: #f8f9fa; border-radius: 10px; border: 1px solid #eee; } .switch-label { font-size: 13px; font-weight: 600; color: #333; display: flex; align-items: center; gap: 6px; } .info-icon { display: inline-flex; align-items: center; justify-content: center; width: 18px; height: 18px; background: #007AFF; color: white; border-radius: 50%; font-size: 12px; font-weight: bold; cursor: pointer; margin-left: 4px; } .info-icon:hover { background: #0056b3; } .switch { position: relative; width: 40px; height: 24px; flex-shrink: 0; } .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: 24px; } .slider:before { position: absolute; content: ""; height: 18px; width: 18px; left: 3px; bottom: 3px; background-color: white; transition: .4s; border-radius: 50%; } input:checked + .slider { background-color: #007AFF; } input:checked + .slider:before { transform: translateX(16px); } .sub-panel { max-height: 45vh; overflow-y: auto; background: #f9f9f9; padding: 12px; border-radius: 10px; border: 1px solid #eee; } .log-entry { margin-bottom: 10px; padding: 10px; background: #fff; border-radius: 8px; border-left: 4px solid #007AFF; font-size: 12px; position: relative; min-height: 60px; } .log-module { color: #007AFF; font-weight: bold; margin-bottom: 3px; font-size: 13px; } .log-content { color: #666; word-break: break-word; font-size: 11px; max-height: 180px; overflow-y: auto; white-space: normal; line-height: 1.4; padding-right: 45px; } .whitelist-btn { position: absolute; top: 8px; right: 8px; background: #34C759; color: #fff; border: none; border-radius: 5px; padding: 3px 8px; font-size: 10px; cursor: pointer; z-index: 1; } .csp-rule { display: flex; align-items: center; justify-content: space-between; padding: 10px; background: #fff; border-radius: 8px; margin-bottom: 6px; } .csp-name { font-size: 12px; color: #333; max-width: 70%; } .whitelist-item { display: flex; align-items: center; justify-content: space-between; flex-wrap: nowrap; width: 100%; padding: 8px; background: #fff; border-radius: 6px; margin-bottom: 5px; } .whitelist-text { font-size: 11px; color: #333; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1 1 auto; min-width: 0; } .whitelist-item button { flex-shrink: 0; margin-left: 8px; } .keyword-highlight { background-color: #fff9c4; } .panel::-webkit-scrollbar { width: 6px; } .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; } .log-entry.whitelisted { background-color: #e8f5e8; } .log-entry.keyword-whitelisted { background-color: #e0f0ff; } .log-entry.blacklisted { background-color: #ffe0e0; } .toast-container { position: fixed; bottom: 30px; left: 50%; transform: translateX(-50%); z-index: 2147483647; pointer-events: none; display: flex; flex-direction: column; align-items: center; gap: 8px; } .toast { background: rgba(0, 0, 0, 0.85); color: #fff; padding: 12px 20px; border-radius: 10px; font-size: 14px; max-width: 85vw; text-align: center; line-height: 1.5; opacity: 0; transform: translateY(10px); animation: toast-in 0.3s ease forwards; white-space: pre-line; word-break: break-word; } @keyframes toast-in { to { opacity: 1; transform: translateY(0); } } @keyframes toast-out { to { opacity: 0; transform: translateY(10px); } } @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; } .whitelist-text { color: #eee; } .keyword-highlight { background-color: #4a4a2c; } .panel::-webkit-scrollbar-track { background: #2c2c2e; } .panel::-webkit-scrollbar-thumb { background: #555; } .panel::-webkit-scrollbar-thumb:hover { background: #666; } .log-entry.whitelisted { background-color: #2a4a2a; } .log-entry.keyword-whitelisted { background-color: #2a3a5a; } .log-entry.blacklisted { background-color: #5a2a2a; } .toast { background: rgba(255, 255, 255, 0.9); color: #1c1c1e; } } @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 panelIdCounter = 0; class PanelManager { constructor() { this.shadowRoot = null; this.settingsContainer = null; this.toastContainer = null; this.ensureShadow(); } ensureShadow() { if (this.shadowRoot) return; this.settingsContainer = _document.createElement('div'); this.settingsContainer.id = 'ad-blocker-settings-container'; this.settingsContainer.style.cssText = 'position:absolute;top:0;left:0;z-index:2147483647;pointer-events:none;'; this.settingsContainer.setAttribute('data-adblock-safe', 'true'); _document.documentElement.appendChild(this.settingsContainer); this.shadowRoot = this.settingsContainer.attachShadow({ mode: 'closed' }); const style = _document.createElement('style'); style.textContent = UI_CSS; this.shadowRoot.appendChild(style); this.toastContainer = _document.createElement('div'); this.toastContainer.className = 'toast-container'; this.shadowRoot.appendChild(this.toastContainer); ProcessedElementsCache.markAsProcessed(this.settingsContainer); } showToast(message, duration = 3500) { if (!this.shadowRoot || !this.toastContainer) return; const toast = _document.createElement('div'); toast.className = 'toast'; toast.textContent = message; this.toastContainer.appendChild(toast); _setTimeout(() => { toast.style.animation = 'toast-out 0.3s ease forwards'; _setTimeout(() => { if (toast.parentNode) toast.parentNode.removeChild(toast); }, 300); }, duration); } closeCurrentMask() { const existingMask = this.shadowRoot?.querySelector('.mask'); if (existingMask) { const panelIdAttr = existingMask.getAttribute('data-panel-id'); if (panelIdAttr) { teardownNavigationBlocking(panelIdAttr); } existingMask.remove(); } } createPanel(options) { const { title, contentHtml, onClose, onBack, buttons = [], hideBackButton = false } = options; this.ensureShadow(); this.closeCurrentMask(); const panelId = `panel_${++panelIdCounter}`; setupNavigationBlocking(panelId); const mask = _document.createElement('div'); mask.className = 'mask'; mask.setAttribute('data-adblock-safe', 'true'); mask.setAttribute('data-panel-id', panelId); const buttonHtml = buttons.map(btn => ``).join(''); const backButtonHtml = hideBackButton ? '' : ``; mask.innerHTML = `
${escapeHtml(title)}
${contentHtml}
${buttonHtml} ${backButtonHtml}
`; mask.addEventListener('click', (e) => { const path = e.composedPath(); const isPanel = path.some(el => Utils.isPanelElement(el)); if (!isPanel && e.target === mask) { teardownNavigationBlocking(panelId); mask.remove(); if (onClose) onClose(); return; } const button = e.target.closest('button'); if (!button) return; const action = button.dataset.action || button.dataset.id; const index = button.dataset.index; if (action === 'backBtn') { teardownNavigationBlocking(panelId); mask.remove(); if (onBack) onBack(); else this.showSettings(); return; } const matchedButton = buttons.find(btn => btn.id === action); if (matchedButton && matchedButton.onclick) { matchedButton.onclick(e, mask); return; } this.handlePanelAction(action, index, button, mask, e); }); mask.addEventListener('change', (e) => { const target = e.target; if (target.matches('.module-toggle')) { const key = target.dataset.key; const checked = target.checked; currentConfig.modules[key] = checked; StorageManager.saveConfig(); if (centralScheduler) centralScheduler.clearCache(); if (key === 'removeInlineScripts') { if (inlineScriptsModule) inlineScriptsModule.init(); } else if (key === 'interceptThirdParty') { if (thirdPartyModule) thirdPartyModule.init(); } if (strongBlockingEnabled) disableStrongBlocking(); const anyModuleEnabled = Object.values(currentConfig.modules).some(v => v === true); if (currentConfig.residualCleanupEnabled && anyModuleEnabled) { ResidualCleaner.init(); } else { ResidualCleaner.stop(); } urlCache.clear(); LogManager.clearLoggedIdentifiers(); if (centralScheduler) centralScheduler.clearCache(); } else if (target.matches('.csp-toggle')) { const id = parseInt(target.dataset.id); const enabled = target.checked; CSPModule.updateRule(id, enabled); currentConfig.modules.manageCSP = currentConfig.cspRules.some(rule => rule.enabled); StorageManager.saveConfig(); _location.reload(); } else if (target.matches('.advanced-toggle')) { const key = target.dataset.key; const checked = target.checked; if (key === 'inlineScriptStrictMode') { currentConfig.inlineScriptStrictMode = checked; StorageManager.saveConfig(); if (inlineScriptsModule) inlineScriptsModule.updateStrictMode(); } else if (key === 'thirdPartyStrictMode') { currentConfig.thirdPartyStrictMode = checked; StorageManager.saveConfig(); if (thirdPartyModule) thirdPartyModule.updateStrictMode(); } else if (key === 'thirdPartyStrictMethod') { currentConfig.thirdPartyStrictMethod = checked; StorageManager.saveConfig(); if (thirdPartyModule) thirdPartyModule.updateStrictMethod(); } else if (key === 'residualCleanupEnabled') { currentConfig.residualCleanupEnabled = checked; StorageManager.saveConfig(); const anyModuleEnabled = Object.values(currentConfig.modules).some(v => v === true); if (currentConfig.residualCleanupEnabled && anyModuleEnabled) { ResidualCleaner.init(); } else { ResidualCleaner.stop(); } } else if (key === 'cssUniversalHideEnabled') { currentConfig.cssUniversalHideEnabled = checked; StorageManager.saveConfig(); CSSUniversalHider.init(); } else if (key === 'iframeUIFix') { currentConfig.iframeUIFix = checked; StorageManager.saveConfig(); } else if (key === 'navigationBlockEnabled') { currentConfig.navigationBlockEnabled = checked; StorageManager.saveConfig(); } else if (key === 'redirectBlockerEnabled') { currentConfig.redirectBlockerEnabled = checked; StorageManager.saveConfig(); } } }); this.shadowRoot.appendChild(mask); mask.style.pointerEvents = 'auto'; mask.querySelector('.panel').style.pointerEvents = 'auto'; return mask; } handlePanelAction(action, index, button, mask, event) { switch (action) { case 'deleteDiaryItem': { const diaryWhitelist = Array.from(currentConfig.whitelist); const idx = parseInt(index); if (!isNaN(idx) && idx >= 0 && idx < diaryWhitelist.length) { const item = diaryWhitelist[idx]; if (item) { currentConfig.whitelist.delete(item); StorageManager.saveConfig(); if (centralScheduler) centralScheduler.clearCache(); urlCache.clear(); LogManager.clearLoggedIdentifiers(); this.showDiaryWhitelistPanel(mask); } } break; } case 'deleteKeywordItem': { const keywords = Array.from(currentConfig.keywordWhitelist); const idx = parseInt(index); if (!isNaN(idx) && idx >= 0 && idx < keywords.length) { const kw = keywords[idx]; if (kw) { currentConfig.keywordWhitelist.delete(kw); StorageManager.saveConfig(); if (centralScheduler) centralScheduler.clearCache(); urlCache.clear(); LogManager.clearLoggedIdentifiers(); this.showKeywordWhitelistPanel(mask); } } break; } case 'deleteBlacklistItem': { const blacklist = Array.from(currentConfig.scriptBlacklist); const idx = parseInt(index); if (!isNaN(idx) && idx >= 0 && idx < blacklist.length) { const kw = blacklist[idx]; if (kw) { currentConfig.scriptBlacklist.delete(kw); StorageManager.saveConfig(); if (centralScheduler) centralScheduler.clearCache(); urlCache.clear(); LogManager.clearLoggedIdentifiers(); this.showScriptBlacklistPanel(mask); } } break; } case 'deleteThirdPartyItem': { const whitelist = currentConfig.thirdPartyWhitelist; const idx = parseInt(index); if (!isNaN(idx) && idx >= 0 && idx < whitelist.length) { const item = whitelist[idx]; if (item) { whitelist.splice(idx, 1); StorageManager.saveConfig(); Whitelisting.removeKeywordsMatchingDomain(item); if (centralScheduler) centralScheduler.clearCache(); urlCache.clear(); LogManager.clearLoggedIdentifiers(); this.showThirdPartyPanel(mask); } } break; } case 'whitelistLog': { const logs = LogManager.logs; const idx = parseInt(index); if (!isNaN(idx) && idx >= 0 && idx < logs.length) { const logEntry = logs[idx]; if (logEntry && logEntry.contentIdentifier) { const isThirdPartyDomain = currentConfig.modules.interceptThirdParty && logEntry.domain; if (isThirdPartyDomain) { const domain = logEntry.domain; if (!currentConfig.thirdPartyWhitelist.includes(domain)) { currentConfig.thirdPartyWhitelist.push(domain); } for (let i = 0; i < logs.length; i++) { const entry = logs[i]; if (entry.domain === domain && entry.contentIdentifier !== logEntry.contentIdentifier) { if (!currentConfig.thirdPartyWhitelist.includes(entry.domain)) { currentConfig.thirdPartyWhitelist.push(entry.domain); } } } StorageManager.saveConfig(); const logDivs = mask.querySelectorAll('.log-entry'); for (let i = 0; i < logDivs.length; i++) { const div = logDivs[i]; const btn = div.querySelector('.whitelist-btn'); if (!btn) continue; const btnIndex = parseInt(btn.dataset.index); if (!isNaN(btnIndex) && logs[btnIndex] && logs[btnIndex].domain === domain) { if (!div.classList.contains('whitelisted')) { div.classList.add('whitelisted'); } btn.disabled = true; btn.textContent = '已加白'; btn.style.backgroundColor = '#999'; } } } else { if (!currentConfig.whitelist.has(logEntry.contentIdentifier)) { currentConfig.whitelist.add(logEntry.contentIdentifier); } StorageManager.saveConfig(); const logDiv = button.closest('.log-entry'); if (logDiv) { logDiv.classList.add('whitelisted'); button.disabled = true; button.textContent = '已加白'; button.style.backgroundColor = '#999'; } } if (centralScheduler) centralScheduler.clearCache(); urlCache.clear(); } } break; } case 'addToBlacklist': { const encodedContent = button.dataset.content; if (encodedContent) { const content = decodeURIComponent(encodedContent); currentConfig.scriptBlacklist.add(content); StorageManager.saveConfig(); const logDiv = button.closest('.log-entry'); if (logDiv) { const targetBtn = logDiv.querySelector('button[data-action="addToBlacklist"]'); if (targetBtn) { targetBtn.disabled = true; targetBtn.textContent = '已加黑'; targetBtn.style.backgroundColor = '#999'; } logDiv.classList.add('blacklisted'); } } break; } case 'addKeywordWhitelist': { const input = mask.querySelector('#keywordWhitelistInput'); const keyword = input.value.trim(); if (keyword) { Whitelisting.addKeyword(keyword); input.value = ''; this.showLogsPanel(mask); } break; } case 'addWhitelist': { const input = mask.querySelector('#newWhitelist'); let value = input.value.trim(); if (value) { value = value.replace(/^https?:\/\//, ''); try { const urlObj = new URL('http://' + value); value = urlObj.hostname; } catch (e) {} const whitelist = currentConfig.thirdPartyWhitelist; if (!whitelist.includes(value)) { whitelist.push(value); StorageManager.saveConfig(); Whitelisting.removeKeywordsMatchingDomain(value); } input.value = ''; if (centralScheduler) centralScheduler.clearCache(); urlCache.clear(); LogManager.clearLoggedIdentifiers(); this.showThirdPartyPanel(mask); } break; } case 'addBlacklist': { const input = mask.querySelector('#newBlacklistKeyword'); const kw = input.value.trim(); if (kw) { currentConfig.scriptBlacklist.add(kw); StorageManager.saveConfig(); input.value = ''; if (centralScheduler) centralScheduler.clearCache(); urlCache.clear(); LogManager.clearLoggedIdentifiers(); this.showScriptBlacklistPanel(mask); } break; } case 'addBlacklistFromDomain': { const domain = decodeURIComponent(button.dataset.domain); if (domain) { currentConfig.scriptBlacklist.add(domain); StorageManager.saveConfig(); const logDiv = button.closest('.log-entry'); if (logDiv) { const targetBtn = logDiv.querySelector('button[data-action="addBlacklistFromDomain"]'); if (targetBtn) { targetBtn.disabled = true; targetBtn.textContent = '域名已加黑'; targetBtn.style.backgroundColor = '#999'; } logDiv.classList.add('blacklisted'); } } break; } case 'clearAllDiary': { if (confirm('确定清空所有日记白名单吗?')) { currentConfig.whitelist.clear(); StorageManager.saveConfig(); if (centralScheduler) centralScheduler.clearCache(); urlCache.clear(); LogManager.clearLoggedIdentifiers(); this.showDiaryWhitelistPanel(mask); } break; } case 'clearAllKeyword': { if (confirm('确定清空所有关键词白名单吗?')) { currentConfig.keywordWhitelist.clear(); StorageManager.saveConfig(); if (centralScheduler) centralScheduler.clearCache(); urlCache.clear(); LogManager.clearLoggedIdentifiers(); this.showKeywordWhitelistPanel(mask); } break; } case 'clearAllBlacklist': { if (confirm('确定清空所有脚本黑名单吗?')) { currentConfig.scriptBlacklist.clear(); StorageManager.saveConfig(); if (centralScheduler) centralScheduler.clearCache(); urlCache.clear(); LogManager.clearLoggedIdentifiers(); this.showScriptBlacklistPanel(mask); } break; } case 'clearAllThirdParty': { if (confirm('确定清空所有第三方白名单吗?')) { currentConfig.thirdPartyWhitelist = []; StorageManager.saveConfig(); if (centralScheduler) centralScheduler.clearCache(); urlCache.clear(); LogManager.clearLoggedIdentifiers(); this.showThirdPartyPanel(mask); } break; } case 'showScriptList': { this.showScriptListPanel(mask); break; } case 'enableCSP': { currentConfig.modules.manageCSP = true; StorageManager.saveConfig(); _location.reload(); break; } case 'disableCSP': { currentConfig.modules.manageCSP = false; StorageManager.saveConfig(); _location.reload(); break; } case 'allOn': { currentConfig.cspRules.forEach(rule => rule.enabled = true); currentConfig.modules.manageCSP = true; StorageManager.saveConfig(); _location.reload(); break; } case 'allOff': { currentConfig.cspRules.forEach(rule => rule.enabled = false); currentConfig.modules.manageCSP = false; StorageManager.saveConfig(); _location.reload(); break; } case 'closePanel': { const panelIdAttr = mask.getAttribute('data-panel-id'); if (panelIdAttr) { teardownNavigationBlocking(panelIdAttr); } mask.remove(); break; } case 'showAdvancedSettings': { this.showAdvancedSettingsPanel(mask); break; } default: break; } } showAdvancedSettingsPanel(parentMask) { parentMask?.remove(); const contentHtml = `
高级选项控制拦截行为的具体细节
导航阻断(强拦截) !
同源跳转拦截 !
移除内嵌脚本 - 严格模式 !
拦截第三方资源 - 严格模式 !
拦截第三方资源 - 严格劫持方式 !
清空残留容器 !
CSS通用隐藏 !
禁止在嵌套框架中显示面板 !
`; this.createPanel({ title: '高级设置中心', contentHtml: contentHtml, buttons: [], onBack: () => this.showSettings() }); const maskElement = this.shadowRoot.querySelector('.mask:last-child'); if (maskElement) { maskElement.querySelectorAll('.info-icon').forEach(icon => { icon.addEventListener('click', (e) => { e.stopPropagation(); const type = icon.dataset.info; let message = ''; if (type === 'navBlock') { message = '启用后,打开设置面板时会阻止页面关闭/刷新,避免操作中断。'; } else if (type === 'redirectBlocker') { message = '启用后,拦截所有跳转到其他域名的链接、表单提交、脚本跳转等,只允许当前域名内的跳转。\n\n若关闭"拦截第三方资源-严格模式",则允许同主域的子域名跳转(如从 www.example.com 到 auth.example.com)。'; } else if (type === 'inlineStrict') { message = '严格模式:额外拦截内联事件(如onclick)和javascript:URL,适用于拦截点击弹窗、悬浮广告等。\n宽松模式(默认):仅移除内嵌脚本内容。'; } else if (type === 'thirdPartyStrict') { message = '严格模式:拦截所有第三方资源(包括子域名和兄弟域名)。\n宽松模式(默认):只拦截完全无关的第三方域名(主域名不同)。\n\n此选项同时影响"同源跳转拦截"的行为。'; } else if (type === 'thirdPartyMethod') { message = '严格劫持方式:在宽松模式(默认通过 setAttribute、fetch、XHR 代理和 MutationObserver 扫描拦截)基础上,额外劫持 appendChild 方法,拦截动态创建的第三方资源。可能影响性能,建议仅在必要时开启。'; } else if (type === 'residualCleanup') { message = '清空残留容器:在浏览器空闲时清理有明显广告样式且内容为空的容器,忽略尺寸很小的元素。'; } else if (type === 'cssUniversalHide') { message = 'CSS通用隐藏:动态检测高z-index(>600)特征的广告元素并隐藏。可能误伤正常元素,建议仅在必要时开启。'; } else if (type === 'iframeUIFix') { message = '当在某些视频网站打开设置面板时,如果视频播放器和网页都弹出了控制面板,开启此选项可能修复该问题。'; } else { message = '点击查看详情'; } this.showToast(message, 5000); }); }); } } showSettings() { const oldMask = this.shadowRoot.querySelector('.mask'); if (oldMask) { const oldPanelId = oldMask.getAttribute('data-panel-id'); if (oldPanelId) teardownNavigationBlocking(oldPanelId); oldMask.remove(); } const moduleSwitches = Object.keys(MODULE_NAMES).map(key => `
${escapeHtml(MODULE_NAMES[key])}
`).join(''); this.createPanel({ title: '🛡️广告拦截设置', contentHtml: `
功能模块:
${moduleSwitches}
`, buttons: [ { id: 'viewLogs', text: `拦截日志 (${LogManager.logs.length})`, onclick: (e, mask) => { this.showLogsPanel(mask); } }, { id: 'manageCSP', text: 'CSP策略管理', onclick: (e, mask) => { this.showCSPPanel(mask); } }, { id: 'manageDiaryWhitelist', text: '日记白名单', onclick: (e, mask) => { this.showDiaryWhitelistPanel(mask); } }, { id: 'manageThirdParty', text: '第三方白名单', onclick: (e, mask) => { this.showThirdPartyPanel(mask); } }, { id: 'manageKeywordWhitelist', text: '关键词白名单', onclick: (e, mask) => { this.showKeywordWhitelistPanel(mask); } }, { id: 'manageScriptBlacklist', text: '脚本黑名单', onclick: (e, mask) => { this.showScriptBlacklistPanel(mask); } }, { id: 'showAdvancedSettings', text: '高级设置中心', onclick: (e, mask) => { this.showAdvancedSettingsPanel(mask); } }, { id: 'closePanel', text: '返回网页', onclick: (e, mask) => { const panelIdAttr = mask.getAttribute('data-panel-id'); if (panelIdAttr) { teardownNavigationBlocking(panelIdAttr); } mask.remove(); }} ], hideBackButton: true }); } showDiaryWhitelistPanel(parentMask) { parentMask?.remove(); const diaryWhitelist = Array.from(currentConfig.whitelist); const itemsHtml = diaryWhitelist.length > 0 ? diaryWhitelist.map((item, index) => { let displayText = item; if (item.startsWith('INLINE_EVENT: ')) { displayText = item.substring('INLINE_EVENT: '.length); } else if (item.startsWith('JAVASCRIPT_URL: ')) { displayText = item.substring('JAVASCRIPT_URL: '.length); } else if (item.startsWith('SCRIPT_CONTENT: ')) { displayText = item.substring('SCRIPT_CONTENT: '.length); } else if (item.startsWith('SCRIPT_SRC: ')) { displayText = item.substring('SCRIPT_SRC: '.length); } else if (item.startsWith('EVAL_HASH: ')) { displayText = 'Eval代码哈希: ' + item.substring('EVAL_HASH: '.length); } else if (item.startsWith('FUNCTION_HASH: ')) { displayText = 'Function代码哈希: ' + item.substring('FUNCTION_HASH: '.length); } else if (item.startsWith('DOCUMENT_WRITE_HASH: ')) { displayText = 'document.write代码哈希: ' + item.substring('DOCUMENT_WRITE_HASH: '.length); } else if (item.startsWith('SETTIMEOUT_HASH: ')) { displayText = 'setTimeout代码哈希: ' + item.substring('SETTIMEOUT_HASH: '.length); } else if (item.startsWith('SETINTERVAL_HASH: ')) { displayText = 'setInterval代码哈希: ' + item.substring('SETINTERVAL_HASH: '.length); } else if (item.startsWith('REQUESTANIMATIONFRAME_HASH: ')) { displayText = 'requestAnimationFrame代码哈希: ' + item.substring('REQUESTANIMATIONFRAME_HASH: '.length); } return `
${escapeHtml(displayText)}
`; }).join('') : '
日记白名单为空
'; this.createPanel({ title: `日记白名单 (${diaryWhitelist.length}项)`, contentHtml: `
查看和管理拦截日记中添加的白名单条目
${itemsHtml}
`, buttons: [], onBack: () => this.showSettings() }); } showKeywordWhitelistPanel(parentMask) { parentMask?.remove(); const keywords = Array.from(currentConfig.keywordWhitelist); const itemsHtml = keywords.length > 0 ? keywords.map((kw, index) => `
${escapeHtml(kw)}
`).join('') : '
关键词白名单为空
'; this.createPanel({ title: `关键词白名单 (${keywords.length}项)`, contentHtml: `
查看和管理通过关键词添加的白名单条目
${itemsHtml}
`, buttons: [], onBack: () => this.showSettings() }); } showScriptListPanel(parentMask) { parentMask?.remove(); const scripts = Array.from(_document.scripts); const scriptItems = scripts.map((script, index) => { const isExternal = !!script.src; let content = isExternal ? script.src : script.textContent; content = Utils.truncateString(content, 500); return { index: index+1, isExternal, content, script }; }); const blacklistSet = currentConfig.scriptBlacklist; const listHtml = scriptItems.length > 0 ? scriptItems.map(item => { const scriptContentToBlacklist = item.isExternal ? item.script.src : item.script.textContent; const isBlacklistedByContent = blacklistSet.has(scriptContentToBlacklist); let domainHint = '', extractButton = '', isDomainBlacklisted = false; if (item.isExternal && item.script.src) { try { const url = new URL(item.script.src, _location.href); const domain = url.hostname; if (domain) { domainHint = `
域名: ${escapeHtml(domain)}
`; isDomainBlacklisted = blacklistSet.has(domain); extractButton = ` `; } } catch(e) {} } const logEntryClass = 'log-entry' + (isBlacklistedByContent || isDomainBlacklisted ? ' blacklisted' : ''); return `
脚本 #${item.index} - ${item.isExternal ? '外联' : '内嵌'}
${escapeHtml(item.content)}
${domainHint} ${extractButton}
`; }).join('') : '
未找到任何脚本
'; this.createPanel({ title: `当前网页脚本列表 (共${scriptItems.length}个)`, contentHtml: `
点击「加黑」将整个脚本内容添加到脚本黑名单;「加黑域名」将脚本域名加入黑名单
${listHtml}
`, buttons: [], onBack: () => this.showScriptBlacklistPanel(null) }); } showScriptBlacklistPanel(parentMask) { parentMask?.remove(); const blacklist = Array.from(currentConfig.scriptBlacklist); const itemsHtml = blacklist.length > 0 ? blacklist.map((kw, index) => `
${escapeHtml(kw)}
`).join('') : '
脚本黑名单为空
'; this.createPanel({ title: `脚本黑名单 (${blacklist.length}项)`, contentHtml: `
脚本黑名单模式将拦截内嵌/外联脚本中匹配这些关键词的资源
${itemsHtml}
`, buttons: [], onBack: () => this.showSettings() }); _setTimeout(() => { const maskElement = this.shadowRoot.querySelector('.mask:last-child'); if (!maskElement) return; const input = maskElement.querySelector('#newBlacklistKeyword'); if (input) { input.focus(); input.addEventListener('keypress', (e) => { if (e.key === 'Enter') { const addBtn = maskElement.querySelector('[data-id="addBlacklist"]'); if (addBtn) addBtn.click(); } }); } }, 0); } showLogsPanel(parentMask) { parentMask?.remove(); const logs = LogManager.logs; const keywordWhitelist = Array.from(currentConfig.keywordWhitelist); const whitelistSet = currentConfig.whitelist; const thirdPartyWhitelist = currentConfig.thirdPartyWhitelist; const logsHtml = logs.length > 0 ? logs.map((log, index) => { const isWhitelisted = whitelistSet.has(log.contentIdentifier); const isKeywordWhitelisted = !isWhitelisted && keywordWhitelist.some(kw => (log.content && log.content.includes(kw)) || (log.domain && log.domain.includes(kw))); const isThirdPartyWhitelisted = !isWhitelisted && !isKeywordWhitelisted && log.domain && urlCache.isWhitelisted(log.domain, thirdPartyWhitelist); const logEntryClass = 'log-entry' + (isWhitelisted ? ' whitelisted' : '') + (isKeywordWhitelisted ? ' keyword-whitelisted' : '') + (isThirdPartyWhitelisted ? ' whitelisted' : ''); let domainHint = log.domain ? `
域名: ${escapeHtml(log.domain)}
` : ''; return `
${escapeHtml(log.module)} - ${escapeHtml(log.element)}
${escapeHtml(log.content)}
${domainHint}
`; }).join('') : '
暂无拦截记录
'; this.createPanel({ title: `拦截日志 (${logs.length}条)`, contentHtml: `
关键词加白(添加包含关键词的脚本到白名单):
${logsHtml}
`, buttons: [], onBack: () => this.showSettings() }); _setTimeout(() => { const maskElement = this.shadowRoot.querySelector('.mask:last-child'); if (!maskElement) return; const input = maskElement.querySelector('#keywordWhitelistInput'); if (input) { input.focus(); input.addEventListener('keypress', (e) => { if (e.key === 'Enter') { const addBtn = maskElement.querySelector('[data-id="addKeywordWhitelist"]'); if (addBtn) addBtn.click(); } }); } }, 0); } showCSPPanel(parentMask) { parentMask?.remove(); const rulesHtml = currentConfig.cspRules.map(rule => `
${escapeHtml(rule.name)}
`).join(''); this.createPanel({ title: 'CSP策略管理', contentHtml: `
当前状态: ${currentConfig.modules.manageCSP ? '✅已启用' : '❌已禁用'}
${rulesHtml}
`, buttons: [], onBack: () => this.showSettings() }); } showThirdPartyPanel(parentMask) { parentMask?.remove(); const whitelist = currentConfig.thirdPartyWhitelist; const itemsHtml = whitelist.length > 0 ? whitelist.map((item, index) => `
${escapeHtml(item)}
`).join('') : '
白名单为空
'; this.createPanel({ title: `第三方白名单 (${whitelist.length}项)`, contentHtml: `
已拦截的第三方域名可以添加到白名单中
${itemsHtml}
`, buttons: [], onBack: () => this.showSettings() }); _setTimeout(() => { const maskElement = this.shadowRoot.querySelector('.mask:last-child'); if (!maskElement) return; const input = maskElement.querySelector('#newWhitelist'); if (input) { input.focus(); input.addEventListener('keypress', (e) => { if (e.key === 'Enter') { const addBtn = maskElement.querySelector('[data-id="addWhitelist"]'); if (addBtn) addBtn.click(); } }); } }, 0); } } class CentralScheduler { constructor(modules) { this.modules = modules; this.elementCheckCache = new WeakSet(); this.urlCheckCache = new LRUCache(500, CONFIG.CACHE_TTL); } 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 (module.enabled && module.checkElement(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) { if (shouldBlockResource(url)) { result = true; } } this.urlCheckCache.set(cacheKey, result, CONFIG.CACHE_TTL); return result; } clearCache() { this.elementCheckCache = new WeakSet(); this.urlCheckCache.clear(); urlCache.clear(); } } let inlineScriptsModule = null; let thirdPartyModule = null; let centralScheduler = null; const UIController = { initialized: false, mutationObserver: null, batchProcessingQueue: [], batchSize: CONFIG.BATCH_SIZE, isProcessingBatch: false, lastProcessTime: 0, panelManager: null, modules: [], init() { if (this.initialized) return; this.initialized = true; this.applyInitialModuleStates(); this.registerMenuCommands(); this.panelManager = new PanelManager(); this.createModules(); this.applyModuleSettings(); this.setupObservers(); this.setupResourceScan(); const anyModuleEnabled = Object.values(currentConfig.modules).some(v => v === true); if (currentConfig.residualCleanupEnabled && anyModuleEnabled) { ResidualCleaner.init(); } CSSUniversalHider.init(); if (currentConfig.redirectBlockerEnabled) { installRedirectBlocker(); } }, applyInitialModuleStates() { Object.keys(DEFAULT_MODULE_STATE).forEach(key => { if (currentConfig.modules[key] === undefined) { currentConfig.modules[key] = DEFAULT_MODULE_STATE[key]; } }); }, registerMenuCommands() { GM_registerMenuCommand('⚙️ 广告拦截设置面板', () => this.panelManager.showSettings()); const spoofStatus = currentConfig.spoofUAEnabled ? '✅ 已启用' : '❌ 已禁用'; GM_registerMenuCommand(`🍎 模拟UA ${spoofStatus}`, () => { currentConfig.spoofUAEnabled = !currentConfig.spoofUAEnabled; StorageManager.saveConfig(); GM_notification({ text: `UA模拟已${currentConfig.spoofUAEnabled ? '启用' : '禁用'},刷新页面生效`, title: '广告拦截器' }); _location.reload(); }); GM_registerMenuCommand('🔄 重置所有设置', () => { if (confirm('确定重置所有设置吗?这将清空所有白名单并关闭所有模块,恢复到初始状态。')) { StorageManager.resetAllSettings(); if (centralScheduler) centralScheduler.clearCache(); urlCache.clear(); LogManager.clearLoggedIdentifiers(); GM_notification({ text: '所有设置已重置', title: '广告拦截器' }); _location.reload(); } }); GM_registerMenuCommand('🗑️ 清空所有白名单', () => { if (confirm('确定清空当前域名的所有白名单(包括日记白名单、第三方白名单、关键词白名单等)吗?')) { Whitelisting.clearAllWhitelists(); if (centralScheduler) centralScheduler.clearCache(); urlCache.clear(); LogManager.clearLoggedIdentifiers(); GM_notification({ text: '所有白名单已清空', title: '广告拦截器' }); _location.reload(); } }); }, createModules() { inlineScriptsModule = new RemoveInlineScriptsModule(); thirdPartyModule = new ThirdPartyInterceptionModule(); this.modules = [ inlineScriptsModule, new RemoveExternalScriptsModule(), new ScriptBlacklistModeModule(), thirdPartyModule ]; }, applyModuleSettings() { this.modules.forEach(module => module.init()); DynamicScriptInterceptor.init(); CSPModule.init(); centralScheduler = new CentralScheduler(this.modules); }, setupObservers() { const relevantModulesEnabled = this.modules.some(m => m.enabled) || currentConfig.modules.blockDynamicScripts; if (!relevantModulesEnabled) return; this.mutationObserver = new _MutationObserver(Utils.throttle((mutations) => { for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (node.nodeType === 1 && !ProcessedElementsCache.isProcessed(node) && !Utils.isParentProcessed(node)) { this.addToBatchProcessingQueue(node); } } } }, CONFIG.THROTTLE_LIMIT)); this.mutationObserver.observe(_document.documentElement, { childList: true, subtree: true }); this.processExistingElementsBatch(); }, setupResourceScan() { if (currentConfig.modules.interceptThirdParty) { _document.addEventListener('DOMContentLoaded', () => { _setTimeout(() => this.scanExistingResources(), 1000); }); } }, scanExistingResources() { const selectors = ['script[src]', 'iframe[src]', 'img[src]', 'img[data-src]', 'embed[src]', 'object[data]', 'link[href]']; selectors.forEach(selector => { try { const elements = _document.querySelectorAll(selector); elements.forEach(element => { if (ProcessedElementsCache.isProcessed(element) || Utils.isParentProcessed(element)) return; const tagName = element.tagName; let url = ''; if (tagName === 'SCRIPT') url = element.src; else if (tagName === 'IFRAME') url = element.src; else if (tagName === 'IMG') url = element.src || element.getAttribute('data-src'); else if (tagName === 'EMBED') url = element.src; else if (tagName === 'OBJECT') url = element.data; else if (tagName === 'LINK') url = element.href; if (url && shouldBlockResource(url)) { const contentIdentifier = Utils.getContentIdentifier(element); if (contentIdentifier && !currentConfig.whitelist.has(contentIdentifier)) { LogManager.add('interceptThirdParty', element, { type: 'THIRD_PARTY_SCAN', detail: `扫描发现: ${tagName}: ${Utils.truncateString(url,500)}` }); } } }); } catch (e) {} }); }, addToBatchProcessingQueue(element) { if (element.getAttribute && element.getAttribute('data-adblock-safe') === 'true') { ProcessedElementsCache.markAsProcessed(element); return; } this.batchProcessingQueue.push(element); if (!this.isProcessingBatch) { this.processBatch(); } }, processBatch() { if (!centralScheduler) return; this.isProcessingBatch = true; const processChunk = () => { const now = Date.now(); if (now - this.lastProcessTime < 16) { _requestAnimationFrame(processChunk); return; } const chunk = this.batchProcessingQueue.splice(0, this.batchSize); this.lastProcessTime = now; chunk.forEach(node => centralScheduler.processElement(node)); if (this.batchProcessingQueue.length > 0) { _requestAnimationFrame(processChunk); } else { this.isProcessingBatch = false; } }; _requestAnimationFrame(processChunk); }, processExistingElementsBatch() { const selector = 'script, iframe, img, a[href], style, link[rel="preload"], link[rel="prefetch"], embed, object, link[href]'; const elementsToProcess = Array.from(_document.querySelectorAll(selector)); const processInChunks = () => { const chunk = elementsToProcess.splice(0, this.batchSize * 2); chunk.forEach(element => { if (!ProcessedElementsCache.isProcessed(element) && !Utils.isParentProcessed(element)) { this.addToBatchProcessingQueue(element); } }); if (elementsToProcess.length > 0) { _setTimeout(processInChunks, 0); } }; processInChunks(); } }; function spoofPlatform(p = 'Mac') { try { Object.defineProperty(navigator, 'platform', { get: () => p, configurable: true, enumerable: true }); } catch(e) {} } StorageManager.loadConfig(); if (currentConfig.spoofUAEnabled) { spoofPlatform('Mac'); } if (currentConfig.modules.blockDynamicScripts) DynamicScriptInterceptor.init(); function suppressUIForIframe() { if (!currentConfig.iframeUIFix) return false; try { return _globals.self !== _globals.top; } catch (e) { return true; } } function initModulesWithoutUI() { if (!UIController.initialized) { UIController.initialized = true; UIController.applyInitialModuleStates(); UIController.createModules(); UIController.applyModuleSettings(); UIController.setupObservers(); UIController.setupResourceScan(); const anyModuleEnabled = Object.values(currentConfig.modules).some(v => v === true); if (currentConfig.residualCleanupEnabled && anyModuleEnabled) { ResidualCleaner.init(); } CSSUniversalHider.init(); if (currentConfig.redirectBlockerEnabled) { installRedirectBlocker(); } } } function safeInit() { if (_document.documentElement) { if (suppressUIForIframe()) { initModulesWithoutUI(); } else { UIController.init(); } if (currentConfig.modules.manageCSP) CSPModule.applyCSP(); return true; } return false; } if (!safeInit()) { const mo = new _MutationObserver(() => { if (safeInit()) mo.disconnect(); }); mo.observe(_document, { childList: true }); _document.addEventListener('DOMContentLoaded', () => { if (!safeInit()) _setTimeout(safeInit, 100); }, { once: true }); _setTimeout(() => { if (!safeInit()) { if (suppressUIForIframe()) { initModulesWithoutUI(); } else { UIController.init(); } } }, 5000); } })();