// ==UserScript==
// @name 小说漫画网页广告拦截器
// @namespace http://tampermonkey.net/
// @version 5.1.0
// @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',
RESIDUAL_SCAN_INTERVAL: 2000,
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 = '';
}
if (_globals._adblock_original_beforeunload) {
_globals.onbeforeunload = _globals._adblock_original_beforeunload;
delete _globals._adblock_original_beforeunload;
} else {
_globals.onbeforeunload = null;
}
_globals._adblock_strongBlockingActive = false;
delete _globals._adblock_original_designMode;
delete _globals._adblock_original_pointerEvents;
let activePanels = new _Set();
let strongBlockingEnabled = false;
let blockingTimer = null;
function enableStrongBlocking() {
if (strongBlockingEnabled) return;
_globals._adblock_original_beforeunload = _globals.onbeforeunload;
_globals.onbeforeunload = function(e) {
e.preventDefault();
e.returnValue = '系统可能不会保存您所做的更改。';
return '系统可能不会保存您所做的更改。';
};
strongBlockingEnabled = true;
_globals._adblock_strongBlockingActive = true;
if (blockingTimer) _clearTimeout(blockingTimer);
blockingTimer = _setTimeout(() => {
if (activePanels.size > 0) {
activePanels.clear();
disableStrongBlocking();
}
}, CONFIG.STRONG_BLOCK_TIMEOUT);
}
function disableStrongBlocking() {
if (!strongBlockingEnabled) return;
_globals.onbeforeunload = _globals._adblock_original_beforeunload || null;
delete _globals._adblock_original_beforeunload;
delete _globals._adblock_original_designMode;
delete _globals._adblock_original_pointerEvents;
strongBlockingEnabled = false;
_globals._adblock_strongBlockingActive = false;
if (blockingTimer) {
_clearTimeout(blockingTimer);
blockingTimer = null;
}
}
function setupNavigationBlocking(panelId) {
activePanels.add(panelId);
if (activePanels.size === 1) {
enableStrongBlocking();
if (currentConfig.modules.blockDynamicScripts) {
DynamicScriptInterceptor.disable();
}
}
}
function teardownNavigationBlocking(panelId) {
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
};
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;
} else {
currentConfig.residualCleanupEnabled = false;
}
if (data.cssUniversalHideEnabled !== undefined) {
currentConfig.cssUniversalHideEnabled = data.cssUniversalHideEnabled;
} else {
currentConfig.cssUniversalHideEnabled = false;
}
if (data.iframeUIFix !== undefined) {
currentConfig.iframeUIFix = data.iframeUIFix;
} else {
currentConfig.iframeUIFix = false;
}
}
} 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;
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
};
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;
this.saveConfig();
const residualKey = `residual_cleanup_prompt_shown_${_location.hostname}`;
GM_setValue(residualKey, 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 ((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;
if (Array.from(currentConfig.keywordWhitelist).some(keyword => 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 = '[未知元素]';
let interceptedContent = '[无法获取内容]';
let contentIdentifier = null;
let 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) {
_clearTimeout(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 = _setTimeout(scanLoop, CONFIG.RESIDUAL_SCAN_INTERVAL);
};
this._scanTimer = _setTimeout(scanLoop, CONFIG.RESIDUAL_SCAN_INTERVAL);
}
};
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 = () => {
if (!this._enabled) return;
this.scanAndHide();
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.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
element.srcset = '';
element.removeAttribute('srcset');
} else if (tagName === 'IFRAME') {
element.src = 'about:blank';
element.style.display = 'none';
} else if (tagName === 'SCRIPT') {
element.src = '';
} else if (tagName === 'LINK' && element.rel === 'stylesheet') {
element.href = '';
} else if (tagName === 'STYLE') {
element.textContent = '';
} else if (tagName === 'EMBED') {
element.src = '';
} else if (tagName === 'OBJECT') {
element.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)}` });
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;
let 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;
if (TAG_HANDLERS[tagName] && TAG_HANDLERS[tagName].srcAttr === name && shouldBlockResource(value)) {
logAndCancel(thisArg, name, value, tagName);
return;
}
return Reflect.apply(target, thisArg, args);
}
});
_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;
if (shouldBlockResource(value)) {
logAndCancel(thisArg, srcAttr, value, tagName);
return;
}
return Reflect.apply(target, thisArg, args);
}
});
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, 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);
return shadow;
};
this.restoredFns.push(() => {
if (proto._adblockOriginalAttachShadow) {
proto.attachShadow = proto._adblockOriginalAttachShadow;
delete proto._adblockOriginalAttachShadow;
}
});
}
}
setupHTMLInterception() {
const self = this;
const filterHTMLString = (html) => {
if (typeof html !== 'string') return html;
let filtered = html;
const urlRegex = /(src|href|data-src|action|poster)\s*=\s*(?:(["'])(.*?)\2|([^\s"'<>]+))/gi;
filtered = filtered.replace(urlRegex, (match, attr, quote, quotedValue, unquotedValue) => {
const url = quotedValue !== undefined ? quotedValue : unquotedValue;
if (url && shouldBlockResource(url)) {
LogManager.add(self.moduleKey, null, { type: 'THIRD_PARTY_HTML_INJECTION', detail: `阻止: ${attr}="${Utils.truncateString(url, 500)}"` });
return `${attr}=""`;
}
return match;
});
const imgTagRegex = /
]*src\s*=\s*(?:(["'])(.*?)\1|([^\s"'<>]+))[^>]*>/gi;
filtered = filtered.replace(imgTagRegex, (match, quote, quotedSrc, unquotedSrc) => {
const src = quotedSrc !== undefined ? quotedSrc : unquotedSrc;
if (src && shouldBlockResource(src)) {
LogManager.add(self.moduleKey, null, { type: 'THIRD_PARTY_HTML_INJECTION', detail: `阻止IMG: src="${Utils.truncateString(src, 500)}"` });
return match.replace(/src\s*=\s*(?:(["'])(.*?)\1|([^\s"'<>]+))/i, 'src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"');
}
return match;
});
const iframeTagRegex = /