CleanURLs (Clean URL Improved)

Remove tracking parameters and redirect to original URL. This software utilizes the URL Interface instead of RegEx.

  1. // ==UserScript==
  2. // @name CleanURLs (Clean URL Improved)
  3. // @namespace i2p.schimon.cleanurl
  4. // @description Remove tracking parameters and redirect to original URL. This software utilizes the URL Interface instead of RegEx.
  5. // @homepageURL https://greasyfork.org/scripts/465933-clean-url-improved
  6. // @supportURL https://greasyfork.org/scripts/465933-clean-url-improved/feedback
  7. // @copyright 2023 - 2025, Schimon Jehudah (http://schimon.i2p)
  8. // @license MIT; https://opensource.org/licenses/MIT
  9. // @grant none
  10. // @run-at document-end
  11. // @match file:///*
  12. // @match *://*/*
  13. // @version 25.05.14
  14. // @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48dGV4dCB5PSIuOWVtIiBmb250LXNpemU9IjkwIj7wn5qlPC90ZXh0Pjwvc3ZnPgo=
  15. // ==/UserScript==
  16.  
  17. // NOTE GM.getValue and GM.setValue
  18. // will be used for statistics and
  19. // custom lists
  20.  
  21. /*
  22.  
  23. NEWS
  24.  
  25. Handle /ref=
  26.  
  27. Added .replace(/\/ref=/g, '/?ref=')
  28.  
  29. Coding like a Japanese
  30.  
  31. TODO
  32.  
  33. 1) Delete empty parameters
  34.  
  35. 2) Statistics
  36. GM.getValue('links-total')
  37. GM.getValue('links-bad')
  38. GM.getValue('links-good')
  39. GM.getValue('parameters-total')
  40. GM.getValue('parameters-bad')
  41. GM.getValue('parameters-good')
  42. GM.getValue('parameters-urls')
  43. GM.getValue('parameters-unclassified')
  44.  
  45. GM.setValue('pbl', 'hostname-parameter')
  46. GM.setValue('pwl', 'hostname-parameter')
  47.  
  48. 3.1) Keep the position on the link (not near)
  49.  
  50. 3.2) Focus cursor on the yellow or white spot.
  51.  
  52. 3.3.) Be more strict on shopping websites (e.g. Remove parameter "tag").
  53.  
  54. FIXME
  55.  
  56. Whitelisted parameters without values are realized to purged-url
  57.  
  58. */
  59.  
  60. /*
  61.  
  62. Simple version of this Userscript
  63. let url = new URL(location.href);
  64. if (url.hash || url.search) {
  65. location.href = url.origin + url.pathname
  66. };
  67.  
  68. */
  69.  
  70. // https://openuserjs.org/scripts/tfr/YouTube_Link_Cleaner
  71.  
  72. // Check whether HTML; otherwise, exit.
  73. //if (!document.contentType == 'text/html')
  74. if (document.doctype == null) return;
  75.  
  76. var itemColor, newURLItem, selectionItem, urlParameter;
  77.  
  78. //let point = [];
  79. const namespace = 'i2p.schimon.cleanurl';
  80.  
  81. // List of url parameters
  82. const urls = [
  83. 'ap_id',
  84. 'media',
  85. 'mortyurl',
  86. 'redirect',
  87. 'ref',
  88. 'rf',
  89. 'source',
  90. 'src',
  91. 'target',
  92. 'url',
  93. 'utm_source',
  94. 'utm_term'];
  95.  
  96. // List of alphabet
  97. const alphabet = 'abcdefghijklmnopqrstuvwxyz';
  98.  
  99. // List of reserved parameters
  100. const whitelist = [
  101. '_action', // roundcube
  102. '_task', // roundcube
  103. 'act', // invision board
  104. 'action', // bugzilla
  105. 'activeTab', // npmjs
  106. 'analyzed', // trac
  107. 'ap_id', // activitypub
  108. 'arch', // postmarketOS
  109. 'archived', // forgejo
  110. 'architecture', // distrowatch.com
  111. 'art', // article
  112. 'artist', // bandcamp jamendo
  113. 'assignee', // forgejo
  114. 'author', // git
  115. 'b32', // i2p
  116. 'bill', // law
  117. 'basedon', // distrowatch.com
  118. 'board', // simple machines forum
  119. 'branch', // postmarketOS
  120. 'CategoryID', // id
  121. 'category', // id
  122. 'categories', // searxng
  123. 'catid', // id
  124. 'cc', // trac
  125. 'ci', // fossil-scm.org
  126. 'code', // code
  127. 'component', // addons.palemoon.org
  128. 'confirmation_token', // dev.gajim.org
  129. 'content', // id
  130. 'contributor', // lulu
  131. 'CurrentMDW', // menuetos.be
  132. 'dark', // yorik.uncreated.net
  133. 'date', // date
  134. 'days', // mediawiki
  135. 'defaultinit', // distrowatch.com
  136. 'desktop', // distrowatch.com
  137. 'distribution', // betterbird.eu
  138. 'diff', // mediawiki
  139. 'do', // flyspray
  140. 'do_union', // bugzilla
  141. 'district', // house.mo.gov
  142. 'distrorange', // distrowatch.com
  143. 'engine', // simplytranslate
  144. 'exp_time', // cdn
  145. 'expires', // cdn
  146. 'ezimgfmt', // cdn image processor
  147. 'feed', // mediawiki
  148. 'feedformat', // mediawiki
  149. 'fid', // mybb
  150. 'file', // tw-cms
  151. 'file_host', // cdn
  152. 'filename', // filename
  153. 'flavor', // blojsom
  154. 'for', // cdn
  155. 'format', // file type
  156. 'from', // redmine
  157. 'func', // tw-cms
  158. 'gid', // entirelydigital.com
  159. 'guid', // guid
  160. 'group', // bugzilla
  161. 'group_id', // sourceforge
  162. 'hash', // cdn
  163. 'hidebots', // mediawiki
  164. 'hit', // tasnimnews.com RSS
  165. 'hl', // language
  166. 'i2paddresshelper', // i2p
  167. 'id', // id
  168. 'ie', // character encoding
  169. 'ip', // ip address
  170. 'isosize', // distrowatch.com
  171. 'item_class', // greasyfork.org
  172. 'item_id', // greasyfork.org
  173. 'iv_load_policy', // invidious
  174. 'jid', // jabber id (xmpp)
  175. 'key', // cdn
  176. 'keywords', // trac
  177. 'labels', // codeberg
  178. 'lang', // language
  179. 'language', // searxng
  180. 'library', // openuserjs.org
  181. 'limit', // mediawiki
  182. 'list', // video
  183. 'locale', // locale
  184. 'logout', // bugzilla
  185. 'lr', // cdn
  186. 'lra', // cdn
  187. 'maintainer', // postmarketOS
  188. 'member', // xmb forum
  189. 'milestone', // forgejo
  190. 'mobileaction', // mediawiki
  191. 'mode', // darkconquest.org
  192. 'module', // tw-cms
  193. 'mortyurl', // morty searxng
  194. 'name', // archlinux
  195. 'news_id', // post
  196. 'netinstall', // distrowatch.com
  197. 'node', // movim
  198. 'notbasedon', // distrowatch.com
  199. 'nsm', // fossil-scm.org
  200. 'oldid', // mediawiki
  201. 'op', // tw-cms
  202. 'order', // bugzilla
  203. 'orderby', // sumo torrent
  204. 'orderBy', // openuserjs.org
  205. 'orderDir', // openuserjs.org
  206. 'origin', // openuserjs.org
  207. 'ostype', // distrowatch.com
  208. 'outputType', // xml
  209. //'p', // search query, page number
  210. 'package', // distrowatch.com
  211. 'package_id', // sourceforge
  212. 'packager', // archlinux, artixlinux, parabola
  213. 'page', // mybb
  214. 'page_id', // picapica.im
  215. //'param',
  216. 'pid', // fluxbb
  217. 'pkg', // distrowatch.com
  218. 'pkgver', // distrowatch.com
  219. 'playlistPosition', // peertube
  220. 'poster', // forgejo
  221. 'public_key', // session
  222. 'preferencesReturnUrl', // return url
  223. 'priority', // trac
  224. 'product', // bugzilla
  225. 'project', // flyspray
  226. 'profile', // copy.sh/v86/
  227. 'pubsub', // rivista xmpp journal
  228. //'q', // search query
  229. 'query', // search query
  230. 'query_format', // bugzilla
  231. 'raw', // portal.mozz.us
  232. 'redirect_to_referer', // dev.gajim.org
  233. 'redlink', // mediawiki
  234. 'ref_type', // gitlab
  235. //'referer', // connect NOTE provided pathname contains connect or register
  236. 'relation', // distrowatch.com
  237. 'repo', // postmarketOS
  238. 'repo_name', // git
  239. 'reporter', // trac
  240. 'reproduced', // trac
  241. 'resolution', // bugzilla
  242. 'requestee', // bugzilla
  243. 'requester', // bugzilla
  244. 'return_to', // connect
  245. 'rolling', // distrowatch.com
  246. //'s', // search query
  247. 'search', // search query
  248. 'searchterm', // treebuilder.de
  249. 'selectedItem', // atlassian
  250. 'showtopic', // invision board
  251. 'show_all_versions', // greasyfork.org
  252. 'showforum', // forums.duke4.net
  253. //'si', // TODO make this specific to yt
  254. 'sign', // cdn
  255. 'signature', // cdn
  256. 'size', // rss
  257. 'sort', // greasyfork.org
  258. 'speed', // cdn
  259. 'ss', // fossil-scm.org
  260. 'st', // invision board
  261. 'start', // page
  262. 'start_time', // media playback
  263. 'state', // forgejo
  264. 'status', // distrowatch.com
  265. 'subcat', // solidtorrents
  266. 'state', // cdn
  267. 'station', // christiannetcast.com / truthradio.com
  268. '__switch_theme', // theanarchistlibrary.org
  269. // 'tag', // id
  270. 'tags', // blasta
  271. 'template', // zapier
  272. 'tid', // mybb
  273. 'title', // send (share) links and mediawiki
  274. 'to', // gitlab
  275. 'top', // hubzilla (atom syndication feed)
  276. 'topic', // simple machines forum
  277. 'type', // file type
  278. //'url', // url NOTE not sure whether to whitelist or blacklist
  279. 'utf8', // encoding
  280. 'urlversion', // mediawiki
  281. 'v1', // greasyfork.org
  282. 'v2', // greasyfork.org
  283. 'version', // greasyfork.org
  284. 'version_id', // gitlab
  285. //'view', // invision board
  286. 'view-source', // git.sr.ht
  287. 'vfx', // fossil-scm.org
  288. //'_x_tr_sl', // translate online service
  289. //'_x_tr_tl=', // translate online service
  290. //'_x_tr_hl=', // translate online service
  291. //'_x_tr_pto', // translate online service
  292. //'_x_tr_hist', // translate online service
  293. 'year' // year
  294. ];
  295.  
  296. // TODO
  297. // List of usefull hash
  298. const goodHash = [
  299. 'advanced', // distrowatch.com
  300. 'simple', // distrowatch.com
  301. ];
  302.  
  303. // List of useless hash
  304. const hash = [
  305. 'back-url',
  306. 'back_url',
  307. 'intcid',
  308. 'niche-',
  309. //'searchinput',
  310. 'src'];
  311.  
  312. // List of useless parameters
  313. const blacklist = [
  314. 'ad',
  315. 'ad-location',
  316. 'ad_medium',
  317. 'ad_name',
  318. 'ad_pvid',
  319. 'ad_sub',
  320. //'ad_tags',
  321. 'adt_ei',
  322. 'advertising-id',
  323. //'aem_p4p_detail',
  324. 'af',
  325. 'aff',
  326. 'aff_fcid',
  327. 'aff_fsk',
  328. 'aff_platform',
  329. 'aff_trace_key',
  330. 'affiliate',
  331. 'affparams',
  332. 'afftrack',
  333. 'afSmartRedirect',
  334. //'aid',
  335. 'algo_exp_id',
  336. 'algo_pvid',
  337. 'ar',
  338. //'ascsubtag',
  339. //'asc_contentid',
  340. 'asgtbndr',
  341. 'atc',
  342. 'ats',
  343. 'autostart',
  344. //'b64e', // breaks yandex
  345. 'bizType',
  346. //'block',
  347. '_bsa_req',
  348. 'bta',
  349. 'businessType',
  350. 'campaign',
  351. 'campaignId',
  352. //'__cf_chl_rt_tk',
  353. //'cid', // breaks sacred magick
  354. 'ck',
  355. //'clickid',
  356. //'client_id',
  357. //'cm_ven',
  358. //'cmd',
  359. 'content-id',
  360. 'crid',
  361. 'cst',
  362. 'cts',
  363. 'curPageLogUid',
  364. //'data', // breaks yandex
  365. //'dchild',
  366. //'dclid',
  367. 'deals-widget',
  368. 'dgcid',
  369. 'dicbo',
  370. //'dt',
  371. 'e9s', // rumble.com
  372. 'edd',
  373. 'edm_click_module',
  374. //'ei',
  375. //'embed',
  376. //'_encoding',
  377. //'etext', // breaks yandex
  378. 'eventSource',
  379. 'fbclid',
  380. 'feature',
  381. 'field-lbr_brands_browse-bin',
  382. 'forced_click',
  383. //'fr',
  384. 'frs',
  385. //'from', // breaks yandex
  386. '_ga',
  387. 'ga_order',
  388. 'ga_search_query',
  389. 'ga_search_type',
  390. 'ga_view_type',
  391. 'gatewayAdapt',
  392. //'gclid',
  393. //'gclsrc',
  394. 'gh_jid',
  395. 'gps-id',
  396. //'gs_lcp',
  397. 'gt',
  398. 'guccounter',
  399. 'hdtime',
  400. //'hosted_button_id',
  401. 'ICID',
  402. 'ico',
  403. 'ig_rid',
  404. //'idzone',
  405. //'iflsig',
  406. 'intake',
  407. 'intcmp',
  408. 'irclickid',
  409. //'irgwc',
  410. //'irpid',
  411. 'is_from_webapp',
  412. 'itid',
  413. //'itok',
  414. //'katds_labels',
  415. //'keywords',
  416. 'keyno',
  417. 'l10n',
  418. 'linkCode',
  419. 'mc',
  420. 'mid',
  421. '__mk_de_DE',
  422. 'mp',
  423. 'nats',
  424. 'nci',
  425. 'obOrigUrl',
  426. 'offer_id',
  427. 'opened-from',
  428. 'optout',
  429. 'oq',
  430. 'organic_search_click',
  431. 'pa',
  432. 'Partner',
  433. 'partner',
  434. 'partner_id',
  435. 'partner_ID',
  436. 'pcampaignid',
  437. 'pd_rd_i',
  438. 'pd_rd_r',
  439. 'pd_rd_w',
  440. 'pd_rd_wg',
  441. 'pdp_npi',
  442. 'pf_rd_i',
  443. 'pf_rd_m',
  444. 'pf_rd_p',
  445. 'pf_rd_r',
  446. 'pf_rd_s',
  447. 'pf_rd_t',
  448. 'pg',
  449. 'PHPSESSID',
  450. 'pk_campaign',
  451. 'pdp_ext_f',
  452. 'pkey',
  453. 'platform',
  454. 'plkey',
  455. 'pqr',
  456. 'pr',
  457. 'pro',
  458. 'prod',
  459. 'prom',
  460. 'promo',
  461. 'promocode',
  462. 'promoid',
  463. 'psc',
  464. 'psprogram',
  465. 'pvid',
  466. 'qid',
  467. //'r',
  468. 'realDomain',
  469. 'recruiter_id',
  470. 'redirect',
  471. 'ref',
  472. 'ref_',
  473. 'ref_src',
  474. 'refcode',
  475. 'referral',
  476. 'referrer',
  477. 'refinements',
  478. 'reftag',
  479. 'related_post_from',
  480. 'rf',
  481. 'rnid',
  482. 'rowan_id1',
  483. 'rowan_msg_id',
  484. //'rss',
  485. //'sCh',
  486. 'sclient',
  487. 'scm',
  488. 'scm_id',
  489. 'scm-url',
  490. //'sd',
  491. 'sender_device',
  492. 'sh',
  493. 'shareId',
  494. 'showVariations',
  495. //'si',
  496. //'sid', // breaks whatsup.org.il
  497. '___SID',
  498. //'site_id',
  499. 'sk',
  500. 'smid',
  501. 'social_params',
  502. 'source',
  503. 'sourceId',
  504. 'sp_csd',
  505. 'spLa',
  506. 'spm',
  507. 'spreadType',
  508. //'sprefix',
  509. 'sr',
  510. 'src',
  511. '_src',
  512. 'src_cmp',
  513. 'src_player',
  514. 'src_src',
  515. 'srcSns',
  516. 'su',
  517. //'sxin_0_pb',
  518. '_t',
  519. //'tag',
  520. 'tcampaign',
  521. 'td',
  522. 'terminal_id',
  523. //'text',
  524. 'th', // Sometimes restored after page load
  525. //'title',
  526. 'tracelog',
  527. 'traffic_id',
  528. 'traffic_source',
  529. 'traffic_type',
  530. 'tt',
  531. 'uact',
  532. 'ug_edm_item_id',
  533. 'userId',
  534. 'utm',
  535. //'utm1',
  536. //'utm2',
  537. //'utm3',
  538. //'utm4',
  539. //'utm5',
  540. //'utm6',
  541. //'utm7',
  542. //'utm8',
  543. //'utm9',
  544. 'utm_campaign',
  545. 'utm_content',
  546. 'utm_id',
  547. 'utm_medium',
  548. 'utm_source',
  549. 'utm_term',
  550. 'uuid',
  551. //'utype',
  552. //'ve',
  553. //'ved',
  554. //'zone'
  555. ];
  556.  
  557. // URL Indexers
  558. const paraIDX = [
  559. 'algo_exp_id',
  560. 'algo_pvid',
  561. 'b64e',
  562. 'cst',
  563. 'cts',
  564. 'data',
  565. 'ei',
  566. //'etext',
  567. 'from',
  568. 'iflsig',
  569. 'gbv',
  570. 'gs_lcp',
  571. 'hdtime',
  572. 'keyno',
  573. 'l10n',
  574. 'mc',
  575. 'oq',
  576. //'q',
  577. 'sei',
  578. 'sclient',
  579. 'sign',
  580. 'source',
  581. 'state',
  582. //'text',
  583. 'uact',
  584. 'uuid',
  585. 'ved'];
  586.  
  587. // Market Places
  588. const paraMKT = [
  589. '___SID',
  590. '_t',
  591. 'ad_pvid',
  592. 'af',
  593. 'aff_fsk',
  594. 'aff_platform',
  595. 'aff_trace_key',
  596. 'afSmartRedirect',
  597. 'bizType',
  598. 'businessType',
  599. 'ck',
  600. 'content-id',
  601. 'crid',
  602. 'curPageLogUid',
  603. 'deals-widget',
  604. 'edm_click_module',
  605. 'gatewayAdapt',
  606. 'gps-id',
  607. 'keywords',
  608. '__mk_de_DE',
  609. 'pd_rd_i',
  610. 'pd_rd_r',
  611. 'pd_rd_w',
  612. 'pd_rd_wg',
  613. 'pdp_npi',
  614. 'pf_rd_i',
  615. 'pf_rd_m',
  616. 'pf_rd_p',
  617. 'pf_rd_r',
  618. 'pf_rd_s',
  619. 'pf_rd_t',
  620. 'platform',
  621. 'pdp_ext_f',
  622. 'ref_',
  623. 'refinements',
  624. 'rnid',
  625. 'rowan_id1',
  626. 'rowan_msg_id',
  627. 'scm',
  628. 'scm_id',
  629. 'scm-url',
  630. 'shareId',
  631. //'showVariations',
  632. 'sk',
  633. 'smid',
  634. 'social_params',
  635. 'spLa',
  636. 'spm',
  637. 'spreadType',
  638. 'sprefix',
  639. 'sr',
  640. 'srcSns',
  641. //'sxin_0_pb',
  642. 'terminal_id',
  643. 'th', // Sometimes restored after page load
  644. 'tracelog',
  645. 'tt',
  646. 'ug_edm_item_id'];
  647.  
  648. // IL
  649. const paraIL = [
  650. 'dicbo',
  651. 'obOrigUrl'];
  652.  
  653. // General
  654. const paraWWW = [
  655. 'aff',
  656. 'promo',
  657. 'promoid',
  658. 'ref',
  659. 'utm_campaign',
  660. 'utm_content',
  661. 'utm_medium',
  662. 'utm_source',
  663. 'utm_term'];
  664.  
  665. // For URL of the Address bar
  666. // Check and modify page address
  667. // TODO Add bar and ask to clean address bar
  668. (function modifyURL() {
  669.  
  670. let
  671. check = [],
  672. // NOTE Marketplace website which uses /ref= instead of ?ref=
  673. // location.href.replace(/\/ref=/g, '/?ref=');
  674. url = new URL(location.href.replace('/ref=', '?ref='));
  675.  
  676. // TODO turn into boolean function
  677. for (let i = 0; i < blacklist.length; i++) {
  678. if (url.searchParams.get(blacklist[i])) {
  679. check.push(blacklist[i]);
  680. url.searchParams.delete(blacklist[i]);
  681. //newURL = url.origin + url.pathname + url.search + url.hash;
  682. }
  683. }
  684.  
  685. // TODO turn into boolean function
  686. for (let i = 0; i < hash.length; i++) {
  687. if (url.hash.startsWith('#' + hash[i])) {
  688. check.push(hash[i]);
  689. //newURL = url.origin + url.pathname + url.search;
  690. }
  691. }
  692.  
  693. if (check.length > 0) {
  694. let newURL = url.origin + url.pathname + url.search;
  695. window.history.pushState(null, null, newURL);
  696. //location.href = newURL;
  697. }
  698.  
  699. })();
  700.  
  701. // NOTE Marketplace website which uses /ref= instead of ?ref=
  702. (function correctSlashRefURLs() {
  703. for (let i = 0; i < document.links.length; i++) {
  704. if (document.links[i].href.includes('/ref=')) {
  705. document.links[i].href = document.links[i].href.replace('/ref=', '?ref=');
  706. document.links[i].setAttribute('slash-ref', '');
  707. }
  708. }
  709. })();
  710.  
  711. (function scanAllURLs() {
  712. for (let i = 0; i < document.links.length; i++) {
  713. let url = new URL(document.links[i].href);
  714. // NOTE Consider BitTorrent Magnet links
  715. // removing trackers would need a warning about
  716. // private torrents, if torrent is not public (dht-enabled)
  717. const allowedProtocols = [
  718. 'finger:', 'ftp:', 'ftps:', 'freenet:',
  719. 'gemini:', 'gopher:', 'http:', 'https:',
  720. 'ipfs:', 'wap:']; // mailto, magnet, xmpp
  721. if (url.search && allowedProtocols.includes(url.protocol)) {
  722. //if (url.search || url.hash) {
  723. document.links[i].setAttribute('href-data', document.links[i].href);
  724. }
  725. }
  726. })();
  727.  
  728. (function scanBadURLs() {
  729. for (let i = 0; i < document.links.length; i++) {
  730. // TODO callback, Mutation Observer, and Event Listener
  731. // TODO Count links increaseByOne('links')
  732. // NOTE To count links, add return statement to function cleanLink()
  733. // return statement will indicate that link is positive for subject
  734. // parameters and therefore should be counted. Counter will be added
  735. // by one, once detected that url is not equal to (new) url.
  736. hash.forEach(j => cleanLink(document.links[i], j, 'hash'));
  737. blacklist.forEach(j => cleanLink(document.links[i], j, 'para'));
  738. }
  739. })();
  740.  
  741. // TODO Add an Event Listener
  742. function cleanLink(link, target, type) {
  743. let url = new URL(link.href);
  744. switch (type) {
  745. case 'hash':
  746. //console.log('hash ' + i)
  747. if (url.hash.startsWith('#' + target)) {
  748. //link.setAttribute('href-data', link.href);
  749. link.href = url.origin + url.pathname + url.search;
  750. //increaseByOne('hashes')
  751. }
  752. break;
  753. case 'para':
  754. //console.log('para ' + i)
  755. if (url.searchParams.get(target)) {
  756. url.searchParams.delete(target);
  757. //link.setAttribute('href-data', link.href);
  758. link.href = url.origin + url.pathname + url.search;
  759. //increaseByOne('parameters')
  760. }
  761. break;
  762. }
  763.  
  764. /*
  765. // EXTRA
  766. // For URL of hyperlinks
  767. for (const a of document.querySelectorAll('a')) {
  768. try{
  769. let url = new URL(a.href);
  770. for (let i = 0; i < blacklist.length; i++) {
  771. if (url.searchParams.get(blacklist[i])) {
  772. url.searchParams.delete(blacklist[i]);
  773. }
  774. }
  775. a.href = url;
  776. } catch (err) {
  777. //console.warn('Found no href for element: ' + a);
  778. //console.error(err);
  779. }
  780. } */
  781.  
  782. }
  783.  
  784. // TODO Hunt (for any) links within attributes using getAttributeNames()[i]
  785.  
  786. // Event Listener
  787. // TODO Scan 'e.target.childNodes' until 'href-data' (link) is found
  788. document.body.addEventListener("mouseover", function(e) { // mouseover works with keyboard too
  789. //if (e.target && e.target.nodeName == "A") {
  790. let hrefData = e.target.getAttribute('href-data');
  791. //if (e.target && hrefData && !document.getElementById(namespace)) {
  792. if (e.target && hrefData && hrefData != document.getElementById('url-original')) {
  793. if (document.getElementById(namespace)) {
  794. document.getElementById(namespace).remove();
  795. }
  796. selectionItem = createButton(e.pageX, e.pageY, hrefData);
  797. hrefData = new URL(hrefData);
  798. selectionItem.append(purgeURL(hrefData));
  799. let types = ['whitelist', 'blacklist', 'original'];
  800. for (let i = 0; i < types.length; i++) {
  801. let button = purgeURL(hrefData, types[i]);
  802. if (types[i] == 'original' && e.target.getAttribute('slash-ref') == '') {
  803. button.href = button.href.replace('?ref=', '/ref=');
  804. }
  805. let exist;
  806. selectionItem.childNodes.forEach(
  807. node => {
  808. if (button.href == node.href) {
  809. exist = true;
  810. }
  811. }
  812. );
  813. if (!exist) {
  814. selectionItem.append(button);
  815. }
  816. }
  817.  
  818. // Check for URLs
  819. for (let i = 0; i < urls.length; i++) {
  820. if (hrefData.searchParams.get(urls[i])) { // hrefData.includes('url=')
  821. urlParameter = hrefData.searchParams.get(urls[i]);
  822. try {
  823. urlParameter = new URL (urlParameter);
  824. } catch {
  825. if (urlParameter.includes('.')) { // NOTE It is a guess
  826. try {
  827. urlParameter = new URL ('http:' + urlParameter);
  828. } catch {}
  829. }
  830. }
  831. if (typeof urlParameter == 'object' && // confirm url object
  832. urlParameter != location.href) { // provided url isn't the same as of page
  833. newURLItem = extractURL(urlParameter);
  834. selectionItem.prepend(newURLItem);
  835. }
  836. }
  837. }
  838.  
  839. /*
  840. // compare original against purged
  841. //if (selectionItem.querySelector(`#url-purged`) &&
  842. // selectionItem.querySelector(`#url-original`)) {
  843. if (selectionItem.querySelector(`#url-purged`)) {
  844. //let urlOrigin = new URL (selectionItem.querySelector(`#url-original`).href);
  845. let urlPurge = new URL (selectionItem.querySelector(`#url-purged`).href);
  846. // NOTE
  847. // These "searchParams.sort" ~~may be~~ *are not* redundant.
  848. // See resUrl.searchParams.sort()
  849. urlPurge.searchParams.sort();
  850. hrefData.searchParams.sort();
  851. //console.log(hrefData.search);
  852. //console.log(urlPurge.search);
  853. if (hrefData.search == urlPurge.search &&
  854. selectionItem.querySelector(`#url-original`)) {
  855. selectionItem.querySelector(`#url-original`).remove();
  856. }
  857. } else
  858. // compare original against safe
  859. if (selectionItem.querySelector(`#url-known`)) {
  860. //let urlOrigin = new URL (selectionItem.querySelector(`#url-original`).href);
  861. let urlKnown = new URL (selectionItem.querySelector(`#url-known`).href);
  862. // NOTE
  863. // These "searchParams.sort" ~~may be~~ *are not* redundant.
  864. // See resUrl.searchParams.sort()
  865. urlKnown.searchParams.sort();
  866. hrefData.searchParams.sort();
  867. //console.log(hrefData.search);
  868. //console.log(urlKnown.search);
  869. if (hrefData.search == urlKnown.search &&
  870. selectionItem.querySelector(`#url-original`)) {
  871. selectionItem.querySelector(`#url-original`).remove();
  872. }
  873. }
  874. */
  875.  
  876. // compare original against safe and purged
  877. // NOTE on "item.href = decodeURI(resUrl)"
  878. // The solution was here.
  879. // Decode was not the issue
  880. // This is a good example to show that
  881. // smaller tasks are as important as bigger tasks
  882. let urlsToCompare = ['#url-known', '#url-purged'];
  883. for (let i = 0; i < urlsToCompare.length; i++) {
  884. if (selectionItem.querySelector(urlsToCompare[i])) {
  885. //let urlOrigin = new URL (selectionItem.querySelector(`#url-original`).href);
  886. let urlToCompare = new URL (selectionItem.querySelector(urlsToCompare[i]).href);
  887. // NOTE
  888. // These "searchParams.sort" ~~may be~~ *are not* redundant.
  889. // See resUrl.searchParams.sort()
  890. urlToCompare.searchParams.sort();
  891. hrefData.searchParams.sort();
  892. //console.log(hrefData.search);
  893. //console.log(urlToCompare.search);
  894. if (hrefData.search == urlToCompare.search &&
  895. selectionItem.querySelector(`#url-original`)) {
  896. selectionItem.querySelector(`#url-original`).remove();
  897. }
  898. }
  899. }
  900.  
  901. // do not add element, if url has only whitelisted parameters and no potential url
  902. // add element, only if a potential url or non-whitelisted parameter was found
  903. let urlTypes = ['url-extracted', 'url-original', 'url-purged'];
  904. for (let i = 0; i < urlTypes.length; i++) {
  905. if (selectionItem.querySelector(`#${urlTypes[i]}`)) {
  906. document.body.append(selectionItem);
  907. return;
  908. }
  909. }
  910.  
  911. // NOTE in case return did not reach
  912. // it means that there is no link to process
  913. e.target.removeAttribute('href-data');
  914.  
  915. //if (!e.target.getAttribute('slash-ref') == '') {
  916. // e.target.removeAttribute('href-data')
  917. //}
  918.  
  919. }
  920. });
  921.  
  922. function createButton(x, y, url) {
  923. // create element
  924. let item = document.createElement(namespace);
  925. // set content
  926. item.id = namespace;
  927. // set position
  928. item.style.all = 'unset';
  929. item.style.position = 'absolute';
  930. //item.style.left = x+5 + 'px';
  931. //item.style.top = y-3 + 'px';
  932. item.style.left = x+45 + 'px';
  933. item.style.top = y-65 + 'px';
  934. // set appearance
  935. item.style.fontFamily = 'none'; // emoji
  936. item.style.background = '#333';
  937. item.style.borderRadius = '5%';
  938. item.style.padding = '3px';
  939. item.style.direction = 'ltr';
  940. item.style.zIndex = 10000;
  941. //item.style.opacity = 0.7;
  942. //item.style.filter = 'brightness(0.7) drop-shadow(2px 4px 6px black)';
  943. item.style.filter = 'brightness(0.7)';
  944. // center character
  945. item.style.justifyContent = 'center';
  946. item.style.alignItems = 'center';
  947. item.style.display = 'flex';
  948. // disable selection marks
  949. item.style.userSelect = 'none';
  950. item.style.cursor = 'default';
  951. // set button behaviour
  952. item.onmouseover = () => {
  953. //item.style.opacity = 1;
  954. //item.style.filter = 'drop-shadow(2px 4px 6px black)';
  955. item.style.filter = 'unset';
  956. };
  957. item.onmouseleave = () => { // onmouseout
  958. // TODO Wait a few seconds
  959. item.remove();
  960. };
  961. return item;
  962. }
  963.  
  964. function extractURL(url) {
  965. let item = document.createElement('a');
  966. item.textContent = '🔗'; // 🧧 🏷️ 🔖
  967. item.title = 'Extracted URL';
  968. item.id = 'url-extracted';
  969. item.style.all = 'unset';
  970. item.style.outline = 'none';
  971. item.style.height = '15px';
  972. item.style.width = '15px';
  973. item.style.padding = '3px';
  974. item.style.margin = '3px';
  975. //item.style.fontSize = '0.9rem' // 90%
  976. item.style.lineHeight = 'normal'; // initial
  977. //item.style.height = 'fit-content';
  978. item.href = url;
  979. return item;
  980. }
  981.  
  982. // TODO Use icons (with shapes) for cases when color is not optimal
  983. function purgeURL(url, listType) {
  984. let orgUrl = null;
  985. let itemTitle, itemId, resUrl;
  986. let item = document.createElement('a');
  987. item.style.all = 'unset';
  988. switch (listType) {
  989. case 'blacklist':
  990. itemColor = 'yellow';
  991. //itemTextContent = '🟡';
  992. itemTitle = 'Semi-safe link'; // Purged URL
  993. itemId = 'url-purged';
  994. resUrl = hrefDataHandler(url, blacklist);
  995. break;
  996. case 'original': // TODO dbclick (double-click)
  997. itemColor = 'orangered';
  998. //itemTextContent = '🔴';
  999. itemTitle = 'Unsafe link'; // Original URL
  1000. itemId = 'url-original';
  1001. //resUrl = encodeURI(url);
  1002. // NOTE By executing url.searchParams.sort()
  1003. // we change the order of parameters
  1004. // which means that we create a new and unique url
  1005. // which means that it can be used to identify users that use this program
  1006. // NOTE We execute url.searchParams.sort()
  1007. // in order to avoid false positive for url of blacklisted parameters
  1008. // but we don't apply that change on item "url-original"
  1009. //url.searchParams.sort();
  1010. orgUrl = url;
  1011. item.style.cursor = `not-allowed`; // no-drop
  1012. item.onmouseenter = () => {
  1013. item.style.background = 'darkorange';
  1014. item.style.filter = `drop-shadow(2px 4px 6px ${itemColor})`;
  1015. };
  1016. item.onmouseout = () => {
  1017. item.style.background = itemColor;
  1018. item.style.filter = 'unset';
  1019. };
  1020. break;
  1021. case 'whitelist':
  1022. itemColor = 'lawngreen';
  1023. //itemTextContent = '🟢';
  1024. itemTitle = 'Safe link'; // Link with whitelisted parameters
  1025. itemId = 'url-known';
  1026. resUrl = hrefDataHandler(url, whitelist);
  1027. break;
  1028. default:
  1029. itemColor = 'antiquewhite';
  1030. //itemTextContent = '⚪';
  1031. itemTitle = 'Base link'; // Link without parameters
  1032. itemId = 'url-base';
  1033. resUrl = url.origin + url.pathname;
  1034. resUrl = new URL(resUrl); // NOTE To avoid error in resUrl.searchParams.sort()
  1035. break;
  1036. }
  1037. item.id = itemId;
  1038. item.title = itemTitle;
  1039. item.style.background = itemColor;
  1040. //item.textContent = itemTextContent;
  1041. item.style.borderRadius = '50%';
  1042. item.style.outline = 'none';
  1043. item.style.height = '15px';
  1044. item.style.width = '15px';
  1045. item.style.padding = '3px';
  1046. item.style.margin = '3px';
  1047. if (orgUrl){
  1048. item.href = orgUrl;
  1049. } else {
  1050. // NOTE Avoid duplicates by sorting parameters of all links
  1051. resUrl.searchParams.sort();
  1052. // NOTE Avoid false positive by decoding
  1053. // TODO decode from ?C=N%3BO%3DD to ?C=N;O=D
  1054. // FIXME decodeURI doesn't appear to work
  1055. // Text page https://mirror.lyrahosting.com/gnu/a2ps/ (columns raw)
  1056. // SOLVED See "urlsToCompare"
  1057. //item.href = decodeURI(resUrl);
  1058. //item.href = decodeURIComponent(resUrl);
  1059. // TODO whitespace turns into plus and
  1060. // then red/white appears for the same uri
  1061. item.href = resUrl;
  1062. }
  1063. return item;
  1064. }
  1065.  
  1066. // NOTE The URL API doesn't list parameters
  1067. // without explicitly calling them, therefore
  1068. // reading lengths of unknown parameters is
  1069. // impossible, hence
  1070. // set a loop from Aa - Zz or
  1071. // add Aa - Zz to whitelist
  1072. function hrefDataHandler(url, listType) {
  1073. url = new URL(url.href);
  1074. // NOTE Avoid duplicates by sorting parameters of all links
  1075. //url.searchParams.sort();
  1076. switch (listType) {
  1077. case whitelist:
  1078. let newURL = new URL (url.origin + url.pathname);
  1079. for (let i = 0; i < whitelist.length; i++) {
  1080. if (url.searchParams.get(whitelist[i])) {
  1081. newURL.searchParams.set(
  1082. whitelist[i],
  1083. url.searchParams.get(whitelist[i]) // catchedValue
  1084. );
  1085. }
  1086. }
  1087.  
  1088. // Whitelist parameters of single character long
  1089. let a2z = alphabet.split('');
  1090. for (let i = 0; i < a2z.length; i++) {
  1091. if (url.searchParams.get(a2z[i])) {
  1092. newURL.searchParams.set(
  1093. a2z[i],
  1094. url.searchParams.get(a2z[i])
  1095. );
  1096. }
  1097. }
  1098.  
  1099. let A2Z = alphabet.toUpperCase().split('');
  1100. for (let i = 0; i < A2Z.length; i++) {
  1101. if (url.searchParams.get(A2Z[i])) {
  1102. newURL.searchParams.set(
  1103. A2Z[i],
  1104. url.searchParams.get(A2Z[i])
  1105. );
  1106. }
  1107. }
  1108.  
  1109. /*
  1110. let a2z = genCharArray('a', 'z');
  1111. for (let i = 0; i < a2z.length; i++) {
  1112. if (url.searchParams.get(a2z[i])) {
  1113. newURL.searchParams.set(
  1114. a2z[i],
  1115. url.searchParams.get(a2z[i]) // catchedValue
  1116. );
  1117. }
  1118. }
  1119.  
  1120. let A2Z = genCharArray('A', 'Z');
  1121. for (let i = 0; i < A2Z.length; i++) {
  1122. if (url.searchParams.get(A2Z[i])) {
  1123. newURL.searchParams.set(
  1124. A2Z[i],
  1125. url.searchParams.get(A2Z[i]) // catchedValue
  1126. );
  1127. }
  1128. }
  1129. */
  1130.  
  1131. url = newURL;
  1132. break;
  1133. case blacklist:
  1134. for (let i = 0; i < blacklist.length; i++) {
  1135. if (url.searchParams.get(blacklist[i])) {
  1136. url.searchParams.delete(blacklist[i]);
  1137. //increaseByOne('parameters')
  1138. }
  1139. }
  1140. //increaseByOne('links')
  1141. break;
  1142. }
  1143. return url;
  1144. }
  1145.  
  1146. // /questions/24597634/how-to-generate-an-array-of-the-alphabet
  1147. function genCharArray(charA, charZ) {
  1148. var a = [], i = charA.charCodeAt(0), j = charZ.charCodeAt(0);
  1149. for (; i <= j; ++i) {
  1150. a.push(String.fromCharCode(i));
  1151. }
  1152. return a;
  1153. }
  1154.  
  1155. async function increaseByOne(key) {
  1156. let currentValue = await GM.getValue(key, 0);
  1157. await GM.setValue(key, currentValue + 1);
  1158. console.log(key);
  1159. console.log(currentValue);
  1160. }
  1161.  
  1162. // NOTE Marketplace website which uses /ref= instead of ?ref=
  1163. // Check for .getAttribute('slash-ref') == ''
  1164. function deleteSerialNumber(url) {
  1165. let newURL = [];
  1166. let pattern = /^[0-9\-]+$/;
  1167. let oldUrl = url.toString().split('/');
  1168. for (const cell of oldUrl) {
  1169. if (!pattern.test(cell)) {
  1170. newURL.push(cell);
  1171. }
  1172. }
  1173. newURL = newURL.join('/');
  1174. return new URL(newURL);
  1175. }