LinkSanitizer

Automatically purges tracking parameters from URLs for privacy and pristine link sharing.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==UserScript==
// @name         LinkSanitizer
// @namespace    https://github.com/local/universal-detracker
// @version      1.0.0
// @description  Automatically purges tracking parameters from URLs for privacy and pristine link sharing.
// @author       r0cketdev1
// @license      MIT
// @match        *://*/*
// @run-at       document-start
// @grant        none
// ==/UserScript==

(function () {
  'use strict';

  // ─── Tracking parameter list ────────────────────────────────────────────────
  // Covers: Google Analytics/Ads, Meta/Facebook, Microsoft/Bing, Twitter/X,
  // HubSpot, Mailchimp, Marketo, Klaviyo, Drip, SendGrid, TikTok, Pinterest,
  // LinkedIn, Snapchat, Yahoo, Reddit, Amazon, and many more.
  const TRACKING_PARAMS = new Set([
    // Google / YouTube
    'utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',
    'utm_id', 'utm_source_platform', 'utm_creative_format', 'utm_marketing_tactic',
    'gclid', 'gclsrc', 'gad_source', 'gbraid', 'wbraid', 'dclid',
    '_ga', '_gl',
    // Meta / Facebook / Instagram
    'fbclid', 'fb_action_ids', 'fb_action_types', 'fb_ref', 'fb_source',
    'fbaid', 'mc_cid', 'mc_eid',
    // Microsoft / Bing
    'msclkid',
    // Twitter / X
    'twclid',
    // TikTok
    'ttclid',
    // Pinterest
    'epik', 'pin_unshorten',
    // Snapchat
    'sccid', 'sc_irclid',
    // LinkedIn
    'li_fat_id', 'trk', 'trkInfo',
    // Yahoo
    'yclid',
    // Reddit
    'rdt_cid',
    // Amazon
    'tag', 'ascsubtag', 'linkCode', 'linkId', 'ref_',
    // HubSpot
    '_hsenc', '_hsmi', 'hsa_acc', 'hsa_ad', 'hsa_cam', 'hsa_grp',
    'hsa_kw', 'hsa_mt', 'hsa_net', 'hsa_src', 'hsa_tgt', 'hsa_ver',
    // Mailchimp
    'mc_cid', 'mc_eid',
    // Marketo
     'mkt_tok',
    // Klaviyo
    '_kx',
    // Drip
    '__s',
    // SendGrid / SparkPost
    'sg_ehash',
    // Vero
    'vero_conv', 'vero_id',
    // Iterable
    'iterableEmailCampaignId', 'iterableTemplateId', 'iterableMessageId',
    // Braze (Appboy)
    'abe',
    // Adobe Analytics / Experience Cloud
    's_cid', 's_kwcid', 'ef_id', 'msid',
    // Outbrain
    'obOrigUrl',
    // Taboola
    'tblci',
    // General
    'ref', 'referrer', 'source', 'campaign', 'affiliate', 'aff_id',
    'click_id', 'clickid', 'cmpid', 'adid', 'adgroupid', 'creative',
    'matchtype', 'network', 'placement', 'position', 'device',
    'devicemodel', 'keyword', 'ad_type', 'ad_format', 'partner_id',
    'irclickid', 'irgwc', 'subid', 'sub_id', 'sub1', 'sub2', 'sub3',
  ]);

  // Regex for wildcard prefix matches (e.g. utm_*, _ga*, etc.)
  const TRACKING_PREFIXES = [
    /^utm_/i,
    /^hsa_/i,
    /^fb_/i,
    /^yclid/i,
    /^mc_/i,
  ];

  // strip tracking params from a URL string
  function cleanURL(rawURL) {
    let url;
    try {
      url = new URL(rawURL);
    } catch {
      return rawURL; // not a valid URL, return unchanged
    }

    const before = url.search;
    const params = url.searchParams;
    const toDelete = [];

    for (const key of params.keys()) {
      const lower = key.toLowerCase();
      if (
        TRACKING_PARAMS.has(lower) ||
        TRACKING_PARAMS.has(key) ||
        TRACKING_PREFIXES.some(rx => rx.test(key))
      ) {
        toDelete.push(key);
      }
    }

    toDelete.forEach(k => params.delete(k));

    const after = url.search;
    return before !== after ? url.toString() : rawURL;
  }

  // Phase 1: Initial sweep on page load 
  function sweepCurrentURL() {
    const clean = cleanURL(window.location.href);
    if (clean !== window.location.href) {
      try {
        window.history.replaceState(
          window.history.state,
          document.title,
          clean
        );
      } catch (e) {
        // Cross-origin or restricted — silently skip
      }
    }
  }

  sweepCurrentURL();

  // Phase 2: Dynamic monitoring for SPAs
  // Override history.pushState
  const originalPushState = history.pushState.bind(history);
  history.pushState = function (state, title, url) {
    const cleanedURL = url ? cleanURL(String(url)) : url;
    return originalPushState(state, title, cleanedURL);
  };

  // Override history.replaceState
  const originalReplaceState = history.replaceState.bind(history);
  history.replaceState = function (state, title, url) {
    const cleanedURL = url ? cleanURL(String(url)) : url;
    return originalReplaceState(state, title, cleanedURL);
  };

  // Listen for popstate (back/forward navigation)
  window.addEventListener('popstate', () => {
    sweepCurrentURL();
  });

  // Phase 3: Copy interceptor
  document.addEventListener('copy', function (e) {
    // Only intercept if clipboard API is writable
    if (!e.clipboardData) return;

    // Try to get selected text
    const selection = window.getSelection();
    if (!selection || selection.isCollapsed) return;

    const selectedText = selection.toString().trim();
    if (!selectedText) return;

    // Check if the selected text looks like a URL
    let isURL = false;
    try {
      const parsed = new URL(selectedText);
      isURL = parsed.protocol === 'http:' || parsed.protocol === 'https:';
    } catch {
      isURL = false;
    }

    if (!isURL) return;

    const cleanedText = cleanURL(selectedText);
    if (cleanedText === selectedText) return; // nothing to clean

    e.preventDefault();
    e.clipboardData.setData('text/plain', cleanedText);
    e.clipboardData.setData('text/html', cleanedText);
  }, true);

  // Phase 3b: Right-click link copy (contextmenu) 
  document.addEventListener('mousedown', function (e) {
    if (e.button !== 2) return; // right-click only
    const anchor = e.target.closest('a[href]');
    if (!anchor) return;

    const originalHref = anchor.getAttribute('href');
    if (!originalHref) return;

    let fullURL;
    try {
      fullURL = new URL(originalHref, window.location.href).toString();
    } catch {
      return;
    }

    const cleanedHref = cleanURL(fullURL);
    if (cleanedHref === fullURL) return;

    // Temporarily swap the href, restore after menu closes
    anchor.setAttribute('href', cleanedHref);
    const restore = () => {
      anchor.setAttribute('href', originalHref);
      document.removeEventListener('mouseup', restore);
    };
    document.addEventListener('mouseup', restore);
  }, true);

})();