Greasy Fork

Greasy Fork is available in English.

Porn Blocker | 色情内容过滤器

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.

当前为 2025-02-15 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Porn Blocker | 色情内容过滤器
// @name:zh-CN   色情内容过滤器
// @name:zh-TW   色情內容過濾器
// @name:zh-HK   色情內容過濾器
// @name:ja      アダルトコンテンツブロッカー
// @name:ko      성인 컨텐츠 차단기
// @name:ru      Блокировщик порнографии
// @namespace    https://noctiro.moe
// @version      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:zh-CN 强大的网页过滤工具,帮助你远离不良网站。功能特点:智能检测色情内容,多语言支持,评分系统,安全浏览保护,支持自定义过滤规则。为了更好的网络环境,从我做起。
// @description:zh-TW 強大的網頁過濾工具,幫助你遠離不良網站。功能特點:智能檢測色情內容,多語言支持,評分系統,安全瀏覽保護,支持自定義過濾規則。為了更好的網絡環境,從我做起。
// @description:zh-HK 強大的網頁過濾工具,幫助你遠離不良網站。功能特點:智能檢測色情內容,多語言支持,評分系統,安全瀏覽保護,支持自定義過濾規則。為了更好的網絡環境,從我做起。
// @description:ja   アダルトコンテンツを自動的にブロックする強力なツールです。機能:アダルトコンテンツの自動検出、多言語対応、スコアリングシステム、カスタマイズ可能なフィルタリング。より良いインターネット環境のために。
// @description:ko   성인 컨텐츠를 자동으로 차단하는 강력한 도구입니다. 기능: 성인 컨텐츠 자동 감지, 다국어 지원, 점수 시스템, 안전 브라우징 보호, 맞춤형 필터링 규칙。
// @description:ru   Мощный инструмент для блокировки неприемлемого контента. Функции: автоматическое определение, многоязычная поддержка, система оценки, настраиваемые правила фильтрации。
// @license      Apache-2.0
// @match        *://*/*
// @run-at       document-start
// @run-at       document-end
// @run-at       document-idle
// @grant        none
// @compatible    Chrome/80+
// @compatible    Firefox/68+
// @compatible    Edge/80+
// @compatible    Opera/66+
// @compatible    Safari/13+
// @compatible    Brave/1.0+
// @compatible    Vivaldi/2.11+
// @compatible    Yandex/20+
// @compatible    UC/13+
// @compatible    QQBrowser/10+
// @compatible    360Browser/13+
// ==/UserScript==

(function () {
    'use strict';

    // 多语言支持
    const i18n = {
        'en': {
            title: '🚫 Access Blocked',
            message: 'This webpage has been identified as inappropriate content.',
            redirect: 'Redirecting in <span class="countdown">4</span> seconds...',
            footer: 'Stay healthy · Stay away from harmful content'
        },
        'zh-CN': {
            title: '🚫 访问已被拦截',
            message: '该网页已被识别为不健康内容。',
            redirect: '<span class="countdown">4</span> 秒后自动跳转...',
            footer: '注意身心健康 · 远离不良网站'
        },
        'zh-TW': {
            title: '🚫 訪問已被攔截',
            message: '該網頁已被識別為不健康內容。',
            redirect: '<span class="countdown">4</span> 秒後自動跳轉...',
            footer: '注意身心健康 · 遠離不良網站'
        },
        'zh-HK': {
            title: '🚫 訪問已被攔截',
            message: '該網頁已被識別為不健康內容。',
            redirect: '<span class="countdown">4</span> 秒後自動跳轉...',
            footer: '注意身心健康 · 遠離不良網站'
        },
        'ja': {
            title: '🚫 アクセスがブロックされました',
            message: 'このページは不適切なコンテンツとして識別されました。',
            redirect: '<span class="countdown">4</span> 秒後にリダイレクトします...',
            footer: '健康に注意 · 有害サイトに近づかない'
        },
        'ko': {
            title: '🚫 접근이 차단됨',
            message: '이 웹페이지가 부적절한 콘텐츠로 식별되었습니다.',
            redirect: '<span class="countdown">4</span>초 후 자동으로 이동됩니다...',
            footer: '건강 관리 · 유해 사이트 멀리하기'
        },
        'ru': {
            title: '🚫 Доступ заблокирован',
            message: 'Эта веб-страница определена как неподходящий контент.',
            redirect: 'Перенаправление через <span class="countdown">4</span> секунды...',
            footer: 'Будьте здоровы · Держитесь подальше от вредного контента'
        }
    };

    // 获取用户语言
    const getUserLanguage = () => {
        const lang = navigator.language || navigator.userLanguage;

        // 检查完整语言代码
        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';
    };

    // 浏览器检测函数
    const getBrowserType = () => {
        const ua = navigator.userAgent.toLowerCase();

        // Add more browser detection
        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('firefox')) return 'firefox';
        if (ua.includes('edg')) return 'edge';
        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';
    };

    // 获取浏览器主页URL
    const getHomePageUrl = () => {
        switch (getBrowserType()) {
            case 'firefox':
                return 'about:home';
            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 'uc':
                return 'ucenterhome://';
            case 'qq':
                return 'qbrowser://home';
            case '360':
                return 'se://newtab';
            case 'maxthon':
                return 'mx://newtab';
            case '2345':
                return '2345explorer://newtab';
            default:
                // Fallback to a safe default
                return 'about:blank';
        }
    };

    // ----------------- 预编译正则规则 (性能优化) -----------------
    const regexCache = {
        // 色情关键词正则(预编译,避免重复生成)
        pornRegex: null,
        // 白名单正则(预编译)
        whitelistRegex: null,
        // .xxx后缀正则
        xxxRegex: /\.xxx$/i
    };

    // ----------------- 配置项(用户可按需修改) -----------------
    const config = {
        // ================== 色情关键词评分规则 ==================
        pornKeywords: {
            // 核心高危词(权重≥3)
            'porn': 3, 'nsfw': 3, 'xxx': 3, 'xxxvideos': 3,
            'hentai': 3, 'incest': 4, 'rape': 4, 'childporn': 4,
            'av': 3,

            // 身体部位关键词(权重2)
            'pussy': 2, 'cock': 2, 'dick': 2, 'boobs': 2, 'tits': 2, 'ass': 2,
            'beaver': 1,

            // 特定群体(权重2~3)
            'teen': 3, 'sis': 2, 'milf': 2, 'cuckold': 3, 'virgin': 2, 'luoli': 2,

            // 具体性行为(权重2~3)
            'anal': 3, 'blowjob': 3, 'creampie': 2, 'bdsm': 2, 'masturbat': 2,
            'doggy': 3, 'handjob': 3, 'footjob': 3, 'rimjob': 3, 'piledriver': 3,
            'legjob': 3, 'heeljob': 3, 'pussyjob': 3, 'assjobs': 3, 'hotdogging': 3,
            'balljob': 3,

            // 其他相关词汇(权重1~2)
            'camgirl': 2, 'webcam': 2, 'onlyfans': 2, 'nsfwgifs': 3,
            'leaked': 2, 'fap': 2, 'erotic': 1, 'escort': 3,
            'tube': 1, 'nude': 3, 'naked': 3, 'upskirt': 2,

            // 高危词
            'pornhub': 4, 'xvideo': 4, 'redtube': 4,
            'javhd': 4, 'youporn': 4, 'spankbang': 4,
            'xnxx': 4, 'xhamster': 4, '4tube': 4,
            'myfreecams': 4, 'missav': 4, 'onlyfans': 2,

            // 变体检测
            'p0rn': 3, 'pr0n': 3, 'pron': 3,
            's3xy': 3, 'sexx': 3, 'adultt': 3,

            // 多语言支持
            '情色': 3, '成人': 3, '做爱': 4,
            'セックス': 3, 'エロ': 3, '淫': 4,
            'секс': 3, 'порн': 3, '性爱': 3, '無修正': 3,
            'ポルノ': 3, 'порно': 3, '色情': 3,
        },

        // ================== 白名单减分规则 ==================
        whitelist: {
            // 强豁免词(权重-8)
            'edu': -10, 'health': -10, 'medical': -10, 'science': -10,
            'gov': -10, 'org': -10, 'official': -10,

            // 常用场景豁免(权重-4)
            'academy': -7, 'clinic': -7, 'therapy': -7,
            'university': -4, 'research': -7, 'news': -7,
            'dictionary': -7, 'library': -7, 'museum': -7,

            // 动物/自然相关(权重-1)
            'animal': -4, 'zoo': -1, 'cat': -1, 'dog': -1,
            'pet': -6, 'bird': -1, 'vet': -1,

            // 科技类(权重-1)
            'tech': -5, 'cloud': -5, 'software': -5, 'cyber': -3,
        },

        // ================== 阈值配置 ==================
        thresholds: {
            block: 3,       // 总分触发阈值(建议3~4)
            path: 2,        // URL路径加分阈值
            chain: 2,         // 域名包含多个敏感词的链式加分(如"teen.porn"得3+2=5)
            whitelist: 2     // 进行白名单减分的最低阈值
        },

        // ================== 域名正则表达式规则 ==================
        domainPatterns: [
            /^mogu\d+\.me$/i,
            /\d{3}[a-zA-Z]{2}/i,
            /[a-zA-Z]{2}\d{3}/i,
        ],
    };

    // 域名正则检测函数
    const checkDomainPatterns = (hostname) => {
        return config.domainPatterns.some(pattern => pattern.test(hostname));
    };

    // 预处理正则(仅初始化一次)
    (function initRegex() {
        // 色情关键词正则:生成 /porn|xxx|sex|.../gi
        const pornTerms = Object.keys(config.pornKeywords).join('|');
        regexCache.pornRegex = new RegExp(`(${pornTerms})`, 'gi');
        // 白名单正则:生成 /education|health|animal/gi
        const whitelistTerms = Object.keys(config.whitelist).join('|');
        regexCache.whitelistRegex = new RegExp(`(${whitelistTerms})`, 'gi');
    })();

    // ----------------- 评分计算函数 -----------------
    const calculateScore = (text) => {
        // 首先计算色情关键词得分
        let score = 0;
        const pornMatches = text.match(regexCache.pornRegex) || [];
        pornMatches.forEach(match => {
            const keyword = match.toLowerCase();
            score += config.pornKeywords[keyword] || 0;
        });

        // 只有当分数达到白名单阈值时才进行白名单评分
        if (score >= config.thresholds.whitelist) {
            const whitelistMatches = text.match(regexCache.whitelistRegex) || [];
            whitelistMatches.forEach(match => {
                const term = match.toLowerCase();
                score += config.whitelist[term] || 0;
            });
        }

        return score;
    };

    // ----------------- 主检测逻辑 -----------------
    const checkUrl = () => {
        const url = new URL(window.location.href);

        // 首先检查域名正则表达式
        if (checkDomainPatterns(url.hostname)) {
            return {
                shouldBlock: true,
                url: url
            };
        }

        // 继续原有的评分检测
        let score = 0;
        const pornMatches = url.hostname.match(regexCache.pornRegex) || [];
        pornMatches.forEach(match => {
            const keyword = match.toLowerCase();
            score += config.pornKeywords[keyword] || 0;
        });

        // 检查路径
        const path = url.pathname + url.search;
        const pathScore = calculateScore(path) * 0.4;
        score += pathScore;

        // 检查标题
        const titlePornScore = calculateScore(document.title || "");
        score += titlePornScore;

        // 如果超过阈值则白名单减分
        if (score >= config.thresholds.whitelist) {
            const hostMatches = url.hostname.match(regexCache.whitelistRegex) || [];
            const titleMatches = document.title.match(regexCache.whitelistRegex) || [];
            function whitelistMatchCount(matches) {
                matches.forEach(match => {
                    const term = match.toLowerCase();
                    score += config.whitelist[term] || 0;
                });
            }
            whitelistMatchCount(hostMatches);
            whitelistMatchCount(titleMatches);
        }

        // 返回评分和URL对象
        return {
            shouldBlock: score >= config.thresholds.block,
            url: url
        };
    };

    // 获取结果
    const { shouldBlock: shouldBlock, url: currentUrl } = checkUrl();

    if (shouldBlock || regexCache.xxxRegex.test(currentUrl.hostname)) {
        const lang = getUserLanguage();
        const text = i18n[lang];
        window.stop();
        document.documentElement.innerHTML = `
            <body>
                <div class="container">
                    <div class="card">
                        <div class="icon-wrapper">
                            <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                                <path d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
                            </svg>
                        </div>
                        <h1>${text.title}</h1>
                        <p>${text.message}<br>${text.redirect}</p>
                        <div class="footer">${text.footer}</div>
                    </div>
                </div>
                <style>
                    :root {
                        --bg-light: #f0f2f5;
                        --card-light: #ffffff;
                        --text-light: #2d3436;
                        --text-secondary-light: #636e72;
                        --text-muted-light: #b2bec3;
                        --accent-light: #ff4757;

                        --bg-dark: #1a1a1a;
                        --card-dark: #2d2d2d;
                        --text-dark: #ffffff;
                        --text-secondary-dark: #a0a0a0;
                        --text-muted-dark: #808080;
                        --accent-dark: #ff6b6b;
                    }

                    @media (prefers-color-scheme: dark) {
                        body {
                            background: var(--bg-dark) !important;
                        }
                        .card {
                            background: var(--card-dark) !important;
                            box-shadow: 0 4px 12px rgba(0,0,0,0.3) !important;
                        }
                        h1 { color: var(--text-dark) !important; }
                        p { color: var(--text-secondary-dark) !important; }
                        .footer { color: var(--text-muted-dark) !important; }
                        .icon-wrapper {
                            background: var(--accent-dark) !important;
                        }
                        .countdown {
                            color: var(--accent-dark);
                        }
                    }

                    body {
                        background: var(--bg-light);
                        margin: 0;
                        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
                        min-height: 100vh;
                        display: flex;
                        align-items: center;
                        justify-content: center;
                    }

                    .container {
                        max-width: 500px;
                        width: 100%;
                    }

                    .card {
                        background: var(--card-light);
                        border-radius: 16px;
                        box-shadow: 0 4px 12px rgba(0,0,0,0.1);
                        padding: 32px;
                        text-align: center;
                        animation: slideIn 0.5s ease-out;
                    }

                    .icon-wrapper {
                        width: 64px;
                        height: 64px;
                        background: var(--accent-light);
                        border-radius: 50%;
                        display: flex;
                        align-items: center;
                        justify-content: center;
                        margin: 0 auto 24px;
                        animation: pulse 2s infinite;
                    }

                    .icon-wrapper svg {
                        stroke: white;
                    }

                    h1 {
                        color: var(--text-light);
                        margin: 0 0 16px;
                        font-size: 24px;
                        font-weight: 600;
                    }

                    p {
                        color: var(--text-secondary-light);
                        margin: 0 0 24px;
                        line-height: 1.6;
                        font-size: 16px;
                    }

                    .footer {
                        color: var(--text-muted-light);
                        font-size: 14px;
                        animation: fadeIn 1s ease-out;
                    }

                    .countdown {
                        font-weight: bold;
                        color: var(--accent-light);
                    }

                    @keyframes slideIn {
                        from { transform: translateY(20px); opacity: 0; }
                        to { transform: translateY(0); opacity: 1; }
                    }

                    @keyframes pulse {
                        0% { transform: scale(1); }
                        50% { transform: scale(1.05); }
                        100% { transform: scale(1); }
                    }

                    @keyframes fadeIn {
                        from { opacity: 0; }
                        to { opacity: 1; }
                    }
                </style>
            </body>
        `;
        
        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);

                                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);
    }
})();