Link Untracker

Remove tracking elements from links

  1. // ==UserScript==
  2. // @name Link Untracker
  3. // @namespace IzzySoft
  4. // @description Remove tracking elements from links
  5. // @license CC BY-NC-SA
  6. // @include *
  7. // @exclude *phpmyadmin*
  8. // @version 13
  9. // @run-at document-idle
  10. // @grant unsafeWindow
  11. // @homepage https://codeberg.org/izzy/userscripts
  12. // ==/UserScript==
  13.  
  14. /* badp: we strip parameters starting with that. Some are taken from NetURL, see
  15. https://github.com/Smile4ever/firefoxaddons/tree/master/Neat%20URL-webext
  16. fb_: Facebook; ga_/_gl: Google; hmb_: HumbleBundle(?); utm_: Urchin Tracker; wt_: Webtrekk; WT.: Webtrends
  17. */
  18. var badp = ['fb_','ga_','_ga','_gl','hmb_','utm_',
  19. 'ei@google.','gws_rd@google.','sei@google.','ved@google.',
  20. 'pd_rd_r@amazon.','pd_rd_w@amazon.','pd_rd_wg@amazon.','psc@amazon.','_encoding@amazon.',
  21. 'wt_','WT.','yclid','referrer','clickfrom','pf_rd'
  22. ];
  23. var anch = ''; /* variable to hold the "anchor part" of the URL, if any – i.e. "#anchor" */
  24. var replace_semicolon = true; /* some sites use ";" to separate URL params;
  25. URLSearchParams can't deal with that and messes up.
  26. If those sites don't work with "&", either set this
  27. to "false" or add that site on the exclude list
  28. */
  29.  
  30. function getRealLinkFromGoogleUrl(a) { /* adapted from https://github.com/Rob--W/dont-track-me-google at 2018-01-03 */
  31. if ((a.hostname === location.hostname || a.hostname === 'www.google.com') &&
  32. /^\/(local_)?url$/.test(a.pathname)) {
  33. // Google Maps / Dito (/local_url?q=<url>)
  34. // Mobile (/url?q=<url>)
  35. var url = /[?&](?:q|url)=((?:https?|ftp)[%:][^&]+)/.exec(a.search);
  36. if (url) {
  37. return decodeURIComponent(url[1]);
  38. }
  39. // Help pages, e.g. safe browsing (/url?...&q=%2Fsupport%2Fanswer...)
  40. url = /[?&](?:q|url)=((?:%2[Ff]|\/)[^&]+)/.exec(a.search);
  41. if (url) {
  42. return a.origin + decodeURIComponent(url[1]);
  43. }
  44. }
  45. return a.href;
  46. }
  47.  
  48. class UrlParams { /* adapted from https://stackoverflow.com/a/45516670 as the built-in URLSearchParams messes up parameters without values */
  49. constructor(search) { /* extracts the query string by stripping the leading "?" and initializes properties */
  50. this.qs = (search || location.search).substr(1);
  51. this.params = {};
  52. this.parseQuerstring();
  53. }
  54. parseQuerstring() { /* walks the query string and extracts the parameters as key-value-pairs */
  55. this.qs.split('&').reduce((a, b) => {
  56. let [key, val] = b.split('=');
  57. a[key] = val;
  58. return a;
  59. }, this.params);
  60. }
  61. filterQuerystring(badparms) { /* eliminates all "bad parameters" (tracking parameters) from our key-value-pairs */
  62. var keysToDelete = [];
  63. for (var key of Object.keys(this.params)) {
  64. for (let p of badparms) {
  65. if (key.startsWith(p)) console.log('removing '+key+' from '+this.qs);
  66. if (key.startsWith(p)) { keysToDelete.push(key); break; }
  67. }
  68. }
  69. for(var key of keysToDelete) {
  70. delete this.params[key];
  71. }
  72. }
  73. getString() { /* concatenates all parameters and returns them as a single query string without the leading "?" */
  74. var str = '';
  75. for (var key of Object.keys(this.params)) {
  76. str = str + '&' + key;
  77. if (this.params[key] != undefined) str = str + '=' + this.params[key]; /* only add the "=value" part if the parameter has a value! */
  78. }
  79. return str.substring(1);
  80. }
  81. }
  82.  
  83. // MetaGer.de uses different methods of obfuscation with tracking URLs. This is catching most of them (hopefully):
  84. function metager1(elem) {
  85. if ( elem.hostname == 'api.smartredirect.de' ) {
  86. var purl = new UrlParams(elem.search);
  87. elem.href = decodeURIComponent(purl.params['url']);
  88. } else if ( elem.href.startsWith ('https://metager.de/partner/r')) {
  89. var purl = new UrlParams(elem.search);
  90. elem.href = decodeURIComponent(purl.params['link']);
  91. } else if ( elem.href.startsWith ('https://metager.de/r/metager')) {
  92. if (elem.text.trim().startsWith('http:') || elem.text.trim().startsWith('https:')) {
  93. elem.href = decodeURI(elem.text.trim());
  94. } else {
  95. elem.href = 'https://'+decodeURI(elem.text.trim());
  96. }
  97. }
  98. }
  99.  
  100. function parseLinks(k) {
  101. for(var i = k; i < document.links.length; i++) { // for some reason stops at 100 loops
  102. if (i - k > 98) return i;
  103. var elem = document.links[i];
  104. if (window.location.hostname == 'metager.de' && elem.className.includes('result-link')) { metager1(elem); continue; }
  105. if (elem.search == '') continue;
  106. if (replace_semicolon) var purl = new UrlParams(elem.search.replace(new RegExp(';','g'),'&'));
  107. else var purl = new UrlParams(elem.search);
  108. purl.filterQuerystring(badp);
  109. if (elem.href.indexOf('#') > 0) anch = '#' + elem.href.split('#')[1];
  110. else anch = '';
  111. elem.href = elem.href.split('?')[0] + '?' + purl.getString() + anch;
  112. elem.href = getRealLinkFromGoogleUrl(elem);
  113. }
  114. return i;
  115. }
  116.  
  117. console.log(document.links.length+' links to parse');
  118. k = 0; while ( k < document.links.length) {
  119. console.log('parsing from '+k);
  120. k = parseLinks(k+1);
  121. console.log('returned '+k)
  122. }
  123. console.log('DONE');