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

})();