Hide Referrer

Hide HTTP Referrer on all websites to prevent destination sites from tracking your origin page

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Hide Referrer
// @namespace    https://github.com/planetoid/userscripts
// @version      1.0.0
// @description  Hide HTTP Referrer on all websites to prevent destination sites from tracking your origin page
// @author       Planetoid
// @match        *://*/*
// @grant        none
// @run-at       document-start
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    // ============================================================
    // Configuration
    // ============================================================

    const CONFIG = {
        // Whether to inject <meta> referrer tag (primary protection)
        injectMetaTag: true,

        // Whether to add rel="noreferrer" to all <a> tags
        patchLinks: true,

        // Whether to intercept window.open so it doesn't send referrer
        patchWindowOpen: true,

        // Whether to observe DOM changes and patch dynamically added links (for SPAs)
        observeDom: true,

        // Excluded domains (referrer hiding will be skipped on these sites)
        // e.g. ['example.com', 'mysite.org']
        excludedDomains: [],

        // Whether to show debug messages in the console
        debug: false,
    };

    // ============================================================
    // Utilities
    // ============================================================

    function log(...args) {
        if (CONFIG.debug) {
            console.log('[Hide Referrer]', ...args);
        }
    }

    function isExcluded() {
        return CONFIG.excludedDomains.some(
            (domain) =>
                location.hostname === domain ||
                location.hostname.endsWith('.' + domain)
        );
    }

    function isExternalLink(anchor) {
        try {
            return anchor.href && new URL(anchor.href).hostname !== location.hostname;
        } catch {
            return false;
        }
    }

    // ============================================================
    // 1. Inject <meta name="referrer" content="no-referrer">
    //    Injected as early as possible during document-start
    // ============================================================

    function injectMetaTag() {
        if (!CONFIG.injectMetaTag) return;

        const meta = document.createElement('meta');
        meta.name = 'referrer';
        meta.content = 'no-referrer';

        // <head> may not exist yet at document-start, try multiple mount points
        const parent =
            document.head ||
            document.documentElement ||
            document.querySelector('head');

        if (parent) {
            parent.insertBefore(meta, parent.firstChild);
            log('Meta tag injected into', parent.tagName);
        } else {
            // Very early stage — wait for the DOM to appear before injecting
            const observer = new MutationObserver(() => {
                const target = document.head || document.documentElement;
                if (target) {
                    target.insertBefore(meta, target.firstChild);
                    log('Meta tag injected (deferred)');
                    observer.disconnect();
                }
            });
            observer.observe(document, { childList: true, subtree: true });
        }
    }

    // ============================================================
    // 2. Add rel="noreferrer noopener" to all <a> tags
    // ============================================================

    function patchAnchor(anchor) {
        if (!anchor.href || anchor.dataset.referrerPatched) return;

        // Only patch external links — remove this check to patch all links
        if (!isExternalLink(anchor)) return;

        const relValues = new Set((anchor.rel || '').split(/\s+/).filter(Boolean));
        relValues.add('noreferrer');
        relValues.add('noopener');
        anchor.rel = [...relValues].join(' ');
        anchor.dataset.referrerPatched = '1';

        log('Patched link:', anchor.href);
    }

    function patchAllLinks() {
        if (!CONFIG.patchLinks) return;
        document.querySelectorAll('a[href]').forEach(patchAnchor);
    }

    // ============================================================
    // 3. Intercept window.open — use an intermediate page to strip referrer
    // ============================================================

    function patchWindowOpen() {
        if (!CONFIG.patchWindowOpen) return;

        const originalOpen = window.open;

        window.open = function (url, target, features) {
            if (url) {
                try {
                    const parsed = new URL(url, location.href);
                    if (parsed.hostname !== location.hostname) {
                        // Use a Blob URL as an intermediate page to strip referrer
                        const html = `
              <!DOCTYPE html>
              <html>
              <head>
                <meta name="referrer" content="no-referrer">
                <meta http-equiv="refresh" content="0;url=${parsed.href}">
              </head>
              <body></body>
              </html>`;
                        const blob = new Blob([html], { type: 'text/html' });
                        const blobUrl = URL.createObjectURL(blob);
                        const win = originalOpen.call(window, blobUrl, target, features);

                        // Revoke Blob URL after a short delay
                        setTimeout(() => URL.revokeObjectURL(blobUrl), 3000);
                        log('window.open intercepted:', url);
                        return win;
                    }
                } catch (e) {
                    log('window.open parse error:', e);
                }
            }
            return originalOpen.call(window, url, target, features);
        };

        log('window.open overridden');
    }

    // ============================================================
    // 4. MutationObserver — watch for dynamically added links
    // ============================================================

    function observeDom() {
        if (!CONFIG.observeDom || !CONFIG.patchLinks) return;

        const observer = new MutationObserver((mutations) => {
            for (const mutation of mutations) {
                for (const node of mutation.addedNodes) {
                    if (node.nodeType !== Node.ELEMENT_NODE) continue;

                    // If the added node itself is an <a>
                    if (node.tagName === 'A') {
                        patchAnchor(node);
                    }

                    // Check the subtree of the added node for <a> elements
                    if (node.querySelectorAll) {
                        node.querySelectorAll('a[href]').forEach(patchAnchor);
                    }
                }
            }
        });

        // Wait for <body> to exist before observing
        function startObserving() {
            if (document.body) {
                observer.observe(document.body, { childList: true, subtree: true });
                log('DOM Observer started');
            } else {
                requestAnimationFrame(startObserving);
            }
        }

        startObserving();
    }

    // ============================================================
    // Main
    // ============================================================

    if (isExcluded()) {
        log('Current domain is excluded, skipping');
        return;
    }

    // Phase 1: document-start (as early as possible)
    injectMetaTag();
    patchWindowOpen();

    // Phase 2: Patch links after DOM is ready
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', () => {
            patchAllLinks();
            observeDom();
        });
    } else {
        patchAllLinks();
        observeDom();
    }

    log('Hide Referrer userscript loaded');
})();