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