LinkSanitizer

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 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);

})();