CleanURLs

Remove tracking parameters and redirect to original URL. This Userscript uses the URL Interface instead of RegEx.

目前為 2023-06-09 提交的版本,檢視 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name        CleanURLs
// @namespace   i2p.schimon.cleanurl
// @description Remove tracking parameters and redirect to original URL. This Userscript uses the URL Interface instead of RegEx.
// @homepageURL https://greasyfork.org/en/scripts/465933-clean-url-improved
// @supportURL  https://greasyfork.org/en/scripts/465933-clean-url-improved/feedback
// @copyright   2023, Schimon Jehudah (http://schimon.i2p)
// @license     MIT; https://opensource.org/licenses/MIT
// @grant       none
// @run-at      document-end
// @match       *://*/*
// @version     23.06.09
// @icon        data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48dGV4dCB5PSIuOWVtIiBmb250LXNpemU9IjkwIj7wn5qlPC90ZXh0Pjwvc3ZnPgo=

// ==/UserScript==

/*

Simple version of this Userscript
let url = new URL(location.href);
if (url.hash || url.search) {
  location.href = url.origin + url.pathname
};

*/

// https://openuserjs.org/scripts/tfr/YouTube_Link_Cleaner

// Check whether HTML; otherwise, exit.
//if (!document.contentType == 'text/html')
if (document.doctype == null) return;

//let point = [];
const namespace = 'i2p.schimon.cleanurl';

// List of url parameters
const urls = [
  'redirect',
  'ref',
  'source',
  'src',
  'url',
  'utm_source'];

// List of reserved parameters
const whitelist = [
  'art',                  // article
  'action',               // wiki
  'bill',                 // law
  'c',                    // cdn
  'category',             // id
  'code',                 // code
  'content',              // id
  'dark',                 // yorik.uncreated.net
  'date',                 // date
  'days',                 // wiki
  'district',             // house.mo.gov
  'exp_time',             // cdn
  'expires',              // cdn
  'ezimgfmt',             // cdn image processor
  'feedformat',           // wiki
  'fid',                  // mybb
  'file_host',            // cdn
  'filename',             // filename
  'for',                  // cdn
  'format',               // file type
  'guid',                 // guid
  'hash',                 // cdn
  'hidebots',             // wiki
  'hl',                   // language
  'id',                   // id
  'ie',                   // character encoding
  'ip',                   // ip address
  'item_class',           // greasyfork
  'item_id',              // greasyfork
  'jid',                  // jabber id (xmpp)
  'key',                  // cdn
  'limit',                // wiki
  'lang',                 // language
  'language',             // language
  'library',              // oujs
  'locale',               // locale
  'lr',                   // cdn
  'lra',                  // cdn
  'mobileaction',         // wiki
  'news_id',              // post
  'order',                // bugzilla
  'orderBy',              // oujs
  'orderDir',             // oujs
  'p',                    // search query / page number
  'page',                 // mybb
  'preferencesReturnUrl', // return url
  'product',              // bugzilla
  'q',                    // search query
  'query',                // search query
  'query_format',         // bugzilla
//'referer',              // signin <-- provided pathname contains login (log-in) or signin (sign-in)
  'resolution',           // bugzilla
  'return_to',            // signin
  's',                    // search query
  'search',               // search query
  'show_all_versions',    // greasyfork
  'sign',                 // cdn
  'signature',            // cdn
  'sort',                 // greasyfork
  'speed',                // cdn
  'start_time',           // media playback
  'state',                // cdn
  '__switch_theme',       // theme (theanarchistlibrary.org)
  'tag',                  // id
  'tid',                  // mybb
  'title',                // send (share) links and wiki
  'type',                 // file type
//'url',                  // url <-- not whitelisted nor blacklisted
  'utf8',                 // encoding
  'urlversion',           // wiki
  'v',                    // video
  'version',              // greasyfork
  //'_x_tr_sl', // translate online service
  //'_x_tr_tl=', // translate online service
  //'_x_tr_hl=', // translate online service
  //'_x_tr_pto', // translate online service
  //'_x_tr_hist', // translate online service
  'year'                  // year
  ];

// List of useless hash
const hash = [
  'back-url',
  'intcid',
  'niche-',
//'searchinput',
  'src'];

// List of useless parameters
const blacklist = [
  'ad',
  'ad_medium',
  'ad_name',
  'ad_pvid',
  'ad_sub',
  //'ad_tags',
  'advertising-id',
  //'aem_p4p_detail',
  'af',
  'aff',
  'aff_fcid',
  'aff_fsk',
  'aff_platform',
  'aff_trace_key',
  'affparams',
  'afSmartRedirect',
  'afftrack',
  'affparams',
  //'aid',
  'algo_exp_id',
  'algo_pvid',
  'ar',
  //'ascsubtag',
  //'asc_contentid',
  'asgtbndr',
  'atc',
  'ats',
  'autostart',
  //'b64e', // breaks yandex
  'bizType',
  //'block',
  'bta',
  'businessType',
  'campaign',
  'campaignId',
  //'__cf_chl_rt_tk',
  'cid',
  'ck',
  //'clickid',
  //'client_id',
  //'cm_ven',
  'content-id',
  'crid',
  'cst',
  'cts',
  'curPageLogUid',
  //'data', // breaks yandex
  //'dchild',
  //'dclid',
  'deals-widget',
  'dicbo',
  //'dt',
  'edd',
  'edm_click_module',
  //'ei',
  //'embed',
  '_encoding',
  //'etext', // breaks yandex
  'eventSource',
  'fbclid',
  'feature',
  'forced_click',
  //'fr',
  'frs',
  //'from', // breaks yandex
  '_ga',
  'ga_order',
  'ga_search_query',
  'ga_search_type',
  'ga_view_type',
  'gatewayAdapt',
  //'gclid',
  //'gclsrc',
  'gh_jid',
  'gps-id',
  //'gs_lcp',
  'gt',
  'guccounter',
  'hdtime',
  'ICID',
  'ico',
  'ig_rid',
  //'idzone',
  //'iflsig',
  //'irgwc',
  //'irpid',
  'itid',
  //'itok',
  //'katds_labels',
  //'keywords',
  'keyno',
  'l10n',
  'linkCode',
  'mc',
  'mid',
  'mp',
  'nats',
  'nci',
  'obOrigUrl',
  'optout',
  'oq',
  'organic_search_click',
  'pa',
  'Partner',
  'partner',
  'partner_id',
  'pcampaignid',
  'pd_rd_i',
  'pd_rd_r',
  'pd_rd_w',
  'pd_rd_wg',
  'pdp_npi',
  'pf_rd_i',
  'pf_rd_m',
  'pf_rd_p',
  'pf_rd_r',
  'pf_rd_s',
  'pf_rd_t',
  'pg',
  'PHPSESSID',
  'pk_campaign',
  'pdp_ext_f',
  'pkey',
  'platform',
  'plkey',
  'pqr',
  'pr',
  'pro',
  'prod',
  'promo',
  'promocode',
  'promoid',
  'psc',
  'psprogram',
  'pvid',
  'qid',
  //'r',
  'realDomain',
  'recruiter_id',
  'redirect',
  'ref',
  'ref_',
  'ref_src',
  'refcode',
  'referrer',
  'refinements',
  'reftag',
  'rowan_id1',
  'rowan_msg_id',
  //'sCh',
  'sclient',
  'scm',
  'scm_id',
  'scm-url',
  //'sd',
  'si',
  '___SID',
  '_src',
  'src_cmp',
  'src_player',
  'src_src',
  'shareId',
  'showVariations',
  'sid',
  //'site_id',
  'sk',
  'smid',
  'social_params',
  'source',
  'sourceId',
  'sp_csd',
  'spLa',
  'spm',
  'spreadType',
  //'sprefix',
  'sr',
  'src',
  'srcSns',
  'su',
  '_t',
  //'tag',
  'tcampaign',
  'td',
  'terminal_id',
  //'text',
  'th', // Sometimes restored after page load
  //'title',
  'tracelog',
  'traffic_id',
  'traffic_type',
  'tt',
  'uact',
  'ug_edm_item_id',
  //'utm1',
  //'utm2',
  //'utm3',
  //'utm4',
  //'utm5',
  //'utm6',
  //'utm7',
  //'utm8',
  //'utm9',
  'utm_campaign',
  'utm_content',
  'utm_medium',
  'utm_source',
  'utm_term',
  'uuid',
  //'utype',
  //'ve',
  //'ved',
  //'zone'
  ];

// URL Indexers
const paraIDX = [
  'algo_exp_id',
  'algo_pvid',
  'b64e',
  'cst',
  'cts',
  'data',
  'ei',
  //'etext',
  'from',
  'iflsig',
  'gbv',
  'gs_lcp',
  'hdtime',
  'keyno',
  'l10n',
  'mc',
  'oq',
  //'q',
  'sei',
  'sclient',
  'sign',
  'source',
  'state',
  //'text',
  'uact',
  'uuid',
  'ved'];

// Market Places 
const paraMKT = [
  '___SID',
  '_t',
  'ad_pvid',
  'af',
  'aff_fsk',
  'aff_platform',
  'aff_trace_key',
  'afSmartRedirect',
  'bizType',
  'businessType',
  'ck',
  'content-id',
  'crid',
  'curPageLogUid',
  'deals-widget',
  'edm_click_module',
  'gatewayAdapt',
  'gps-id',
  'keywords',
  'pd_rd_i',
  'pd_rd_r',
  'pd_rd_w',
  'pd_rd_wg',
  'pdp_npi',
  'pf_rd_i',
  'pf_rd_m',
  'pf_rd_p',
  'pf_rd_r',
  'pf_rd_s',
  'pf_rd_t',
  'platform',
  'pdp_ext_f',
  'ref_',
  'refinements',
  'rowan_id1',
  'rowan_msg_id',
  'scm',
  'scm_id',
  'scm-url',
  'shareId',
  //'showVariations',
  'sk',
  'smid',
  'social_params',
  'spLa',
  'spm',
  'spreadType',
  'sr',
  'srcSns',
  'terminal_id',
  'th', // Sometimes restored after page load
  'tracelog',
  'tt',
  'ug_edm_item_id'];

// IL
const paraIL = [
  'dicbo',
  'obOrigUrl'];

// General
const paraWWW = [
  'aff',
  'promo',
  'promoid',
  'ref',
  'utm_campaign',
  'utm_content',
  'utm_medium',
  'utm_source',
  'utm_term'];

// For URL of the Address bar
// Check and modify page address
// TODO Add bar and ask to clean address bar
(function modifyURL() {

  let
    check = [],
    url = new URL(location.href);

  // TODO turn into boolean function
  for (let i = 0; i < blacklist.length; i++) {
    if (url.searchParams.get(blacklist[i])) {
      check.push(blacklist[i]);
      url.searchParams.delete(blacklist[i]);
      //newURL = url.origin + url.pathname + url.search + url.hash;
    }
  }

  // TODO turn into boolean function
  for (let i = 0; i < hash.length; i++) {
    if (url.hash.startsWith('#' + hash[i])) {
      check.push(hash[i]);
      //newURL = url.origin + url.pathname + url.search;
    }
  }

  if (check.length > 0) {
    let newURL = url.origin + url.pathname + url.search;
    window.history.pushState(null, null, newURL);
    //location.href = newURL;
  }

})();

(function scanAllURLs() {
  for (let i = 0; i < document.links.length; i++) {
    let url = new URL(document.links[i].href);
    if (url.search) {
    //if (url.search || url.hash) {
      document.links[i].setAttribute('href-data', document.links[i].href);
    }
  }
})();

(function scanBadURLs() {
  for (let i = 0; i < document.links.length; i++) {
    // TODO callback, Mutation Observer, and Event Listener
    hash.forEach(j => cleanLink(document.links[i], j, 'hash'));
    blacklist.forEach(j => cleanLink(document.links[i], j, 'para'));
  }
})();

// TODO Add an Event Listener
function cleanLink(link, target, type) {
  let url = new URL(link.href);
  switch (type) {
    case 'hash':
      //console.log('hash ' + i)
      if (url.hash.startsWith('#' + target)) {
        //link.setAttribute('href-data', link.href);
        link.href = url.origin + url.pathname + url.search;
      }
      break;
    case 'para':
      //console.log('para ' + i)
      if (url.searchParams.get(target)) { 
        url.searchParams.delete(target);
        //link.setAttribute('href-data', link.href);
        link.href = url.origin + url.pathname + url.search;
      }
      break;
  }

  /*
  // EXTRA
  // For URL of hyperlinks
  for (const a of document.querySelectorAll('a')) {
    try{
      let url = new URL(a.href);
      for (let i = 0; i < blacklist.length; i++) {
        if (url.searchParams.get(blacklist[i])) {
          url.searchParams.delete(blacklist[i]);
        }
      }
      a.href = url;
    } catch (err) {
      //console.warn('Found no href for element: ' + a);
      //console.error(err);
    }
  } */

}

// TODO Hunt (for any) links within attributes using getAttributeNames()[i]

// Event Listener
// TODO Scan 'e.target.childNodes' until 'href-data' (link) is found
document.body.addEventListener("mouseover", function(e) { // mouseover works with keyboard too
  //if (e.target && e.target.nodeName == "A") {
  hrefData = e.target.getAttribute('href-data');
  //if (e.target && hrefData && !document.getElementById(namespace)) {
  if (e.target && hrefData && hrefData != document.getElementById('url-original')) {
    if (document.getElementById(namespace)) {
      document.getElementById(namespace).remove();
    }
    selectionItem = createButton(e.pageX, e.pageY, hrefData);
    hrefData = new URL(hrefData);
    selectionItem.append(purgeURL(hrefData));
    let types = ['whitelist', 'blacklist', 'original']
    for (let i = 0; i < types.length; i++) {
      let button = purgeURL(hrefData, types[i]);
      let exist;
      selectionItem.childNodes.forEach(
        node => {
          if (button.href == node.href) {
            exist = true;
          }
        }
      )
      if (!exist) {
        selectionItem.append(button);
      }
    }

    // Check for URLs
    for (let i = 0; i < urls.length; i++) {
      if (hrefData.searchParams.get(urls[i])) { // hrefData.includes('url=')
        urlParameter = hrefData.searchParams.get(urls[i]);
        try {
          urlParameter = new URL (urlParameter);
        } catch {
          if (urlParameter.includes('.')) {  // NOTE It is a guess
            try {
              urlParameter = new URL ('http:' + urlParameter);
            } catch {}
          }
        }
        if (typeof urlParameter == 'object') {
          newURLItem = extractURL(urlParameter);
          selectionItem.prepend(newURLItem);
        }
      }
    }

    // compare original against purged
    if (selectionItem.querySelector(`#url-purged`)) {
      //let urlOrigin = new URL (selectionItem.querySelector(`#url-original`).href);
      let urlPurge = new URL (selectionItem.querySelector(`#url-purged`).href);
      console.log(urlPurge.searchParams.sort())
      console.log(hrefData.searchParams.sort())
      if (hrefData.search == urlPurge.search) {
        selectionItem.querySelector(`#url-original`).remove();
      }
    }

    // do not add element, if url has only whitelisted parameters and no potential url
    // add element, only if a potential url or non-whitelisted parameter was found
    let urlTypes = ['url-extracted', 'url-original', 'url-purged'];
    for (let i = 0; i < urlTypes.length; i++) {
      if (selectionItem.querySelector(`#${urlTypes[i]}`)) {
        document.body.append(selectionItem);
        return;
      }
    }

    // NOTE in case return did not reach
    e.target.removeAttribute('href-data')

  }
});

function createButton(x, y, url) {
  // create element
  let item = document.createElement(namespace);
  // set content
  item.id = namespace;
  // set position
  item.style.all = 'unset';
  item.style.position = 'absolute';
  item.style.left = x+5 + 'px';
  item.style.top = y-3 + 'px';
  // set appearance
  item.style.fontFamily = 'none'; // emoji
  item.style.background = '#333';
  item.style.borderRadius = '5%';
  item.style.padding = '3px';
  item.style.zIndex = 10000;
  //item.style.opacity = 0.7;
  item.style.filter = 'brightness(0.7)'
  // center character
  item.style.justifyContent = 'center';
  item.style.alignItems = 'center';
  item.style.display = 'flex';
  // disable selection marks
  item.style.userSelect = 'none';
  item.style.cursor = 'default';
  // set button behaviour
  item.onmouseover = () => {
    //item.style.opacity = 1;
    item.style.filter = 'unset';
  };
  item.onmouseleave = () => { // onmouseout
    // TODO Wait a few seconds
    item.remove();
  };
  return item;
}

function extractURL(url) {
  let item = document.createElement('a');
  item.textContent = '🔗'; // 🧧 🏷️ 🔖
  item.id = 'url-extracted';
  item.style.all = 'unset';
  item.style.outline = 'none';
  item.style.height = '15px';
  item.style.width = '15px';
  item.style.padding = '3px';
  item.style.margin = '3px';
  //item.style.fontSize = '0.9rem' // 90%
  item.style.lineHeight = 'normal'; // initial
  //item.style.height = 'fit-content';
  item.href = url;
  return item;
}

// TODO Use icons (with shapes) for cases when color is not optimal
function purgeURL(url, listType) {
  let itemTitle, itemId, resUrl;
  let item = document.createElement('a');
  item.style.all = 'unset';
  switch (listType) {
    case 'blacklist':
      itemColor = 'yellow';
      //itemTextContent = '🟡';
      itemTitle = 'Clean link'; // Purged URL
      itemId = 'url-purged';
      resUrl = hrefDataHandler(url, blacklist);
      break;
    case 'original': // TODO dbclick (double-click)
      itemColor = 'orangered';
      //itemTextContent = '🔴';
      itemTitle = 'Unsafe link'; // Original URL
      itemId = 'url-original';
      resUrl = url;
      item.style.cursor = `not-allowed`; // no-drop
      item.onmouseenter = () => {
        item.style.filter = `drop-shadow(2px 4px 6px ${itemColor})`;
      };
      item.onmouseout = () => {
        item.style.filter = 'unset';
      };
      break;
    case 'whitelist':
      itemColor = 'lawngreen';
      //itemTextContent = '🟢';
      itemTitle = 'Safe link'; // Link with whitelisted parameters
      itemId = 'url-known';
      resUrl = hrefDataHandler(url, whitelist);
      break;
    default:
      itemColor = 'antiquewhite';
      //itemTextContent = '⚪';
      itemTitle = 'Base link'; // Link without parameters
      itemId = 'url-base';
      resUrl = url.origin + url.pathname;
      break;
  }
  item.id = itemId;
  item.title = itemTitle;
  item.style.background = itemColor;
  //item.textContent = itemTextContent;
  item.style.borderRadius = '50%';
  item.style.outline = 'none';
  item.style.height = '15px';
  item.style.width = '15px';
  item.style.padding = '3px';
  item.style.margin = '3px';
  item.href = resUrl;
  return item;
}

function hrefDataHandler(url, listType) {
  url = new URL(url.href);
  url.searchParams.sort();
  switch (listType) {
    case whitelist:
      let newURL = new URL (url.origin + url.pathname);
      for (let i = 0; i < whitelist.length; i++) {
        if (url.searchParams.get(whitelist[i])) {
          newURL.searchParams.set(
            whitelist[i],
            url.searchParams.get(whitelist[i]) // catchedValue
          );
        }
      }
      url = newURL;
      break;
    case blacklist:
      for (let i = 0; i < blacklist.length; i++) {
        if (url.searchParams.get(blacklist[i])) {
          url.searchParams.delete(blacklist[i]);
        }
      }
      break;
  }
  return url;
}