De-Tracker (Anti-Tracking + Facebook Safe)

Removes tracking params, disables link ping, unwraps redirectors, and prevents Facebook tracking without blocking the site.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         De-Tracker (Anti-Tracking + Facebook Safe)
// @namespace    https://greasyfork.org/en/users/1258004-jake-shue
// @version      2.2
// @description  Removes tracking params, disables link ping, unwraps redirectors, and prevents Facebook tracking without blocking the site.
// @author       you
// @match        *://*/*
// @run-at       document-start
// @grant        none
// @license     none
// ==/UserScript==

(function() {
  'use strict';

  const TRACK_PARAMS = new Set([
    'utm_source','utm_medium','utm_campaign','utm_term','utm_content','utm_id','utm_name','utm_reader','utm_referrer',
    'ga_source','ga_medium','ga_campaign','ga_content','ga_term',
    'gclid','dclid','gbraid','wbraid','fbclid','twclid','ttclid','yclid','msclkid',
    'mc_eid','mc_cid','_hsenc','_hsmi','vero_conv','vero_id','mkt_tok','oly_anon_id','oly_enc_id',
    'pk_campaign','pk_kwd','pk_source','pk_medium',
    'igshid','spm','si','ref','ref_src','ref_url','src','sc_channel','sc_campaign','sc_content','sc_medium',
    'ved','ei','usg','oq','uact','source','rlz','sxsrf',
    'campaign','campaignId','tracking','track','trk','ncid','ncid_src'
  ]);

  const REDIRECT_PARAM_CANDIDATES = [
    'url','u','dest','destination','redir','redirect','redirect_url','redirect_uri',
    'r','to','target','q','link','l','out','o','external','next','continue','goto','href','request','RU'
  ];

  const REDIRECT_PATTERNS = [
    {hostRe: /(^|\.)google\.[a-z.]+$/i, pathRe: /^\/(url|imgres|aclk|amp\/s|amp\/t)/i},
    {hostRe: /(^|\.)facebook\.com$/i, pathRe: /^\/l\.php$/i},
    {hostRe: /(^|\.)l\.facebook\.com$/i, pathRe: /^\//},
    {hostRe: /(^|\.)out\.reddit\.com$/i, pathRe: /^\//},
    {hostRe: /(^|\.)link\.medium\.com$/i, pathRe: /^\//},
    {hostRe: /(^|\.)steamcommunity\.com$/i, pathRe: /^\/linkfilter\/$/i},
    {hostRe: /(^|\.)youtube\.com$/i, pathRe: /^\/redirect$/i},
    {hostRe: /(^|\.)r\.t\.umblr\.com$/i, pathRe: /^\//},
    {hostRe: /(^|\.)news\.yahoo\.com$/i, pathRe: /^\//},
    {hostRe: /(^|\.)safelinks\.protection\.outlook\.com$/i, pathRe: /^\//},
    {hostRe: /(^|\.)lnkd\.in$/i, pathRe: /^\//}
  ];

  function safeDecode(str) {
    try { return decodeURIComponent(str); } catch { return str; }
  }

  function stripTrackParams(u) {
    const url = new URL(u, location.href);
    for (const key of [...url.searchParams.keys()]) {
      if (TRACK_PARAMS.has(key)) url.searchParams.delete(key);
    }
    if (url.hash && url.hash.includes('=')) {
      const hash = url.hash.replace(/^#/, '');
      const sp = new URLSearchParams(hash);
      let changed = false;
      for (const key of [...sp.keys()]) {
        if (TRACK_PARAMS.has(key)) {
          sp.delete(key);
          changed = true;
        }
      }
      if (changed) {
        const newHash = sp.toString();
        url.hash = newHash ? '#' + newHash : '';
      }
    }
    return url.toString();
  }

  function looksLikeRedirector(u) {
    const url = new URL(u, location.href);
    return REDIRECT_PATTERNS.some(p => p.hostRe.test(url.hostname) && p.pathRe.test(url.pathname));
  }

  function extractRedirectTarget(u) {
    const url = new URL(u, location.href);
    for (const key of REDIRECT_PARAM_CANDIDATES) {
      const v = url.searchParams.get(key);
      if (v && /^https?:\/\//i.test(v)) return safeDecode(v);
    }
    for (const [key, value] of url.searchParams.entries()) {
      if (!value || value.length < 8) continue;
      if (/^[A-Za-z0-9+/_-]+=*$/.test(value)) {
        try {
          const decoded = atob(value.replace(/-/g, '+').replace(/_/g, '/'));
          if (/^https?:\/\//i.test(decoded)) return decoded;
        } catch {}
      }
    }
    return null;
  }

  function cleanLinkHref(a) {
    if (!a || !a.href) return;
    if (!/^https?:/i.test(a.href)) return;
    if (looksLikeRedirector(a.href)) {
      const target = extractRedirectTarget(a.href);
      if (target) a.href = target;
    }
    a.href = stripTrackParams(a.href);
    if (a.hasAttribute('ping')) a.removeAttribute('ping');
    ['onmousedown','onclick','onmouseup'].forEach(k => {
      if (a.hasAttribute(k)) a.removeAttribute(k);
    });
    ['saferedirecturl','tracking','clicktrack','click_id'].forEach(k => {
      const dk = 'data-' + k;
      if (a.hasAttribute(dk)) a.removeAttribute(dk);
    });
  }

  function cleanAllLinks(root=document) {
    const links = root.querySelectorAll ? root.querySelectorAll('a[href]') : [];
    for (const a of links) cleanLinkHref(a);
  }

  function cleanLocationBar() {
    try {
      const cleaned = stripTrackParams(location.href);
      if (cleaned !== location.href) {
        history.replaceState(history.state, '', cleaned);
      }
    } catch {}
  }

  function onPreClick(e) {
    let el = e.target;
    while (el && el !== document && !(el instanceof HTMLAnchorElement)) el = el.parentElement;
    if (el && el instanceof HTMLAnchorElement) {
      cleanLinkHref(el);
    }
  }

  function onCopy(e) {
    const sel = document.getSelection();
    if (!sel) return;
    const node = sel.anchorNode && sel.anchorNode.parentElement;
    const a = node && node.closest && node.closest('a[href]');
    if (a && /^https?:/i.test(a.href)) {
      const cleaned = stripTrackParams(a.href);
      try {
        e.clipboardData.setData('text/plain', cleaned);
        e.preventDefault();
      } catch {}
    }
  }

  function patchHistory() {
    const origPush = history.pushState;
    const origReplace = history.replaceState;
    history.pushState = function(state, title, url) {
      if (typeof url === 'string') url = stripTrackParams(new URL(url, location.href).toString());
      const ret = origPush.apply(this, [state, title, url]);
      queueMicrotask(cleanLocationBar);
      return ret;
    };
    history.replaceState = function(state, title, url) {
      if (typeof url === 'string') url = stripTrackParams(new URL(url, location.href).toString());
      const ret = origReplace.apply(this, [state, title, url]);
      queueMicrotask(cleanLocationBar);
      return ret;
    };
    window.addEventListener('popstate', cleanLocationBar, true);
  }

  function startObserver() {
    const obs = new MutationObserver(muts => {
      for (const m of muts) {
        if (m.type === 'childList') {
          m.addedNodes.forEach(node => {
            if (node && node.nodeType === 1) {
              if (node.tagName === 'A') {
                cleanLinkHref(node);
              } else {
                cleanAllLinks(node);
              }
            }
          });
        } else if (m.type === 'attributes' && m.target.tagName === 'A' && m.attributeName === 'href') {
          cleanLinkHref(m.target);
        }
      }
    });
    obs.observe(document.documentElement || document, {
      childList: true,
      subtree: true,
      attributes: true,
      attributeFilter: ['href']
    });
  }

  function init() {
    patchHistory();
    cleanLocationBar();
    cleanAllLinks(document);
    startObserver();
    document.addEventListener('click', onPreClick, true);
    document.addEventListener('auxclick', onPreClick, true);
    document.addEventListener('copy', onCopy, true);
  }

  try { init(); } catch {}
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', () => { try { init(); } catch {} }, { once: true });
  }
})();