// ==UserScript== // @name Porn Blocker | 色情内容过滤器 // @name:en Porn Blocker // @name:zh-CN 色情内容过滤器 // @name:zh-TW 色情內容過濾器 // @name:zh-HK 色情內容過濾器 // @name:ja アダルトコンテンツブロッカー // @name:ko 성인 컨텐츠 차단기 // @name:ru Блокировщик порнографии // @namespace https://noctiro.moe // @version 2.1.7 // @description A powerful content blocker that helps protect you from inappropriate websites. Features: Auto-detection of adult content, Multi-language support, Smart scoring system, Safe browsing protection. // @description:en A powerful content blocker that helps protect you from inappropriate websites. Features: Auto-detection of adult content, Multi-language support, Smart scoring system, Safe browsing protection. // @description:zh-CN 强大的网页过滤工具,帮助你远离不良网站。功能特点:智能检测色情内容,多语言支持,评分系统,安全浏览保护,支持自定义过滤规则。为了更好的网络环境,从我做起。 // @description:zh-TW 強大的網頁過濾工具,幫助你遠離不良網站。功能特點:智能檢測色情內容,多語言支持,評分系統,安全瀏覽保護,支持自定義過濾規則。為了更好的網絡環境,從我做起。 // @description:zh-HK 強大的網頁過濾工具,幫助你遠離不良網站。功能特點:智能檢測色情內容,多語言支持,評分系統,安全瀏覽保護,支持自定義過濾規則。為了更好的網絡環境,從我做起。 // @description:ja アダルトコンテンツを自動的にブロックする強力なツールです。機能:アダルトコンテンツの自動検出、多言語対応、スコアリングシステム、カスタマイズ可能なフィルタリング。より良いインターネット環境のために。 // @description:ko 성인 컨텐츠를 자동으로 차단하는 강력한 도구입니다. 기능: 성인 컨텐츠 자동 감지, 다국어 지원, 점수 시스템, 안전 브라우징 보호, 맞춤형 필터링 규칙。 // @description:ru Мощный инструмент для блокировки неприемлемого контента. Функции: автоматическое определение, многоязычная поддержка, система оценки, настраиваемые правила фильтрации。 // @author Noctiro // @license Apache-2.0 // @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI2NCIgaGVpZ2h0PSI2NCI+CiA8ZGVmcz4KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iZ3JhZCIgeDE9IjAlIiB5MT0iMCUiIHgyPSIxMDAlIiB5Mj0iMTAwJSI+CiAgICAgIDxzdG9wIG9mZnNldD0iMCUiIHN0b3AtY29sb3I9IiNGRjhBNjUiLz4KICAgICAgPHN0b3Agb2Zmc2V0PSIxMDAiIHN0b3AtY29sb3I9IiNGRkQ1NEYiLz4KICAgIDwvbGluZWFyR3JhZGllbnQ+CiA8L2RlZnM+CiA8cmVjdCB3aWR0aD0iNjQiIGhlaWdodD0iNjQiIGZpbGw9InVybCgjZ3JhZCkiIHJ4PSI4IiByeT0iOCI+PC9yZWN0PgogPHRleHQgeD0iMzIiIHk9IjQ2IiBmb250LWZhbWlseT0iQXJpYWwiIGZvbnQtc2l6ZT0iMzYiIGZvbnQtd2VpZ2h0PSJib2xkIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjRkZGRkZGIj5SMTg8L3RleHQ+CiA8bGluZSB4MT0iMTIiIHkxPSIxMiIgeDI9IjUyIiB5Mj0iNTIiIHN0cm9rZT0iI0QzMkYyRiIgc3Ryb2tlLXdpZHRoPSI2IiBzdHJva2UtbGluZWNhcD0icm91bmQiLz4KPC9zdmc+ // @match *://*/* // @run-at document-start // @run-at document-end // @run-at document-idle // @grant GM_setValue // @grant GM_getValue // @downloadURL none // ==/UserScript== (function () { 'use strict'; // ===== 多语言支持 ===== const i18n = { 'en': { title: '🚫 Access Denied', message: 'This page contains content that may harm your well-being.', redirect: 'You will be redirected in 4 seconds…', footer: 'Cherish your mind · Stay away from harmful sites', debug: { reason: 'Block Reason (for false positive report):', score: 'Score:', keywords: 'Matched Keywords:', url: 'URL:' } }, 'zh-CN': { title: '🚫 访问受限', message: '该页面包含有害信息,可能危害您的身心健康。', redirect: '将在 4 秒后自动跳转……', footer: '珍爱健康 · 远离有害信息', debug: { reason: '拦截原因(若误报,反馈时请提供):', score: '总分:', keywords: '命中关键词:', url: 'URL:' } }, 'zh-TW': { title: '🚫 存取受限', message: '此頁面含有有害資訊,可能危害您的身心健康。', redirect: '將於 4 秒後自動跳轉……', footer: '珍愛健康 · 遠離有害資訊', debug: { reason: '攔截原因(如誤判請回報):', score: '總分:', keywords: '命中關鍵詞:', url: 'URL:' } }, 'zh-HK': { title: '🚫 存取受限', message: '此網頁含有有害資訊,或會損害您的身心健康。', redirect: '4 秒後將自動引導離開……', footer: '珍重健康 · 遠離有害內容', debug: { reason: '攔截原因(如誤判請回報):', score: '總分:', keywords: '命中關鍵詞:', url: 'URL:' } }, 'ja': { title: '🚫 アクセス制限', message: 'このページには心身に悪影響を及ぼす可能性のある情報が含まれています。', redirect: '4 秒後に自動的にページが移動します……', footer: '心と体を大切に · 有害サイトに近づかない', debug: { reason: 'ブロック理由(誤判報告時にご記入ください):', score: 'スコア:', keywords: '一致したキーワード:', url: 'URL:' } }, 'ko': { title: '🚫 접근 제한', message: '이 페이지에는 신체와 정신에 해를 끼칠 수 있는 정보가 포함되어 있습니다.', redirect: '4초 후 자동으로 이동됩니다……', footer: '건강을 소중히 · 유해 사이트는 멀리', debug: { reason: '차단 사유(오탐 시 신고):', score: '점수:', keywords: '일치 키워드:', url: 'URL:' } }, 'ru': { title: '🚫 Доступ ограничен', message: 'Эта страница содержит материалы, которые могут нанести вред вашему здоровью.', redirect: 'Перенаправление произойдёт через 4 секунды……', footer: 'Берегите здоровье · Держитесь подальше от вредных сайтов', debug: { reason: 'Причина блокировки (для жалоб на ложные срабатывания):', score: 'Счёт:', keywords: 'Совпавшие ключевые слова:', url: 'URL:' } } }; // ===== 工具函数 ===== function getUserLanguage() { // 优先使用 navigator.languages const langs = navigator.languages && navigator.languages.length ? navigator.languages : [navigator.language || navigator.userLanguage]; for (const lang of langs) { if (i18n[lang]) return lang; if (lang.startsWith('zh')) { const region = lang.toLowerCase(); if (region.includes('tw') || region.includes('hant')) return 'zh-TW'; if (region.includes('hk')) return 'zh-HK'; return 'zh-CN'; } const shortLang = lang.split('-')[0]; if (i18n[shortLang]) return shortLang; } return 'en'; } function getBrowserType() { const ua = navigator.userAgent.toLowerCase(); // 1. User-Agent Client Hints (modern Chromium-based browsers) if (navigator.userAgentData && Array.isArray(navigator.userAgentData.brands)) { const brands = navigator.userAgentData.brands.map(b => b.brand.toLowerCase()); if (brands.includes('microsoft edge')) return 'edge'; if (brands.includes('google chrome')) return 'chrome'; if (brands.includes('brave')) return 'brave'; if (brands.includes('vivaldi')) return 'vivaldi'; if (brands.includes('opera') || brands.includes('opr')) return 'opera'; if (brands.includes('arc')) return 'arc'; // If none of the above, it's some other Chromium variant if (brands.includes('chromium')) return 'chromium'; } // 2. Arc-specific CSS variable detection (Arc adds --arc-palette-background) if (window.getComputedStyle(document.documentElement) .getPropertyValue('--arc-palette-background')) { return 'arc'; } // 3. Traditional UA substring checks for non-Chromium or unhinted cases if (ua.includes('ucbrowser')) return 'uc'; if (ua.includes('qqbrowser')) return 'qq'; if (ua.includes('2345explorer')) return '2345'; if (ua.includes('360') || ua.includes('qihu')) return '360'; if (ua.includes('maxthon')) return 'maxthon'; if (ua.includes('via')) return 'via'; if (ua.includes('waterfox')) return 'waterfox'; if (ua.includes('palemoon')) return 'palemoon'; if (ua.includes('torbrowser') || (ua.includes('firefox') && ua.includes('tor'))) return 'tor'; if (ua.includes('focus')) return 'firefox-focus'; if (ua.includes('firefox')) return 'firefox'; if (ua.includes('edg/')) return 'edge'; // Edge Chromium if (ua.includes('opr/') || ua.includes('opera')) return 'opera'; if (ua.includes('brave')) return 'brave'; if (ua.includes('vivaldi')) return 'vivaldi'; if (ua.includes('yabrowser')) return 'yandex'; if (ua.includes('chrome')) return 'chrome'; if (ua.includes('safari') && !ua.includes('chrome')) return 'safari'; return 'other'; } function getHomePageUrl() { switch (getBrowserType()) { case 'firefox': return 'about:home'; case 'tor': return 'about:home'; // Tor uses Firefox's UI case 'waterfox': return 'about:home'; // Waterfox mirrors Firefox case 'palemoon': return 'about:home'; // Pale Moon custom but similar case 'chrome': return 'chrome://newtab'; case 'edge': return 'edge://newtab'; case 'safari': return 'topsites://'; case 'opera': return 'opera://startpage'; case 'brave': return 'brave://newtab'; case 'vivaldi': return 'vivaldi://newtab'; case 'yandex': return 'yandex://newtab'; case 'arc': return 'arc://start'; // Arc’s default start page case 'via': return 'via://home'; // Fallbacks for lesser-known or legacy browsers case 'uc': return 'ucenterhome://'; case 'qq': return 'qbrowser://home'; case '360': return 'se://newtab'; case 'maxthon': return 'mx://newtab'; case '2345': return '2345explorer://newtab'; default: return 'about:blank'; } } // ===== 配置项 ===== const config = { // ================== 域名关键词 ================== domainDetection: { // 常见成人网站域名关键词(权重4) 'pornhub': 4, 'xvideo': 4, 'redtube': 4, 'xnxx': 4, 'xhamster': 4, '4tube': 4, 'youporn': 4, 'spankbang': 4, 'myfreecams': 4, 'missav': 4, 'rule34': 4, 'youjizz': 4, 'onlyfans': 4, 'paidaa': 4, 'haijiao': 4, // 核心违规词(权重3-4) 'porn': 3, 'nsfw': 3, 'hentai': 3, 'incest': 4, 'rape': 4, 'childporn': 4, // 身体部位关键词(权重2) 'pussy': 2, 'cock': 2, 'dick': 2, 'boobs': 2, 'tits': 2, 'ass': 2, 'beaver': 1, // 特定群体(权重2-3) 'cuckold': 3, 'virgin': 2, 'luoli': 2, 'gay': 2, // 具体违规行为(权重2-3) 'blowjob': 3, 'creampie': 2, 'bdsm': 2, 'masturbat': 2, 'handjob': 3, 'footjob': 3, 'rimjob': 3, // 其他相关词汇(权重1-2) 'camgirl': 2, 'nude': 3, 'naked': 3, 'upskirt': 2, // 特定地区成人站点域名特征(权重4) 'jav': 4, // 域名变体检测(权重3) 'p0rn': 3, 'pr0n': 3, 'pron': 3, 's3x': 3, 'sexx': 3, // 强豁免词(权重-30) 'edu': -30, 'health': -30, 'medical': -30, 'science': -30, 'gov': -30, 'org': -30, 'official': -30, // 常用场景豁免(权重-15) 'academy': -15, 'clinic': -15, 'therapy': -15, 'university': -4, 'research': -15, 'news': -15, 'dictionary': -15, 'library': -15, 'museum': -15, // 动物/自然相关(权重-1) 'animal': -4, 'zoo': -1, 'cat': -1, 'dog': -1, 'pet': -6, 'bird': -1, // 科技类(权重-5) 'tech': -5, 'cloud': -5, 'software': -5, 'cyber': -3, // 在线聊天/论坛常用词 'forum': -10, 'bbs': -10, 'community': -10, }, // ================== 内容检测 ================== contentDetection: { // 核心违规词(权重3-4)- 严格边界检测 '\\b(?:porn|pr[o0]n)\\b': 3, // porn及其变体 'nsfw': 3, '\\bhentai\\b': 3, '\\binces*t\\b': 4, '\\br[a@]pe\\b': 4, '(?:child|kid|teen)(?:porn)': 4, '海角社区': 4, // 身体部位关键词(权重2)- 边界和上下文检测 'puss(?:y|ies)\\b': 2, '\\bco*ck(?:s)?(?!tail|roach|pit|er)\\b': 2, // 排除cocktail等 '\\bdick(?:s)?(?!ens|tionary|tate)\\b': 2, // 排除dickens等 '\\bb[o0]{2,}bs?\\b': 2, '\\btits?\\b': 2, '(? { // 1. /pattern/flags 形式 if (k.startsWith('/') && (k.endsWith('/i') || k.endsWith('/gi'))) { const lastSlash = k.lastIndexOf('/'); const pattern = k.slice(1, lastSlash); const flags = k.slice(lastSlash + 1); return { regex: new RegExp(pattern, flags), weight: v, raw: k }; } else if (k.startsWith('/') && k.endsWith('/')) { return { regex: new RegExp(k.slice(1, -1)), weight: v, raw: k }; } // 2. 纯单词(只含字母数字下划线),自动加\b else if (/^\w+$/.test(k)) { return { regex: new RegExp(`\\b${escapeRegExp(k)}\\b`, 'i'), weight: v, raw: k }; } // 3. 其它复杂正则,直接用,不加\b else { return { regex: new RegExp(k, 'i'), weight: v, raw: k }; } }); } function compileSafeRegexes(domains) { return domains.map(domain => { // 检查是否为通用顶级域名(不包含点号) if (domain.indexOf('.') === -1) { // 通用顶级域名匹配,支持多级子域名 return new RegExp(`^([^.]+\\.)*${domain}$`, 'i'); } else { // 主域名及其子域名匹配 // 转义点号,并创建匹配主域名或其任意层级子域名的正则 const escapedDomain = domain.replace(/\./g, '\\.'); return new RegExp(`^([^.]+\\.)*${escapedDomain}$`, 'i'); } }); } const compiledDomainRegexes = compileKeywordRegexes(config.domainDetection); const compiledContentRegexes = compileKeywordRegexes(config.contentDetection); const compiledSafeSites = compileSafeRegexes(config.safeSites); function isSafeSite(hostname) { return compiledSafeSites.some(re => re.test(hostname)); } // ===== 评分函数 ===== function calculateScore(text, isDomain = false) { if (!text) return 0; let score = 0; // 如果是安全网站,直接返回负分 if (isDomain && isSafeSite(text)) { return -30; } // 使用对应的规则集进行评分 const regexSet = isDomain ? compiledDomainRegexes : compiledContentRegexes; for (const { regex, weight, raw } of regexSet) { const matches = text.match(regex); if (matches) { // 代数和:正负分数直接相加 score += weight * matches.length; } } return score; } function getAllVisibleText(element) { if (!element) return ""; const textSet = new Set(); try { const walker = document.createTreeWalker( element, NodeFilter.SHOW_TEXT, { acceptNode: (node) => { const parent = node.parentElement; if (!parent || /^(SCRIPT|STYLE|NOSCRIPT|IFRAME|META|LINK)$/i.test(parent.tagName) || parent.hidden || getComputedStyle(parent).display === 'none' || getComputedStyle(parent).visibility === 'hidden' || getComputedStyle(parent).opacity === '0') { return NodeFilter.FILTER_REJECT; } const text = node.textContent.trim(); if (!text || text.length < config.contentCheck.textNodeMinLength) { return NodeFilter.FILTER_REJECT; } return NodeFilter.FILTER_ACCEPT; } } ); let node; while (node = walker.nextNode()) { textSet.add(node.textContent.trim()); } } catch (e) { } return Array.from(textSet).join(' '); } // ===== 敏感词超级正则编译 ===== function buildSuperRegexAndMap(obj) { const entries = Object.entries(obj); const groupMap = {}; let groupIndex = 1; let patternParts = []; for (const [k, v] of entries) { let pattern = ''; // 1. /pattern/flags 形式 if (k.startsWith('/') && (k.endsWith('/i') || k.endsWith('/gi'))) { const lastSlash = k.lastIndexOf('/'); pattern = k.slice(1, lastSlash); } else if (k.startsWith('/') && k.endsWith('/')) { pattern = k.slice(1, -1); } else if (/^\w+$/.test(k)) { pattern = `\\b${escapeRegExp(k)}\\b`; } else { pattern = k; } // 用命名分组区分 const groupName = `kw${groupIndex}`; patternParts.push(`(?<${groupName}>${pattern})`); groupMap[groupName] = { raw: k, weight: v }; groupIndex++; } const superPattern = patternParts.join('|'); const superRegex = new RegExp(superPattern, 'gi'); return { superRegex, groupMap }; } // ===== 重新编译敏感词超级正则 ===== const { superRegex: contentSuperRegex, groupMap: contentGroupMap } = buildSuperRegexAndMap(config.contentDetection); // ===== detectAdultContent 优化:超级正则批量检测 ===== function detectAdultContent(debug = false) { let totalScore = 0; let matches = []; let textSet = new Set(); try { const walker = document.createTreeWalker( document.body, NodeFilter.SHOW_TEXT, { acceptNode: (node) => { const parent = node.parentElement; if (!parent || /^(SCRIPT|STYLE|NOSCRIPT|IFRAME|META|LINK)$/i.test(parent.tagName) || parent.hidden || getComputedStyle(parent).display === 'none' || getComputedStyle(parent).visibility === 'hidden' || getComputedStyle(parent).opacity === '0') { return NodeFilter.FILTER_REJECT; } const text = node.textContent.trim(); if (!text || text.length < config.contentCheck.textNodeMinLength) { return NodeFilter.FILTER_REJECT; } return NodeFilter.FILTER_ACCEPT; } } ); let node; while (node = walker.nextNode()) { textSet.add(node.textContent.trim()); } } catch (e) { } const allText = Array.from(textSet).join(' ').slice(0, 10000); // 超级正则批量检测 let m; while ((m = contentSuperRegex.exec(allText)) !== null) { for (const group in m.groups) { if (m.groups[group]) { const info = contentGroupMap[group]; totalScore += info.weight; matches.push(info.raw); } } } // 图片alt/title检测 const images = document.querySelectorAll('img[alt], img[title]'); for (const img of images) { const imgText = `${img.alt} ${img.title}`.trim(); if (imgText) { let m2; while ((m2 = contentSuperRegex.exec(imgText)) !== null) { for (const group in m2.groups) { if (m2.groups[group]) { const info = contentGroupMap[group]; totalScore += info.weight * 0.3; matches.push(info.raw); } } } } } // meta标签检测 const metaTags = document.querySelectorAll('meta[name="description"], meta[name="keywords"]'); for (const meta of metaTags) { const content = meta.content; if (content) { let m3; while ((m3 = contentSuperRegex.exec(content)) !== null) { for (const group in m3.groups) { if (m3.groups[group]) { const info = contentGroupMap[group]; totalScore += info.weight * 0.2; matches.push(info.raw); } } } } } const isHighRisk = totalScore >= config.contentCheck.adultContentThreshold; if (debug) { return { detected: isHighRisk, score: totalScore, matches: Array.from(new Set(matches)).filter(Boolean) }; } return isHighRisk; } // ===== 黑名单管理器 ===== async function gmGet(key, def) { if (typeof GM_getValue === 'function') { const v = await GM_getValue(key); return v === undefined ? def : v; } return def; } async function gmSet(key, value) { if (typeof GM_setValue === 'function') { await GM_setValue(key, value); } } function createBlacklistEntry(host, reason = '', note = '') { return { host, reason, note, added: Date.now(), expire: getExpireTimestamp(), version: blacklistManager.CURRENT_VERSION }; } function getExpireTimestamp() { const BLACKLIST_EXPIRE_DAYS = 30; return Date.now() + BLACKLIST_EXPIRE_DAYS * 24 * 60 * 60 * 1000; } const blacklistManager = { BLACKLIST_KEY: 'pornblocker-blacklist', BLACKLIST_VERSION_KEY: 'pornblocker-blacklist-version', CURRENT_VERSION: '3.0', // 升级数据库版本,弃用旧数据 // 只在版本号不一致时清空旧数据 async checkAndUpgradeVersion() { const storedVersion = await gmGet(this.BLACKLIST_VERSION_KEY, null); if (storedVersion !== this.CURRENT_VERSION) { await GM_setValue(this.BLACKLIST_VERSION_KEY, this.CURRENT_VERSION); await GM_setValue(this.BLACKLIST_KEY, []); } }, // 获取黑名单 async getBlacklist() { // 确保版本检查已完成 await this.checkAndUpgradeVersion(); let data = await gmGet(this.BLACKLIST_KEY, []); // 自动清理过期和升级结构 const now = Date.now(); let changed = false; const valid = (Array.isArray(data) ? data : []).filter(item => { if (typeof item === 'string') return true; // 兼容老数据 if (item && item.host && item.expire && item.expire > now) return true; changed = true; return false; }).map(item => { if (typeof item === 'string') { changed = true; let entry = createBlacklistEntry(item, 'legacy', '自动升级'); // 补全debugInfo字段 entry.debugInfo = { reason: 'legacy', score: 0, matches: [], time: Date.now(), url: '' }; return entry; } // 结构升级:补全缺失字段 if (!item.version) item.version = this.CURRENT_VERSION; if (!item.added) item.added = now; if (!item.reason) item.reason = ''; if (!item.note) item.note = ''; // 补全debugInfo字段 if (!item.debugInfo) { item.debugInfo = { reason: item.reason || 'blacklist', score: item.score || 0, matches: item.matches || [], time: item.added, url: '' }; } else { if (!item.debugInfo.reason) item.debugInfo.reason = item.reason || 'blacklist'; if (item.debugInfo.score == null && item.score != null) item.debugInfo.score = item.score; if (!item.debugInfo.matches) item.debugInfo.matches = item.matches || []; if (!item.debugInfo.time) item.debugInfo.time = item.added; if (!item.debugInfo.url) item.debugInfo.url = ''; } return item; }); if (changed) { this.saveBlacklist(valid); } return valid; }, async saveBlacklist(list) { await gmSet(this.BLACKLIST_KEY, list); }, async addToBlacklist(hostname, reason = '', note = '', debugInfo = undefined) { if (!hostname) return false; // 安全站点检查,禁止加入黑名单 if (isSafeSite(hostname)) { return false; } let list = await this.getBlacklist(); if (list.some(item => (typeof item === 'string' ? item : item.host) === hostname)) return true; let entry = createBlacklistEntry(hostname, reason, note); // 修正:始终保存debugInfo字段,且补全reason/score/matches if (!debugInfo) debugInfo = {}; if (!debugInfo.reason) debugInfo.reason = reason || 'blacklist'; if (debugInfo.score == null && entry.score != null) debugInfo.score = entry.score; if (!debugInfo.matches) debugInfo.matches = []; debugInfo.time = Date.now(); debugInfo.url = window.location ? window.location.href : ''; entry.debugInfo = debugInfo; list.push(entry); await this.saveBlacklist(list); return true; }, async isBlacklisted(hostname) { let list = await this.getBlacklist(); return list.some(item => (typeof item === 'string' ? item : item.host) === hostname); }, async removeFromBlacklist(hostname) { let list = await this.getBlacklist(); list = list.filter(item => (typeof item === 'string') ? item : item.host !== hostname); await this.saveBlacklist(list); return true; }, // 新增批量清理过期条目方法 async cleanExpired() { let list = await this.getBlacklist(); const now = Date.now(); const valid = list.filter(item => (typeof item === 'string') || (item && item.expire && item.expire > now)); await this.saveBlacklist(valid); return valid.length; } }; // 立即执行版本检查 (async function initBlacklist() { await blacklistManager.checkAndUpgradeVersion(); })(); // ===== 检测主流程 ===== const regexCache = { domainRegex: new RegExp(Object.keys(config.domainDetection).join('|'), 'gi'), xxxRegex: /\.xxx$/i }; function checkDomainPatterns(hostname) { return regexCache.xxxRegex.test(hostname); } async function checkUrl() { const url = new URL(window.location.href); const hostname = url.hostname; // 优先检查安全网站,如果是安全网站直接返回不拦截 if (isSafeSite(hostname)) { return { shouldBlock: false, url }; } // 从黑名单读取调试信息 const blackList = await blacklistManager.getBlacklist(); const blackEntry = blackList.find(item => (typeof item === 'string' ? item : item.host) === hostname); if (blackEntry) { let debugInfo = blackEntry.debugInfo || {}; debugInfo.reason = (debugInfo.reason || blackEntry.reason || 'blacklist') + ' (blacklist)'; debugInfo.score = debugInfo.score != null ? debugInfo.score : blackEntry.score; debugInfo.matches = debugInfo.matches || blackEntry.matches || []; debugInfo.time = blackEntry.added; debugInfo.url = window.location.href; return { shouldBlock: true, url, reason: debugInfo.reason, debugInfo }; } // 检查域名模式 if (checkDomainPatterns(hostname)) { const debugInfo = { reason: 'domain-pattern', score: undefined, matches: [], time: Date.now(), url: window.location.href }; await blacklistManager.addToBlacklist(hostname, 'domain-pattern', '', debugInfo); return { shouldBlock: true, url, reason: 'domain-pattern', debugInfo }; } // 检查域名评分 let score = calculateScore(hostname, true); if (score >= config.thresholds.block) { const debugInfo = { reason: 'domain', score: score, matches: [], time: Date.now(), url: window.location.href }; await blacklistManager.addToBlacklist(hostname, 'domain', '', debugInfo); return { shouldBlock: true, url, reason: 'domain', score, debugInfo }; } // 检查标题是否违规 const currentTitle = document.title; if (currentTitle) { const titleScore = calculateScore(currentTitle); if (titleScore >= config.thresholds.block * 0.75) { // 标题评分阈值降低25% let matches = []; for (const { regex, raw } of compiledContentRegexes) { if (regex.test(currentTitle)) { matches.push(raw); } } const debugInfo = { reason: 'title', score: titleScore, matches: matches, time: Date.now(), url: window.location.href }; await blacklistManager.addToBlacklist(hostname, 'title', '', debugInfo); return { shouldBlock: true, url, reason: 'title', score: titleScore, matches, debugInfo }; } } // URL路径检查 const path = url.pathname + url.search; const pathScore = calculateScore(path) * 0.4; // 路径评分权重降低 if (pathScore >= config.thresholds.block) { const debugInfo = { reason: 'path', score: pathScore, matches: [], time: Date.now(), url: window.location.href }; await blacklistManager.addToBlacklist(hostname, 'path', '', debugInfo); return { shouldBlock: true, url, reason: 'path', score: pathScore, debugInfo }; } // 内容检测 if (document.body) { const contentResult = detectAdultContent(true); if (contentResult.detected) { const debugInfo = { reason: 'content', score: contentResult.score, matches: contentResult.matches, time: Date.now(), url: window.location.href }; await blacklistManager.addToBlacklist(hostname, 'content', '', debugInfo); return { shouldBlock: true, url, reason: 'content', score: contentResult.score, matches: contentResult.matches, debugInfo }; } enhancedDynamicContentCheck(); } else { document.addEventListener('DOMContentLoaded', () => { const contentResult = detectAdultContent(true); if (contentResult.detected) { const debugInfo = { reason: 'content', score: contentResult.score, matches: contentResult.matches, time: Date.now(), url: window.location.href }; blacklistManager.addToBlacklist(hostname, 'content', '', debugInfo); handleBlockedContent(debugInfo); } enhancedDynamicContentCheck(); }); } // 所有检查通过,不拦截 return { shouldBlock: false, url }; } // ===== 动态内容与标题检测 ===== function enhancedDynamicContentCheck() { // 智能动态内容检测:结合 MutationObserver 和递增定时检测 const hostname = window.location.hostname; let triggered = false; let interval = 3000; // 初始间隔3秒 const maxInterval = 12000; // 最大间隔12秒 let timer = null; let count = 0; let lastMutationTime = Date.now(); let pendingImmediateCheck = false; function checkAndBlock(immediate = false) { if (triggered) return; const contentResult = detectAdultContent(true); if (contentResult.detected) { triggered = true; const debugInfo = { reason: 'dynamic-content', score: contentResult.score, matches: contentResult.matches, time: Date.now(), url: window.location.href }; blacklistManager.addToBlacklist(hostname, 'dynamic-content', '', debugInfo); handleBlockedContent(debugInfo); if (timer) clearTimeout(timer); observer.disconnect(); return; } // 递增间隔,最大不超过15秒 if (!immediate) { count++; interval = Math.min(5000 + count * 1000, maxInterval); timer = setTimeout(() => checkAndBlock(false), interval); } } // MutationObserver 监听大规模变动 const observer = new MutationObserver((mutations) => { if (triggered) return; let majorChange = false; for (const m of mutations) { // 只要有主要内容区域变动或节点大增减就算大变动 if ( (m.target && m.target.nodeType === 1 && ( m.target.matches && ( m.target.matches('main, article, section, .main-content, .article-content, .post-content') ) )) || (m.addedNodes && m.addedNodes.length > 5) || (m.removedNodes && m.removedNodes.length > 5) ) { majorChange = true; break; } } if (majorChange) { // 立即检测 if (!pendingImmediateCheck) { pendingImmediateCheck = true; setTimeout(() => { checkAndBlock(true); pendingImmediateCheck = false; }, 200); // 稍作防抖 } } else { // 小变动,刷新递增定时器 lastMutationTime = Date.now(); if (timer) clearTimeout(timer); timer = setTimeout(() => checkAndBlock(false), interval); } }); observer.observe(document.body, { childList: true, subtree: true, characterData: true }); // 首次检测延迟0.5秒 timer = setTimeout(() => checkAndBlock(false), 500); // 超时自动断开 observer setTimeout(() => { observer.disconnect(); if (timer) clearTimeout(timer); }, 30000); } const setupTitleObserver = () => { let titleObserver = null; try { // 监听 title 标签变化 const titleElement = document.querySelector('title'); if (titleElement) { titleObserver = new MutationObserver(async (mutations) => { for (const mutation of mutations) { const newTitle = mutation.target.textContent; if (!newTitle) continue; console.log(`[Title Change] New title: "${newTitle}"`); // 使用 contentDetection 的规则计算标题分数 const titleScore = calculateScore(newTitle || ""); // 标题分数权重提高 (因为标题更重要) if (titleScore >= config.thresholds.block * 0.75) { console.log(`[Title Score] ${titleScore} exceeds threshold`); const hostname = window.location.hostname; // 收集匹配到的关键词 let matches = []; for (const { regex, raw } of compiledContentRegexes) { if (regex.test(newTitle)) { matches.push(raw); } } const debugInfo = { reason: 'title', score: titleScore, matches: matches, time: Date.now(), url: window.location.href }; await blacklistManager.addToBlacklist(hostname, 'title', '', debugInfo); titleObserver.disconnect(); handleBlockedContent(debugInfo); return; } } }); titleObserver.observe(titleElement, { subtree: true, characterData: true, childList: true }); } // 监听 title 标签的添加 const headObserver = new MutationObserver((mutations) => { for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (node.nodeName === 'TITLE') { setupTitleObserver(); headObserver.disconnect(); return; } } } }); headObserver.observe(document.head, { childList: true, subtree: true }); // 设置超时清理 setTimeout(() => { titleObserver?.disconnect(); headObserver?.disconnect(); }, config.contentCheck.observerTimeout); } catch (e) { console.error('Error in setupTitleObserver:', e); } return titleObserver; }; // ===== 拦截页面渲染 ===== function getDebugInfo(result) { if (!result) return null; // 优先读取debugInfo并补全reason/score/matches if (result.debugInfo) { let info = { ...result.debugInfo }; if (!info.reason && result.reason) info.reason = result.reason; if (info.score == null && result.score != null) info.score = result.score; if (!info.matches && result.matches) info.matches = result.matches; if (!info.time) info.time = Date.now(); if (!info.url) info.url = window.location ? window.location.href : ''; if (info.reason && !/\(blacklist\)/.test(info.reason)) { info.reason = info.reason + ' (blacklist)'; } return info; } if (result.reason === 'content' || result.reason === 'dynamic-content') { return { reason: result.reason, score: result.score, matches: result.matches, time: Date.now(), url: window.location.href }; } let score = result.score || 0; let matches = result.matches || []; return { reason: result.reason, score, matches, time: Date.now(), url: window.location.href }; } const handleBlockedContent = (debugInfo) => { const lang = getUserLanguage(); const text = i18n[lang]; document.title = text.title; try { window.stop(); } catch (e) { /* ignore */ } // 调试信息展示(多语言,保证内容不为空) let debugHtml = ''; if (debugInfo) { const d = (text.debug || i18n['en'].debug); const reason = debugInfo.reason || '-'; const score = debugInfo.score != null ? debugInfo.score : '-'; let keywords = '-'; if (Array.isArray(debugInfo.matches) && debugInfo.matches.length > 0) { keywords = debugInfo.matches.join(', '); } // 保证每项单独一行且无多余换行 let debugLines = [ `${d.reason} ${reason}`, `${d.score} ${score}`, `${d.keywords} ${keywords}` ]; if (debugInfo.time) debugLines.push(`Time: ${new Date(debugInfo.time).toLocaleString()}`); if (debugInfo.url) debugLines.push(`${d.url} ${debugInfo.url}`); debugHtml = `
${debugLines.join('
')}
`; } try { document.documentElement.innerHTML = `

${text.title}

${text.message}
${text.redirect}

${debugHtml}
`; } catch (e) { // 兼容性容错:如果 innerHTML 报错,降级为简单跳转 window.location.href = getHomePageUrl(); } let timeLeft = 4; const countdownEl = document.querySelector('.countdown'); const countdownInterval = setInterval(() => { timeLeft--; if (countdownEl) countdownEl.textContent = timeLeft; if (timeLeft <= 0) { clearInterval(countdownInterval); try { const homeUrl = getHomePageUrl(); if (window.history.length > 1) { const iframe = document.createElement('iframe'); iframe.style.display = 'none'; document.body.appendChild(iframe); iframe.onload = () => { try { const prevUrl = iframe.contentWindow.location.href; const prevScore = calculateScore(new URL(prevUrl).hostname, true); if (prevScore >= config.thresholds.block) { window.location.href = homeUrl; } else { window.history.back(); } } catch (e) { window.location.href = homeUrl; } document.body.removeChild(iframe); }; iframe.src = 'about:blank'; } else { window.location.href = homeUrl; } } catch (e) { window.location.href = getHomePageUrl(); } } }, 1000); }; // ===== 主入口 ===== (async function () { const result = await checkUrl(); if (result.shouldBlock || regexCache.xxxRegex.test(result.url.hostname)) { handleBlockedContent(getDebugInfo(result)); } else { setupTitleObserver(); } })(); // ===== 自动清理黑名单(每天一次) ===== (function autoCleanBlacklist() { try { const key = 'pornblocker-last-clean'; const now = Date.now(); let last = 0; try { last = parseInt(localStorage.getItem(key) || '0', 10); } catch (e) { } if (!last || now - last > 86400000) { blacklistManager.cleanExpired(); localStorage.setItem(key, now.toString()); } } catch (e) { } })(); })();