Affiliate Cleaner

Removes affiliate tracking parameters from links, the current URL, cookies, and outgoing POST requests. Handles both query and path-based trackers.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Affiliate Cleaner
// @name:zh-CN   AFF/REF链接自动清理
// @description:zh-CN 在全部网页中尽可能删除值为整数或UUID的AFF/REF相关参数和链接,包括POST请求,并处理路径中的AFF链接。
// @namespace    http://tampermonkey.net/
// @version      1.4
// @description  Removes affiliate tracking parameters from links, the current URL, cookies, and outgoing POST requests. Handles both query and path-based trackers.
// @author       0x7
// @match        *://*/*
// @grant        none
// @run-at       document-start
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // Matches parameters starting with 'aff' or 'ref' (case-insensitive)
    const AFFILIATE_PARAM_REGEX = /^(aff|ref)/i;
    // Matches values that are purely integers
    const INTEGER_VALUE_REGEX = /^\d+$/;
    // Matches values that are in UUID format (case-insensitive)
    const UUID_VALUE_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
    // Matches affiliate patterns embedded in the URL path, like 'aff.php?aff=123/'
    const AFFILIATE_PATH_REGEX = /aff\.php\?aff=\d+\//i;


    /**
     * Checks if a URL parameter key and value match the affiliate tracking pattern for query parameters.
     * @param {string} key - The parameter key.
     * @param {string} value - The parameter value.
     * @returns {boolean} - True if it's an affiliate parameter to be removed.
     */
    const isAffiliateParam = (key, value) => {
        if (!AFFILIATE_PARAM_REGEX.test(key)) {
            return false;
        }
        // Check if the value is either an integer or a UUID
        return INTEGER_VALUE_REGEX.test(value) || UUID_VALUE_REGEX.test(value);
    };

    /**
     * Cleans a URL by removing specified affiliate tracking parameters from the path and query string.
     * @param {string} urlString - The URL to clean.
     * @returns {string} - The cleaned URL.
     */
    const cleanUrl = (urlString) => {
        let currentUrl = urlString;

        // --- Part 1: Clean Path-based Affiliate Links ---
        if (AFFILIATE_PATH_REGEX.test(currentUrl)) {
            currentUrl = currentUrl.replace(AFFILIATE_PATH_REGEX, '');
            console.log(`[Affiliate Cleaner] Removed path segment from URL: ${urlString}`);
        }

        // --- Part 2: Clean Query-based Affiliate Links ---
        try {
            const url = new URL(currentUrl);
            let paramsChanged = false;
            const newParams = new URLSearchParams();

            url.searchParams.forEach((value, key) => {
                if (!isAffiliateParam(key, value)) {
                    newParams.append(key, value);
                } else {
                    paramsChanged = true;
                    console.log(`[Affiliate Cleaner] Removed query param: ${key}=${value} from URL: ${url.hostname}`);
                }
            });

            if (paramsChanged) {
                url.search = newParams.toString();
                return url.toString();
            }

            // If we are here, either only the path was changed, or nothing was changed.
            // Returning url.toString() handles both cases correctly.
            return url.toString();

        } catch (e) {
            // This might happen for "javascript:void(0)" or if path cleaning resulted in an invalid URL.
            // Return the URL after path cleaning, as it's our best effort.
            return currentUrl;
        }
    };

    /**
     * Cleans the current page's URL in the address bar without reloading.
     */
    const cleanCurrentUrl = () => {
        const originalUrl = window.location.href;
        const cleanedUrl = cleanUrl(originalUrl);
        if (originalUrl !== cleanedUrl) {
            window.history.replaceState(null, '', cleanedUrl);
            console.log('[Affiliate Cleaner] Cleaned current page URL.');
        }
    };

    /**
     * Scans and cleans all <a> tags currently in the DOM.
     */
    const cleanAllLinks = () => {
        document.querySelectorAll('a').forEach(link => {
            if (link.href) {
                const originalHref = link.href;
                const cleanedHref = cleanUrl(originalHref);
                if (originalHref !== cleanedHref) {
                    link.href = cleanedHref;
                }
            }
        });
    };

    /**
     * Removes cookies whose names match the affiliate pattern.
     * This is a best-effort attempt, as domain/path specifics can be tricky.
     */
    const cleanCookies = () => {
        const cookies = document.cookie.split(';');
        for (const cookie of cookies) {
            const eqPos = cookie.indexOf('=');
            const name = eqPos > -1 ? cookie.substr(0, eqPos).trim() : cookie.trim();

            // This part is interpretive: we assume the cookie *name* starts with 'aff' or 'ref'.
            if (AFFILIATE_PARAM_REGEX.test(name)) {
                const domain = window.location.hostname;
                const path = '/';
                // Expire the cookie by setting its date to the past.
                document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=${domain}; path=${path};`;
                // Attempt to remove from parent domain as well
                const parentDomain = domain.substring(domain.indexOf('.') + 1);
                document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=${parentDomain}; path=${path};`;
                console.log(`[Affiliate Cleaner] Removed cookie: ${name}`);
            }
        }
    };

    /**
     * Cleans data objects for POST requests (FormData, URLSearchParams, or JSON).
     * @param {*} data - The data to be sent.
     * @returns {*} - The cleaned data.
     */
    const cleanRequestData = (data) => {
        if (data instanceof URLSearchParams || data instanceof FormData) {
            const keysToDelete = [];
            for (const [key, value] of data.entries()) {
                if (typeof value === 'string' && isAffiliateParam(key, value)) {
                    keysToDelete.push(key);
                }
            }
            keysToDelete.forEach(key => {
                data.delete(key);
                console.log(`[Affiliate Cleaner] Removed param from POST data: ${key}`);
            });
        } else if (typeof data === 'string') {
            try {
                // Attempt to parse as JSON
                const jsonData = JSON.parse(data);
                let dataChanged = false;
                for (const key in jsonData) {
                    if (Object.prototype.hasOwnProperty.call(jsonData, key)) {
                        const value = jsonData[key];
                        if ((typeof value === 'number' || typeof value === 'string') && isAffiliateParam(key, String(value))) {
                            delete jsonData[key];
                            dataChanged = true;
                            console.log(`[Affiliate Cleaner] Removed param from JSON POST data: ${key}`);
                        }
                    }
                }
                if (dataChanged) {
                    return JSON.stringify(jsonData);
                }
            } catch (e) {
                // Not a JSON string, could be URL-encoded string.
                const params = new URLSearchParams(data);
                const cleanedParams = cleanUrl(`http://dummy.com?${params.toString()}`).split('?')[1] || '';
                if (cleanedParams !== params.toString()) {
                    return cleanedParams;
                }
            }
        }
        return data;
    };

    /**
     * Intercept fetch requests to clean their body.
     */
    const originalFetch = window.fetch;
    window.fetch = function(input, init) {
        if (init && init.body && init.method && init.method.toUpperCase() === 'POST') {
            init.body = cleanRequestData(init.body);
        }
        return originalFetch.apply(this, arguments);
    };

    /**
     * Intercept XMLHttpRequest to clean the data sent.
     */
    const originalSend = XMLHttpRequest.prototype.send;
    XMLHttpRequest.prototype.send = function(body) {
        if (body) {
            body = cleanRequestData(body);
        }
        return originalSend.apply(this, [body]);
    };

    // --- Main Execution ---

    // 1. Clean the URL as soon as the script runs. This handles the 302 redirect case upon arrival.
    cleanCurrentUrl();
    cleanCookies();

    // 2. When the initial DOM is loaded, clean all links.
    document.addEventListener('DOMContentLoaded', () => {
        cleanAllLinks();
    });

    // 3. Set up a MutationObserver to clean links added to the page dynamically.
    const observer = new MutationObserver((mutations) => {
        mutations.forEach((mutation) => {
            if (mutation.addedNodes.length) {
                mutation.addedNodes.forEach(node => {
                    // We only care about element nodes
                    if (node.nodeType === 1) {
                        // Check if the node itself is a link
                        if (node.tagName === 'A' && node.href) {
                            node.href = cleanUrl(node.href);
                        }
                        // Check for any links within the added node
                        node.querySelectorAll('a').forEach(link => {
                            if (link.href) {
                                link.href = cleanUrl(link.href);
                            }
                        });
                    }
                });
            }
        });
    });

    // Start observing the entire document body for changes.
    // Use a try-catch in case the body isn't ready immediately on some pages.
    const observeBody = () => {
        if (document.body) {
            observer.observe(document.body, {
                childList: true,
                subtree: true
            });
        } else {
            // If body is not available, wait for it.
            window.addEventListener('DOMContentLoaded', () => {
                 observer.observe(document.body, {
                    childList: true,
                    subtree: true
                });
            });
        }
    };

    observeBody();

    const origSetAttribute = Element.prototype.setAttribute;
    Element.prototype.setAttribute = function(name, value) {
        if (this.tagName === 'A' && name === 'href' && typeof value === 'string') {
            value = cleanUrl(value);
        }
        return origSetAttribute.call(this, name, value);
    };

    const hrefDesc = Object.getOwnPropertyDescriptor(HTMLAnchorElement.prototype, 'href');
    Object.defineProperty(HTMLAnchorElement.prototype, 'href', {
        set: function(v) {
            hrefDesc.set.call(this, cleanUrl(v));
        },
        get: hrefDesc.get
    });
    cleanAllLinks();
})();