├── .eslintrc.json ├── .gitattributes ├── .github └── ISSUE_TEMPLATE │ ├── add-website.md │ ├── broken-website.md │ ├── bug.md │ ├── feature-request.md │ └── update-website.md ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── LICENSE ├── README.md ├── data ├── rules.js ├── supported-sites.txt └── tidy.user.js ├── lib ├── config.d.ts ├── config.js ├── handlers.d.ts ├── handlers.js ├── index.d.ts ├── index.js ├── interface.d.ts ├── interface.js ├── tidyurl.min.js ├── utils.d.ts └── utils.js ├── package-lock.json ├── package.json ├── src ├── config.ts ├── handlers.ts ├── index.ts ├── interface.ts ├── postbuild.ts └── utils.ts ├── test.ts ├── text-logo.png ├── tsconfig.json └── webpack.config.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/recommended" 10 | ], 11 | "parser": "@typescript-eslint/parser", 12 | "parserOptions": { 13 | "ecmaVersion": 12, 14 | "sourceType": "module" 15 | }, 16 | "plugins": [ 17 | "@typescript-eslint" 18 | ], 19 | "rules": { 20 | "no-explicit-any": false, 21 | "indent": [ 22 | "error", 23 | 4 24 | ], 25 | "linebreak-style": [ 26 | "error", 27 | "windows" 28 | ], 29 | "quotes": [ 30 | "error", 31 | "single" 32 | ], 33 | "semi": [ 34 | "error", 35 | "always" 36 | ] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/add-website.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Add Website 3 | about: Request for a new domain to be added 4 | title: 'Website: example.com' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Example URLs:** 11 | 12 | ``` 13 | https://example.com/page?utm_source=tracking&blah=label 14 | https://example.com/example?bad=param 15 | ``` 16 | 17 | **Bad params:** 18 | 19 | - utm_source 20 | - bad 21 | - 22 | 23 | **Additional context:** 24 | Add any other information about the website here that might be useful to know. 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/broken-website.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Broken Website 3 | about: Report a broken website 4 | title: 'Broken: example.com' 5 | labels: bug 6 | assignees: DrKain 7 | --- 8 | 9 | **URL before cleaning:** 10 | 11 | - https://example.com/example-page?query=example 12 | 13 | **Current result when cleaning:** 14 | 15 | - https://example.com/example-page?query=example 16 | 17 | **Expected outcome:** 18 | 19 | - https://example.com/example-page 20 | 21 | **Extra Information:** 22 | 23 | - Browser or OS: 24 | - Version: 25 | 26 | **Additional context** 27 | Add any other context about the problem here (optional) 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Report an error or bug 4 | title: 'Bug: ' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | Please provide as much detail as possible, including version number and platform (userscript, node, browser, ect). 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Request for something to be added or changed 4 | title: 'Request: ' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | Please provide as much detail as possible. 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/update-website.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Update Website 3 | about: Request for a domain to be updated 4 | title: 'Update: example.com' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **URLs:** 11 | 12 | ``` 13 | https://example.com/page?utm_source=tracking&blah=label 14 | https://example.com/example?bad=param 15 | ``` 16 | 17 | **Bad params:** 18 | 19 | - utm_source 20 | - example 21 | - 22 | 23 | **Additional context:** 24 | Add any other information about the website here that might be useful to know. 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # parcel-bundler cache (https://parceljs.org/) 61 | .cache 62 | 63 | # next.js build output 64 | .next 65 | 66 | # nuxt.js build output 67 | .nuxt 68 | 69 | # vuepress build output 70 | .vuepress/dist 71 | 72 | # Serverless directories 73 | .serverless 74 | 75 | # FuseBox cache 76 | .fusebox/ 77 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | data/rules.js -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "none", 3 | "tabWidth": 4, 4 | "semi": true, 5 | "singleQuote": true, 6 | "quoteProps": "as-needed", 7 | "bracketSpacing": true, 8 | "arrowParens": "always", 9 | "endOfLine": "lf", 10 | "printWidth": 1000 11 | } 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 DrKain (https://github.com/DrKain) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [![Tidy-URL](text-logo.png)](#) 2 | 3 | A Node Package & Userscript that removes tracking or garbage parameters from URLs making them shorter, cleaner and a lot nicer to read. 4 | 5 | [![NPM](https://img.shields.io/npm/v/tidy-url)](https://www.npmjs.com/package/tidy-url) 6 | [![NPM](https://img.shields.io/npm/dt/tidy-url)](https://www.npmjs.com/package/tidy-url) 7 | [![NPM](https://img.shields.io/npm/types/tidy-url)](https://www.npmjs.com/package/tidy-url) 8 | 9 | ## Install: 10 | 11 | You can use this automatically in the browser with the [userscript](https://github.com/DrKain/tidy-url/wiki/Userscript). 12 | 13 | - [Node Package Manager](https://github.com/DrKain/tidy-url/wiki/Node-Package) (NPM) 14 | - [Browser](https://github.com/DrKain/tidy-url/wiki/Userscript) (userscript) 15 | - [jsDelivr](https://github.com/DrKain/tidy-url/wiki/jsDelivr) 16 | 17 | ## NodeJS 18 | 19 | ``` 20 | npm install tidy-url 21 | ``` 22 | 23 | ### Require 24 | 25 | ```js 26 | import { TidyURL } from 'tidy-url'; 27 | // or 28 | const { TidyURL } = require('tidy-url'); 29 | ``` 30 | 31 | ### Usage 32 | 33 | Then pass it a URL and let the magic happen: 34 | 35 | ```js 36 | const link = TidyURL.clean('https://open.spotify.com/track/1hhZQVLXpg10ySFQFxGbih?si=-k8RwDQwTCK923jxZuy07w&utm_source=copy-link'); 37 | console.log(link.url); // https://open.spotify.com/track/1hhZQVLXpg10ySFQFxGbih 38 | ``` 39 | 40 | ### Validating 41 | 42 | You can validate a URL using the `validate` function. 43 | 44 | ```js 45 | TidyURL.validate('https://example.com'); // true 46 | TidyURL.validate('cat'); // false 47 | TidyURL.validate('google.com'); // false (protocol is required!) 48 | ``` 49 | 50 | ### AMP & Redirects 51 | 52 | By default, tidy-url will remove redirect parameters and AMP links if the rule supports it. 53 | You can disable this feature with `allowRedirects` and `allowAMP`. 54 | Examples: 55 | 56 | ```ts 57 | // These are the defaults. 58 | TidyURL.config.setMany({ 59 | allowAMP: false, 60 | allowRedirects: true 61 | }); 62 | 63 | TidyURL.clean('https://www.google.com/amp/s/github.com'); 64 | TidyURL.clean('https://steamcommunity.com/linkfilter/?url=https://github.com'); 65 | // Result for both: https://github.com 66 | ``` 67 | 68 | _More info about AMP on [the wiki](https://github.com/DrKain/tidy-url/wiki/AMP-Links)_. 69 | 70 | ### Note 71 | 72 | You will always receive a valid response, even if nothing was modified. For example: 73 | 74 | ```js 75 | const link = TidyURL.clean('https://duckduckgo.com/this-is-fine'); 76 | 77 | link.url; // https://duckduckgo.com/this-is-fine 78 | link.info.reduction; // 0 (percent) 79 | ``` 80 | 81 | ### Supported Sites 82 | 83 | You can view all custom supported sites [here](https://github.com/DrKain/tidy-url/wiki/Supported-Sites). 84 | However, the global rules will be enough to work with thousands of sites around the internet. You should be able to pass any URL for cleaning. 85 | Request direct support for a website [here](https://github.com/DrKain/tidy-url/issues/new?assignees=&labels=&template=add-website.md&title=Website%3A+example.com) 86 | 87 | ### Response 88 | 89 | The response will always be an object with details of what was cleaned or modified in the URL. 90 | This can be used for debugging, testing or a simple way of letting users know they could have sent a shorter link. 91 | 92 | ```json 93 | { 94 | "url": "https://open.spotify.com/track/1hhZQVLXpg10ySFQFxGbih", 95 | "info": { 96 | "original": "https://open.spotify.com/track/1hhZQVLXpg10ySFQFxGbih?si=-k8RwDQwTCK923jxZuy07w&utm_source=copy-link", 97 | "reduction": 47, 98 | "difference": 47, 99 | "replace": [], 100 | "removed": [ 101 | { 102 | "key": "utm_source", 103 | "value": "copy-link" 104 | }, 105 | { 106 | "key": "si", 107 | "value": "-k8RwDQwTCK923jxZuy07w" 108 | } 109 | ], 110 | "match": [ 111 | { 112 | "rules": ["si", "utm_source", "context"], 113 | "replace": [], 114 | "redirect": "", 115 | "name": "spotify.com", 116 | "match": "/open.spotify.com/i" 117 | } 118 | ], 119 | "decoded": null, 120 | "isNewHost": false, 121 | "fullClean": true 122 | } 123 | } 124 | ``` 125 | 126 | ### Example 127 | 128 | Turn these monstrosities: 129 | 130 | ``` 131 | https://poetsroad.bandcamp.com/?from=search&search_item_id=1141951669&search_item_type=b&search_match_part=%3F&search_page_id=1748155363&search_page_no=1&search_rank=1&search_sig=a9a9cbdfc454df7c2999f097dc8a216b 132 | 133 | https://www.audible.com/pd/Project-Hail-Mary-Audiobook/B08G9PRS1K?plink=GZIIiCHG0Uo5V8ND&ref=a_hp_c9_adblp13nmpxxp13n-mpl-dt-c_1_2&pf_rd_p=164101a8-2aab-4c5e-91ee-1f39e10719e6&pf_rd_r=2Q5R6VH8HJAD48PSQRS4 134 | 135 | https://www.amazon.com/Alexander-Theatre-Sessions-Poets-Fall/dp/B08NT852YT/ref=sr_1_1?dchild=1&keywords=Poets+of+the+fall&qid=1621684985&sr=8-1 136 | 137 | https://open.spotify.com/track/1hhZQVLXpg10ySFQFxGbih?si=-k8RwDQwTCK923jxZuy07w&utm_source=copy-link 138 | 139 | https://www.aliexpress.com/item/1005001913861188.html?spm=a2g0o.productlist.0.0.b1c55e86sFKsxH&algo_pvid=b4648621-2371-4d1e-9a9c-89b4d6c59395&algo_expid=b4648621-2371-4d1e-9a9c-89b4d6c59395-0&btsid=0b0a556816216865399393168e562d&ws_ab_test=searchweb0_0,searchweb201602_,searchweb201603_ 140 | 141 | https://www.google.com/search?q=cat&source=hp&ei=AwGpYKzyE7uW4-EPy_CnSA&iflsig=AINFCbYAAAAAYKkPE4rmSi0Im0sHgmOVb3DYosyq2B0B&oq=cat&gs_lcp=Cgdnd3Mtd2l6EAMyBQguEJMCMgIILjICCAAyAggAMgIILjICCAAyAggAMgIILjICCC4yAgguOggIABDqAhCPAToLCC4QxwEQowIQkwI6CAguEMcBEKMCUNgEWIQHYMwIaAFwAHgAgAHIAYgB2ASSAQMyLTOYAQCgAQGqAQdnd3Mtd2l6sAEK&sclient=gws-wiz&ved=0ahUKEwjs_9PdrN3wAhU7yzgGHUv4CQkQ4dUDCAY&uact=5 142 | 143 | https://www.emjcd.com/links-i/?d=eyJzdXJmZXIiOiIxMDAzMDQ3Mjg5ODMzODAxMDI6VlBTbFlUN3JBeHpsIiwibGFzdENsaWNrTmFtZSI6IkxDTEsiLCJsYXN0Q2xpY2tWYWx1ZSI6ImNqbyF4aTU5LXZ0Zm1nOTkiLCJkZXN0aW5hdGlvblVybCI6Imh0dHBzOi8vd3d3LnZ1ZHUuY29tL2NvbnRlbnQvbW92aWVzL2RldGFpbHMvTW9vbmxpZ2h0LVNlYXNvbi0xLzEzMzEyMCIsInNpZCI6IltzdWJpZF92YWx1ZV0iLCJ0eXBlIjoiZGxnIiwicGlkIjo5MDExNjczLCJldmVudElkIjoiMGFjZGE1ZDdmNzNlMTFlYzgyYWM3NDliMGExYzBlMGUiLCJjalNlc3Npb24iOiIyZjBjNGNjYi1lNmVmLTQ0YzItYjIzYy02NzNjZjY2MTZlMTYiLCJsb3lhbHR5RXhwaXJhdGlvbiI6MCwicmVkaXJlY3RlZFRvTGl2ZXJhbXAiOmZhbHNlLCJjakNvbnNlbnRFbnVtIjoiTkVWRVJfQVNLRUQifQ%3D%3D 144 | 145 | https://www.youtube.com/redirect?event=video_description&redir_token=QCFCLUhqbUVVVVc2Vm53OGdFMi15NU1vSzloWkZveGcyUXxBQ3Jtc0tsR143azQxRVpxZ3lUampXUEkyaTdpdy1reU1OVGcyb3pmOUhzU22Ldm5QZ0tueEMzMy1TQTA1Mm85SEpCUW14UHlq11ZCUVlhU3QzdW52U2Uyd01pbTVINDRjNlhf124ySEZqMHBJbnFEWDdiMTNUVQ&q=https%3A%2F%2Ftomscott.com%2F&v=k7fXbdRH9v4 146 | ``` 147 | 148 | Into these: 149 | 150 | ``` 151 | https://poetsroad.bandcamp.com/ 152 | 153 | https://www.audible.com/pd/Project-Hail-Mary-Audiobook/B08G9PRS1K 154 | 155 | https://www.amazon.com/Alexander-Theatre-Sessions-Poets-Fall/dp/B08NT852YT 156 | 157 | https://open.spotify.com/track/1hhZQVLXpg10ySFQFxGbih 158 | 159 | https://www.aliexpress.com/item/1005001913861188.html 160 | 161 | https://www.google.com/search?q=cat 162 | 163 | https://www.vudu.com/content/movies/details/Moonlight-Season-1/133120 164 | 165 | https://tomscott.com/ 166 | ``` 167 | 168 | ## 🤝 Contributing 169 | 170 | Contributions, issues and feature requests are welcome and greatly appreciated! 171 | Feel free to check [issues page](https://github.com/DrKain/tidy-url/issues). 172 | If you find a website that is not supported, please create an issue and I'll be more than happy to add it. 173 | 174 | ## 👤 Author 175 | 176 | This project was made by **Kain (ksir.pw)** 177 | Feel free to contact me if you have any trouble with this package. 178 | 179 | - Website: [ksir.pw](https://ksir.pw) 180 | - Github: [@DrKain](https://github.com/DrKain) 181 | - Discord: drkain 182 | -------------------------------------------------------------------------------- /data/rules.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | name: 'Global', 4 | match: /.*/, 5 | rules: [ 6 | // https://en.wikipedia.org/wiki/UTM_parameters 7 | 'utm_source', 'utm_medium', 'utm_term', 'utm_campaign', 8 | 'utm_content', 'utm_name', 'utm_cid', 'utm_reader', 'utm_viz_id', 9 | 'utm_pubreferrer', 'utm_swu', 'utm_social-type', 'utm_brand', 10 | 'utm_team', 'utm_feeditemid', 'utm_id', 'utm_marketing_tactic', 11 | 'utm_creative_format', 'utm_campaign_id', 'utm_source_platform', 12 | 'utm_timestamp', 'utm_souce', 'utm_couponvalue', 13 | // ITM parameters, a variant of UTM parameters 14 | 'itm_source', 'itm_medium', 'itm_term', 'itm_campaign', 'itm_content', 15 | 'itm_channel', 'itm_source_s', 'itm_medium_s', 'itm_campaign_s', 16 | 'itm_audience', 17 | // INT parameters, another variant of UTM 18 | 'int_source', 'int_cmp_name', 'int_cmp_id', 'int_cmp_creative', 19 | 'int_medium', 'int_campaign', 'int_content', 20 | // piwik (https://github.com/DrKain/tidy-url/issues/49) 21 | 'pk_campaign', 'pk_cpn', 'pk_source', 'pk_medium', 22 | 'pk_keyword', 'pk_kwd', 'pk_content', 'pk_cid', 23 | 'piwik_campaign', 'piwik_cpn', 'piwik_source', 'piwik_medium', 24 | 'piwik_keyword', 'piwik_kwd', 'piwik_content', 'piwik_cid', 25 | // Google Ads 26 | 'gclid', 'ga_source', 'ga_medium', 'ga_term', 'ga_content', 'ga_campaign', 27 | 'ga_place', 'gclid', 'gclsrc', 28 | // https://github.com/DrKain/tidy-url/issues/21 29 | 'hsa_cam', 'hsa_grp', 'hsa_mt', 'hsa_src', 'hsa_ad', 'hsa_acc', 30 | 'hsa_net', 'hsa_kw', 'hsa_tgt', 'hsa_ver', 'hsa_la', 'hsa_ol', 31 | // Facebook 32 | 'fbclid', 33 | // Olytics 34 | 'oly_enc_id', 'oly_anon_id', 35 | // Vero 36 | 'vero_id', 'vero_conv', 37 | // Drip 38 | '__s', 39 | // HubSpot 40 | '_hsenc', '_hsmi', '__hssc', '__hstc', '__hsfp', 'hsCtaTracking', 41 | // Marketo 42 | 'mkt_tok', 43 | // Matomo (https://github.com/DrKain/tidy-url/issues/47) 44 | 'mtm_campaign', 'mtm_keyword', 'mtm_kwd', 'mtm_source', 'mtm_medium', 45 | 'mtm_content', 'mtm_cid', 'mtm_group', 'mtm_placement', 46 | // Oracle Eloqua 47 | 'elqTrackId', 'elq', 'elqaid', 'elqat', 'elqCampaignId', 'elqTrack', 48 | // MailChimp 49 | 'mc_cid', 'mc_eid', 50 | // Other 51 | 'ncid', 'cmpid', 'mbid', 52 | // Reddit Ads (https://github.com/DrKain/tidy-url/issues/31) 53 | 'rdt_cid' 54 | ] 55 | }, 56 | { 57 | name: 'audible.com', 58 | match: /www.audible.com/i, 59 | rules: ['qid', 'sr', 'pf_rd_p', 'pf_rd_r', 'plink', 'ref'] 60 | }, 61 | { 62 | name: 'bandcamp.com', 63 | match: /.*.bandcamp.com/gi, 64 | rules: [ 65 | 'from', 'search_item_id', 'search_item_type', 'search_match_part', 'search_page_id', 66 | 'search_page_no', 'search_rank', 'search_sig' 67 | ] 68 | }, 69 | { 70 | name: 'amazon.com', 71 | match: /amazon\.[a-z0-9]{0,3}/i, 72 | rules: [ 73 | 'psc', 'colid', 'coliid', 'linkId', 'tag', 'linkCode', 'ms3_c', 74 | 'pf_rd_s', 'pf_rd_t', ' pf_rd_i', 'pf_rd_m', 'pd_rd_w', 'qid', 'sr', 75 | 'keywords', 'dchild', 'ref', 'ref_', 'rnid', 'pf_rd_r', 'pf_rd_p', 'pd_rd_r', 76 | 'smid', 'pd_rd_wg', 'content-id', 'spLa', 'crid', 'sprefix', 77 | 'hvlocint', 'hvdvcmdl', 'hvptwo', 'hvpone', 'hvpos', 78 | 'qu', 'pd_rd_i', 'nc2', 'nc1', 'trk', 'sc_icampaign', 'trkCampaign', 79 | 'ufe', 'sc_icontent', 'sc_ichannel', 'sc_iplace', 'sc_country', 80 | 'sc_outcome', 'sc_geo', 'sc_campaign', 'sc_channel' 81 | ], 82 | replace: [/(\/ref|&ref_)=[^\/?]*/i] 83 | }, 84 | { 85 | name: 'reddit.com', 86 | match: /.*.reddit.com/i, 87 | rules: [ 88 | 'ref_campaign', 'ref_source', 'tags', 'keyword', 'channel', 'campaign', 89 | 'user_agent', 'domain', 'base_url', '$android_deeplink_path', 90 | '$deeplink_path', '$og_redirect', 'share_id', 'correlation_id', 'ref', 91 | 'rdt' 92 | ] 93 | }, 94 | { 95 | name: 'app.link', 96 | match: /.*\.app\.link/i, 97 | rules: [ 98 | 'tags', 'keyword', 'channel', 'campaign', 99 | 'user_agent', 'domain', 'base_url', '$android_deeplink_path', 100 | '$deeplink_path', '$og_redirect', 'compact_view', 'dnt', 101 | 'adblock', 'geoip_country', 'referrer_domain', 102 | 'referrer_url' 103 | ] 104 | }, 105 | { 106 | name: 'twitch.tv', 107 | match: /www.twitch.tv/i, 108 | rules: ['tt_medium', 'tt_content', 'tt_email_id'] 109 | }, 110 | { 111 | name: 'twitch.tv-email', 112 | match_href: true, 113 | match: /www.twitch.tv\/r\/e/i, 114 | decode: { handler: 'twitch.tv-email', targetPath: true } 115 | }, 116 | { 117 | name: 'blog.twitch.tv', 118 | match: /blog.twitch.tv/i, 119 | rules: ['utm_referrer'] 120 | }, 121 | { 122 | name: 'pixiv.net', 123 | match: /www.pixiv.net/i, 124 | rules: ['p', 'i', 'g'] 125 | }, 126 | { 127 | name: 'spotify.com', 128 | match: /open.spotify.com/i, 129 | rules: [ 130 | 'si', 'utm_source', 'context', 'sp_cid', 131 | '_branch_match_id', '_branch_referrer' 132 | ], 133 | allow: ['go', 'nd'] 134 | }, 135 | { 136 | name: 'aliexpress.com', 137 | match: /^(?:https?:\/\/)?(?:[^.]+\.)?aliexpress\.(?:[a-z]{2,}){1,}/i, 138 | rules: [ 139 | '_t', 'spm', 'algo_pvid', 'algo_expid', 'btsid', 'ws_ab_test', 140 | 'initiative_id', 'origin', 'widgetId', 'tabType', 'productId', 141 | 'productIds', 'gps-id', 'scm', 'scm_id', 'scm-url', 'pvid', 142 | 'algo_exp_id', 'pdp_pi', 'fromRankId', 'sourceType', 'utparam', 143 | 'gatewayAdapt', '_evo_buckets', 'tpp_rcmd_bucket_id', 'scenario', 144 | 'pdp_npi', 'tt', 'spreadType', 'srcSns', 'bizType', 'social_params', 145 | 'aff_fcid', 'aff_fsk', 'aff_platform', 'aff_trace_key', 'shareId', 146 | 'platform', 'businessType', 'terminal_id', 'afSmartRedirect', 'sk', 147 | 'gbraid' 148 | ], 149 | allow: ['sku_id', 'pdp_ext_f'] 150 | }, 151 | { 152 | name: 'google.com', 153 | match: /www.google\..*/i, 154 | rules: [ 155 | 'sourceid', 'client', 'aqs', 'sxsrf', 'uact', 'ved', 'iflsig', 'source', 156 | 'ei', 'oq', 'gs_lcp', 'sclient', 'bih', 'biw', 'sa', 'dpr', 'rlz', 157 | 'gs_lp', 'sca_esv', 'si', 'gs_l', 'gs_lcrp' 158 | ], 159 | amp: { 160 | regex: /www\.google\.(?:.*)\/amp\/s\/(.*)/gim, 161 | }, 162 | redirect: 'url' 163 | }, 164 | { 165 | name: 'youtube.com', 166 | match: /.*.youtube.com/i, 167 | rules: ['gclid', 'feature', 'app', 'src', 'lId', 'cId', 'embeds_referring_euri'], 168 | redirect: 'q' 169 | }, 170 | { 171 | name: 'humblebundle.com', 172 | match: /www.humblebundle.com/i, 173 | rules: ['hmb_source', 'hmb_medium', 'hmb_campaign', 'mcID', 'linkID'], 174 | allow: ['partner'] 175 | }, 176 | { 177 | name: 'greenmangaming.com', 178 | match: /www.greenmangaming.com/i, 179 | rules: ['CJEVENT', 'cjevent', 'irclickid', 'irgwc', 'pdpgatetoken'] 180 | }, 181 | { 182 | name: 'fanatical.com', 183 | match: /www.fanatical.com/i, 184 | rules: ['cj_pid', 'cj_aid', 'aff_track', 'CJEVENT', 'cjevent'] 185 | }, 186 | { 187 | name: 'newsweek.com', 188 | match: /www.newsweek.com/i, 189 | rules: ['subref', 'amp'] 190 | }, 191 | { 192 | name: 'imgur.com', 193 | match: /imgur.com/i, 194 | rules: ['source'] 195 | }, 196 | { 197 | name: 'plex.tv', 198 | match: /.*.plex.tv/i, 199 | rules: ['origin', 'plex_utm', 'sl', 'ckhid'] 200 | }, 201 | { 202 | name: 'imdb.com', 203 | match: /^.*\.imdb\.com/i, 204 | rules: [ 205 | 'ref_', 'ref\\_', 'pf_rd_m', 'pf_rd_r', 'pf_rd_p', 'pf_rd_s', 206 | 'pf_rd_t', 'pf_rd_i', 'ref_hp_hp_e_2', 'rf', 'ref' 207 | ] 208 | }, 209 | { 210 | name: 'gog.com', 211 | match: /www.gog.com/i, 212 | rules: [ 213 | 'at_gd', 'rec_scenario_id', 'rec_sub_source_id', 'rec_item_id', 214 | 'vds_id', 'prod_id', 'rec_source' 215 | ] 216 | }, 217 | { 218 | name: 'tiktok.com', 219 | match: /www.tiktok.com/i, 220 | rules: [ 221 | 'is_copy_url', 'is_from_webapp', 'sender_device', 'sender_web_id', 222 | 'sec_user_id', 'share_app_id', 'share_item_id', 'share_link_id', 223 | 'social_sharing', '_r', 'source', 'user_id', 'u_code', 'tt_from', 224 | 'share_author_id', 'sec_uid', 'checksum', '_d', 'refer', 'enter_from', 225 | 'enter_method', 'attr_medium', 'attr_source' 226 | ], 227 | allow: ['lang'] 228 | }, 229 | { 230 | name: 'tiktok.com/link', 231 | match: /tiktok\.com\/link\/v2/i, 232 | match_href: true, 233 | redirect: 'target' 234 | }, 235 | { 236 | name: 'facebook.com', 237 | match: /.*.facebook.com/i, 238 | rules: ['fbclid', 'fb_ref', 'fb_source', 'referral_code', 'referral_story_type', 'tracking', 'ref'], 239 | redirect: 'u', 240 | exclude: [ 241 | /www\.facebook\.com\/sharer/gi 242 | ] 243 | }, 244 | { 245 | name: 'yandex.com', 246 | match: /yandex.com/i, 247 | rules: ['lr', 'from', 'grhow', 'origin', '_openstat'] 248 | }, 249 | { 250 | name: 'store.steampowered.com', 251 | match: /store.steampowered.com/i, 252 | rules: ['snr'] 253 | }, 254 | { 255 | name: 'findojobs.co.nz', 256 | match: /www.findojobs.co.nz/i, 257 | rules: ['source'] 258 | }, 259 | { 260 | name: 'linkedin.com', 261 | match: /.*.linkedin.com/i, 262 | rules: [ 263 | 'contextUrn', 'destRedirectURL', 'lipi', 'licu', 'trk', 264 | 'trkInfo', 'originalReferer', 'upsellOrderOrigin', 265 | 'upsellTrk', 'upsellTrackingId', 'src', 'trackingId', 266 | 'midToken', 'midSig', 'trkEmail', 'eid' 267 | ], 268 | allow: [ 'otpToken' ] 269 | }, 270 | { 271 | name: 'indeed.com', 272 | match: /.*.indeed.com/i, 273 | rules: ['from', 'attributionid'] 274 | }, 275 | { 276 | name: 'discord.com', 277 | match: /(?:.*\.)?discord\.com/i, 278 | rules: ['source' , 'ref'] 279 | }, 280 | { 281 | name: 'medium.com', 282 | match: /medium.com/i, 283 | rules: ['source'] 284 | }, 285 | { 286 | name: 'twitter.com', 287 | match: /twitter.com/i, 288 | rules: ['s', 'src', 'ref_url', 'ref_src'] 289 | }, 290 | { 291 | name: 'voidu.com', 292 | match: /voidu.com/i, 293 | rules: ['affiliate'] 294 | }, 295 | { 296 | name: 'wingamestore.com', 297 | match: /wingamestore.com/i, 298 | rules: ['ars'] 299 | }, 300 | { 301 | name: 'gamebillet.com', 302 | match: /gamebillet.com/i, 303 | rules: ['affiliate'] 304 | }, 305 | { 306 | name: 'gamesload.com', 307 | match: /^www.gamesload.com/i, 308 | rules: ['affil'], 309 | allow: ['REF'] 310 | }, 311 | { 312 | name: 'mightyape', 313 | match: /mightyape.(co.nz|com.au)/i, 314 | rules: ['m'] 315 | }, 316 | { 317 | name: 'apple.com', 318 | match: /.*.apple.com/i, 319 | rules: ['uo', 'app', 'at', 'ct', 'ls', 'pt', 'mt', 'itsct', 'itscg', 'referrer', 'src', 'cid'] 320 | }, 321 | { 322 | name: 'music.apple.com', 323 | match: /music.apple.com/i, 324 | rules: ['i', 'lId', 'cId', 'sr', 'src'] 325 | }, 326 | { 327 | name: 'play.google.com', 328 | match: /play.google.com/i, 329 | rules: ['referrer', 'pcampaignid'] 330 | }, 331 | { 332 | name: 'adtraction.com', 333 | match: /adtraction.com/i, 334 | redirect: 'url' 335 | }, 336 | { 337 | name: 'dpbolvw.net', 338 | match: /dpbolvw.net/i, 339 | redirect: 'url' 340 | }, 341 | { 342 | name: 'lenovo.com', 343 | match: /.*.lenovo.com/i, 344 | rules: ['PID', 'clickid', 'irgwc', 'cid', 'acid', 'linkTrack'] 345 | }, 346 | { 347 | name: 'itch.io', 348 | match: /itch.io/i, 349 | rules: ['fbclid'] 350 | }, 351 | { 352 | name: 'steamcommunity.com', 353 | match: /steamcommunity.com/i, 354 | redirect: 'url' 355 | }, 356 | { 357 | name: 'steamcommunity.com/linkfilter', 358 | match: /steamcommunity.com\/linkfilter/i, 359 | redirect: 'u', 360 | match_href: true 361 | }, 362 | { 363 | name: 'microsoft.com', 364 | match: /microsoft.com/i, 365 | rules: ['refd', 'icid'] 366 | }, 367 | { 368 | name: 'berrybase.de', 369 | match: /berrybase.de/i, 370 | rules: ['sPartner'] 371 | }, 372 | { 373 | name: 'instagram.com', 374 | match: /instagram.com/i, 375 | rules: ['igshid', 'igsh', 'source'], 376 | redirect: 'u' 377 | }, 378 | { 379 | name: 'hubspot.com', 380 | match: /hubspot.com/i, 381 | rules: ['hubs_content-cta', 'hubs_content'] 382 | }, 383 | { 384 | name: 'ebay.com', 385 | match: /^(?:https?:\/\/)?(?:[^.]+\.)?ebay\.[a-z0-9]{0,3}/i, 386 | rules: [ 387 | 'amdata', 'var', 'hash', '_trkparms', '_trksid', '_from', 'mkcid', 388 | 'mkrid', 'campid', 'toolid', 'mkevt', 'customid', 'siteid', 'ufes_redirect', 389 | 'ff3', 'pub', 'media', 'widget_ver', 'ssspo', 'sssrc', 'ssuid' 390 | ], 391 | allow: ['epid', '_nkw'] 392 | }, 393 | { 394 | name: 'shopee.com', 395 | match: /^(?:https?:\/\/)?(?:[^.]+\.)?shopee\.[a-z0-9]{0,3}/i, 396 | rules: [ 397 | 'af_siteid', 'pid', 'af_click_lookback', 'af_viewthrough_lookback', 398 | 'is_retargeting', 'af_reengagement_window', 'af_sub_siteid', 'c' 399 | ] 400 | }, 401 | { 402 | name: 'msn.com', 403 | match: /www.msn.com/i, 404 | rules: ['ocid', 'cvid', 'pc'] 405 | }, 406 | { 407 | name: 'nuuvem.com', 408 | match: /www.nuuvem.com/i, 409 | rules: ['ranMID', 'ranEAID', 'ranSiteID'] 410 | }, 411 | { 412 | name: 'sjv.io', 413 | match: /.*.sjv.io/i, 414 | redirect: 'u' 415 | }, 416 | { 417 | name: 'linksynergy.com', 418 | match: /.*.linksynergy.com/i, 419 | rules: ['id', 'mid'], 420 | redirect: 'murl' 421 | }, 422 | { 423 | name: 'cnbc.com', 424 | match: /www.cnbc.com/i, 425 | rules: ['__source'] 426 | }, 427 | { 428 | name: 'yahoo.com', 429 | match: /yahoo.com/i, 430 | rules: [ 431 | 'guce_referrer', 'guce_referrer_sig', 'guccounter', 432 | 'soc_src', 'soc_trk', 'tsrc' 433 | ] 434 | }, 435 | { 436 | name: 'techcrunch.com', 437 | match: /techcrunch.com/i, 438 | rules: ['guce_referrer', 'guce_referrer_sig', 'guccounter'] 439 | }, 440 | { 441 | name: 'office.com', 442 | match: /office.com/i, 443 | rules: ['from'] 444 | }, 445 | { 446 | name: 'ticketmaster.co.nz', 447 | match: /ticketmaster.co.nz/i, 448 | rules: ['tm_link'] 449 | }, 450 | { 451 | name: 'bostonglobe.com', 452 | match: /bostonglobe.com/i, 453 | rules: ['p1'] 454 | }, 455 | { 456 | name: 'ampproject.org', 457 | match: /cdn.ampproject.org/i, 458 | rules: ['amp_gsa', 'amp_js_v', 'usqp', 'outputType'], 459 | amp: { 460 | regex: /cdn\.ampproject\.org\/v\/s\/(.*)\#(aoh|csi|referrer|amp)/gim 461 | } 462 | }, 463 | { 464 | name: 'nbcnews.com', 465 | match: /nbcnews.com/i, 466 | rules: ['fbclid'], 467 | amp: { 468 | replace: { 469 | text: 'www.nbcnews.com/news/amp/', 470 | with: 'www.nbcnews.com/news/' 471 | } 472 | } 473 | }, 474 | { 475 | name: 'countdown.co.nz', 476 | match: /www.countdown.co.nz/i, 477 | rules: ['promo_name', 'promo_creative', 'promo_position', 'itemID'] 478 | }, 479 | { 480 | name: 'etsy.com', 481 | match: /www.etsy.com/i, 482 | rules: ['click_key', 'click_sum', 'rec_type', 'ref', 'frs', 'sts', 'dd_referrer'] 483 | }, 484 | { 485 | name: 'wattpad.com', 486 | match: /www.wattpad.com/i, 487 | rules: ['wp_page', 'wp_uname', 'wp_originator'] 488 | }, 489 | { 490 | name: 'redirect.viglink.com', 491 | match: /redirect.viglink.com/i, 492 | redirect: 'u' 493 | }, 494 | { 495 | name: 'noctre.com', 496 | match: /www.noctre.com/i, 497 | rules: ['aff'] 498 | }, 499 | { 500 | name: 'dreamgame.com', 501 | match: /www.dreamgame.com/i, 502 | rules: ['affiliate'] 503 | }, 504 | { 505 | name: 'startpage.com', 506 | match: /.*.startpage.com/i, 507 | rules: ['source'] 508 | }, 509 | { 510 | name: '2game.com', 511 | match: /^2game.com/i, 512 | rules: ['ref'] 513 | }, 514 | { 515 | name: 'jdoqocy.com', 516 | match: /^www.jdoqocy.com/i, 517 | redirect: 'url' 518 | }, 519 | { 520 | name: 'gamesplanet.com', 521 | match: /^(?:.*\.|)gamesplanet\.com/i, 522 | rules: ['ref'] 523 | }, 524 | { 525 | name: 'gamersgate.com', 526 | match: /www.gamersgate.com/i, 527 | rules: ['aff'] 528 | }, 529 | { 530 | name: 'gate.sc', 531 | match: /gate.sc/i, 532 | redirect: 'url' 533 | }, 534 | { 535 | name: 'getmusicbee.com', 536 | match: /^getmusicbee.com/i, 537 | redirect: 'r' 538 | }, 539 | { 540 | name: 'imp.i305175.net', 541 | match: /^imp.i305175.net/i, 542 | redirect: 'u' 543 | }, 544 | { 545 | name: 'qflm.net', 546 | match: /.*.qflm.net/i, 547 | redirect: 'u' 548 | }, 549 | { 550 | name: 'anrdoezrs.net', 551 | match: /anrdoezrs.net/i, 552 | amp: { 553 | regex: /(?:.*)\/links\/(?:.*)\/type\/dlg\/sid\/\[subid_value\]\/(.*)/gi 554 | } 555 | }, 556 | { 557 | name: 'emjcd.com', 558 | match: /^www.emjcd.com/i, 559 | decode: { 560 | param: 'd', 561 | lookFor: 'destinationUrl' 562 | } 563 | }, 564 | { 565 | name: 'go2cloud.org', 566 | match: /^.*.go2cloud.org/i, 567 | redirect: 'aff_unique1' 568 | }, 569 | { 570 | name: 'bn5x.net', 571 | match: /^.*.bn5x.net/i, 572 | redirect: 'u' 573 | }, 574 | { 575 | name: 'tvguide.com', 576 | match: /^www.tvguide.com/i, 577 | amp: { regex: /(.*)\#link=/i } 578 | }, 579 | { 580 | name: 'ranker.com', 581 | match: /^(www|blog).ranker.com/i, 582 | rules: ['ref', 'rlf', 'l', 'li_source', 'li_medium'] 583 | }, 584 | { 585 | name: 'tkqlhce.com', 586 | match: /^www.tkqlhce.com/i, 587 | redirect: 'url' 588 | }, 589 | { 590 | name: 'flexlinkspro.com', 591 | match: /^track.flexlinkspro.com/i, 592 | redirect: 'url' 593 | }, 594 | { 595 | name: 'watchworthy.app', 596 | match: /^watchworthy.app/i, 597 | rules: ['ref'] 598 | }, 599 | { 600 | name: 'hbomax.com', 601 | match: /^trk.hbomax.com/i, 602 | redirect: 'url' 603 | }, 604 | { 605 | name: 'squarespace.com', 606 | match: /^.*.squarespace.com/i, 607 | rules: ['subchannel', 'source', 'subcampaign', 'campaign', 'channel', '_ga'] 608 | }, 609 | { 610 | name: 'baidu.com', 611 | match: /^www.baidu.com/i, 612 | rules: [ 613 | 'rsv_spt', 'rsv_idx', 'rsv_pq', 'rsv_t', 'rsv_bp', 'rsv_dl', 614 | 'rsv_iqid', 'rsv_enter', 'rsv_sug1', 'rsv_sug2', 'rsv_sug3', 615 | 'rsv_sug4', 'rsv_sug5', 'rsv_sug6', 'rsv_sug7', 'rsv_sug8', 616 | 'rsv_sug9', 'rsv_sug7', 'rsv_btype', 617 | 'tn', 'sa', 'rsf', 'rqid', 'usm', '__pc2ps_ab', 'p_signature', 618 | 'p_sign', 'p_timestamp', 'p_tk', 'oq' 619 | ], 620 | allow: ['wd', 'ie'] 621 | }, 622 | { 623 | name: 'primevideo.com', 624 | match: /^www.primevideo.com/i, 625 | rules: ['dclid'], 626 | replace: [/\/ref=[^\/?]*/i] 627 | }, 628 | { 629 | name: 'threadless.com', 630 | match: /^www.threadless.com/i, 631 | rules: ['itm_source_s', 'itm_medium_s', 'itm_campaign_s'], 632 | }, 633 | { 634 | name: 'wsj.com', 635 | match: /^www.wsj.com/i, 636 | rules: ['mod'], 637 | }, 638 | { 639 | name: 'thewarehouse.co.nz', 640 | match: /^www.thewarehouse.co.nz/i, 641 | rules: ['sfmc_j', 'sfmc_id', 'sfmc_mid', 'sfmc_uid', 'sfmc_id', 'sfmc_activityid'], 642 | }, 643 | { 644 | name: 'awstrack.me', 645 | match: /^.*awstrack.me/i, 646 | amp: { regex: /awstrack.me\/L0\/(.*)/ } 647 | }, 648 | { 649 | name: 'express.co.uk', 650 | match: /^www.express.co.uk/i, 651 | replace: [/\/amp$/i] 652 | }, 653 | { 654 | name: 'ko-fi.com', 655 | match: /^ko-fi.com/i, 656 | rules: ['ref', 'src'] 657 | }, 658 | { 659 | name: 'indiegala.com', 660 | match: /^www.indiegala.com/i, 661 | rules: ['ref'] 662 | }, 663 | { 664 | name: 'l.messenger.com', 665 | match: /^l.messenger.com/i, 666 | redirect: 'u' 667 | }, 668 | { 669 | name: 'transparency.fb.com', 670 | match: /^transparency.fb.com/i, 671 | rules: ['source'] 672 | }, 673 | { 674 | name: 'manymorestores.com', 675 | match: /^www.manymorestores.com/i, 676 | rules: ['ref'] 677 | }, 678 | { 679 | name: 'macgamestore.com', 680 | match: /^www.macgamestore.com/i, 681 | rules: ['ars'] 682 | }, 683 | { 684 | name: 'blizzardgearstore.com', 685 | match: /^www.blizzardgearstore.com/i, 686 | rules: ['_s'] 687 | }, 688 | { 689 | name: 'playbook.com', 690 | match: /^www.playbook.com/i, 691 | rules: ['p'] 692 | }, 693 | { 694 | name: 'cookiepro.com', 695 | match: /^.*.cookiepro.com/i, 696 | rules: ['source', 'referral'] 697 | }, 698 | { 699 | name: 'pinterest.com', 700 | match: /^www\.pinterest\..*/i, 701 | rules: ['rs'] 702 | }, 703 | { 704 | name: 'bing.com', 705 | match: /^www\.bing\.com/i, 706 | rules: [ 707 | 'qs', 'form', 'sp', 'pq', 'sc', 'sk', 'cvid', 'FORM', 708 | 'ck', 'simid', 'thid', 'cdnurl', 'pivotparams', 'ghsh', 'ghacc', 709 | 'ccid', '', 'ru' 710 | ], 711 | readded: ['sim','exph', 'expw', 'vt', 'mediaurl', 'first'], 712 | allow: ['q', 'tsc', 'iss', 'id', 'view', 'setlang'], 713 | }, 714 | { 715 | name: 'jf79.net', 716 | match: /^jf79\.net/i, 717 | rules: ['li', 'wi', 'ws', 'ws2'] 718 | }, 719 | { 720 | name: 'frankenergie.nl', 721 | match: /^www\.frankenergie\.nl/i, 722 | rules: ['aff_id'] 723 | }, 724 | { 725 | name: 'nova.cz', 726 | match: /^.*\.nova\.cz/i, 727 | rules: ['sznclid'], 728 | }, 729 | { 730 | name: 'cnn.com', 731 | match: /.*.cnn.com/i, 732 | rules: ['hpt', 'iid'], 733 | amp: { 734 | replace: { 735 | text: 'amp.cnn.com/cnn/', 736 | with: 'www.cnn.com/' 737 | } 738 | }, 739 | exclude: [ 740 | /e.newsletters.cnn.com/gi, 741 | ] 742 | }, 743 | { 744 | name: 'amp.scmp.com', 745 | match: /amp\.scmp\.com/i, 746 | amp: { 747 | replace: { 748 | text: 'amp.scmp.com', 749 | with: 'scmp.com' 750 | } 751 | } 752 | }, 753 | { 754 | name: 'justwatch.com', 755 | match: /click\.justwatch\.com/i, 756 | rules: ['cx','uct_country', 'uct_buybox', 'sid'], 757 | redirect: 'r' 758 | }, 759 | { 760 | name: 'psychologytoday.com', 761 | match: /www\.psychologytoday\.com/i, 762 | rules: ['amp'] 763 | }, 764 | { 765 | name: 'mouser.com', 766 | match: /www\.mouser\.com/i, 767 | rules: ['qs'] 768 | }, 769 | { 770 | name: 'awin1.com', 771 | match: /www\.awin1\.com/i, 772 | redirect: 'ued', 773 | rules: ['awinmid', 'awinaffid', 'clickref'] 774 | }, 775 | { 776 | name: 'syteapi.com', 777 | match: /syteapi\.com/i, 778 | decode: { param: 'url', encoding: 'base64' } 779 | }, 780 | { 781 | name: 'castorama.fr', 782 | match: /www\.castorama\.fr/i, 783 | rules: ['syte_ref'] 784 | }, 785 | { 786 | name: 'quizlet.com', 787 | match: /quizlet\.com/i, 788 | rules: ['funnelUUID', 'source'] 789 | }, 790 | { 791 | name: 'pbtech.co.nz', 792 | match: /www\.pbtech\.co\.nz/i, 793 | rules: ['qr'] 794 | }, 795 | { 796 | name: 'matomo.org', 797 | match: /matomo\.org/i, 798 | rules: [ 799 | 'menu', 'footer', 'header', 'hp-reasons-learn', 'hp-top', 800 | 'above-fold', 'step1-hp', 'mid-hp', 'take-back-control-hp', 801 | 'hp-reasons-icon', 'hp-reasons-heading', 'hp-reasons-p', 802 | 'hp-bottom', 'footer', 'menu' 803 | ] 804 | }, 805 | { 806 | name: 'eufy.com', 807 | match: /eufy\.com/i, 808 | rules: ['ref'] 809 | }, 810 | { 811 | name: 'newsflare.com', 812 | match: /www\.newsflare\.com/i, 813 | rules: ['jwsource'] 814 | }, 815 | { 816 | name: 'wish.com', 817 | match: /www\.wish\.com/i, 818 | rules: ['share'] 819 | }, 820 | { 821 | name: 'change.org', 822 | match: /www\.change\.org/i, 823 | rules: ['source_location'] 824 | }, 825 | { 826 | name: 'washingtonpost.com', 827 | match: /.*\.washingtonpost\.com/i, 828 | rules: ['itid', 's_l'] 829 | }, 830 | { 831 | name: 'lowes.com', 832 | match: /www\.lowes\.com/i, 833 | rules: ['cm_mmc', 'ds_rl', 'gbraid'] 834 | }, 835 | { 836 | name: 'stacks.wellcomecollection.org', 837 | match: /stacks\.wellcomecollection\.org/i, 838 | rules: ['source'] 839 | }, 840 | { 841 | name: 'redbubble.com', 842 | match: /.*\.redbubble\.com/i, 843 | rules: ['ref'] 844 | }, 845 | { 846 | name: 'inyourarea.co.uk', 847 | match: /inyourarea.co.uk/i, 848 | rules: ['from_reach_primary_nav', 'from_reach_footer_nav', 'branding'] 849 | }, 850 | { 851 | name: 'fiverr.com', 852 | match: /.*\.fiverr\.com/i, 853 | rules: [ 854 | 'source', 'context_referrer', 'referrer_gig_slug', 855 | 'ref_ctx_id', 'funnel', 'imp_id' 856 | ] 857 | }, 858 | { 859 | name: 'kqzyfj.com', 860 | match: /www\.kqzyfj\.com/i, 861 | redirect: 'url', 862 | rules: ['cjsku', 'pubdata'] 863 | }, 864 | { 865 | name: 'marca.com', 866 | match: /.*\.marca\.com/i, 867 | rules: ['intcmp', 's_kw', 'emk'] 868 | }, 869 | { 870 | name: 'marcaentradas.com', 871 | match: /.*\.marcaentradas\.com/i, 872 | rules: ['intcmp', 's_kw'] 873 | }, 874 | { 875 | name: 'honeycode.aws', 876 | match: /.*\.honeycode\.aws/i, 877 | rules: [ 878 | 'trackingId', 'sc_icampaign', 'sc_icontent', 'sc_ichannel', 879 | 'sc_iplace', 'sc_country', 'sc_outcome', 'sc_geo', 880 | 'sc_campaign', 'sc_channel', 'trkCampaign', 'trk' 881 | ] 882 | }, 883 | { 884 | name: 'news.artnet.com', 885 | match: /news\.artnet\.com/i, 886 | replace: [/\/amp-page$/i] 887 | }, 888 | { 889 | name: 'studentbeans.com', 890 | match: /www\.studentbeans\.com/i, 891 | rules: ['source'] 892 | }, 893 | { 894 | name: 'boxofficemojo.com', 895 | match: /boxofficemojo.com/i, 896 | rules: ['ref_'] 897 | }, 898 | { 899 | name: 'solodeportes.com.ar', 900 | match: /www.solodeportes.com.ar/i, 901 | rules: ['nosto', 'refSrc'] 902 | }, 903 | { 904 | name: 'amp.dw.com', 905 | match: /amp.dw.com/i, 906 | amp: { 907 | replace: { 908 | text: 'amp.dw.com', 909 | with: 'dw.com' 910 | } 911 | } 912 | }, 913 | { 914 | name: 'joybuggy.com', 915 | match: /joybuggy.com/i, 916 | rules: ['ref'] 917 | }, 918 | { 919 | name: 'etail.market', 920 | match: /etail.market/i, 921 | rules: ['tracking'] 922 | }, 923 | { 924 | name: 'myanimelist.net', 925 | match: /myanimelist.net/i, 926 | rules: ['_location', 'click_type', 'click_param'] 927 | }, 928 | { 929 | name: 'support-dev.discord.com', 930 | match: /support-dev.discord.com/i, 931 | rules: ['ref'] 932 | }, 933 | { 934 | name: 'dlgamer.com', 935 | match: /dlgamer.com/i, 936 | rules: ['affil'] 937 | }, 938 | { 939 | name: 'newsletter.manor.ch', 940 | match: /newsletter.manor.ch/i, 941 | rules: ['user_id_1'], 942 | rev: true 943 | }, 944 | { 945 | name: 'knowyourmeme.com', 946 | match: /amp.knowyourmeme.com/i, 947 | amp: { 948 | replace: { 949 | text: 'amp.knowyourmeme.com', 950 | with: 'knowyourmeme.com' 951 | } 952 | } 953 | }, 954 | { 955 | name: 'ojrq.net', 956 | match: /ojrq.net/i, 957 | redirect: 'return' 958 | }, 959 | { 960 | name: 'click.pstmrk.it', 961 | match: /click.pstmrk.it/i, 962 | amp: { regex: /click\.pstmrk\.it\/(?:[a-zA-Z0-9]){1,2}\/(.*?)\//gim } 963 | }, 964 | { 965 | name: 'track.roeye.co.nz', 966 | match: /track.roeye.co.nz/i, 967 | redirect: 'path' 968 | }, 969 | { 970 | name: 'producthunt.com', 971 | match: /producthunt.com/i, 972 | rules: ['ref'] 973 | }, 974 | { 975 | name: 'cbsnews.com', 976 | match: /www.cbsnews.com/i, 977 | rules: ['ftag', 'intcid'], 978 | amp: { 979 | replace: { 980 | text: 'cbsnews.com/amp/', 981 | with: 'cbsnews.com/' 982 | } 983 | } 984 | }, 985 | { 986 | name: 'jobs.venturebeat.com', 987 | match: /jobs.venturebeat.com/i, 988 | rules: ['source'] 989 | }, 990 | { 991 | name: 'api.ffm.to', 992 | match: /api.ffm.to/i, 993 | decode: { 994 | param: 'cd', 995 | lookFor: 'destUrl' 996 | } 997 | }, 998 | { 999 | name: 'wfaa.com', 1000 | match: /www.wfaa.com/i, 1001 | rules: ['ref'] 1002 | }, 1003 | { 1004 | name: 'buyatoyota.com', 1005 | match: /www.buyatoyota.com/i, 1006 | rules: ['siteid'] 1007 | }, 1008 | { 1009 | name: 'independent.co.uk', 1010 | match: /www.independent.co.uk/i, 1011 | rules: ['amp', 'regSourceMethod'] 1012 | }, 1013 | { 1014 | name: 'lenovo.vzew.net', 1015 | match: /lenovo.vzew.net/i, 1016 | redirect: 'u' 1017 | }, 1018 | { 1019 | name: 'stats.newswire.com', 1020 | match: /stats.newswire.com/i, 1021 | decode: { param: 'final' } 1022 | }, 1023 | { 1024 | name: 'cooking.nytimes.com', 1025 | match: /cooking.nytimes.com/i, 1026 | rules: [ 1027 | 'smid', 'variant', 'algo', 'req_id', 'surface', 'imp_id', 1028 | 'action', 'region', 'module', 'pgType' 1029 | ] 1030 | }, 1031 | { 1032 | name: 'optigruen.com', 1033 | match: /www\.optigruen\.[a-z0-9]{0,3}/i, 1034 | rules: ['cHash', 'chash', 'mdrv'] 1035 | }, 1036 | { 1037 | name: 'osi.rosenberger.com', 1038 | match: /osi.rosenberger.com/i, 1039 | rules: ['cHash', 'chash'] 1040 | }, 1041 | { 1042 | name: 'cbc.ca', 1043 | match: /cbc.ca/i, 1044 | rules: ['__vfz', 'cmp', 'referrer'] 1045 | }, 1046 | { 1047 | name: 'local12.com', 1048 | match: /local12.com/i, 1049 | rules: ['_gl'] 1050 | }, 1051 | { 1052 | name: 'eufylife.com', 1053 | match: /eufylife.com/i, 1054 | rules: ['ref'] 1055 | }, 1056 | { 1057 | name: 'walmart.com', 1058 | match: /walmart.com/i, 1059 | rules: [ 1060 | 'athAsset', 'povid', 'wmlspartner', 'athcpid', 'athpgid', 'athznid', 1061 | 'athmtid', 'athstid', 'athguid', 'athwpid', 'athtvid', 'athcgid', 1062 | 'athieid', 'athancid', 'athbdg', 'campaign_id', 'eventST', 'bt', 1063 | 'pos', 'rdf', 'tax', 'plmt', 'mloc', 'pltfm', 'pgId', 'pt', 'spQs', 1064 | 'adUid', 'adsRedirect' 1065 | ], 1066 | redirect: 'rd' 1067 | }, 1068 | { 1069 | name: 'adclick.g.doubleclick.net', 1070 | match: /adclick.g.doubleclick.net/i, 1071 | redirect: 'adurl' 1072 | }, 1073 | { 1074 | name: 'dyno.gg', 1075 | match: /dyno.gg/i, 1076 | rules: ['ref'] 1077 | }, 1078 | { 1079 | name: 'eufylife.com', 1080 | match: /eufylife.com/i, 1081 | rules: ['ref'] 1082 | }, 1083 | { 1084 | name: 'connect.studentbeans.com', 1085 | match: /connect.studentbeans.com/i, 1086 | rules: ['ref'] 1087 | }, 1088 | { 1089 | name: 'urldefense.proofpoint.com', 1090 | match: /urldefense.proofpoint.com/i, 1091 | decode: { 1092 | param: 'u', 1093 | handler: 'urldefense.proofpoint.com' 1094 | } 1095 | }, 1096 | { 1097 | name: 'curseforge.com', 1098 | match: /www.curseforge.com/i, 1099 | redirect: 'remoteurl' 1100 | }, 1101 | { 1102 | name: 's.pemsrv.com', 1103 | match: /s.pemsrv.com/i, 1104 | rules: [ 1105 | 'cat', 'idzone', 'type', 'sub', 'block', 1106 | 'el', 'tags', 'cookieconsent', 'scr_info' 1107 | ], 1108 | redirect: 'p' 1109 | }, 1110 | { 1111 | name: 'chess.com', 1112 | match: /www.chess.com/i, 1113 | rules: ['c'] 1114 | }, 1115 | { 1116 | name: 'porndude.link', 1117 | match: /porndude.link/i, 1118 | rules: ['ref'] 1119 | }, 1120 | { 1121 | name: 'xvideos.com', 1122 | match: /xvideos.com/i, 1123 | rules: ['sxcaf'] 1124 | }, 1125 | { 1126 | name: 'xvideos.red', 1127 | match: /xvideos.red/i, 1128 | rules: ['sxcaf', 'pmsc', 'pmln'] 1129 | }, 1130 | { 1131 | name: 'xhamster.com', 1132 | match: /xhamster.com/i, 1133 | rules: ['source'] 1134 | }, 1135 | { 1136 | name: 'patchbot.io', 1137 | match: /patchbot.io/i, 1138 | decode: { 1139 | targetPath: true, 1140 | handler: 'patchbot.io' 1141 | } 1142 | }, 1143 | { 1144 | name: 'milkrun.com', 1145 | match: /milkrun.com/i, 1146 | rules: [ 1147 | '_branch_match_id', 1148 | '_branch_referrer' 1149 | ] 1150 | }, 1151 | { 1152 | name: 'gog.salesmanago.com', 1153 | match: /gog.salesmanago.com/i, 1154 | rules: [ 1155 | 'smclient', 'smconv', 'smlid' 1156 | ], 1157 | redirect: 'url' 1158 | }, 1159 | { 1160 | name: 'dailymail.co.uk', 1161 | match: /dailymail.co.uk/i, 1162 | rules: ['reg_source', 'ito'] 1163 | }, 1164 | { 1165 | name: 'stardockentertainment.info', 1166 | match: /www.stardockentertainment.info/i, 1167 | decode: { 1168 | targetPath: true, 1169 | handler: 'stardockentertainment.info', 1170 | } 1171 | }, 1172 | { 1173 | name: 'steam.gs', 1174 | match: /steam.gs/i, 1175 | decode: { 1176 | targetPath: true, 1177 | handler: 'steam.gs' 1178 | } 1179 | }, 1180 | { 1181 | name: '0yxjo.mjt.lu', 1182 | match: /0yxjo.mjt.lu/i, 1183 | decode: { 1184 | targetPath: true, 1185 | handler: '0yxjo.mjt.lu', 1186 | } 1187 | }, 1188 | { 1189 | name: 'click.redditmail.com', 1190 | match: /click.redditmail.com/i, 1191 | decode: { 1192 | targetPath: true, 1193 | handler: 'click.redditmail.com', 1194 | } 1195 | }, 1196 | { 1197 | name: 'deals.dominos.co.nz', 1198 | match: /deals.dominos.co.nz/i, 1199 | decode:{ 1200 | targetPath: true, 1201 | handler: 'deals.dominos.co.nz' 1202 | } 1203 | }, 1204 | { 1205 | name: 'hashnode.com', 1206 | match: /(?:.*\.)?hashnode\.com/i, 1207 | rules: ['source'] 1208 | }, 1209 | { 1210 | name: 's.amazon-adsystem.com', 1211 | match: /s.amazon-adsystem.com/i, 1212 | rules: ['dsig', 'd', 'ex-fch', 'ex-fargs', 'cb'], 1213 | redirect: 'rd' 1214 | }, 1215 | { 1216 | name: 'hypable.com', 1217 | match: /www.hypable.com/i, 1218 | amp: { replace: { text: /amp\/$/gi } } 1219 | }, 1220 | { 1221 | name: 'theguardian.com', 1222 | match: /theguardian.com/i, 1223 | amp: { 1224 | replace: { 1225 | text: 'amp.theguardian.com', 1226 | with: 'theguardian.com' 1227 | } 1228 | }, 1229 | rules: ['INTCMP', 'acquisitionData', 'REFPVID'] 1230 | }, 1231 | { 1232 | name: 'indiatoday.in', 1233 | match: /www.indiatoday.in/i, 1234 | amp: { 1235 | replace: { 1236 | text: 'www.indiatoday.in/amp/', 1237 | with: 'www.indiatoday.in/' 1238 | } 1239 | } 1240 | }, 1241 | { 1242 | name: 'seek.co.nz', 1243 | match: /www.seek.co.nz/i, 1244 | rules: ['tracking', 'sc_trk'] 1245 | }, 1246 | { 1247 | name: 'seekvolunteer.co.nz', 1248 | match: /seekvolunteer.co.nz/i, 1249 | rules: ['tracking', 'sc_trk'] 1250 | }, 1251 | { 1252 | name: 'seekbusiness.com.au', 1253 | match: /www.seekbusiness.com.au/i, 1254 | rules: ['tracking', 'cid'] 1255 | }, 1256 | { 1257 | name: 'garageclothing.com', 1258 | match: /www.garageclothing.com/i, 1259 | rules: ['syte_ref', 'site_ref'] 1260 | }, 1261 | { 1262 | name: 'urbandictionary.com', 1263 | match: /urbandictionary.com/i, 1264 | rules: ['amp'] 1265 | }, 1266 | { 1267 | name: 'norml.org', 1268 | match: /norml.org/i, 1269 | rules: ['amp'] 1270 | }, 1271 | { 1272 | name: 'nbcconnecticut.com', 1273 | match: /www.nbcconnecticut.com/i, 1274 | rules: ['amp'] 1275 | }, 1276 | { 1277 | name: 'pbs.org', 1278 | match: /www.pbs.org/i, 1279 | amp: { 1280 | replace: { 1281 | text: 'pbs.org/newshour/amp/', 1282 | with: 'pbs.org/newshour/' 1283 | } 1284 | } 1285 | }, 1286 | { 1287 | name: 'm.thewire.in', 1288 | match: /m.thewire.in/i, 1289 | amp: { 1290 | replace: { 1291 | text: 'm.thewire.in/article/', 1292 | with: 'thewire.in/' 1293 | }, 1294 | sliceTrailing: '/amp' 1295 | } 1296 | }, 1297 | { 1298 | name: 'ladysmithchronicle.com', 1299 | match: /www.ladysmithchronicle.com/i, 1300 | rules: ['ref'] 1301 | }, 1302 | { 1303 | name: 'businesstoday.in', 1304 | match: /www.businesstoday.in/i, 1305 | amp: { 1306 | replace: { 1307 | text: 'www.businesstoday.in/amp/markets/', 1308 | with: 'www.businesstoday.in/markets/' 1309 | } 1310 | } 1311 | }, 1312 | { 1313 | name: 'turnto10.com', 1314 | match: /turnto10.com/i, 1315 | amp: { 1316 | replace: { 1317 | text: 'turnto10.com/amp/', 1318 | with: 'turnto10.com/' 1319 | } 1320 | } 1321 | }, 1322 | { 1323 | name: 'gadgets360.com', 1324 | match: /www.gadgets360.com/i, 1325 | amp: { 1326 | sliceTrailing: '/amp' 1327 | } 1328 | }, 1329 | { 1330 | name: 'm10news.com', 1331 | match: /m10news.com/i, 1332 | rules: ['amp'] 1333 | }, 1334 | { 1335 | name: 'elprogreso.es', 1336 | match: /www.elprogreso.es/i, 1337 | amp: { 1338 | replace: { 1339 | text: '.amp.html', 1340 | with: '.html' 1341 | } 1342 | } 1343 | }, 1344 | { 1345 | name: 'thenewsminute.com', 1346 | match: /www.thenewsminute.com/i, 1347 | amp: { 1348 | replace: { 1349 | text: 'www.thenewsminute.com/amp/story/', 1350 | with: 'www.thenewsminute.com/' 1351 | } 1352 | } 1353 | }, 1354 | { 1355 | name: 'mirror.co.uk', 1356 | match: /www.mirror.co.uk/i, 1357 | amp: { 1358 | sliceTrailing: '.amp' 1359 | } 1360 | }, 1361 | { 1362 | name: 'libretro.com', 1363 | match: /www.libretro.com/i, 1364 | rules: ['amp'] 1365 | }, 1366 | { 1367 | name: 'the-sun.com', 1368 | match: /www.the-sun.com/i, 1369 | amp: { 1370 | sliceTrailing: 'amp/' 1371 | } 1372 | }, 1373 | { 1374 | name: 'bostonherald.com', 1375 | match: /bostonherald.com/i, 1376 | rules: ['g2i_source', 'g2i_medium', 'g2i_campaign'], 1377 | amp: { 1378 | sliceTrailing: 'amp/' 1379 | } 1380 | }, 1381 | { 1382 | name: 'playshoptitans.com', 1383 | match: /playshoptitans.com/i, 1384 | rules: ['af_ad', 'pid', 'source_caller', 'shortlink', 'c'] 1385 | }, 1386 | { 1387 | name: 'news.com.au', 1388 | match: /www.news.com.au/i, 1389 | rules: ['sourceCode'] 1390 | }, 1391 | { 1392 | name: 'wvva.com', 1393 | match: /www.wvva.com/i, 1394 | rules: ['outputType'] 1395 | }, 1396 | { 1397 | name: 'realestate.com.au', 1398 | match: /www.realestate.com.au/i, 1399 | rules: [ 1400 | 'campaignType', 'campaignChannel', 'campaignName', 1401 | 'campaignContent', 'campaignSource', 'campaignPlacement', 1402 | 'sourcePage', 'sourceElement', 'cid' 1403 | ] 1404 | }, 1405 | { 1406 | name: 'redirectingat.com', 1407 | match: /redirectingat.com/i, 1408 | redirect: 'url', 1409 | rules: ['id', 'xcust', 'xs'], 1410 | decode: { handler: 'redirectingat.com', targetPath: true } 1411 | }, 1412 | { 1413 | name: 'map.sewoon.org', 1414 | match: /map.sewoon.org/i, 1415 | rules: ['cid'] 1416 | }, 1417 | { 1418 | name: 'vi-control.net', 1419 | match: /[&?]source=https:\/\/vi-control\.net\/community$/, 1420 | match_href: true, 1421 | rules: ['source'] 1422 | }, 1423 | { 1424 | name: 'rosequake.com', 1425 | match: /www.rosequake.com/i, 1426 | rules: ['edmID', 'linkID', 'userID', 'em', 'taskItemID'], 1427 | redirect: 'to' 1428 | }, 1429 | { 1430 | name: 'rekrute.com', 1431 | match: /www.rekrute.com/i, 1432 | rules: ['clear'], 1433 | // Malicious domain. This rule will bypass the XSS attempt 1434 | redirect: 'keyword' 1435 | }, 1436 | { 1437 | name: 'go.skimresources.com', 1438 | match: /go.skimresources.com/i, 1439 | rules: ['id', 'xs', 'xcust'], 1440 | redirect: 'url' 1441 | }, 1442 | { 1443 | name: 'khnum-ezi.com', 1444 | match: /khnum-ezi.com/i, 1445 | rules: [ 1446 | 'browserWidth', 'browserHeight', 'iframeDetected', 1447 | 'webdriverDetected', 'gpu', 'timezone', 'visitid', 1448 | 'type', 'timezoneName' 1449 | ] 1450 | }, 1451 | { 1452 | name: 'theatlantic.com', 1453 | match: /(?:www|accounts)\.theatlantic\.com/i, 1454 | rules: ['source', 'referral'] 1455 | }, 1456 | { 1457 | name: 'rumble.com', 1458 | match: /rumble.com/i, 1459 | rules: ['e9s'] 1460 | }, 1461 | { 1462 | name: 'cointiply.com', 1463 | match: /cointiply.com/i, 1464 | rules: ['source_cta'] 1465 | }, 1466 | { 1467 | name: 'dev.to', 1468 | match: /dev.to/, 1469 | rules: ['t', 's', 'u'], 1470 | redirect: 'u' 1471 | }, 1472 | { 1473 | name: 'milda-clq.com', 1474 | // Linked to (khnum-ezi.com) as they are the same 1475 | match: /milda-clq.com/, 1476 | rules: [ 1477 | 'visitid', 'type', 'browserWidth', 1478 | 'browserHeight', 'webdriverDetected', 1479 | 'timezone', 'gpu', 'iframeDetected', 1480 | 'timezoneName' 1481 | ] 1482 | } 1483 | ] 1484 | -------------------------------------------------------------------------------- /data/supported-sites.txt: -------------------------------------------------------------------------------- 1 | Total unique rules: 905 2 | 3 | | Match | Rules | 4 | | :---------------------------- | :---- | 5 | | 0yxjo.mjt.lu | 1 | 6 | | 2game.com | 1 | 7 | | adclick.g.doubleclick.net | 1 | 8 | | adtraction.com | 1 | 9 | | aliexpress.com | 43 | 10 | | amazon.com | 50 | 11 | | amp.dw.com | 1 | 12 | | amp.scmp.com | 1 | 13 | | ampproject.org | 5 | 14 | | anrdoezrs.net | 1 | 15 | | api.ffm.to | 1 | 16 | | app.link | 16 | 17 | | apple.com | 12 | 18 | | audible.com | 6 | 19 | | awin1.com | 4 | 20 | | awstrack.me | 1 | 21 | | baidu.com | 30 | 22 | | bandcamp.com | 8 | 23 | | berrybase.de | 1 | 24 | | bing.com | 18 | 25 | | blizzardgearstore.com | 1 | 26 | | blog.twitch.tv | 1 | 27 | | bn5x.net | 1 | 28 | | bostonglobe.com | 1 | 29 | | bostonherald.com | 4 | 30 | | boxofficemojo.com | 1 | 31 | | businesstoday.in | 1 | 32 | | buyatoyota.com | 1 | 33 | | castorama.fr | 1 | 34 | | cbc.ca | 3 | 35 | | cbsnews.com | 3 | 36 | | change.org | 1 | 37 | | chess.com | 1 | 38 | | click.pstmrk.it | 1 | 39 | | click.redditmail.com | 1 | 40 | | cnbc.com | 1 | 41 | | cnn.com | 3 | 42 | | connect.studentbeans.com | 1 | 43 | | cookiepro.com | 2 | 44 | | cooking.nytimes.com | 10 | 45 | | countdown.co.nz | 4 | 46 | | curseforge.com | 1 | 47 | | dailymail.co.uk | 2 | 48 | | deals.dominos.co.nz | 1 | 49 | | discord.com | 2 | 50 | | dlgamer.com | 1 | 51 | | dpbolvw.net | 1 | 52 | | dreamgame.com | 1 | 53 | | dyno.gg | 1 | 54 | | ebay.com | 21 | 55 | | elprogreso.es | 1 | 56 | | emjcd.com | 1 | 57 | | etail.market | 1 | 58 | | etsy.com | 7 | 59 | | eufy.com | 1 | 60 | | eufylife.com | 1 | 61 | | eufylife.com | 1 | 62 | | express.co.uk | 1 | 63 | | facebook.com | 8 | 64 | | fanatical.com | 5 | 65 | | findojobs.co.nz | 1 | 66 | | fiverr.com | 6 | 67 | | flexlinkspro.com | 1 | 68 | | frankenergie.nl | 1 | 69 | | gadgets360.com | 1 | 70 | | gamebillet.com | 1 | 71 | | gamersgate.com | 1 | 72 | | gamesload.com | 1 | 73 | | gamesplanet.com | 1 | 74 | | garageclothing.com | 2 | 75 | | gate.sc | 1 | 76 | | getmusicbee.com | 1 | 77 | | go.skimresources.com | 4 | 78 | | go2cloud.org | 1 | 79 | | gog.com | 7 | 80 | | gog.salesmanago.com | 4 | 81 | | google.com | 24 | 82 | | greenmangaming.com | 5 | 83 | | hashnode.com | 1 | 84 | | hbomax.com | 1 | 85 | | honeycode.aws | 12 | 86 | | hubspot.com | 2 | 87 | | humblebundle.com | 5 | 88 | | hypable.com | 1 | 89 | | imdb.com | 11 | 90 | | imgur.com | 1 | 91 | | imp.i305175.net | 1 | 92 | | indeed.com | 2 | 93 | | independent.co.uk | 2 | 94 | | indiatoday.in | 1 | 95 | | indiegala.com | 1 | 96 | | instagram.com | 4 | 97 | | inyourarea.co.uk | 3 | 98 | | itch.io | 1 | 99 | | jdoqocy.com | 1 | 100 | | jf79.net | 4 | 101 | | jobs.venturebeat.com | 1 | 102 | | joybuggy.com | 1 | 103 | | justwatch.com | 5 | 104 | | khnum-ezi.com | 9 | 105 | | knowyourmeme.com | 1 | 106 | | ko-fi.com | 2 | 107 | | kqzyfj.com | 3 | 108 | | l.messenger.com | 1 | 109 | | ladysmithchronicle.com | 1 | 110 | | lenovo.com | 6 | 111 | | lenovo.vzew.net | 1 | 112 | | libretro.com | 1 | 113 | | linkedin.com | 16 | 114 | | linksynergy.com | 3 | 115 | | local12.com | 1 | 116 | | lowes.com | 3 | 117 | | m.thewire.in | 1 | 118 | | m10news.com | 1 | 119 | | macgamestore.com | 1 | 120 | | manymorestores.com | 1 | 121 | | map.sewoon.org | 1 | 122 | | marca.com | 3 | 123 | | marcaentradas.com | 2 | 124 | | matomo.org | 15 | 125 | | medium.com | 1 | 126 | | microsoft.com | 2 | 127 | | mightyape | 1 | 128 | | milkrun.com | 2 | 129 | | mirror.co.uk | 1 | 130 | | mouser.com | 1 | 131 | | msn.com | 3 | 132 | | music.apple.com | 5 | 133 | | myanimelist.net | 3 | 134 | | nbcconnecticut.com | 1 | 135 | | nbcnews.com | 2 | 136 | | news.artnet.com | 1 | 137 | | news.com.au | 1 | 138 | | newsflare.com | 1 | 139 | | newsletter.manor.ch | 1 | 140 | | newsweek.com | 2 | 141 | | noctre.com | 1 | 142 | | norml.org | 1 | 143 | | nova.cz | 1 | 144 | | nuuvem.com | 3 | 145 | | office.com | 1 | 146 | | ojrq.net | 1 | 147 | | optigruen.com | 3 | 148 | | osi.rosenberger.com | 2 | 149 | | patchbot.io | 1 | 150 | | pbs.org | 1 | 151 | | pbtech.co.nz | 1 | 152 | | pinterest.com | 1 | 153 | | pixiv.net | 3 | 154 | | play.google.com | 2 | 155 | | playbook.com | 1 | 156 | | playshoptitans.com | 5 | 157 | | plex.tv | 4 | 158 | | porndude.link | 1 | 159 | | primevideo.com | 2 | 160 | | producthunt.com | 1 | 161 | | psychologytoday.com | 1 | 162 | | qflm.net | 1 | 163 | | quizlet.com | 2 | 164 | | ranker.com | 5 | 165 | | realestate.com.au | 9 | 166 | | redbubble.com | 1 | 167 | | reddit.com | 16 | 168 | | redirect.viglink.com | 1 | 169 | | redirectingat.com | 5 | 170 | | rekrute.com | 2 | 171 | | rosequake.com | 6 | 172 | | s.amazon-adsystem.com | 6 | 173 | | s.pemsrv.com | 10 | 174 | | seek.co.nz | 2 | 175 | | seekbusiness.com.au | 2 | 176 | | seekvolunteer.co.nz | 2 | 177 | | shopee.com | 8 | 178 | | sjv.io | 1 | 179 | | solodeportes.com.ar | 2 | 180 | | spotify.com | 6 | 181 | | squarespace.com | 6 | 182 | | stacks.wellcomecollection.org | 1 | 183 | | stardockentertainment.info | 1 | 184 | | startpage.com | 1 | 185 | | stats.newswire.com | 1 | 186 | | steam.gs | 1 | 187 | | steamcommunity.com | 1 | 188 | | steamcommunity.com/linkfilter | 1 | 189 | | store.steampowered.com | 1 | 190 | | studentbeans.com | 1 | 191 | | support-dev.discord.com | 1 | 192 | | syteapi.com | 1 | 193 | | techcrunch.com | 3 | 194 | | the-sun.com | 1 | 195 | | theguardian.com | 4 | 196 | | thenewsminute.com | 1 | 197 | | thewarehouse.co.nz | 6 | 198 | | threadless.com | 3 | 199 | | ticketmaster.co.nz | 1 | 200 | | tiktok.com | 23 | 201 | | tiktok.com/link | 1 | 202 | | tkqlhce.com | 1 | 203 | | track.roeye.co.nz | 1 | 204 | | transparency.fb.com | 1 | 205 | | turnto10.com | 1 | 206 | | tvguide.com | 1 | 207 | | twitch.tv | 3 | 208 | | twitch.tv-email | 1 | 209 | | twitter.com | 4 | 210 | | urbandictionary.com | 1 | 211 | | urldefense.proofpoint.com | 1 | 212 | | vi-control.net | 1 | 213 | | voidu.com | 1 | 214 | | walmart.com | 30 | 215 | | washingtonpost.com | 2 | 216 | | watchworthy.app | 1 | 217 | | wattpad.com | 3 | 218 | | wfaa.com | 1 | 219 | | wingamestore.com | 1 | 220 | | wish.com | 1 | 221 | | wsj.com | 1 | 222 | | wvva.com | 1 | 223 | | xhamster.com | 1 | 224 | | xvideos.com | 1 | 225 | | xvideos.red | 3 | 226 | | yahoo.com | 6 | 227 | | yandex.com | 5 | 228 | | youtube.com | 8 | -------------------------------------------------------------------------------- /data/tidy.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Tidy URL 3 | // @namespace https://ksir.pw 4 | // @version 1.18.3 5 | // @description Cleans/removes garbage or tracking parameters from URLs 6 | // @author Kain (ksir.pw) 7 | // @include * 8 | // @icon data:image/gif;base64,R0lGODlhEAAQAMIDAAAAAIAAAP8AAP///////////////////yH5BAEKAAQALAAAAAAQABAAAAMuSLrc/jA+QBUFM2iqA2ZAMAiCNpafFZAs64Fr66aqjGbtC4WkHoU+SUVCLBohCQA7 9 | // @updateURL https://github.com/DrKain/tidy-url/raw/main/data/tidy.user.js 10 | // @downloadURL https://github.com/DrKain/tidy-url/raw/main/data/tidy.user.js 11 | // @require https://github.com/DrKain/tidy-url/raw/main/lib/tidyurl.min.js 12 | // @homepage https://github.com/DrKain/tidy-url/ 13 | // @supportURL https://github.com/DrKain/tidy-url/issues 14 | // @grant none 15 | // @run-at document-start 16 | // ==/UserScript== 17 | 18 | // Use a fresh instance of TidyURL 19 | const _tidy = new tidyurl.TidyCleaner(); 20 | 21 | _tidy.config.setMany({ 22 | // Don't log invalid links 23 | silent: false, 24 | // Enable/disable redirect and amp rules 25 | allowAMP: false, 26 | allowCustomHandlers: true, 27 | allowRedirects: true 28 | }); 29 | 30 | // Set to false if you don't want page links to be cleaned 31 | // If you encounter any problems please report the link on GitHub 32 | // ---> https://github.com/DrKain/tidy-url/issues 33 | const clean_pages = true; 34 | // Used for optimization when there's too many links on a page 35 | const use_optimization = true; 36 | // Number of links on the page before using optimization 37 | const opti_threshold = 100; 38 | const opti_dataname = 'tidyurl'; 39 | // Time between each cleanup (in milliseconds) 40 | const clean_interval = 1000; 41 | 42 | const log = (msg) => console.log(`[tidy-url] ${msg}`); 43 | 44 | (() => { 45 | const link = _tidy.clean(window.location.href); 46 | 47 | // If the modified URL is different from the original 48 | if (link.url !== link.info.original) { 49 | if (link.info.isNewHost) window.location.href = link.url; 50 | else window.history.pushState('', '', link.url); 51 | } 52 | })(); 53 | 54 | (() => { 55 | let ready = true; 56 | let last_count = 0; 57 | let selector = 'a'; 58 | 59 | if (!clean_pages) return; 60 | 61 | const do_clean = () => { 62 | if (ready) { 63 | ready = false; 64 | 65 | if (use_optimization && last_count >= opti_threshold) { 66 | selector = `a:not([data-${opti_dataname}])`; 67 | } 68 | 69 | const links = document.querySelectorAll(selector); 70 | last_count = links.length; 71 | 72 | // if (links.length > 0) log('Processing ' + links.length + ' links'); 73 | for (const link of links) { 74 | // Don't clean links that have already been cleaned 75 | // This is to prevent slowing down pages when there are a lot of links 76 | // For example, endless scroll on reddit 77 | if (use_optimization && selector !== 'a') { 78 | link.setAttribute(`data-${opti_dataname}`, '1'); 79 | } 80 | try { 81 | // Make sure it's a valid URL 82 | new URL(link.href); 83 | // Run the cleaner 84 | const cleaned = _tidy.clean(link.href); 85 | // If the new URL is shorter, apply it 86 | if (cleaned.info.reduction > 0) { 87 | log('Cleaned: ' + link.href); 88 | link.setAttribute('href', cleaned.url); 89 | } 90 | } catch (error) { 91 | // Ignore invalid URLs 92 | } 93 | } 94 | setTimeout(() => (ready = true), clean_interval); 95 | } 96 | }; 97 | 98 | const MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver; 99 | const observer = new MutationObserver(do_clean); 100 | observer.observe(document, { childList: true, subtree: true }); 101 | window.addEventListener('load', () => setInterval(do_clean, clean_interval)); 102 | do_clean(); 103 | })(); 104 | -------------------------------------------------------------------------------- /lib/config.d.ts: -------------------------------------------------------------------------------- 1 | import { IConfig } from './interface'; 2 | export declare class TidyConfig implements IConfig { 3 | allowAMP: boolean; 4 | allowCustomHandlers: boolean; 5 | allowRedirects: boolean; 6 | silent: boolean; 7 | /** 8 | * Fetch a copy of the current config. 9 | * You can then pass this to `setMany` if 10 | * you want to sync with another TidyConfig instance. 11 | * 12 | * @returns A copy of the current config 13 | */ 14 | copy(): { 15 | allowAMP: boolean; 16 | allowCustomHandlers: boolean; 17 | allowRedirects: boolean; 18 | silent: boolean; 19 | }; 20 | /** 21 | * You can just use `config.key` but yeah. 22 | * @param key The key you're wanting to get the value of 23 | * @returns The value 24 | */ 25 | get(key: keyof IConfig): boolean; 26 | /** 27 | * Set a single config option. If you want to set multiple at once 28 | * use `setMany` 29 | * @param key Option to set 30 | * @param value Value to set it to 31 | */ 32 | set(key: keyof IConfig, value: boolean): void; 33 | /** 34 | * Set multiple config options at once by passing it an object. 35 | * @param obj An object containing any number of config options 36 | */ 37 | setMany(obj: Partial): void; 38 | } 39 | -------------------------------------------------------------------------------- /lib/config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.TidyConfig = void 0; 4 | var TidyConfig = /** @class */ (function () { 5 | function TidyConfig() { 6 | this.allowAMP = false; 7 | this.allowCustomHandlers = true; 8 | this.allowRedirects = true; 9 | this.silent = true; 10 | } 11 | /** 12 | * Fetch a copy of the current config. 13 | * You can then pass this to `setMany` if 14 | * you want to sync with another TidyConfig instance. 15 | * 16 | * @returns A copy of the current config 17 | */ 18 | TidyConfig.prototype.copy = function () { 19 | return { 20 | allowAMP: this.allowAMP, 21 | allowCustomHandlers: this.allowCustomHandlers, 22 | allowRedirects: this.allowRedirects, 23 | silent: this.silent 24 | }; 25 | }; 26 | /** 27 | * You can just use `config.key` but yeah. 28 | * @param key The key you're wanting to get the value of 29 | * @returns The value 30 | */ 31 | TidyConfig.prototype.get = function (key) { 32 | return this[key]; 33 | }; 34 | /** 35 | * Set a single config option. If you want to set multiple at once 36 | * use `setMany` 37 | * @param key Option to set 38 | * @param value Value to set it to 39 | */ 40 | TidyConfig.prototype.set = function (key, value) { 41 | this[key] = value; 42 | }; 43 | /** 44 | * Set multiple config options at once by passing it an object. 45 | * @param obj An object containing any number of config options 46 | */ 47 | TidyConfig.prototype.setMany = function (obj) { 48 | var _this = this; 49 | Object.keys(obj).forEach(function (_key) { 50 | var _a; 51 | var key = _key; 52 | var val = (_a = obj[key]) !== null && _a !== void 0 ? _a : _this[key]; 53 | if (typeof _this[key] === 'undefined') { 54 | throw new Error("'" + key + "' is not a valid config key"); 55 | } 56 | _this.set(key, val); 57 | }); 58 | }; 59 | return TidyConfig; 60 | }()); 61 | exports.TidyConfig = TidyConfig; 62 | -------------------------------------------------------------------------------- /lib/handlers.d.ts: -------------------------------------------------------------------------------- 1 | import { IHandler } from './interface'; 2 | /** 3 | * This is currently experimental while I decide on how I want to restructure the main code to make it easier to follow. 4 | * There will need to be handlers for each process of the "clean" as well as these custom cases for sites that mix it up. 5 | * If you would like to help or give your thoughts feel free to open an issue on GitHub. 6 | */ 7 | export declare const handlers: { 8 | [key: string]: IHandler; 9 | }; 10 | -------------------------------------------------------------------------------- /lib/handlers.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.handlers = void 0; 4 | var utils_1 = require("./utils"); 5 | /** 6 | * This is currently experimental while I decide on how I want to restructure the main code to make it easier to follow. 7 | * There will need to be handlers for each process of the "clean" as well as these custom cases for sites that mix it up. 8 | * If you would like to help or give your thoughts feel free to open an issue on GitHub. 9 | */ 10 | exports.handlers = {}; 11 | exports.handlers['patchbot.io'] = { 12 | exec: function (_str, args) { 13 | try { 14 | var dec = args.decoded.replace(/%3D/g, '='); 15 | return { url: decodeURIComponent(dec.split('|')[2]) }; 16 | } 17 | catch (error) { 18 | if (("" + error).startsWith('URIError')) 19 | error = new Error('Unable to decode URI component. The URL may be invalid'); 20 | return { url: args.originalURL, error: error }; 21 | } 22 | } 23 | }; 24 | exports.handlers['urldefense.proofpoint.com'] = { 25 | exec: function (_str, args) { 26 | try { 27 | var arg = args.urlParams.get('u'); 28 | if (arg === null) 29 | throw new Error('Target parameter (u) was null'); 30 | var url = decodeURIComponent(arg.replace(/-/g, '%')).replace(/_/g, '/').replace(/%2F/g, '/'); 31 | return { url: url }; 32 | } 33 | catch (error) { 34 | return { url: args.originalURL, error: error }; 35 | } 36 | } 37 | }; 38 | exports.handlers['stardockentertainment.info'] = { 39 | exec: function (str, args) { 40 | try { 41 | var target = str.split('/').pop(); 42 | var url = ''; 43 | if (typeof target == 'undefined') 44 | throw new Error('Undefined target'); 45 | url = utils_1.decodeBase64(target); 46 | if (url.includes('watch>v=')) 47 | url = url.replace('watch>v=', 'watch?v='); 48 | return { url: url }; 49 | } 50 | catch (error) { 51 | return { url: args.originalURL, error: error }; 52 | } 53 | } 54 | }; 55 | exports.handlers['steam.gs'] = { 56 | exec: function (str, args) { 57 | try { 58 | var target = str.split('%3Eutm_').shift(); 59 | var url = ''; 60 | if (target) 61 | url = target; 62 | return { url: url }; 63 | } 64 | catch (error) { 65 | return { url: args.originalURL, error: error }; 66 | } 67 | } 68 | }; 69 | exports.handlers['0yxjo.mjt.lu'] = { 70 | exec: function (str, args) { 71 | try { 72 | var target = str.split('/').pop(); 73 | var url = ''; 74 | if (typeof target == 'undefined') 75 | throw new Error('Undefined target'); 76 | url = utils_1.decodeBase64(target); 77 | return { url: url }; 78 | } 79 | catch (error) { 80 | return { url: args.originalURL, error: error }; 81 | } 82 | } 83 | }; 84 | exports.handlers['click.redditmail.com'] = { 85 | exec: function (str, args) { 86 | try { 87 | var reg = /https:\/\/click\.redditmail\.com\/CL0\/(.*?)\//gi; 88 | var matches = utils_1.regexExtract(reg, str); 89 | if (typeof matches[1] === 'undefined') 90 | throw new Error('regexExtract failed to find a URL'); 91 | var url = decodeURIComponent(matches[1]); 92 | return { url: url }; 93 | } 94 | catch (error) { 95 | return { url: args.originalURL, error: error }; 96 | } 97 | } 98 | }; 99 | exports.handlers['deals.dominos.co.nz'] = { 100 | exec: function (str, args) { 101 | try { 102 | var target = str.split('/').pop(); 103 | var url = ''; 104 | if (!target) 105 | throw new Error('Missing target'); 106 | url = utils_1.decodeBase64(target); 107 | return { url: url }; 108 | } 109 | catch (error) { 110 | return { url: args.originalURL, error: error }; 111 | } 112 | } 113 | }; 114 | exports.handlers['redirectingat.com'] = { 115 | exec: function (str, args) { 116 | var _a; 117 | try { 118 | var url = ''; 119 | var _b = str.split('?id'), host = _b[0], target = _b[1], _other = _b.slice(2); 120 | // Make sure the redirect rule hasn't already processed this 121 | if (host === 'https://go.redirectingat.com/') { 122 | var decoded = decodeURIComponent(target); 123 | var corrected = new URL(host + "?id=" + decoded); 124 | var param = corrected.searchParams.get('url'); 125 | // Make sure the decoded parameters are a valid URL 126 | if (param && utils_1.validateURL(param) === true) { 127 | url = param; 128 | } 129 | else { 130 | throw Error((_a = 'Handler failed, result: ' + param) !== null && _a !== void 0 ? _a : 'No param'); 131 | } 132 | } 133 | else { 134 | // If the host is different nothing needs to be modified 135 | url = args.originalURL; 136 | } 137 | return { url: url }; 138 | } 139 | catch (error) { 140 | return { url: args.originalURL, error: error }; 141 | } 142 | } 143 | }; 144 | exports.handlers['twitch.tv-email'] = { 145 | note: 'This is used for email tracking', 146 | exec: function (str, args) { 147 | try { 148 | // This is the regex used to extract the decodable string 149 | var reg = /www\.twitch\.tv\/r\/e\/(.*?)\//; 150 | var url = ''; 151 | // Extract the decodable string from the URL 152 | var data = utils_1.regexExtract(reg, str); 153 | // The second result is what we want 154 | var decode = utils_1.decodeBase64(data[1]); 155 | // Parse the string, this should be JSON 156 | var parse = JSON.parse(decode); 157 | /** 158 | * This one is a bit tricky. I don't use Twitch often so I've limited it to "twitch_favorite_up", 159 | * In my case this was when a streamer I follow came online. 160 | */ 161 | if (parse['name'] === 'twitch_favorite_up') { 162 | url = 'https://www.twitch.tv/' + parse.channel; 163 | } 164 | return { url: url }; 165 | } 166 | catch (error) { 167 | return { url: args.originalURL, error: error }; 168 | } 169 | } 170 | }; 171 | -------------------------------------------------------------------------------- /lib/index.d.ts: -------------------------------------------------------------------------------- 1 | import { IRule, IData } from './interface'; 2 | import { TidyConfig } from './config'; 3 | export declare class TidyCleaner { 4 | rules: IRule[]; 5 | /** 6 | * Stores config options for this cleaner. If you would like to 7 | * use multiple configs simply create a new instance 8 | */ 9 | config: TidyConfig; 10 | /** 11 | * Contains all logged information from the last clean, even if `config.silent` was `true`. 12 | * This will be reset when a new URL is cleaned. This is for debugging and not to be relied upon 13 | */ 14 | loglines: { 15 | type: string; 16 | message: string; 17 | }[]; 18 | /** 19 | * The full list of all rules with default value 20 | * that are not used in the main rules file to save space. 21 | */ 22 | get expandedRules(): IRule[]; 23 | constructor(); 24 | /** 25 | * Only log to the console if debug is enabled 26 | * @param str Message 27 | */ 28 | private log; 29 | /** 30 | * Rebuild to ensure trailing slashes or encoded characters match. 31 | * @param url Any URL 32 | */ 33 | rebuild(url: string): string; 34 | /** 35 | * This lets users know when they are using the deprecated variables that will 36 | * be removed in a few updates. 37 | */ 38 | private syncDeprecatedToConfig; 39 | /** @deprecated Import `validateURL` instead */ 40 | validate(url: string): boolean; 41 | /** 42 | * Clean a URL 43 | * @param _url Any URL 44 | * @returns IData 45 | */ 46 | clean(_url: string, allowReclean?: boolean): IData; 47 | } 48 | export declare const TidyURL: TidyCleaner; 49 | export declare const clean: (url: string) => IData; 50 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __spreadArray = (this && this.__spreadArray) || function (to, from) { 3 | for (var i = 0, il = from.length, j = to.length; i < il; i++, j++) 4 | to[j] = from[i]; 5 | return to; 6 | }; 7 | Object.defineProperty(exports, "__esModule", { value: true }); 8 | exports.clean = exports.TidyURL = exports.TidyCleaner = void 0; 9 | var utils_1 = require("./utils"); 10 | var interface_1 = require("./interface"); 11 | var handlers_1 = require("./handlers"); 12 | var config_1 = require("./config"); 13 | var $github = 'https://github.com/DrKain/tidy-url'; 14 | var TidyCleaner = /** @class */ (function () { 15 | function TidyCleaner() { 16 | this.rules = []; 17 | /** 18 | * Stores config options for this cleaner. If you would like to 19 | * use multiple configs simply create a new instance 20 | */ 21 | this.config = new config_1.TidyConfig(); 22 | /** 23 | * Contains all logged information from the last clean, even if `config.silent` was `true`. 24 | * This will be reset when a new URL is cleaned. This is for debugging and not to be relied upon 25 | */ 26 | this.loglines = []; 27 | try { 28 | this.syncDeprecatedToConfig(); 29 | // Load the rules 30 | this.rules = require('../data/rules.js'); 31 | } 32 | catch (error) { 33 | // If this fails nothing can be cleaned 34 | this.log("" + error, 'error'); 35 | this.rules = []; 36 | } 37 | } 38 | Object.defineProperty(TidyCleaner.prototype, "expandedRules", { 39 | /** 40 | * The full list of all rules with default value 41 | * that are not used in the main rules file to save space. 42 | */ 43 | get: function () { 44 | return this.rules.map(function (rule) { 45 | return Object.assign({ 46 | rules: [], 47 | replace: [], 48 | exclude: [], 49 | redirect: '', 50 | amp: null, 51 | decode: null 52 | }, rule); 53 | }); 54 | }, 55 | enumerable: false, 56 | configurable: true 57 | }); 58 | /** 59 | * Only log to the console if debug is enabled 60 | * @param str Message 61 | */ 62 | TidyCleaner.prototype.log = function (str, type) { 63 | this.loglines.push({ type: type, message: str }); 64 | if (this.config.silent !== false) 65 | console.log("[" + type + "] " + str); 66 | }; 67 | /** 68 | * Rebuild to ensure trailing slashes or encoded characters match. 69 | * @param url Any URL 70 | */ 71 | TidyCleaner.prototype.rebuild = function (url) { 72 | var original = new URL(url); 73 | return original.protocol + '//' + original.host + original.pathname + original.search + original.hash; 74 | }; 75 | /** 76 | * This lets users know when they are using the deprecated variables that will 77 | * be removed in a few updates. 78 | */ 79 | TidyCleaner.prototype.syncDeprecatedToConfig = function () { }; 80 | /** @deprecated Import `validateURL` instead */ 81 | TidyCleaner.prototype.validate = function (url) { 82 | return utils_1.validateURL(url); 83 | }; 84 | /** 85 | * Clean a URL 86 | * @param _url Any URL 87 | * @returns IData 88 | */ 89 | TidyCleaner.prototype.clean = function (_url, allowReclean) { 90 | var _a; 91 | if (allowReclean === void 0) { allowReclean = true; } 92 | if (!allowReclean) 93 | this.loglines = []; 94 | this.syncDeprecatedToConfig(); 95 | // Default values 96 | var data = { 97 | url: _url, 98 | info: { 99 | original: _url, 100 | reduction: 0, 101 | difference: 0, 102 | replace: [], 103 | removed: [], 104 | handler: null, 105 | match: [], 106 | decoded: null, 107 | is_new_host: false, 108 | isNewHost: false, 109 | full_clean: false, 110 | fullClean: false 111 | } 112 | }; 113 | // Make sure the URL is valid before we try to clean it 114 | if (!utils_1.validateURL(_url)) { 115 | if (_url !== 'undefined' && _url.length > 0) { 116 | this.log('Invalid URL: ' + _url, 'error'); 117 | } 118 | return data; 119 | } 120 | // If there's no params, we can skip the rest of the process 121 | if (this.config.allowAMP && utils_1.urlHasParams(_url) === false) { 122 | data.url = data.info.original; 123 | return data; 124 | } 125 | // Rebuild to ensure trailing slashes or encoded characters match 126 | var url = this.rebuild(_url); 127 | data.url = url; 128 | // List of parmeters that will be deleted if found 129 | var to_remove = []; 130 | var original = new URL(url); 131 | var cleaner = original.searchParams; 132 | var cleaner_ci = new URLSearchParams(); 133 | var pathname = original.pathname; 134 | // Case insensitive cleaner for the redirect rule 135 | cleaner.forEach(function (v, k) { return cleaner_ci.append(k.toLowerCase(), v); }); 136 | // Loop through the rules and match them to the host name 137 | for (var _i = 0, _b = this.expandedRules; _i < _b.length; _i++) { 138 | var rule = _b[_i]; 139 | // Match the host or the full URL 140 | var match_s = original.host; 141 | if (rule.match_href === true) 142 | match_s = original.href; 143 | // Reset lastIndex 144 | rule.match.lastIndex = 0; 145 | if (rule.match.exec(match_s) !== null) { 146 | // Loop through the rules and add to to_remove 147 | to_remove = __spreadArray(__spreadArray([], to_remove), (rule.rules || [])); 148 | data.info.replace = __spreadArray(__spreadArray([], data.info.replace), (rule.replace || [])); 149 | data.info.match.push(rule); 150 | } 151 | } 152 | // Stop cleaning if any exclude rule matches 153 | var ex_pass = true; 154 | for (var _c = 0, _d = data.info.match; _c < _d.length; _c++) { 155 | var rule = _d[_c]; 156 | for (var _e = 0, _f = rule.exclude; _e < _f.length; _e++) { 157 | var reg = _f[_e]; 158 | reg.lastIndex = 0; 159 | if (reg.exec(url) !== null) 160 | ex_pass = false; 161 | } 162 | } 163 | if (!ex_pass) { 164 | data.url = data.info.original; 165 | return data; 166 | } 167 | // Check if the match has any amp rules, if not we can redirect 168 | var hasAmpRule = data.info.match.find(function (item) { return item.amp; }); 169 | if (this.config.allowAMP === true && hasAmpRule === undefined) { 170 | // Make sure there are no parameters before resetting 171 | if (!utils_1.urlHasParams(url)) { 172 | data.url = data.info.original; 173 | return data; 174 | } 175 | } 176 | // Delete any matching parameters 177 | for (var _g = 0, to_remove_1 = to_remove; _g < to_remove_1.length; _g++) { 178 | var key = to_remove_1[_g]; 179 | if (cleaner.has(key)) { 180 | data.info.removed.push({ key: key, value: cleaner.get(key) }); 181 | cleaner.delete(key); 182 | } 183 | } 184 | // Update the pathname if needed 185 | for (var _h = 0, _j = data.info.replace; _h < _j.length; _h++) { 186 | var key = _j[_h]; 187 | var changed = pathname.replace(key, ''); 188 | if (changed !== pathname) 189 | pathname = changed; 190 | } 191 | // Rebuild URL 192 | data.url = original.protocol + '//' + original.host + pathname + original.search + original.hash; 193 | // Redirect if the redirect parameter exists 194 | if (this.config.allowRedirects) { 195 | for (var _k = 0, _l = data.info.match; _k < _l.length; _k++) { 196 | var rule = _l[_k]; 197 | if (!rule.redirect) 198 | continue; 199 | var target = rule.redirect; 200 | var value = cleaner_ci.get(target); 201 | // Sometimes the parameter is encoded 202 | var isEncoded = utils_1.decodeURL(value, interface_1.EEncoding.urlc); 203 | if (isEncoded !== value && utils_1.validateURL(isEncoded)) 204 | value = isEncoded; 205 | if (target.length && cleaner_ci.has(target)) { 206 | if (utils_1.validateURL(value)) { 207 | data.url = "" + value + original.hash; 208 | if (allowReclean) 209 | data.url = this.clean(data.url, false).url; 210 | } 211 | else { 212 | this.log('Failed to redirect: ' + value, 'error'); 213 | } 214 | } 215 | } 216 | } 217 | // De-amp the URL 218 | if (this.config.allowAMP === false) { 219 | for (var _m = 0, _o = data.info.match; _m < _o.length; _m++) { 220 | var rule = _o[_m]; 221 | try { 222 | // Ensure at least one rule exists 223 | if (rule.amp && (rule.amp.regex || rule.amp.replace || rule.amp.sliceTrailing)) { 224 | // Handle replacing text in the URL 225 | if (rule.amp.replace) { 226 | data.info.handler = rule.name; 227 | this.log('AMP Replace: ' + rule.amp.replace.text, 'info'); 228 | var toReplace = rule.amp.replace.text; 229 | var toReplaceWith = (_a = rule.amp.replace.with) !== null && _a !== void 0 ? _a : ''; 230 | data.url = data.url.replace(toReplace, toReplaceWith); 231 | } 232 | // Use RegEx capture groups 233 | if (rule.amp.regex && data.url.match(rule.amp.regex)) { 234 | data.info.handler = rule.name; 235 | this.log('AMP RegEx: ' + rule.amp.regex, 'info'); 236 | rule.amp.regex.lastIndex = 0; 237 | var result = rule.amp.regex.exec(data.url); 238 | // If there is a result, replace the URL 239 | if (result && result[1]) { 240 | var target = decodeURIComponent(result[1]); 241 | // Add the protocol when it's missing 242 | if (!target.startsWith('https')) 243 | target = 'https://' + target; 244 | // Valiate the URL to make sure it's still good 245 | if (utils_1.validateURL(target)) { 246 | // Sometimes the result is another domain that has its own tracking parameters 247 | // So a re-clean can be useful. 248 | data.url = allowReclean ? this.clean(target, false).url : target; 249 | } 250 | } 251 | else { 252 | this.log('AMP RegEx failed to get a result for ' + rule.name, 'error'); 253 | } 254 | } 255 | // TODO: Apply to existing rules 256 | if (rule.amp.sliceTrailing) { 257 | if (data.url.endsWith(rule.amp.sliceTrailing)) { 258 | data.url = data.url.slice(0, -rule.amp.sliceTrailing.length); 259 | } 260 | } 261 | // Remove trailing amp/ or /amp 262 | if (data.url.endsWith('%3Famp')) 263 | data.url = data.url.slice(0, -6); 264 | if (data.url.endsWith('amp/')) 265 | data.url = data.url.slice(0, -4); 266 | } 267 | } 268 | catch (error) { 269 | this.log("" + error, 'error'); 270 | } 271 | } 272 | } 273 | // Decode handler 274 | for (var _p = 0, _q = data.info.match; _p < _q.length; _p++) { 275 | var rule = _q[_p]; 276 | try { 277 | this.log("Processing decode rule (" + rule.name + ")", 'debug'); 278 | if (!rule.decode) 279 | continue; 280 | // Make sure the target parameter exists 281 | if (!cleaner.has(rule.decode.param) && rule.decode.targetPath !== true) 282 | continue; 283 | // These will almost always be clickjacking links, so use the allowRedirects rule if enabled 284 | if (!this.config.allowRedirects) 285 | continue; 286 | // Don't process the decode handler if it's disabled 287 | if (this.config.allowCustomHandlers === false && rule.decode.handler) 288 | continue; 289 | // Decode the string using selected encoding 290 | var encoding = rule.decode.encoding || 'base64'; 291 | // Sometimes the website path is what we need to decode 292 | var lastPath = pathname.split('/').pop(); 293 | // This will be null if the param doesn't exist 294 | var param = cleaner.get(rule.decode.param); 295 | // Use a default string 296 | var encodedString = ''; 297 | if (lastPath === undefined) 298 | lastPath = ''; 299 | // Decide what we are decoding 300 | if (param === null) 301 | encodedString = lastPath; 302 | else if (param) 303 | encodedString = param; 304 | else 305 | continue; 306 | if (typeof encodedString !== 'string') { 307 | this.log("Expected " + encodedString + " to be a string", 'error'); 308 | continue; 309 | } 310 | var decoded = utils_1.decodeURL(encodedString, encoding); 311 | var target = ''; 312 | var recleanData = null; 313 | // If the response is JSON, decode and look for a key 314 | if (utils_1.isJSON(decoded)) { 315 | var json = JSON.parse(decoded); 316 | target = json[rule.decode.lookFor]; 317 | // Add to the info response 318 | data.info.decoded = json; 319 | } 320 | else if (this.config.allowCustomHandlers === true && rule.decode.handler) { 321 | // Run custom URL handlers for websites 322 | var handler = handlers_1.handlers[rule.decode.handler]; 323 | if (typeof handler === 'undefined') { 324 | this.log('Handler was not found for ' + rule.decode.handler, 'error'); 325 | } 326 | if (rule.decode.handler && handler) { 327 | data.info.handler = rule.decode.handler; 328 | // Pass the handler a bunch of information it can use 329 | var result = handler.exec(data.url, { 330 | decoded: decoded, 331 | lastPath: lastPath, 332 | urlParams: new URL(data.url).searchParams, 333 | fullPath: pathname, 334 | originalURL: data.url 335 | }); 336 | // If the handler threw an error or the URL is invalid 337 | if (result.error || utils_1.validateURL(result.url) === false || result.url.trim() === '') { 338 | if (result.error) 339 | this.log(result.error, 'error'); 340 | else 341 | this.log('Unknown error with decode handler, empty response returned', 'error'); 342 | } 343 | // result.url will always by the original URL when an error is thrown 344 | recleanData = result.url; 345 | } 346 | } 347 | else { 348 | // If the response is a string we can continue 349 | target = decoded; 350 | } 351 | // Re-clean the URL after handler result 352 | target = allowReclean ? this.clean(recleanData !== null && recleanData !== void 0 ? recleanData : target, false).url : recleanData !== null && recleanData !== void 0 ? recleanData : target; 353 | // If the key we want exists and is a valid url then update the data url 354 | if (target && target !== '' && utils_1.validateURL(target)) { 355 | data.url = "" + target + original.hash; 356 | } 357 | } 358 | catch (error) { 359 | this.log("" + error, 'error'); 360 | } 361 | } 362 | // Handle empty hash / anchors 363 | if (_url.endsWith('#')) { 364 | data.url += '#'; 365 | url += '#'; 366 | } 367 | // Remove empty values when requested 368 | for (var _r = 0, _s = data.info.match; _r < _s.length; _r++) { 369 | var rule = _s[_r]; 370 | if (rule.rev) 371 | data.url = data.url.replace(/=(?=&|$)/gm, ''); 372 | } 373 | var diff = utils_1.getLinkDiff(data.url, url); 374 | data.info = Object.assign(data.info, diff); 375 | // If the link is longer then we have an issue 376 | if (data.info.reduction < 0) { 377 | this.log("Reduction is " + data.info.reduction + ". Please report this link on GitHub: " + $github + "/issues\n" + data.info.original, 'error'); 378 | data.url = data.info.original; 379 | } 380 | data.info.fullClean = true; 381 | data.info.full_clean = true; 382 | // Reset the original URL if there is no change, just to be safe 383 | if (data.info.difference === 0 && data.info.reduction === 0) { 384 | data.url = data.info.original; 385 | } 386 | return data; 387 | }; 388 | return TidyCleaner; 389 | }()); 390 | exports.TidyCleaner = TidyCleaner; 391 | exports.TidyURL = new TidyCleaner(); 392 | var clean = function (url) { return exports.TidyURL.clean(url); }; 393 | exports.clean = clean; 394 | -------------------------------------------------------------------------------- /lib/interface.d.ts: -------------------------------------------------------------------------------- 1 | export interface IRule { 2 | /** Name of the website */ 3 | name: string; 4 | /** Regex to test against the host */ 5 | match: RegExp; 6 | /** Regex to test against the full URL */ 7 | match_href: boolean; 8 | /** All parameters that match these rules will be removed */ 9 | rules: string[]; 10 | /** 11 | * Used in special cases where parts of the URL needs to be modified. 12 | * See the amazon.com rule for an example. 13 | */ 14 | replace: any[]; 15 | /** 16 | * Used to auto-redirect to a different URL based on the parameter. 17 | * This is used to skip websites that track external links. 18 | */ 19 | redirect: string; 20 | /** 21 | * There's a whole number of reasons why you don't want AMP links, 22 | * too many to fit in this description. 23 | * See this link for more info: https://redd.it/ehrq3z 24 | */ 25 | amp: { 26 | /** 27 | * Standard AMP handling using RegExp capture groups. 28 | */ 29 | regex?: RegExp; 30 | /** 31 | * Replace text in the URL. If `with` is used the text will be 32 | * replaced with what you set instead of removing it. 33 | */ 34 | replace?: { 35 | /** The text or RegEx you want to replace */ 36 | text: string | RegExp; 37 | /** The text you want to replace it with. Optional */ 38 | with?: string; 39 | /** Currently has no effect, this will change in another update */ 40 | target?: 'host' | 'full'; 41 | }; 42 | /** 43 | * Slice off a trailing string, these are usually "/amp" or "amp/" 44 | * This setting should help prevent breaking any pages. 45 | */ 46 | sliceTrailing?: string; 47 | }; 48 | /** 49 | * Used to decode a parameter or path, then redirect based on the returned object 50 | */ 51 | decode: { 52 | /** Target parameter */ 53 | param?: string; 54 | /** If the decoded response is JSON, this will look for a certain key */ 55 | lookFor?: string; 56 | /** Decide what encoding to use */ 57 | encoding?: EEncoding; 58 | /** Target the full path instead of a parameter */ 59 | targetPath?: boolean; 60 | /** Use a custom handler found in handlers.ts */ 61 | handler?: string; 62 | }; 63 | /** Remove empty values */ 64 | rev: boolean; 65 | } 66 | export interface IData { 67 | /** Cleaned URL */ 68 | url: string; 69 | /** Some debugging information about what was changed */ 70 | info: { 71 | /** Original URL before cleaning */ 72 | original: string; 73 | /** URL reduction as a percentage */ 74 | reduction: number; 75 | /** Number of characters removed */ 76 | difference: number; 77 | /** RegEx Replacements */ 78 | replace: any[]; 79 | /** Parameters that were removed */ 80 | removed: { 81 | key: string; 82 | value: string; 83 | }[]; 84 | /** Handler used */ 85 | handler: string | null; 86 | /** Rules matched */ 87 | match: any[]; 88 | /** The decoded object from the decode parameter (if it exists) */ 89 | decoded: { 90 | [key: string]: any; 91 | } | null; 92 | /** @deprecated Please use `isNewHost`. This will be removed in the next major update. */ 93 | is_new_host: boolean; 94 | /** If the compared links have different hosts */ 95 | isNewHost: boolean; 96 | /** @deprecated Please use `fullClean`. This will be removed in the next major update. */ 97 | full_clean: boolean; 98 | /** If the code reached the end of the clean without error */ 99 | fullClean: boolean; 100 | }; 101 | } 102 | export declare enum EEncoding { 103 | base64 = "base64", 104 | base32 = "base32", 105 | base45 = "base45", 106 | url = "url", 107 | urlc = "urlc", 108 | binary = "binary", 109 | hex = "hex" 110 | } 111 | export interface IConfig { 112 | /** 113 | * There's a whole number of reasons why you don't want AMP links, 114 | * too many to fit in this description. 115 | * See this link for more info: https://redd.it/ehrq3z 116 | */ 117 | allowAMP: boolean; 118 | /** 119 | * Custom handlers for specific websites that use tricky URLs 120 | * that make it harder to "clean" 121 | */ 122 | allowCustomHandlers: boolean; 123 | /** 124 | * Used to auto-redirect to a different URL based on the parameter. 125 | * This is used to skip websites that track external links. 126 | */ 127 | allowRedirects: boolean; 128 | /** Nothing logged to console */ 129 | silent: boolean; 130 | } 131 | export interface IHandlerArgs { 132 | /** The attemp made at decoding the string, may be invalid */ 133 | decoded: string; 134 | /** The last part of the URL path, split by a forward slash */ 135 | lastPath: string; 136 | /** The full URL path excluding the host */ 137 | fullPath: string; 138 | /** A fresh copy of URLSearchParams */ 139 | urlParams: URLSearchParams; 140 | /** The original URL */ 141 | readonly originalURL: string; 142 | } 143 | export interface IHandler { 144 | readonly note?: string; 145 | exec: ( 146 | /** The original URL */ 147 | str: string, 148 | /** Various args that can be used when writing a handler */ 149 | args: IHandlerArgs) => { 150 | /** The original URL */ 151 | url: string; 152 | error?: any; 153 | }; 154 | } 155 | export interface ILinkDiff { 156 | /** @deprecated Please use isNewHost */ 157 | is_new_host: boolean; 158 | /** If the compared links have different hosts */ 159 | isNewHost: boolean; 160 | difference: number; 161 | reduction: number; 162 | } 163 | export interface IGuessEncoding { 164 | base64: boolean; 165 | isJSON: boolean; 166 | } 167 | -------------------------------------------------------------------------------- /lib/interface.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.EEncoding = void 0; 4 | var EEncoding; 5 | (function (EEncoding) { 6 | EEncoding["base64"] = "base64"; 7 | EEncoding["base32"] = "base32"; 8 | EEncoding["base45"] = "base45"; 9 | EEncoding["url"] = "url"; 10 | EEncoding["urlc"] = "urlc"; 11 | EEncoding["binary"] = "binary"; 12 | EEncoding["hex"] = "hex"; 13 | })(EEncoding = exports.EEncoding || (exports.EEncoding = {})); 14 | -------------------------------------------------------------------------------- /lib/tidyurl.min.js: -------------------------------------------------------------------------------- 1 | var tidyurl;(()=>{var e={525:e=>{e.exports=[{name:"Global",match:/.*/,rules:["utm_source","utm_medium","utm_term","utm_campaign","utm_content","utm_name","utm_cid","utm_reader","utm_viz_id","utm_pubreferrer","utm_swu","utm_social-type","utm_brand","utm_team","utm_feeditemid","utm_id","utm_marketing_tactic","utm_creative_format","utm_campaign_id","utm_source_platform","utm_timestamp","utm_souce","utm_couponvalue","itm_source","itm_medium","itm_term","itm_campaign","itm_content","itm_channel","itm_source_s","itm_medium_s","itm_campaign_s","itm_audience","int_source","int_cmp_name","int_cmp_id","int_cmp_creative","int_medium","int_campaign","int_content","pk_campaign","pk_cpn","pk_source","pk_medium","pk_keyword","pk_kwd","pk_content","pk_cid","piwik_campaign","piwik_cpn","piwik_source","piwik_medium","piwik_keyword","piwik_kwd","piwik_content","piwik_cid","gclid","ga_source","ga_medium","ga_term","ga_content","ga_campaign","ga_place","gclid","gclsrc","hsa_cam","hsa_grp","hsa_mt","hsa_src","hsa_ad","hsa_acc","hsa_net","hsa_kw","hsa_tgt","hsa_ver","hsa_la","hsa_ol","fbclid","oly_enc_id","oly_anon_id","vero_id","vero_conv","__s","_hsenc","_hsmi","__hssc","__hstc","__hsfp","hsCtaTracking","mkt_tok","mtm_campaign","mtm_keyword","mtm_kwd","mtm_source","mtm_medium","mtm_content","mtm_cid","mtm_group","mtm_placement","elqTrackId","elq","elqaid","elqat","elqCampaignId","elqTrack","mc_cid","mc_eid","ncid","cmpid","mbid","rdt_cid"]},{name:"audible.com",match:/www.audible.com/i,rules:["qid","sr","pf_rd_p","pf_rd_r","plink","ref"]},{name:"bandcamp.com",match:/.*.bandcamp.com/gi,rules:["from","search_item_id","search_item_type","search_match_part","search_page_id","search_page_no","search_rank","search_sig"]},{name:"amazon.com",match:/amazon\.[a-z0-9]{0,3}/i,rules:["psc","colid","coliid","linkId","tag","linkCode","ms3_c","pf_rd_s","pf_rd_t"," pf_rd_i","pf_rd_m","pd_rd_w","qid","sr","keywords","dchild","ref","ref_","rnid","pf_rd_r","pf_rd_p","pd_rd_r","smid","pd_rd_wg","content-id","spLa","crid","sprefix","hvlocint","hvdvcmdl","hvptwo","hvpone","hvpos","qu","pd_rd_i","nc2","nc1","trk","sc_icampaign","trkCampaign","ufe","sc_icontent","sc_ichannel","sc_iplace","sc_country","sc_outcome","sc_geo","sc_campaign","sc_channel"],replace:[/(\/ref|&ref_)=[^\/?]*/i]},{name:"reddit.com",match:/.*.reddit.com/i,rules:["ref_campaign","ref_source","tags","keyword","channel","campaign","user_agent","domain","base_url","$android_deeplink_path","$deeplink_path","$og_redirect","share_id","correlation_id","ref","rdt"]},{name:"app.link",match:/.*\.app\.link/i,rules:["tags","keyword","channel","campaign","user_agent","domain","base_url","$android_deeplink_path","$deeplink_path","$og_redirect","compact_view","dnt","adblock","geoip_country","referrer_domain","referrer_url"]},{name:"twitch.tv",match:/www.twitch.tv/i,rules:["tt_medium","tt_content","tt_email_id"]},{name:"twitch.tv-email",match_href:!0,match:/www.twitch.tv\/r\/e/i,decode:{handler:"twitch.tv-email",targetPath:!0}},{name:"blog.twitch.tv",match:/blog.twitch.tv/i,rules:["utm_referrer"]},{name:"pixiv.net",match:/www.pixiv.net/i,rules:["p","i","g"]},{name:"spotify.com",match:/open.spotify.com/i,rules:["si","utm_source","context","sp_cid","_branch_match_id","_branch_referrer"],allow:["go","nd"]},{name:"aliexpress.com",match:/^(?:https?:\/\/)?(?:[^.]+\.)?aliexpress\.(?:[a-z]{2,}){1,}/i,rules:["_t","spm","algo_pvid","algo_expid","btsid","ws_ab_test","initiative_id","origin","widgetId","tabType","productId","productIds","gps-id","scm","scm_id","scm-url","pvid","algo_exp_id","pdp_pi","fromRankId","sourceType","utparam","gatewayAdapt","_evo_buckets","tpp_rcmd_bucket_id","scenario","pdp_npi","tt","spreadType","srcSns","bizType","social_params","aff_fcid","aff_fsk","aff_platform","aff_trace_key","shareId","platform","businessType","terminal_id","afSmartRedirect","sk","gbraid"],allow:["sku_id","pdp_ext_f"]},{name:"google.com",match:/www.google\..*/i,rules:["sourceid","client","aqs","sxsrf","uact","ved","iflsig","source","ei","oq","gs_lcp","sclient","bih","biw","sa","dpr","rlz","gs_lp","sca_esv","si","gs_l","gs_lcrp"],amp:{regex:/www\.google\.(?:.*)\/amp\/s\/(.*)/gim},redirect:"url"},{name:"youtube.com",match:/.*.youtube.com/i,rules:["gclid","feature","app","src","lId","cId","embeds_referring_euri"],redirect:"q"},{name:"humblebundle.com",match:/www.humblebundle.com/i,rules:["hmb_source","hmb_medium","hmb_campaign","mcID","linkID"],allow:["partner"]},{name:"greenmangaming.com",match:/www.greenmangaming.com/i,rules:["CJEVENT","cjevent","irclickid","irgwc","pdpgatetoken"]},{name:"fanatical.com",match:/www.fanatical.com/i,rules:["cj_pid","cj_aid","aff_track","CJEVENT","cjevent"]},{name:"newsweek.com",match:/www.newsweek.com/i,rules:["subref","amp"]},{name:"imgur.com",match:/imgur.com/i,rules:["source"]},{name:"plex.tv",match:/.*.plex.tv/i,rules:["origin","plex_utm","sl","ckhid"]},{name:"imdb.com",match:/^.*\.imdb\.com/i,rules:["ref_","ref\\_","pf_rd_m","pf_rd_r","pf_rd_p","pf_rd_s","pf_rd_t","pf_rd_i","ref_hp_hp_e_2","rf","ref"]},{name:"gog.com",match:/www.gog.com/i,rules:["at_gd","rec_scenario_id","rec_sub_source_id","rec_item_id","vds_id","prod_id","rec_source"]},{name:"tiktok.com",match:/www.tiktok.com/i,rules:["is_copy_url","is_from_webapp","sender_device","sender_web_id","sec_user_id","share_app_id","share_item_id","share_link_id","social_sharing","_r","source","user_id","u_code","tt_from","share_author_id","sec_uid","checksum","_d","refer","enter_from","enter_method","attr_medium","attr_source"],allow:["lang"]},{name:"tiktok.com/link",match:/tiktok\.com\/link\/v2/i,match_href:!0,redirect:"target"},{name:"facebook.com",match:/.*.facebook.com/i,rules:["fbclid","fb_ref","fb_source","referral_code","referral_story_type","tracking","ref"],redirect:"u",exclude:[/www\.facebook\.com\/sharer/gi]},{name:"yandex.com",match:/yandex.com/i,rules:["lr","from","grhow","origin","_openstat"]},{name:"store.steampowered.com",match:/store.steampowered.com/i,rules:["snr"]},{name:"findojobs.co.nz",match:/www.findojobs.co.nz/i,rules:["source"]},{name:"linkedin.com",match:/.*.linkedin.com/i,rules:["contextUrn","destRedirectURL","lipi","licu","trk","trkInfo","originalReferer","upsellOrderOrigin","upsellTrk","upsellTrackingId","src","trackingId","midToken","midSig","trkEmail","eid"],allow:["otpToken"]},{name:"indeed.com",match:/.*.indeed.com/i,rules:["from","attributionid"]},{name:"discord.com",match:/(?:.*\.)?discord\.com/i,rules:["source","ref"]},{name:"medium.com",match:/medium.com/i,rules:["source"]},{name:"twitter.com",match:/twitter.com/i,rules:["s","src","ref_url","ref_src"]},{name:"voidu.com",match:/voidu.com/i,rules:["affiliate"]},{name:"wingamestore.com",match:/wingamestore.com/i,rules:["ars"]},{name:"gamebillet.com",match:/gamebillet.com/i,rules:["affiliate"]},{name:"gamesload.com",match:/^www.gamesload.com/i,rules:["affil"],allow:["REF"]},{name:"mightyape",match:/mightyape.(co.nz|com.au)/i,rules:["m"]},{name:"apple.com",match:/.*.apple.com/i,rules:["uo","app","at","ct","ls","pt","mt","itsct","itscg","referrer","src","cid"]},{name:"music.apple.com",match:/music.apple.com/i,rules:["i","lId","cId","sr","src"]},{name:"play.google.com",match:/play.google.com/i,rules:["referrer","pcampaignid"]},{name:"adtraction.com",match:/adtraction.com/i,redirect:"url"},{name:"dpbolvw.net",match:/dpbolvw.net/i,redirect:"url"},{name:"lenovo.com",match:/.*.lenovo.com/i,rules:["PID","clickid","irgwc","cid","acid","linkTrack"]},{name:"itch.io",match:/itch.io/i,rules:["fbclid"]},{name:"steamcommunity.com",match:/steamcommunity.com/i,redirect:"url"},{name:"steamcommunity.com/linkfilter",match:/steamcommunity.com\/linkfilter/i,redirect:"u",match_href:!0},{name:"microsoft.com",match:/microsoft.com/i,rules:["refd","icid"]},{name:"berrybase.de",match:/berrybase.de/i,rules:["sPartner"]},{name:"instagram.com",match:/instagram.com/i,rules:["igshid","igsh","source"],redirect:"u"},{name:"hubspot.com",match:/hubspot.com/i,rules:["hubs_content-cta","hubs_content"]},{name:"ebay.com",match:/^(?:https?:\/\/)?(?:[^.]+\.)?ebay\.[a-z0-9]{0,3}/i,rules:["amdata","var","hash","_trkparms","_trksid","_from","mkcid","mkrid","campid","toolid","mkevt","customid","siteid","ufes_redirect","ff3","pub","media","widget_ver","ssspo","sssrc","ssuid"],allow:["epid","_nkw"]},{name:"shopee.com",match:/^(?:https?:\/\/)?(?:[^.]+\.)?shopee\.[a-z0-9]{0,3}/i,rules:["af_siteid","pid","af_click_lookback","af_viewthrough_lookback","is_retargeting","af_reengagement_window","af_sub_siteid","c"]},{name:"msn.com",match:/www.msn.com/i,rules:["ocid","cvid","pc"]},{name:"nuuvem.com",match:/www.nuuvem.com/i,rules:["ranMID","ranEAID","ranSiteID"]},{name:"sjv.io",match:/.*.sjv.io/i,redirect:"u"},{name:"linksynergy.com",match:/.*.linksynergy.com/i,rules:["id","mid"],redirect:"murl"},{name:"cnbc.com",match:/www.cnbc.com/i,rules:["__source"]},{name:"yahoo.com",match:/yahoo.com/i,rules:["guce_referrer","guce_referrer_sig","guccounter","soc_src","soc_trk","tsrc"]},{name:"techcrunch.com",match:/techcrunch.com/i,rules:["guce_referrer","guce_referrer_sig","guccounter"]},{name:"office.com",match:/office.com/i,rules:["from"]},{name:"ticketmaster.co.nz",match:/ticketmaster.co.nz/i,rules:["tm_link"]},{name:"bostonglobe.com",match:/bostonglobe.com/i,rules:["p1"]},{name:"ampproject.org",match:/cdn.ampproject.org/i,rules:["amp_gsa","amp_js_v","usqp","outputType"],amp:{regex:/cdn\.ampproject\.org\/v\/s\/(.*)\#(aoh|csi|referrer|amp)/gim}},{name:"nbcnews.com",match:/nbcnews.com/i,rules:["fbclid"],amp:{replace:{text:"www.nbcnews.com/news/amp/",with:"www.nbcnews.com/news/"}}},{name:"countdown.co.nz",match:/www.countdown.co.nz/i,rules:["promo_name","promo_creative","promo_position","itemID"]},{name:"etsy.com",match:/www.etsy.com/i,rules:["click_key","click_sum","rec_type","ref","frs","sts","dd_referrer"]},{name:"wattpad.com",match:/www.wattpad.com/i,rules:["wp_page","wp_uname","wp_originator"]},{name:"redirect.viglink.com",match:/redirect.viglink.com/i,redirect:"u"},{name:"noctre.com",match:/www.noctre.com/i,rules:["aff"]},{name:"dreamgame.com",match:/www.dreamgame.com/i,rules:["affiliate"]},{name:"startpage.com",match:/.*.startpage.com/i,rules:["source"]},{name:"2game.com",match:/^2game.com/i,rules:["ref"]},{name:"jdoqocy.com",match:/^www.jdoqocy.com/i,redirect:"url"},{name:"gamesplanet.com",match:/^(?:.*\.|)gamesplanet\.com/i,rules:["ref"]},{name:"gamersgate.com",match:/www.gamersgate.com/i,rules:["aff"]},{name:"gate.sc",match:/gate.sc/i,redirect:"url"},{name:"getmusicbee.com",match:/^getmusicbee.com/i,redirect:"r"},{name:"imp.i305175.net",match:/^imp.i305175.net/i,redirect:"u"},{name:"qflm.net",match:/.*.qflm.net/i,redirect:"u"},{name:"anrdoezrs.net",match:/anrdoezrs.net/i,amp:{regex:/(?:.*)\/links\/(?:.*)\/type\/dlg\/sid\/\[subid_value\]\/(.*)/gi}},{name:"emjcd.com",match:/^www.emjcd.com/i,decode:{param:"d",lookFor:"destinationUrl"}},{name:"go2cloud.org",match:/^.*.go2cloud.org/i,redirect:"aff_unique1"},{name:"bn5x.net",match:/^.*.bn5x.net/i,redirect:"u"},{name:"tvguide.com",match:/^www.tvguide.com/i,amp:{regex:/(.*)\#link=/i}},{name:"ranker.com",match:/^(www|blog).ranker.com/i,rules:["ref","rlf","l","li_source","li_medium"]},{name:"tkqlhce.com",match:/^www.tkqlhce.com/i,redirect:"url"},{name:"flexlinkspro.com",match:/^track.flexlinkspro.com/i,redirect:"url"},{name:"watchworthy.app",match:/^watchworthy.app/i,rules:["ref"]},{name:"hbomax.com",match:/^trk.hbomax.com/i,redirect:"url"},{name:"squarespace.com",match:/^.*.squarespace.com/i,rules:["subchannel","source","subcampaign","campaign","channel","_ga"]},{name:"baidu.com",match:/^www.baidu.com/i,rules:["rsv_spt","rsv_idx","rsv_pq","rsv_t","rsv_bp","rsv_dl","rsv_iqid","rsv_enter","rsv_sug1","rsv_sug2","rsv_sug3","rsv_sug4","rsv_sug5","rsv_sug6","rsv_sug7","rsv_sug8","rsv_sug9","rsv_sug7","rsv_btype","tn","sa","rsf","rqid","usm","__pc2ps_ab","p_signature","p_sign","p_timestamp","p_tk","oq"],allow:["wd","ie"]},{name:"primevideo.com",match:/^www.primevideo.com/i,rules:["dclid"],replace:[/\/ref=[^\/?]*/i]},{name:"threadless.com",match:/^www.threadless.com/i,rules:["itm_source_s","itm_medium_s","itm_campaign_s"]},{name:"wsj.com",match:/^www.wsj.com/i,rules:["mod"]},{name:"thewarehouse.co.nz",match:/^www.thewarehouse.co.nz/i,rules:["sfmc_j","sfmc_id","sfmc_mid","sfmc_uid","sfmc_id","sfmc_activityid"]},{name:"awstrack.me",match:/^.*awstrack.me/i,amp:{regex:/awstrack.me\/L0\/(.*)/}},{name:"express.co.uk",match:/^www.express.co.uk/i,replace:[/\/amp$/i]},{name:"ko-fi.com",match:/^ko-fi.com/i,rules:["ref","src"]},{name:"indiegala.com",match:/^www.indiegala.com/i,rules:["ref"]},{name:"l.messenger.com",match:/^l.messenger.com/i,redirect:"u"},{name:"transparency.fb.com",match:/^transparency.fb.com/i,rules:["source"]},{name:"manymorestores.com",match:/^www.manymorestores.com/i,rules:["ref"]},{name:"macgamestore.com",match:/^www.macgamestore.com/i,rules:["ars"]},{name:"blizzardgearstore.com",match:/^www.blizzardgearstore.com/i,rules:["_s"]},{name:"playbook.com",match:/^www.playbook.com/i,rules:["p"]},{name:"cookiepro.com",match:/^.*.cookiepro.com/i,rules:["source","referral"]},{name:"pinterest.com",match:/^www\.pinterest\..*/i,rules:["rs"]},{name:"bing.com",match:/^www\.bing\.com/i,rules:["qs","form","sp","pq","sc","sk","cvid","FORM","ck","simid","thid","cdnurl","pivotparams","ghsh","ghacc","ccid","","ru"],readded:["sim","exph","expw","vt","mediaurl","first"],allow:["q","tsc","iss","id","view","setlang"]},{name:"jf79.net",match:/^jf79\.net/i,rules:["li","wi","ws","ws2"]},{name:"frankenergie.nl",match:/^www\.frankenergie\.nl/i,rules:["aff_id"]},{name:"nova.cz",match:/^.*\.nova\.cz/i,rules:["sznclid"]},{name:"cnn.com",match:/.*.cnn.com/i,rules:["hpt","iid"],amp:{replace:{text:"amp.cnn.com/cnn/",with:"www.cnn.com/"}},exclude:[/e.newsletters.cnn.com/gi]},{name:"amp.scmp.com",match:/amp\.scmp\.com/i,amp:{replace:{text:"amp.scmp.com",with:"scmp.com"}}},{name:"justwatch.com",match:/click\.justwatch\.com/i,rules:["cx","uct_country","uct_buybox","sid"],redirect:"r"},{name:"psychologytoday.com",match:/www\.psychologytoday\.com/i,rules:["amp"]},{name:"mouser.com",match:/www\.mouser\.com/i,rules:["qs"]},{name:"awin1.com",match:/www\.awin1\.com/i,redirect:"ued",rules:["awinmid","awinaffid","clickref"]},{name:"syteapi.com",match:/syteapi\.com/i,decode:{param:"url",encoding:"base64"}},{name:"castorama.fr",match:/www\.castorama\.fr/i,rules:["syte_ref"]},{name:"quizlet.com",match:/quizlet\.com/i,rules:["funnelUUID","source"]},{name:"pbtech.co.nz",match:/www\.pbtech\.co\.nz/i,rules:["qr"]},{name:"matomo.org",match:/matomo\.org/i,rules:["menu","footer","header","hp-reasons-learn","hp-top","above-fold","step1-hp","mid-hp","take-back-control-hp","hp-reasons-icon","hp-reasons-heading","hp-reasons-p","hp-bottom","footer","menu"]},{name:"eufy.com",match:/eufy\.com/i,rules:["ref"]},{name:"newsflare.com",match:/www\.newsflare\.com/i,rules:["jwsource"]},{name:"wish.com",match:/www\.wish\.com/i,rules:["share"]},{name:"change.org",match:/www\.change\.org/i,rules:["source_location"]},{name:"washingtonpost.com",match:/.*\.washingtonpost\.com/i,rules:["itid","s_l"]},{name:"lowes.com",match:/www\.lowes\.com/i,rules:["cm_mmc","ds_rl","gbraid"]},{name:"stacks.wellcomecollection.org",match:/stacks\.wellcomecollection\.org/i,rules:["source"]},{name:"redbubble.com",match:/.*\.redbubble\.com/i,rules:["ref"]},{name:"inyourarea.co.uk",match:/inyourarea.co.uk/i,rules:["from_reach_primary_nav","from_reach_footer_nav","branding"]},{name:"fiverr.com",match:/.*\.fiverr\.com/i,rules:["source","context_referrer","referrer_gig_slug","ref_ctx_id","funnel","imp_id"]},{name:"kqzyfj.com",match:/www\.kqzyfj\.com/i,redirect:"url",rules:["cjsku","pubdata"]},{name:"marca.com",match:/.*\.marca\.com/i,rules:["intcmp","s_kw","emk"]},{name:"marcaentradas.com",match:/.*\.marcaentradas\.com/i,rules:["intcmp","s_kw"]},{name:"honeycode.aws",match:/.*\.honeycode\.aws/i,rules:["trackingId","sc_icampaign","sc_icontent","sc_ichannel","sc_iplace","sc_country","sc_outcome","sc_geo","sc_campaign","sc_channel","trkCampaign","trk"]},{name:"news.artnet.com",match:/news\.artnet\.com/i,replace:[/\/amp-page$/i]},{name:"studentbeans.com",match:/www\.studentbeans\.com/i,rules:["source"]},{name:"boxofficemojo.com",match:/boxofficemojo.com/i,rules:["ref_"]},{name:"solodeportes.com.ar",match:/www.solodeportes.com.ar/i,rules:["nosto","refSrc"]},{name:"amp.dw.com",match:/amp.dw.com/i,amp:{replace:{text:"amp.dw.com",with:"dw.com"}}},{name:"joybuggy.com",match:/joybuggy.com/i,rules:["ref"]},{name:"etail.market",match:/etail.market/i,rules:["tracking"]},{name:"myanimelist.net",match:/myanimelist.net/i,rules:["_location","click_type","click_param"]},{name:"support-dev.discord.com",match:/support-dev.discord.com/i,rules:["ref"]},{name:"dlgamer.com",match:/dlgamer.com/i,rules:["affil"]},{name:"newsletter.manor.ch",match:/newsletter.manor.ch/i,rules:["user_id_1"],rev:!0},{name:"knowyourmeme.com",match:/amp.knowyourmeme.com/i,amp:{replace:{text:"amp.knowyourmeme.com",with:"knowyourmeme.com"}}},{name:"ojrq.net",match:/ojrq.net/i,redirect:"return"},{name:"click.pstmrk.it",match:/click.pstmrk.it/i,amp:{regex:/click\.pstmrk\.it\/(?:[a-zA-Z0-9]){1,2}\/(.*?)\//gim}},{name:"track.roeye.co.nz",match:/track.roeye.co.nz/i,redirect:"path"},{name:"producthunt.com",match:/producthunt.com/i,rules:["ref"]},{name:"cbsnews.com",match:/www.cbsnews.com/i,rules:["ftag","intcid"],amp:{replace:{text:"cbsnews.com/amp/",with:"cbsnews.com/"}}},{name:"jobs.venturebeat.com",match:/jobs.venturebeat.com/i,rules:["source"]},{name:"api.ffm.to",match:/api.ffm.to/i,decode:{param:"cd",lookFor:"destUrl"}},{name:"wfaa.com",match:/www.wfaa.com/i,rules:["ref"]},{name:"buyatoyota.com",match:/www.buyatoyota.com/i,rules:["siteid"]},{name:"independent.co.uk",match:/www.independent.co.uk/i,rules:["amp","regSourceMethod"]},{name:"lenovo.vzew.net",match:/lenovo.vzew.net/i,redirect:"u"},{name:"stats.newswire.com",match:/stats.newswire.com/i,decode:{param:"final"}},{name:"cooking.nytimes.com",match:/cooking.nytimes.com/i,rules:["smid","variant","algo","req_id","surface","imp_id","action","region","module","pgType"]},{name:"optigruen.com",match:/www\.optigruen\.[a-z0-9]{0,3}/i,rules:["cHash","chash","mdrv"]},{name:"osi.rosenberger.com",match:/osi.rosenberger.com/i,rules:["cHash","chash"]},{name:"cbc.ca",match:/cbc.ca/i,rules:["__vfz","cmp","referrer"]},{name:"local12.com",match:/local12.com/i,rules:["_gl"]},{name:"eufylife.com",match:/eufylife.com/i,rules:["ref"]},{name:"walmart.com",match:/walmart.com/i,rules:["athAsset","povid","wmlspartner","athcpid","athpgid","athznid","athmtid","athstid","athguid","athwpid","athtvid","athcgid","athieid","athancid","athbdg","campaign_id","eventST","bt","pos","rdf","tax","plmt","mloc","pltfm","pgId","pt","spQs","adUid","adsRedirect"],redirect:"rd"},{name:"adclick.g.doubleclick.net",match:/adclick.g.doubleclick.net/i,redirect:"adurl"},{name:"dyno.gg",match:/dyno.gg/i,rules:["ref"]},{name:"eufylife.com",match:/eufylife.com/i,rules:["ref"]},{name:"connect.studentbeans.com",match:/connect.studentbeans.com/i,rules:["ref"]},{name:"urldefense.proofpoint.com",match:/urldefense.proofpoint.com/i,decode:{param:"u",handler:"urldefense.proofpoint.com"}},{name:"curseforge.com",match:/www.curseforge.com/i,redirect:"remoteurl"},{name:"s.pemsrv.com",match:/s.pemsrv.com/i,rules:["cat","idzone","type","sub","block","el","tags","cookieconsent","scr_info"],redirect:"p"},{name:"chess.com",match:/www.chess.com/i,rules:["c"]},{name:"porndude.link",match:/porndude.link/i,rules:["ref"]},{name:"xvideos.com",match:/xvideos.com/i,rules:["sxcaf"]},{name:"xvideos.red",match:/xvideos.red/i,rules:["sxcaf","pmsc","pmln"]},{name:"xhamster.com",match:/xhamster.com/i,rules:["source"]},{name:"patchbot.io",match:/patchbot.io/i,decode:{targetPath:!0,handler:"patchbot.io"}},{name:"milkrun.com",match:/milkrun.com/i,rules:["_branch_match_id","_branch_referrer"]},{name:"gog.salesmanago.com",match:/gog.salesmanago.com/i,rules:["smclient","smconv","smlid"],redirect:"url"},{name:"dailymail.co.uk",match:/dailymail.co.uk/i,rules:["reg_source","ito"]},{name:"stardockentertainment.info",match:/www.stardockentertainment.info/i,decode:{targetPath:!0,handler:"stardockentertainment.info"}},{name:"steam.gs",match:/steam.gs/i,decode:{targetPath:!0,handler:"steam.gs"}},{name:"0yxjo.mjt.lu",match:/0yxjo.mjt.lu/i,decode:{targetPath:!0,handler:"0yxjo.mjt.lu"}},{name:"click.redditmail.com",match:/click.redditmail.com/i,decode:{targetPath:!0,handler:"click.redditmail.com"}},{name:"deals.dominos.co.nz",match:/deals.dominos.co.nz/i,decode:{targetPath:!0,handler:"deals.dominos.co.nz"}},{name:"hashnode.com",match:/(?:.*\.)?hashnode\.com/i,rules:["source"]},{name:"s.amazon-adsystem.com",match:/s.amazon-adsystem.com/i,rules:["dsig","d","ex-fch","ex-fargs","cb"],redirect:"rd"},{name:"hypable.com",match:/www.hypable.com/i,amp:{replace:{text:/amp\/$/gi}}},{name:"theguardian.com",match:/theguardian.com/i,amp:{replace:{text:"amp.theguardian.com",with:"theguardian.com"}},rules:["INTCMP","acquisitionData","REFPVID"]},{name:"indiatoday.in",match:/www.indiatoday.in/i,amp:{replace:{text:"www.indiatoday.in/amp/",with:"www.indiatoday.in/"}}},{name:"seek.co.nz",match:/www.seek.co.nz/i,rules:["tracking","sc_trk"]},{name:"seekvolunteer.co.nz",match:/seekvolunteer.co.nz/i,rules:["tracking","sc_trk"]},{name:"seekbusiness.com.au",match:/www.seekbusiness.com.au/i,rules:["tracking","cid"]},{name:"garageclothing.com",match:/www.garageclothing.com/i,rules:["syte_ref","site_ref"]},{name:"urbandictionary.com",match:/urbandictionary.com/i,rules:["amp"]},{name:"norml.org",match:/norml.org/i,rules:["amp"]},{name:"nbcconnecticut.com",match:/www.nbcconnecticut.com/i,rules:["amp"]},{name:"pbs.org",match:/www.pbs.org/i,amp:{replace:{text:"pbs.org/newshour/amp/",with:"pbs.org/newshour/"}}},{name:"m.thewire.in",match:/m.thewire.in/i,amp:{replace:{text:"m.thewire.in/article/",with:"thewire.in/"},sliceTrailing:"/amp"}},{name:"ladysmithchronicle.com",match:/www.ladysmithchronicle.com/i,rules:["ref"]},{name:"businesstoday.in",match:/www.businesstoday.in/i,amp:{replace:{text:"www.businesstoday.in/amp/markets/",with:"www.businesstoday.in/markets/"}}},{name:"turnto10.com",match:/turnto10.com/i,amp:{replace:{text:"turnto10.com/amp/",with:"turnto10.com/"}}},{name:"gadgets360.com",match:/www.gadgets360.com/i,amp:{sliceTrailing:"/amp"}},{name:"m10news.com",match:/m10news.com/i,rules:["amp"]},{name:"elprogreso.es",match:/www.elprogreso.es/i,amp:{replace:{text:".amp.html",with:".html"}}},{name:"thenewsminute.com",match:/www.thenewsminute.com/i,amp:{replace:{text:"www.thenewsminute.com/amp/story/",with:"www.thenewsminute.com/"}}},{name:"mirror.co.uk",match:/www.mirror.co.uk/i,amp:{sliceTrailing:".amp"}},{name:"libretro.com",match:/www.libretro.com/i,rules:["amp"]},{name:"the-sun.com",match:/www.the-sun.com/i,amp:{sliceTrailing:"amp/"}},{name:"bostonherald.com",match:/bostonherald.com/i,rules:["g2i_source","g2i_medium","g2i_campaign"],amp:{sliceTrailing:"amp/"}},{name:"playshoptitans.com",match:/playshoptitans.com/i,rules:["af_ad","pid","source_caller","shortlink","c"]},{name:"news.com.au",match:/www.news.com.au/i,rules:["sourceCode"]},{name:"wvva.com",match:/www.wvva.com/i,rules:["outputType"]},{name:"realestate.com.au",match:/www.realestate.com.au/i,rules:["campaignType","campaignChannel","campaignName","campaignContent","campaignSource","campaignPlacement","sourcePage","sourceElement","cid"]},{name:"redirectingat.com",match:/redirectingat.com/i,redirect:"url",rules:["id","xcust","xs"],decode:{handler:"redirectingat.com",targetPath:!0}},{name:"map.sewoon.org",match:/map.sewoon.org/i,rules:["cid"]},{name:"vi-control.net",match:/[&?]source=https:\/\/vi-control\.net\/community$/,match_href:!0,rules:["source"]},{name:"rosequake.com",match:/www.rosequake.com/i,rules:["edmID","linkID","userID","em","taskItemID"],redirect:"to"},{name:"rekrute.com",match:/www.rekrute.com/i,rules:["clear"],redirect:"keyword"},{name:"go.skimresources.com",match:/go.skimresources.com/i,rules:["id","xs","xcust"],redirect:"url"},{name:"khnum-ezi.com",match:/khnum-ezi.com/i,rules:["browserWidth","browserHeight","iframeDetected","webdriverDetected","gpu","timezone","visitid","type","timezoneName"]}]},28:(e,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0}),r.TidyConfig=void 0;var a=function(){function e(){this.allowAMP=!1,this.allowCustomHandlers=!0,this.allowRedirects=!0,this.silent=!0}return e.prototype.copy=function(){return{allowAMP:this.allowAMP,allowCustomHandlers:this.allowCustomHandlers,allowRedirects:this.allowRedirects,silent:this.silent}},e.prototype.get=function(e){return this[e]},e.prototype.set=function(e,r){this[e]=r},e.prototype.setMany=function(e){var r=this;Object.keys(e).forEach((function(a){var t,c=a,i=null!==(t=e[c])&&void 0!==t?t:r[c];if(void 0===r[c])throw new Error("'"+c+"' is not a valid config key");r.set(c,i)}))},e}();r.TidyConfig=a},463:(e,r,a)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0}),r.handlers=void 0;var t=a(185);r.handlers={},r.handlers["patchbot.io"]={exec:function(e,r){try{var a=r.decoded.replace(/%3D/g,"=");return{url:decodeURIComponent(a.split("|")[2])}}catch(e){return(""+e).startsWith("URIError")&&(e=new Error("Unable to decode URI component. The URL may be invalid")),{url:r.originalURL,error:e}}}},r.handlers["urldefense.proofpoint.com"]={exec:function(e,r){try{var a=r.urlParams.get("u");if(null===a)throw new Error("Target parameter (u) was null");return{url:decodeURIComponent(a.replace(/-/g,"%")).replace(/_/g,"/").replace(/%2F/g,"/")}}catch(e){return{url:r.originalURL,error:e}}}},r.handlers["stardockentertainment.info"]={exec:function(e,r){try{var a=e.split("/").pop(),c="";if(void 0===a)throw new Error("Undefined target");return(c=t.decodeBase64(a)).includes("watch>v=")&&(c=c.replace("watch>v=","watch?v=")),{url:c}}catch(e){return{url:r.originalURL,error:e}}}},r.handlers["steam.gs"]={exec:function(e,r){try{var a=e.split("%3Eutm_").shift(),t="";return a&&(t=a),{url:t}}catch(e){return{url:r.originalURL,error:e}}}},r.handlers["0yxjo.mjt.lu"]={exec:function(e,r){try{var a=e.split("/").pop();if(void 0===a)throw new Error("Undefined target");return{url:t.decodeBase64(a)}}catch(e){return{url:r.originalURL,error:e}}}},r.handlers["click.redditmail.com"]={exec:function(e,r){try{var a=t.regexExtract(/https:\/\/click\.redditmail\.com\/CL0\/(.*?)\//gi,e);if(void 0===a[1])throw new Error("regexExtract failed to find a URL");return{url:decodeURIComponent(a[1])}}catch(e){return{url:r.originalURL,error:e}}}},r.handlers["deals.dominos.co.nz"]={exec:function(e,r){try{var a=e.split("/").pop();if(!a)throw new Error("Missing target");return{url:t.decodeBase64(a)}}catch(e){return{url:r.originalURL,error:e}}}},r.handlers["redirectingat.com"]={exec:function(e,r){var a;try{var c="",i=e.split("?id"),o=i[0],m=i[1];if(i.slice(2),"https://go.redirectingat.com/"===o){var n=decodeURIComponent(m),s=new URL(o+"?id="+n).searchParams.get("url");if(!s||!0!==t.validateURL(s))throw Error(null!==(a="Handler failed, result: "+s)&&void 0!==a?a:"No param");c=s}else c=r.originalURL;return{url:c}}catch(e){return{url:r.originalURL,error:e}}}},r.handlers["twitch.tv-email"]={note:"This is used for email tracking",exec:function(e,r){try{var a="",c=t.regexExtract(/www\.twitch\.tv\/r\/e\/(.*?)\//,e),i=t.decodeBase64(c[1]),o=JSON.parse(i);return"twitch_favorite_up"===o.name&&(a="https://www.twitch.tv/"+o.channel),{url:a}}catch(e){return{url:r.originalURL,error:e}}}}},156:function(e,r,a){"use strict";var t=this&&this.__spreadArray||function(e,r){for(var a=0,t=r.length,c=e.length;a0&&this.log("Invalid URL: "+e,"error"),m;if(this.config.allowAMP&&!1===c.urlHasParams(e))return m.url=m.info.original,m;var n=this.rebuild(e);m.url=n;var s=[],l=new URL(n),u=l.searchParams,d=new URLSearchParams,h=l.pathname;u.forEach((function(e,r){return d.append(r.toLowerCase(),e)}));for(var p=0,w=this.expandedRules;p{"use strict";var a;Object.defineProperty(r,"__esModule",{value:!0}),r.EEncoding=void 0,(a=r.EEncoding||(r.EEncoding={})).base64="base64",a.base32="base32",a.base45="base45",a.url="url",a.urlc="urlc",a.binary="binary",a.hex="hex"},185:(e,r,a)=>{"use strict";var t;Object.defineProperty(r,"__esModule",{value:!0}),r.decodeURL=r.regexExtract=r.getLinkDiff=r.guessEncoding=r.isB64=r.validateURL=r.urlHasParams=r.isJSON=r.decodeBase64=void 0;var c=a(407);r.decodeBase64=function(e){try{return"undefined"==typeof atob?Buffer.from(e,"base64").toString("binary"):atob(e)}catch(r){return e}},r.isJSON=function(e){try{return"object"==typeof JSON.parse(e)}catch(e){return!1}},r.urlHasParams=function(e){return new URL(e).searchParams.toString().length>0},r.validateURL=function(e){try{var r=new URL(e).protocol.toLowerCase();if(!["http:","https:"].includes(r))throw new Error("Not acceptable protocol: "+r);return!0}catch(r){if("undefined"!==e&&"null"!==e&&e.length>0)throw new Error("Invalid URL: "+e);return!1}},r.isB64=function(e){try{return/^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/.test(e)}catch(e){return!1}},r.guessEncoding=function(e){return{base64:r.isB64(e),isJSON:r.isJSON(e)}},r.getLinkDiff=function(e,r){var a=new URL(e),t=new URL(r);return{is_new_host:a.host!==t.host,isNewHost:a.host!==t.host,difference:r.length-e.length,reduction:+(100-e.length/r.length*100).toFixed(2)}},r.regexExtract=function(e,r){var a=null,t=[],c=0;return null!==(a=e.exec(r))&&10!==c&&(c++,a.index===e.lastIndex&&e.lastIndex++,a.forEach((function(e){return t.push(e)}))),t};var i=function(e){return e},o=((t={})[c.EEncoding.url]=function(e){return decodeURI(e)},t[c.EEncoding.urlc]=function(e){return decodeURIComponent(e)},t[c.EEncoding.base32]=i,t[c.EEncoding.base45]=i,t[c.EEncoding.base64]=function(e){return r.decodeBase64(e)},t[c.EEncoding.binary]=i,t[c.EEncoding.hex]=function(e){for(var r=e.toString(),a="",t=0;t string; 11 | /** 12 | * Checks if data is valid JSON. The result will be either `true` or `false`. 13 | * @param data Any string that might be JSON 14 | * @returns true or false 15 | */ 16 | export declare const isJSON: (data: string) => boolean; 17 | /** 18 | * Check if a domain has any URL parameters 19 | * @param url Any valid URL 20 | * @returns true / false 21 | */ 22 | export declare const urlHasParams: (url: string) => boolean; 23 | /** 24 | * Determine if the input is a valid URL or not. This will only 25 | * accept http and https protocols. 26 | * @param url Any URL 27 | * @returns true / false 28 | */ 29 | export declare const validateURL: (url: string) => boolean; 30 | /** 31 | * Check if a string is b64. For now this should only be 32 | * used in testing. 33 | * @param str Any possible b64 string 34 | * @returns true/false 35 | */ 36 | export declare const isB64: (str: string) => boolean; 37 | /** 38 | * DO NOT USE THIS IN HANDLERS. 39 | * This is purely for use in testing to save time. 40 | * This is not reliable, there are many incorrect 41 | * matches and it will fail in a lot of cases. 42 | * Do not use it anywhere else. 43 | * @param str Any string 44 | * @returns An object with possible encodings 45 | */ 46 | export declare const guessEncoding: (str: string) => IGuessEncoding; 47 | /** 48 | * Calculates the difference between two links and returns an object of information. 49 | * @param firstURL Any valid URL 50 | * @param secondURL Any valid URL 51 | * @returns The difference between two links 52 | */ 53 | export declare const getLinkDiff: (firstURL: string, secondURL: string) => ILinkDiff; 54 | export declare const regexExtract: (regex: RegExp, str: string) => string[]; 55 | /** 56 | * Attempts to decode a URL or string using the selected method. 57 | * If the decoding fails the original string will be returned. 58 | * `encoding` is optional and will default to base64 59 | * @param str String to decode 60 | * @param encoding Encoding to use 61 | * @returns decoded string 62 | */ 63 | export declare const decodeURL: (str: string, encoding?: EEncoding) => string; 64 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var _a; 3 | Object.defineProperty(exports, "__esModule", { value: true }); 4 | exports.decodeURL = exports.regexExtract = exports.getLinkDiff = exports.guessEncoding = exports.isB64 = exports.validateURL = exports.urlHasParams = exports.isJSON = exports.decodeBase64 = void 0; 5 | var interface_1 = require("./interface"); 6 | /** 7 | * Accepts any base64 string and attempts to decode it. 8 | * If run through the browser `atob` will be used, otherwise 9 | * the code will use `Buffer.from`. 10 | * If there's an error the original string will be returned. 11 | * @param str String to be decoded 12 | * @returns Decoded string 13 | */ 14 | var decodeBase64 = function (str) { 15 | try { 16 | var result = str; 17 | if (typeof atob === 'undefined') { 18 | result = Buffer.from(str, 'base64').toString('binary'); 19 | } 20 | else { 21 | result = atob(str); 22 | } 23 | return result; 24 | } 25 | catch (error) { 26 | return str; 27 | } 28 | }; 29 | exports.decodeBase64 = decodeBase64; 30 | /** 31 | * Checks if data is valid JSON. The result will be either `true` or `false`. 32 | * @param data Any string that might be JSON 33 | * @returns true or false 34 | */ 35 | var isJSON = function (data) { 36 | try { 37 | var sample = JSON.parse(data); 38 | if (typeof sample !== 'object') 39 | return false; 40 | return true; 41 | } 42 | catch (error) { 43 | return false; 44 | } 45 | }; 46 | exports.isJSON = isJSON; 47 | /** 48 | * Check if a domain has any URL parameters 49 | * @param url Any valid URL 50 | * @returns true / false 51 | */ 52 | var urlHasParams = function (url) { 53 | return new URL(url).searchParams.toString().length > 0; 54 | }; 55 | exports.urlHasParams = urlHasParams; 56 | /** 57 | * Determine if the input is a valid URL or not. This will only 58 | * accept http and https protocols. 59 | * @param url Any URL 60 | * @returns true / false 61 | */ 62 | var validateURL = function (url) { 63 | try { 64 | var pass = ['http:', 'https:']; 65 | var test = new URL(url); 66 | var prot = test.protocol.toLowerCase(); 67 | if (!pass.includes(prot)) { 68 | throw new Error('Not acceptable protocol: ' + prot); 69 | } 70 | return true; 71 | } 72 | catch (error) { 73 | if (url !== 'undefined' && url !== 'null' && url.length > 0) { 74 | throw new Error("Invalid URL: " + url); 75 | } 76 | return false; 77 | } 78 | }; 79 | exports.validateURL = validateURL; 80 | /** 81 | * Check if a string is b64. For now this should only be 82 | * used in testing. 83 | * @param str Any possible b64 string 84 | * @returns true/false 85 | */ 86 | var isB64 = function (str) { 87 | try { 88 | var regex = /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/; 89 | return regex.test(str); 90 | } 91 | catch (error) { 92 | // Using try/catch to be safe 93 | return false; 94 | } 95 | }; 96 | exports.isB64 = isB64; 97 | /** 98 | * DO NOT USE THIS IN HANDLERS. 99 | * This is purely for use in testing to save time. 100 | * This is not reliable, there are many incorrect 101 | * matches and it will fail in a lot of cases. 102 | * Do not use it anywhere else. 103 | * @param str Any string 104 | * @returns An object with possible encodings 105 | */ 106 | var guessEncoding = function (str) { 107 | return { 108 | base64: exports.isB64(str), 109 | isJSON: exports.isJSON(str) 110 | }; 111 | }; 112 | exports.guessEncoding = guessEncoding; 113 | /** 114 | * Calculates the difference between two links and returns an object of information. 115 | * @param firstURL Any valid URL 116 | * @param secondURL Any valid URL 117 | * @returns The difference between two links 118 | */ 119 | var getLinkDiff = function (firstURL, secondURL) { 120 | var oldUrl = new URL(firstURL); 121 | var newUrl = new URL(secondURL); 122 | return { 123 | is_new_host: oldUrl.host !== newUrl.host, 124 | isNewHost: oldUrl.host !== newUrl.host, 125 | difference: secondURL.length - firstURL.length, 126 | reduction: +(100 - (firstURL.length / secondURL.length) * 100).toFixed(2) 127 | }; 128 | }; 129 | exports.getLinkDiff = getLinkDiff; 130 | var regexExtract = function (regex, str) { 131 | var matches = null; 132 | var result = []; 133 | var i = 0; 134 | // Limit to 10 to avoid infinite loop 135 | if ((matches = regex.exec(str)) !== null && i !== 10) { 136 | i++; 137 | if (matches.index === regex.lastIndex) 138 | regex.lastIndex++; 139 | matches.forEach(function (v) { return result.push(v); }); 140 | } 141 | return result; 142 | }; 143 | exports.regexExtract = regexExtract; 144 | /** 145 | * These are methods that have not been written yet, 146 | * the original string will be returned. 147 | */ 148 | var _placeholder = function (decoded) { return decoded; }; 149 | var decoders = (_a = {}, 150 | _a[interface_1.EEncoding.url] = function (decoded) { return decodeURI(decoded); }, 151 | _a[interface_1.EEncoding.urlc] = function (decoded) { return decodeURIComponent(decoded); }, 152 | _a[interface_1.EEncoding.base32] = _placeholder, 153 | _a[interface_1.EEncoding.base45] = _placeholder, 154 | _a[interface_1.EEncoding.base64] = function (decoded) { return exports.decodeBase64(decoded); }, 155 | _a[interface_1.EEncoding.binary] = _placeholder, 156 | _a[interface_1.EEncoding.hex] = function (decoded) { 157 | var hex = decoded.toString(); 158 | var out = ''; 159 | for (var i = 0; i < hex.length; i += 2) { 160 | out += String.fromCharCode(parseInt(hex.substr(i, 2), 16)); 161 | } 162 | return out; 163 | }, 164 | _a); 165 | /** 166 | * Attempts to decode a URL or string using the selected method. 167 | * If the decoding fails the original string will be returned. 168 | * `encoding` is optional and will default to base64 169 | * @param str String to decode 170 | * @param encoding Encoding to use 171 | * @returns decoded string 172 | */ 173 | var decodeURL = function (str, encoding) { 174 | if (encoding === void 0) { encoding = interface_1.EEncoding.base64; } 175 | try { 176 | return decoders[encoding](str); 177 | } 178 | catch (error) { 179 | return str; 180 | } 181 | }; 182 | exports.decodeURL = decodeURL; 183 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tidy-url", 3 | "version": "1.18.3", 4 | "description": "Cleans/removes tracking or garbage parameters from URLs", 5 | "main": "lib/index.js", 6 | "types": "lib/index.d.ts", 7 | "scripts": { 8 | "test": "ts-node test.ts", 9 | "build": "tsc && webpack && ts-node src/postbuild.ts", 10 | "prepare": "npm run build", 11 | "version": "git add -A src", 12 | "postversion": "git push && git push --tags" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/DrKain/tidy-url.git" 17 | }, 18 | "keywords": [ 19 | "tidy", 20 | "clean", 21 | "sanitize", 22 | "url", 23 | "website", 24 | "tracking", 25 | "remove", 26 | "junk" 27 | ], 28 | "author": "", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/DrKain/tidy-url/issues" 32 | }, 33 | "homepage": "https://github.com/DrKain/tidy-url#readme", 34 | "devDependencies": { 35 | "@types/node": "^14.17.0", 36 | "@typescript-eslint/eslint-plugin": "^5.17.0", 37 | "@typescript-eslint/parser": "^5.17.0", 38 | "eslint": "^8.12.0", 39 | "prettier": "^2.3.0", 40 | "ts-loader": "^9.2.8", 41 | "typescript": "^4.2.4", 42 | "webpack": "^5.90.3", 43 | "webpack-cli": "^5.1.4" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import { IConfig } from './interface'; 2 | 3 | export class TidyConfig implements IConfig { 4 | public allowAMP: boolean = false; 5 | public allowCustomHandlers: boolean = true; 6 | public allowRedirects: boolean = true; 7 | public silent: boolean = true; 8 | 9 | /** 10 | * Fetch a copy of the current config. 11 | * You can then pass this to `setMany` if 12 | * you want to sync with another TidyConfig instance. 13 | * 14 | * @returns A copy of the current config 15 | */ 16 | public copy() { 17 | return { 18 | allowAMP: this.allowAMP, 19 | allowCustomHandlers: this.allowCustomHandlers, 20 | allowRedirects: this.allowRedirects, 21 | silent: this.silent 22 | }; 23 | } 24 | 25 | /** 26 | * You can just use `config.key` but yeah. 27 | * @param key The key you're wanting to get the value of 28 | * @returns The value 29 | */ 30 | public get(key: keyof IConfig) { 31 | return this[key]; 32 | } 33 | 34 | /** 35 | * Set a single config option. If you want to set multiple at once 36 | * use `setMany` 37 | * @param key Option to set 38 | * @param value Value to set it to 39 | */ 40 | public set(key: keyof IConfig, value: boolean) { 41 | this[key] = value; 42 | } 43 | 44 | /** 45 | * Set multiple config options at once by passing it an object. 46 | * @param obj An object containing any number of config options 47 | */ 48 | public setMany(obj: Partial) { 49 | Object.keys(obj).forEach((_key) => { 50 | const key: keyof IConfig = _key as any; 51 | const val: boolean = obj[key] ?? this[key]; 52 | 53 | if (typeof this[key] === 'undefined') { 54 | throw new Error(`'${key}' is not a valid config key`); 55 | } 56 | 57 | this.set(key, val); 58 | }); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/handlers.ts: -------------------------------------------------------------------------------- 1 | import { IHandler } from './interface'; 2 | import { decodeBase64, isJSON, regexExtract, validateURL } from './utils'; 3 | 4 | /** 5 | * This is currently experimental while I decide on how I want to restructure the main code to make it easier to follow. 6 | * There will need to be handlers for each process of the "clean" as well as these custom cases for sites that mix it up. 7 | * If you would like to help or give your thoughts feel free to open an issue on GitHub. 8 | */ 9 | export const handlers: { [key: string]: IHandler } = {}; 10 | 11 | handlers['patchbot.io'] = { 12 | exec: (_str, args) => { 13 | try { 14 | const dec = args.decoded.replace(/%3D/g, '='); 15 | return { url: decodeURIComponent(dec.split('|')[2]) }; 16 | } catch (error) { 17 | if (`${error}`.startsWith('URIError')) error = new Error('Unable to decode URI component. The URL may be invalid'); 18 | return { url: args.originalURL, error }; 19 | } 20 | } 21 | }; 22 | 23 | handlers['urldefense.proofpoint.com'] = { 24 | exec: (_str, args) => { 25 | try { 26 | const arg = args.urlParams.get('u'); 27 | 28 | if (arg === null) throw new Error('Target parameter (u) was null'); 29 | const url = decodeURIComponent(arg.replace(/-/g, '%')).replace(/_/g, '/').replace(/%2F/g, '/'); 30 | 31 | return { url }; 32 | } catch (error) { 33 | return { url: args.originalURL, error }; 34 | } 35 | } 36 | }; 37 | 38 | handlers['stardockentertainment.info'] = { 39 | exec: (str, args) => { 40 | try { 41 | const target = str.split('/').pop(); 42 | let url = ''; 43 | 44 | if (typeof target == 'undefined') throw new Error('Undefined target'); 45 | url = decodeBase64(target); 46 | if (url.includes('watch>v=')) url = url.replace('watch>v=', 'watch?v='); 47 | 48 | return { url: url }; 49 | } catch (error) { 50 | return { url: args.originalURL, error }; 51 | } 52 | } 53 | }; 54 | 55 | handlers['steam.gs'] = { 56 | exec: (str, args) => { 57 | try { 58 | const target = str.split('%3Eutm_').shift(); 59 | let url = ''; 60 | 61 | if (target) url = target; 62 | 63 | return { url: url }; 64 | } catch (error) { 65 | return { url: args.originalURL, error }; 66 | } 67 | } 68 | }; 69 | 70 | handlers['0yxjo.mjt.lu'] = { 71 | exec: (str, args) => { 72 | try { 73 | const target = str.split('/').pop(); 74 | let url = ''; 75 | 76 | if (typeof target == 'undefined') throw new Error('Undefined target'); 77 | url = decodeBase64(target); 78 | 79 | return { url: url }; 80 | } catch (error) { 81 | return { url: args.originalURL, error }; 82 | } 83 | } 84 | }; 85 | 86 | handlers['click.redditmail.com'] = { 87 | exec: (str, args) => { 88 | try { 89 | const reg = /https:\/\/click\.redditmail\.com\/CL0\/(.*?)\//gi; 90 | const matches = regexExtract(reg, str); 91 | 92 | if (typeof matches[1] === 'undefined') throw new Error('regexExtract failed to find a URL'); 93 | const url = decodeURIComponent(matches[1]); 94 | 95 | return { url: url }; 96 | } catch (error) { 97 | return { url: args.originalURL, error }; 98 | } 99 | } 100 | }; 101 | 102 | handlers['deals.dominos.co.nz'] = { 103 | exec: (str, args) => { 104 | try { 105 | const target = str.split('/').pop(); 106 | let url = ''; 107 | 108 | if (!target) throw new Error('Missing target'); 109 | url = decodeBase64(target); 110 | 111 | return { url }; 112 | } catch (error) { 113 | return { url: args.originalURL, error }; 114 | } 115 | } 116 | }; 117 | 118 | handlers['redirectingat.com'] = { 119 | exec(str, args) { 120 | try { 121 | let url = ''; 122 | const [host, target, ..._other] = str.split('?id'); 123 | 124 | // Make sure the redirect rule hasn't already processed this 125 | if (host === 'https://go.redirectingat.com/') { 126 | const decoded = decodeURIComponent(target); 127 | const corrected = new URL(`${host}?id=${decoded}`); 128 | const param = corrected.searchParams.get('url'); 129 | 130 | // Make sure the decoded parameters are a valid URL 131 | if (param && validateURL(param) === true) { 132 | url = param; 133 | } else { 134 | throw Error('Handler failed, result: ' + param ?? 'No param'); 135 | } 136 | } else { 137 | // If the host is different nothing needs to be modified 138 | url = args.originalURL; 139 | } 140 | 141 | return { url }; 142 | } catch (error) { 143 | return { url: args.originalURL, error }; 144 | } 145 | } 146 | }; 147 | 148 | handlers['twitch.tv-email'] = { 149 | note: 'This is used for email tracking', 150 | exec(str, args) { 151 | try { 152 | // This is the regex used to extract the decodable string 153 | const reg = /www\.twitch\.tv\/r\/e\/(.*?)\//; 154 | let url = ''; 155 | // Extract the decodable string from the URL 156 | let data = regexExtract(reg, str); 157 | // The second result is what we want 158 | let decode = decodeBase64(data[1]); 159 | // Parse the string, this should be JSON 160 | let parse = JSON.parse(decode); 161 | 162 | /** 163 | * This one is a bit tricky. I don't use Twitch often so I've limited it to "twitch_favorite_up", 164 | * In my case this was when a streamer I follow came online. 165 | */ 166 | if (parse['name'] === 'twitch_favorite_up') { 167 | url = 'https://www.twitch.tv/' + parse.channel; 168 | } 169 | 170 | return { url }; 171 | } catch (error) { 172 | return { url: args.originalURL, error }; 173 | } 174 | } 175 | }; 176 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { decodeURL, getLinkDiff, isJSON, urlHasParams, validateURL } from './utils'; 2 | import { IRule, IData, EEncoding } from './interface'; 3 | import { handlers } from './handlers'; 4 | import { TidyConfig } from './config'; 5 | 6 | const $github = 'https://github.com/DrKain/tidy-url'; 7 | 8 | export class TidyCleaner { 9 | public rules: IRule[] = []; 10 | 11 | /** 12 | * Stores config options for this cleaner. If you would like to 13 | * use multiple configs simply create a new instance 14 | */ 15 | public config: TidyConfig = new TidyConfig(); 16 | 17 | /** 18 | * Contains all logged information from the last clean, even if `config.silent` was `true`. 19 | * This will be reset when a new URL is cleaned. This is for debugging and not to be relied upon 20 | */ 21 | public loglines: { type: string; message: string }[] = []; 22 | 23 | /** 24 | * The full list of all rules with default value 25 | * that are not used in the main rules file to save space. 26 | */ 27 | get expandedRules() { 28 | return this.rules.map((rule) => { 29 | return Object.assign( 30 | { 31 | rules: [], 32 | replace: [], 33 | exclude: [], 34 | redirect: '', 35 | amp: null, 36 | decode: null 37 | }, 38 | rule 39 | ) as IRule; 40 | }); 41 | } 42 | 43 | constructor() { 44 | try { 45 | this.syncDeprecatedToConfig(); 46 | // Load the rules 47 | this.rules = require('../data/rules.js'); 48 | } catch (error) { 49 | // If this fails nothing can be cleaned 50 | this.log(`${error}`, 'error'); 51 | this.rules = []; 52 | } 53 | } 54 | 55 | /** 56 | * Only log to the console if debug is enabled 57 | * @param str Message 58 | */ 59 | private log(str: string, type: 'all' | 'error' | 'info' | 'warn' | 'debug') { 60 | this.loglines.push({ type, message: str }); 61 | if (this.config.silent !== false) console.log(`[${type}] ${str}`); 62 | } 63 | 64 | /** 65 | * Rebuild to ensure trailing slashes or encoded characters match. 66 | * @param url Any URL 67 | */ 68 | public rebuild(url: string): string { 69 | const original = new URL(url); 70 | return original.protocol + '//' + original.host + original.pathname + original.search + original.hash; 71 | } 72 | 73 | /** 74 | * This lets users know when they are using the deprecated variables that will 75 | * be removed in a few updates. 76 | */ 77 | private syncDeprecatedToConfig() {} 78 | 79 | /** @deprecated Import `validateURL` instead */ 80 | public validate(url: string): boolean { 81 | return validateURL(url); 82 | } 83 | 84 | /** 85 | * Clean a URL 86 | * @param _url Any URL 87 | * @returns IData 88 | */ 89 | public clean(_url: string, allowReclean = true): IData { 90 | if (!allowReclean) this.loglines = []; 91 | 92 | this.syncDeprecatedToConfig(); 93 | 94 | // Default values 95 | const data: IData = { 96 | url: _url, 97 | info: { 98 | original: _url, 99 | reduction: 0, 100 | difference: 0, 101 | replace: [], 102 | removed: [], 103 | handler: null, 104 | match: [], 105 | decoded: null, 106 | is_new_host: false, 107 | isNewHost: false, 108 | full_clean: false, 109 | fullClean: false 110 | } 111 | }; 112 | 113 | // Make sure the URL is valid before we try to clean it 114 | if (!validateURL(_url)) { 115 | if (_url !== 'undefined' && _url.length > 0) { 116 | this.log('Invalid URL: ' + _url, 'error'); 117 | } 118 | return data; 119 | } 120 | 121 | // If there's no params, we can skip the rest of the process 122 | if (this.config.allowAMP && urlHasParams(_url) === false) { 123 | data.url = data.info.original; 124 | return data; 125 | } 126 | 127 | // Rebuild to ensure trailing slashes or encoded characters match 128 | let url = this.rebuild(_url); 129 | data.url = url; 130 | 131 | // List of parmeters that will be deleted if found 132 | let to_remove: string[] = []; 133 | 134 | const original = new URL(url); 135 | const cleaner = original.searchParams; 136 | const cleaner_ci = new URLSearchParams(); 137 | 138 | let pathname = original.pathname; 139 | 140 | // Case insensitive cleaner for the redirect rule 141 | cleaner.forEach((v, k) => cleaner_ci.append(k.toLowerCase(), v)); 142 | 143 | // Loop through the rules and match them to the host name 144 | for (const rule of this.expandedRules) { 145 | // Match the host or the full URL 146 | let match_s = original.host; 147 | if (rule.match_href === true) match_s = original.href; 148 | // Reset lastIndex 149 | rule.match.lastIndex = 0; 150 | if (rule.match.exec(match_s) !== null) { 151 | // Loop through the rules and add to to_remove 152 | to_remove = [...to_remove, ...(rule.rules || [])]; 153 | data.info.replace = [...data.info.replace, ...(rule.replace || [])]; 154 | data.info.match.push(rule); 155 | } 156 | } 157 | 158 | // Stop cleaning if any exclude rule matches 159 | let ex_pass = true; 160 | for (const rule of data.info.match) { 161 | for (const reg of rule.exclude) { 162 | reg.lastIndex = 0; 163 | if (reg.exec(url) !== null) ex_pass = false; 164 | } 165 | } 166 | 167 | if (!ex_pass) { 168 | data.url = data.info.original; 169 | return data; 170 | } 171 | 172 | // Check if the match has any amp rules, if not we can redirect 173 | const hasAmpRule = data.info.match.find((item) => item.amp); 174 | if (this.config.allowAMP === true && hasAmpRule === undefined) { 175 | // Make sure there are no parameters before resetting 176 | if (!urlHasParams(url)) { 177 | data.url = data.info.original; 178 | return data; 179 | } 180 | } 181 | 182 | // Delete any matching parameters 183 | for (const key of to_remove) { 184 | if (cleaner.has(key)) { 185 | data.info.removed.push({ key, value: cleaner.get(key) as string }); 186 | cleaner.delete(key); 187 | } 188 | } 189 | 190 | // Update the pathname if needed 191 | for (const key of data.info.replace) { 192 | const changed = pathname.replace(key, ''); 193 | if (changed !== pathname) pathname = changed; 194 | } 195 | 196 | // Rebuild URL 197 | data.url = original.protocol + '//' + original.host + pathname + original.search + original.hash; 198 | 199 | // Redirect if the redirect parameter exists 200 | if (this.config.allowRedirects) { 201 | for (const rule of data.info.match) { 202 | if (!rule.redirect) continue; 203 | 204 | const target = rule.redirect; 205 | let value = cleaner_ci.get(target) as string; 206 | 207 | // Sometimes the parameter is encoded 208 | const isEncoded = decodeURL(value, EEncoding.urlc); 209 | if (isEncoded !== value && validateURL(isEncoded)) value = isEncoded; 210 | 211 | if (target.length && cleaner_ci.has(target)) { 212 | if (validateURL(value)) { 213 | data.url = `${value}` + original.hash; 214 | if (allowReclean) data.url = this.clean(data.url, false).url; 215 | } else { 216 | this.log('Failed to redirect: ' + value, 'error'); 217 | } 218 | } 219 | } 220 | } 221 | 222 | // De-amp the URL 223 | if (this.config.allowAMP === false) { 224 | for (const rule of data.info.match) { 225 | try { 226 | // Ensure at least one rule exists 227 | if (rule.amp && (rule.amp.regex || rule.amp.replace || rule.amp.sliceTrailing)) { 228 | // Handle replacing text in the URL 229 | if (rule.amp.replace) { 230 | data.info.handler = rule.name; 231 | this.log('AMP Replace: ' + rule.amp.replace.text, 'info'); 232 | const toReplace = rule.amp.replace.text; 233 | const toReplaceWith = rule.amp.replace.with ?? ''; 234 | data.url = data.url.replace(toReplace, toReplaceWith); 235 | } 236 | 237 | // Use RegEx capture groups 238 | if (rule.amp.regex && data.url.match(rule.amp.regex)) { 239 | data.info.handler = rule.name; 240 | this.log('AMP RegEx: ' + rule.amp.regex, 'info'); 241 | 242 | rule.amp.regex.lastIndex = 0; 243 | const result = rule.amp.regex.exec(data.url); 244 | 245 | // If there is a result, replace the URL 246 | if (result && result[1]) { 247 | let target = decodeURIComponent(result[1]); 248 | // Add the protocol when it's missing 249 | if (!target.startsWith('https')) target = 'https://' + target; 250 | // Valiate the URL to make sure it's still good 251 | if (validateURL(target)) { 252 | // Sometimes the result is another domain that has its own tracking parameters 253 | // So a re-clean can be useful. 254 | data.url = allowReclean ? this.clean(target, false).url : target; 255 | } 256 | } else { 257 | this.log('AMP RegEx failed to get a result for ' + rule.name, 'error'); 258 | } 259 | } 260 | 261 | // TODO: Apply to existing rules 262 | if (rule.amp.sliceTrailing) { 263 | if (data.url.endsWith(rule.amp.sliceTrailing)) { 264 | data.url = data.url.slice(0, -rule.amp.sliceTrailing.length); 265 | } 266 | } 267 | 268 | // Remove trailing amp/ or /amp 269 | if (data.url.endsWith('%3Famp')) data.url = data.url.slice(0, -6); 270 | if (data.url.endsWith('amp/')) data.url = data.url.slice(0, -4); 271 | } 272 | } catch (error) { 273 | this.log(`${error}`, 'error'); 274 | } 275 | } 276 | } 277 | 278 | // Decode handler 279 | for (const rule of data.info.match) { 280 | try { 281 | this.log(`Processing decode rule (${rule.name})`, 'debug'); 282 | if (!rule.decode) continue; 283 | // Make sure the target parameter exists 284 | if (!cleaner.has(rule.decode.param) && rule.decode.targetPath !== true) continue; 285 | // These will almost always be clickjacking links, so use the allowRedirects rule if enabled 286 | if (!this.config.allowRedirects) continue; 287 | // Don't process the decode handler if it's disabled 288 | if (this.config.allowCustomHandlers === false && rule.decode.handler) continue; 289 | // Decode the string using selected encoding 290 | const encoding = rule.decode.encoding || 'base64'; 291 | // Sometimes the website path is what we need to decode 292 | let lastPath = pathname.split('/').pop(); 293 | // This will be null if the param doesn't exist 294 | const param = cleaner.get(rule.decode.param); 295 | // Use a default string 296 | let encodedString: string = ''; 297 | 298 | if (lastPath === undefined) lastPath = ''; 299 | 300 | // Decide what we are decoding 301 | if (param === null) encodedString = lastPath; 302 | else if (param) encodedString = param; 303 | else continue; 304 | 305 | if (typeof encodedString !== 'string') { 306 | this.log(`Expected ${encodedString} to be a string`, 'error'); 307 | continue; 308 | } 309 | 310 | let decoded = decodeURL(encodedString, encoding); 311 | let target = ''; 312 | let recleanData = null; 313 | 314 | // If the response is JSON, decode and look for a key 315 | if (isJSON(decoded)) { 316 | const json = JSON.parse(decoded); 317 | target = json[rule.decode.lookFor]; 318 | // Add to the info response 319 | data.info.decoded = json; 320 | } else if (this.config.allowCustomHandlers === true && rule.decode.handler) { 321 | // Run custom URL handlers for websites 322 | const handler = handlers[rule.decode.handler]; 323 | 324 | if (typeof handler === 'undefined') { 325 | this.log('Handler was not found for ' + rule.decode.handler, 'error'); 326 | } 327 | 328 | if (rule.decode.handler && handler) { 329 | data.info.handler = rule.decode.handler; 330 | 331 | // Pass the handler a bunch of information it can use 332 | const result = handler.exec(data.url, { 333 | decoded, 334 | lastPath, 335 | urlParams: new URL(data.url).searchParams, 336 | fullPath: pathname, 337 | originalURL: data.url 338 | }); 339 | 340 | // If the handler threw an error or the URL is invalid 341 | if (result.error || validateURL(result.url) === false || result.url.trim() === '') { 342 | if (result.error) this.log(result.error, 'error'); 343 | else this.log('Unknown error with decode handler, empty response returned', 'error'); 344 | } 345 | 346 | // result.url will always by the original URL when an error is thrown 347 | recleanData = result.url; 348 | } 349 | } else { 350 | // If the response is a string we can continue 351 | target = decoded; 352 | } 353 | 354 | // Re-clean the URL after handler result 355 | target = allowReclean ? this.clean(recleanData ?? target, false).url : recleanData ?? target; 356 | 357 | // If the key we want exists and is a valid url then update the data url 358 | if (target && target !== '' && validateURL(target)) { 359 | data.url = `${target}` + original.hash; 360 | } 361 | } catch (error) { 362 | this.log(`${error}`, 'error'); 363 | } 364 | } 365 | 366 | // Handle empty hash / anchors 367 | if (_url.endsWith('#')) { 368 | data.url += '#'; 369 | url += '#'; 370 | } 371 | 372 | // Remove empty values when requested 373 | for (const rule of data.info.match) { 374 | if (rule.rev) data.url = data.url.replace(/=(?=&|$)/gm, ''); 375 | } 376 | 377 | const diff = getLinkDiff(data.url, url); 378 | data.info = Object.assign(data.info, diff); 379 | 380 | // If the link is longer then we have an issue 381 | if (data.info.reduction < 0) { 382 | this.log(`Reduction is ${data.info.reduction}. Please report this link on GitHub: ${$github}/issues\n${data.info.original}`, 'error'); 383 | data.url = data.info.original; 384 | } 385 | 386 | data.info.fullClean = true; 387 | data.info.full_clean = true; 388 | 389 | // Reset the original URL if there is no change, just to be safe 390 | if (data.info.difference === 0 && data.info.reduction === 0) { 391 | data.url = data.info.original; 392 | } 393 | 394 | return data; 395 | } 396 | } 397 | 398 | export const TidyURL = new TidyCleaner(); 399 | export const clean = (url: string) => TidyURL.clean(url); 400 | -------------------------------------------------------------------------------- /src/interface.ts: -------------------------------------------------------------------------------- 1 | export interface IRule { 2 | /** Name of the website */ 3 | name: string; 4 | /** Regex to test against the host */ 5 | match: RegExp; 6 | /** Regex to test against the full URL */ 7 | match_href: boolean; 8 | /** All parameters that match these rules will be removed */ 9 | rules: string[]; 10 | /** 11 | * Used in special cases where parts of the URL needs to be modified. 12 | * See the amazon.com rule for an example. 13 | */ 14 | replace: any[]; 15 | /** 16 | * Used to auto-redirect to a different URL based on the parameter. 17 | * This is used to skip websites that track external links. 18 | */ 19 | redirect: string; 20 | /** 21 | * There's a whole number of reasons why you don't want AMP links, 22 | * too many to fit in this description. 23 | * See this link for more info: https://redd.it/ehrq3z 24 | */ 25 | amp: { 26 | /** 27 | * Standard AMP handling using RegExp capture groups. 28 | */ 29 | regex?: RegExp; 30 | /** 31 | * Replace text in the URL. If `with` is used the text will be 32 | * replaced with what you set instead of removing it. 33 | */ 34 | replace?: { 35 | /** The text or RegEx you want to replace */ 36 | text: string | RegExp; 37 | /** The text you want to replace it with. Optional */ 38 | with?: string; 39 | /** Currently has no effect, this will change in another update */ 40 | target?: 'host' | 'full'; 41 | }; 42 | /** 43 | * Slice off a trailing string, these are usually "/amp" or "amp/" 44 | * This setting should help prevent breaking any pages. 45 | */ 46 | sliceTrailing?: string; 47 | }; 48 | /** 49 | * Used to decode a parameter or path, then redirect based on the returned object 50 | */ 51 | decode: { 52 | /** Target parameter */ 53 | param?: string; 54 | /** If the decoded response is JSON, this will look for a certain key */ 55 | lookFor?: string; 56 | /** Decide what encoding to use */ 57 | encoding?: EEncoding; 58 | /** Target the full path instead of a parameter */ 59 | targetPath?: boolean; 60 | /** Use a custom handler found in handlers.ts */ 61 | handler?: string; 62 | }; 63 | /** Remove empty values */ 64 | rev: boolean; 65 | } 66 | 67 | export interface IData { 68 | /** Cleaned URL */ 69 | url: string; 70 | /** Some debugging information about what was changed */ 71 | info: { 72 | /** Original URL before cleaning */ 73 | original: string; 74 | /** URL reduction as a percentage */ 75 | reduction: number; 76 | /** Number of characters removed */ 77 | difference: number; 78 | /** RegEx Replacements */ 79 | replace: any[]; 80 | /** Parameters that were removed */ 81 | removed: { key: string; value: string }[]; 82 | /** Handler used */ 83 | handler: string | null; 84 | /** Rules matched */ 85 | match: any[]; 86 | /** The decoded object from the decode parameter (if it exists) */ 87 | decoded: { [key: string]: any } | null; 88 | /** @deprecated Please use `isNewHost`. This will be removed in the next major update. */ 89 | is_new_host: boolean; 90 | /** If the compared links have different hosts */ 91 | isNewHost: boolean; 92 | /** @deprecated Please use `fullClean`. This will be removed in the next major update. */ 93 | full_clean: boolean; 94 | /** If the code reached the end of the clean without error */ 95 | fullClean: boolean; 96 | }; 97 | } 98 | 99 | export enum EEncoding { 100 | base64 = 'base64', 101 | base32 = 'base32', 102 | base45 = 'base45', 103 | url = 'url', 104 | urlc = 'urlc', 105 | binary = 'binary', 106 | hex = 'hex' 107 | } 108 | 109 | export interface IConfig { 110 | /** 111 | * There's a whole number of reasons why you don't want AMP links, 112 | * too many to fit in this description. 113 | * See this link for more info: https://redd.it/ehrq3z 114 | */ 115 | allowAMP: boolean; 116 | /** 117 | * Custom handlers for specific websites that use tricky URLs 118 | * that make it harder to "clean" 119 | */ 120 | allowCustomHandlers: boolean; 121 | /** 122 | * Used to auto-redirect to a different URL based on the parameter. 123 | * This is used to skip websites that track external links. 124 | */ 125 | allowRedirects: boolean; 126 | /** Nothing logged to console */ 127 | silent: boolean; 128 | } 129 | 130 | export interface IHandlerArgs { 131 | /** The attemp made at decoding the string, may be invalid */ 132 | decoded: string; 133 | /** The last part of the URL path, split by a forward slash */ 134 | lastPath: string; 135 | /** The full URL path excluding the host */ 136 | fullPath: string; 137 | /** A fresh copy of URLSearchParams */ 138 | urlParams: URLSearchParams; 139 | /** The original URL */ 140 | readonly originalURL: string; 141 | } 142 | 143 | export interface IHandler { 144 | readonly note?: string; 145 | exec: ( 146 | /** The original URL */ 147 | str: string, 148 | /** Various args that can be used when writing a handler */ 149 | args: IHandlerArgs 150 | ) => { 151 | /** The original URL */ 152 | url: string; 153 | error?: any; 154 | }; 155 | } 156 | 157 | export interface ILinkDiff { 158 | /** @deprecated Please use isNewHost */ 159 | is_new_host: boolean; 160 | /** If the compared links have different hosts */ 161 | isNewHost: boolean; 162 | difference: number; 163 | reduction: number; 164 | } 165 | 166 | export interface IGuessEncoding { 167 | base64: boolean; 168 | isJSON: boolean; 169 | } 170 | -------------------------------------------------------------------------------- /src/postbuild.ts: -------------------------------------------------------------------------------- 1 | const { readFileSync, writeFileSync } = require('fs'); 2 | 3 | // This is the script that runs after building 4 | 5 | const version = require('../package.json').version; 6 | let data = readFileSync('./data/tidy.user.js', 'utf-8'); 7 | const rules = require('../data/rules.js'); 8 | 9 | /** 10 | * Update userscript version (to make it fetch the new rules) 11 | */ 12 | const bumpUserscript = () => { 13 | data = data.replace(/\/\/ @version (.*)/gi, '// @version ' + version); 14 | console.log('Bumped userscript version'); 15 | writeFileSync('./data/tidy.user.js', data); 16 | }; 17 | 18 | /** 19 | * Generate supported-sites.txt 20 | * It's messy, but that's fine. It's not important. 21 | */ 22 | const generateSupported = () => { 23 | try { 24 | let p = 0; 25 | let count = 0; 26 | let body = 'Total unique rules: %RULE_COUNT%\n\n'; 27 | 28 | // Sort rules and set padding width 29 | let lines = rules 30 | .filter((rule: any) => { 31 | if (rule.name === 'Global') count = rule.rules.length; 32 | return rule.name !== 'Global'; 33 | }) 34 | .sort((a: any, b: any) => a.name.localeCompare(b.name)) 35 | .map((rule: any) => { 36 | if (rule.name.length > p) p = rule.name.length; 37 | return rule; 38 | }); 39 | 40 | // Create table header 41 | body += ['| Match'.padEnd(p + 2, ' ') + ' | Rules |', '| :'.padEnd(p + 2, '-') + ' | :---- |'].join('\n') + '\n'; 42 | 43 | // Append rules to table 44 | body += lines 45 | .map((rule: any) => { 46 | // prettier-ignore 47 | const n = (rule.rules ? rule.rules.length : 0) + 48 | (rule.replace ? rule.replace.length : 0) + 49 | (rule.decode ? 1: 0) + 50 | (rule.redirect ? 1 : 0) + 51 | (rule.amp ? 1 : 0); 52 | count += n; 53 | return `| ${rule.name.padEnd(p, ' ')} | ${`${n}`.padEnd(5, ' ')} |`; 54 | }) 55 | .join('\n'); 56 | 57 | // Update the rule count 58 | body = body.replace('%RULE_COUNT%', count as any); 59 | 60 | // Write 61 | writeFileSync('./data/supported-sites.txt', body); 62 | console.log('Updated supported-sites.txt'); 63 | } catch (e) { 64 | console.log(e); 65 | } 66 | }; 67 | 68 | bumpUserscript(); 69 | generateSupported(); 70 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { EEncoding, IGuessEncoding, ILinkDiff } from './interface'; 2 | 3 | /** 4 | * Accepts any base64 string and attempts to decode it. 5 | * If run through the browser `atob` will be used, otherwise 6 | * the code will use `Buffer.from`. 7 | * If there's an error the original string will be returned. 8 | * @param str String to be decoded 9 | * @returns Decoded string 10 | */ 11 | export const decodeBase64 = (str: string): string => { 12 | try { 13 | let result = str; 14 | 15 | if (typeof atob === 'undefined') { 16 | result = Buffer.from(str, 'base64').toString('binary'); 17 | } else { 18 | result = atob(str); 19 | } 20 | 21 | return result; 22 | } catch (error) { 23 | return str; 24 | } 25 | }; 26 | 27 | /** 28 | * Checks if data is valid JSON. The result will be either `true` or `false`. 29 | * @param data Any string that might be JSON 30 | * @returns true or false 31 | */ 32 | export const isJSON = (data: string): boolean => { 33 | try { 34 | const sample = JSON.parse(data); 35 | if (typeof sample !== 'object') return false; 36 | return true; 37 | } catch (error) { 38 | return false; 39 | } 40 | }; 41 | 42 | /** 43 | * Check if a domain has any URL parameters 44 | * @param url Any valid URL 45 | * @returns true / false 46 | */ 47 | export const urlHasParams = (url: string): boolean => { 48 | return new URL(url).searchParams.toString().length > 0; 49 | }; 50 | 51 | /** 52 | * Determine if the input is a valid URL or not. This will only 53 | * accept http and https protocols. 54 | * @param url Any URL 55 | * @returns true / false 56 | */ 57 | export const validateURL = (url: string): boolean => { 58 | try { 59 | const pass = ['http:', 'https:']; 60 | const test = new URL(url); 61 | const prot = test.protocol.toLowerCase(); 62 | 63 | if (!pass.includes(prot)) { 64 | throw new Error('Not acceptable protocol: ' + prot); 65 | } 66 | 67 | return true; 68 | } catch (error) { 69 | if (url !== 'undefined' && url !== 'null' && url.length > 0) { 70 | throw new Error(`Invalid URL: ` + url); 71 | } 72 | return false; 73 | } 74 | }; 75 | 76 | /** 77 | * Check if a string is b64. For now this should only be 78 | * used in testing. 79 | * @param str Any possible b64 string 80 | * @returns true/false 81 | */ 82 | export const isB64 = (str: string): boolean => { 83 | try { 84 | const regex = /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/; 85 | return regex.test(str); 86 | } catch (error) { 87 | // Using try/catch to be safe 88 | return false; 89 | } 90 | }; 91 | 92 | /** 93 | * DO NOT USE THIS IN HANDLERS. 94 | * This is purely for use in testing to save time. 95 | * This is not reliable, there are many incorrect 96 | * matches and it will fail in a lot of cases. 97 | * Do not use it anywhere else. 98 | * @param str Any string 99 | * @returns An object with possible encodings 100 | */ 101 | export const guessEncoding = (str: string): IGuessEncoding => { 102 | return { 103 | base64: isB64(str), 104 | isJSON: isJSON(str) 105 | }; 106 | }; 107 | 108 | /** 109 | * Calculates the difference between two links and returns an object of information. 110 | * @param firstURL Any valid URL 111 | * @param secondURL Any valid URL 112 | * @returns The difference between two links 113 | */ 114 | export const getLinkDiff = (firstURL: string, secondURL: string): ILinkDiff => { 115 | const oldUrl = new URL(firstURL); 116 | const newUrl = new URL(secondURL); 117 | 118 | return { 119 | is_new_host: oldUrl.host !== newUrl.host, 120 | isNewHost: oldUrl.host !== newUrl.host, 121 | difference: secondURL.length - firstURL.length, 122 | reduction: +(100 - (firstURL.length / secondURL.length) * 100).toFixed(2) 123 | }; 124 | }; 125 | 126 | export const regexExtract = (regex: RegExp, str: string): string[] => { 127 | let matches = null; 128 | let result: string[] = []; 129 | let i = 0; 130 | 131 | // Limit to 10 to avoid infinite loop 132 | if ((matches = regex.exec(str)) !== null && i !== 10) { 133 | i++; 134 | if (matches.index === regex.lastIndex) regex.lastIndex++; 135 | matches.forEach((v) => result.push(v)); 136 | } 137 | 138 | return result; 139 | }; 140 | 141 | /** 142 | * These are methods that have not been written yet, 143 | * the original string will be returned. 144 | */ 145 | const _placeholder = (decoded: string) => decoded; 146 | const decoders: Record string> = { 147 | [EEncoding.url]: (decoded: string) => decodeURI(decoded), 148 | [EEncoding.urlc]: (decoded: string) => decodeURIComponent(decoded), 149 | [EEncoding.base32]: _placeholder, 150 | [EEncoding.base45]: _placeholder, 151 | [EEncoding.base64]: (decoded: string) => decodeBase64(decoded), 152 | [EEncoding.binary]: _placeholder, 153 | [EEncoding.hex]: (decoded: string) => { 154 | let hex = decoded.toString(); 155 | let out = ''; 156 | for (var i = 0; i < hex.length; i += 2) { 157 | out += String.fromCharCode(parseInt(hex.substr(i, 2), 16)); 158 | } 159 | return out; 160 | } 161 | }; 162 | 163 | /** 164 | * Attempts to decode a URL or string using the selected method. 165 | * If the decoding fails the original string will be returned. 166 | * `encoding` is optional and will default to base64 167 | * @param str String to decode 168 | * @param encoding Encoding to use 169 | * @returns decoded string 170 | */ 171 | export const decodeURL = (str: string, encoding: EEncoding = EEncoding.base64): string => { 172 | try { 173 | return decoders[encoding](str); 174 | } catch (error) { 175 | return str; 176 | } 177 | }; 178 | -------------------------------------------------------------------------------- /test.ts: -------------------------------------------------------------------------------- 1 | import { TidyURL } from './src'; 2 | import { decodeBase64, guessEncoding } from './src/utils'; 3 | 4 | TidyURL.config.setMany({ 5 | silent: false, 6 | allowAMP: false, 7 | allowCustomHandlers: true, 8 | allowRedirects: true 9 | }); 10 | 11 | const tests = [ 12 | // Delete test URLs before commit 13 | '' 14 | ]; 15 | 16 | for (const test of tests) { 17 | if (test.length === 0) continue; 18 | const link = TidyURL.clean(test); 19 | 20 | // All tests should pass before publishing 21 | if (link.info.reduction < 0) { 22 | console.log(link.url); 23 | throw Error('Reduction less than 0'); 24 | } 25 | 26 | // If last link, log additional information that can be used for debugging 27 | if (test === tests[tests.length - 1]) { 28 | const params = new URL(link.url).searchParams; 29 | console.log(link); 30 | console.log('--- start:params ---'); 31 | params.forEach((val, key) => { 32 | // This is just to save time when testing URLs 33 | const possible = guessEncoding(val); 34 | if (possible.base64) console.log(`'${key}' might be base64: ${decodeBase64(val)}`); 35 | if (possible.isJSON) console.log(`'${key}' might be JSON: ${JSON.stringify(val)}`); 36 | if (/script/i.test(val)) console.warn(`'${key}' Possible XSS`); 37 | console.log({ [key]: val }); 38 | }); 39 | console.log('--- end:params ---'); 40 | } 41 | 42 | console.log(TidyURL.loglines); 43 | console.log('Input: ' + link.info.original); 44 | console.log('Clean: ' + link.url); 45 | console.log('New Host: ' + link.info.isNewHost); 46 | console.log(`${link.info.reduction}% smaller (${link.info.difference} characters)\n\n`); 47 | } 48 | -------------------------------------------------------------------------------- /text-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrKain/tidy-url/234e706d71ec5485875f5fb876d191a65a298d0c/text-logo.png -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "outDir": "./lib", 7 | "strict": true, 8 | "esModuleInterop": true 9 | }, 10 | "include": ["src"], 11 | "exclude": ["node_modules", "src/postbuild.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | mode: 'production', 5 | entry: './src/index.ts', 6 | module: { 7 | rules: [ 8 | { 9 | test: /\.tsx?$/, 10 | use: 'ts-loader', 11 | exclude: /node_modules/ 12 | } 13 | ] 14 | }, 15 | resolve: { 16 | extensions: ['.tsx', '.ts', '.js'] 17 | }, 18 | output: { 19 | filename: 'tidyurl.min.js', 20 | path: path.resolve(__dirname, 'lib'), 21 | libraryTarget: 'var', 22 | library: 'tidyurl' 23 | } 24 | }; 25 | --------------------------------------------------------------------------------