LinkSanitizer

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==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);

})();