URL Tracking Parameter Cleaner

Automatically strips known privacy-invading tracking parameters from all URLs, links, and network requests

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да инсталирате разширение, като например Tampermonkey .

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

// ==UserScript==
// @name         URL Tracking Parameter Cleaner
// @namespace    VVJMIFRyYWNraW5nIFBhcmFtZXRlciBDbGVhbmVy
// @version      1.0
// @description  Automatically strips known privacy-invading tracking parameters from all URLs, links, and network requests
// @author       smed79
// @license      GPLv3
// @icon         https://i25.servimg.com/u/f25/11/94/21/24/utpc10.png
// @match        *://*/*
// @run-at       document-start
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // Tracking parameters - wildcard * supported
    const rawParams = `action_ref_*, action_type_*, hsa_*, itm_*, matomo_*, mkt_*, mtm_*, ns_*, piwik_*, pk_*, 
    sb_referer_*, sms_*, trk_*, tw_*, url_bnm_*, utm_*, zone_*, __s, _branch_match_id, _bta_c, 
    _bta_tid, _ga, _gac, _gl, _hsenc, _hsmi, _ke, _openstat, action_object_map, ad_id, 
    adgroupid, adjust_campaign, adjust_tracker, af_campaign, af_channel, af_keyword, af_medium, 
    af_source, awc, bclid, campaign_id, campid, cid, cj_event, cjevent, click_id, cmpid, 
    customid, dclid, dm_i, ef_id, elqTrack, epik, fb_action_ids, fb_action_types, fb_ref, 
    fb_source, fbclid, gbraid, gclid, gclsrc, gdffi, gdfms, gdftrk, hc_location, hc_ref, 
    hootPostID, hsCtaTracking, hubspotUtk, idzone, igshid, irclid, lptoken, mc_cid, mc_eid, 
    mkcid, mkevt, mkrid, mkwid, msclkid, ndclid, pcampaignid, pcrid, psid, pub_id, pubfeed, 
    publisherid, rdt, ref, ref_campaign, ref_source, s_kwcid, scm, scontext_r, si, srsltid, 
    toolid, ttclid, twclid, uclick, wbraid, wprov, wt_mc, WT.mc_id, WT.nav, yclid, zanpid, 
    zoneid`.split(',').map(p => p.trim().toLowerCase());

    // Pre-compile Regexes for performance
    const trackingRegexes = rawParams.map(pattern => {
        if (pattern.includes('*')) {
            const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&');
            const regex = escaped.replace(/\*/g, '[^&=]*');
            return new RegExp(`^${regex}$`, 'i');
        }
        return new RegExp(`^${pattern}$`, 'i');
    });

    function isTrackingParam(paramName) {
        return trackingRegexes.some(regex => regex.test(paramName));
    }

    function cleanUrl(url) {
        if (!url || typeof url !== 'string' || url.startsWith('blob:') || url.startsWith('data:') || url.startsWith('javascript:')) return url; // Check to skip blob and data URLs
        try {
            const urlObj = new URL(url, window.location.origin);
            const params = new URLSearchParams(urlObj.search);

            // Safely iterate by converting keys to an array first
            const keysToDelete = Array.from(params.keys()).filter(isTrackingParam);
            
            if (keysToDelete.length === 0) return url; // No changes needed
            
            keysToDelete.forEach(key => params.delete(key));
            urlObj.search = params.toString();

            // Return relative path if original URL was relative (started with /)
            if (url.startsWith('/') && !url.startsWith('//')) {
                return urlObj.pathname + urlObj.search + urlObj.hash;
            }
            
            return urlObj.toString();
        } catch (e) {
            return url;
        }
    }

    function cleanElementAttribute(element, attribute) {
        if (element.hasAttribute(attribute)) {
            const url = element.getAttribute(attribute);
            // Relaxed the check to allow relative URLs like "/path?utm_..."
            if (url && !url.startsWith('javascript:') && !url.startsWith('mailto:')) {
                const cleaned = cleanUrl(url);
                if (cleaned !== url) {
                    element.setAttribute(attribute, cleaned);
                }
            }
        }
    }

    function cleanPage() {
        document.querySelectorAll('a[href]').forEach(el => cleanElementAttribute(el, 'href'));
        document.querySelectorAll('form[action]').forEach(el => cleanElementAttribute(el, 'action'));
        document.querySelectorAll('iframe[src], script[src], img[src]').forEach(el => cleanElementAttribute(el, 'src'));

        // Clean img/picture srcset
        document.querySelectorAll('[srcset]').forEach(el => {
            let srcset = el.getAttribute('srcset');
            if (srcset) {
                const cleanedSrcset = srcset.split(',').map(part => {
                    const [url, ...rest] = part.trim().split(' ');
                    return cleanUrl(url) + (rest.length ? ' ' + rest.join(' ') : '');
                }).join(',');
                if (cleanedSrcset !== srcset) el.setAttribute('srcset', cleanedSrcset);
            }
        });

        // Clean meta refresh
        document.querySelectorAll('meta[http-equiv="refresh" i]').forEach(meta => {
            const content = meta.getAttribute('content');
            if (content) {
                const match = content.match(/url=([^;]+)/i);
                if (match) {
                    const url = match[1].replace(/^['"]|['"]$/g, '');
                    const cleanedUrl = cleanUrl(url);
                    if (cleanedUrl !== url) {
                        meta.setAttribute('content', content.replace(url, cleanedUrl));
                    }
                }
            }
        });

        // Clean data attributes
        document.querySelectorAll('[data-url], [data-href], [data-src]').forEach(element => {
            ['data-url', 'data-href', 'data-src'].forEach(attr => cleanElementAttribute(element, attr));
        });
    }

    function injectPageScript() {
        // Passing our functions into the page context
        const pageScript = `
            (function() {
                const trackingRegexes = [${trackingRegexes.map(r => r.toString()).join(',')}];

                function isTrackingParam(paramName) {
                    return trackingRegexes.some(regex => regex.test(paramName));
                }

                function cleanUrl(url) {
                    if (!url || typeof url !== 'string' || url.startsWith('blob:') || url.startsWith('data:') || url.startsWith('javascript:')) return url; // Check to skip blob and data URLs
                    try {
                        const urlObj = new URL(url, window.location.origin);
                        const params = new URLSearchParams(urlObj.search);
                        const keysToDelete = Array.from(params.keys()).filter(isTrackingParam);
                        if (keysToDelete.length === 0) return url;
                        
                        keysToDelete.forEach(key => params.delete(key));
                        urlObj.search = params.toString();
                        
                        if (url.startsWith('/') && !url.startsWith('//')) {
                            return urlObj.pathname + urlObj.search + urlObj.hash;
                        }
                        return urlObj.toString();
                    } catch (e) {
                        return url;
                    }
                }

                // Intercept window.location
                const originalLocationAssign = window.location.assign;
                const originalLocationReplace = window.location.replace;
                
                try {
                    const originalHrefSetter = Object.getOwnPropertyDescriptor(Location.prototype, 'href').set;
                    Object.defineProperty(Location.prototype, 'href', {
                        set: function(url) {
                            originalHrefSetter.call(this, cleanUrl(url));
                        },
                        get: Object.getOwnPropertyDescriptor(Location.prototype, 'href').get
                    });
                } catch(e) {} // Fails safely in strict environments

                window.location.assign = function(url) { 
                    originalLocationAssign.call(window.location, cleanUrl(url)); 
                };
                window.location.replace = function(url) { 
                    originalLocationReplace.call(window.location, cleanUrl(url)); 
                };

                // Intercept XHR
                const originalXhrOpen = XMLHttpRequest.prototype.open;
                XMLHttpRequest.prototype.open = function(method, url, ...rest) {
                    const stringUrl = typeof url === 'string' ? url : url.toString();
                    const cleaned = cleanUrl(stringUrl);
                    // Only modify if the URL actually changed
                    if (cleaned !== stringUrl) {
                        return originalXhrOpen.call(this, method, cleaned, ...rest);
                    }
                    return originalXhrOpen.call(this, method, url, ...rest);
                };
            })();
        `;

        const script = document.createElement('script');
        script.textContent = pageScript;
        if (document.documentElement) document.documentElement.appendChild(script);
    }

    // Intercept fetch
    const originalFetch = window.fetch;
    window.fetch = function(...args) {
        if (args[0]) {
            if (typeof args[0] === 'string') {
                const cleaned = cleanUrl(args[0]);
                if (cleaned !== args[0]) {
                    args[0] = cleaned;
                }
            } else if (typeof args[0] === 'object' && args[0].url) {
                const cleaned = cleanUrl(args[0].url);
                // ONLY clone the Request object if the URL had tracking params removed
                if (cleaned !== args[0].url) {
                    args[0] = new Request(cleaned, args[0]);
                }
            }
        }
        return originalFetch.apply(this, args);
    };

    // Clean current page URL in address bar if tracking params exist
    const cleanedCurrentUrl = cleanUrl(window.location.href);
    if (cleanedCurrentUrl !== window.location.href) {
        window.history.replaceState(null, '', cleanedCurrentUrl);
    }

    // Debounce function to prevent lag from MutationObserver
    let timeout;
    const observer = new MutationObserver((mutations) => {
        const hasAdditions = mutations.some(m => m.addedNodes.length > 0);
        if (hasAdditions) {
            clearTimeout(timeout);
            timeout = setTimeout(() => cleanPage(), 300); // Wait 300ms after DOM settles
        }
    });

    injectPageScript();

    // Initial Run on DOMContentLoaded
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', cleanPage);
    } else {
        cleanPage();
    }

    observer.observe(document.documentElement || document.body, { childList: true, subtree: true });
})();