├── .gitignore ├── LICENSE ├── README.md ├── build.py ├── css ├── help.css ├── popup.css └── redirector.css ├── help.html ├── icon.html ├── images ├── icon-dark-theme-128.png ├── icon-dark-theme-16.png ├── icon-dark-theme-19.png ├── icon-dark-theme-32.png ├── icon-dark-theme-38.png ├── icon-dark-theme-48.png ├── icon-dark-theme-64.png ├── icon-light-theme-128.png ├── icon-light-theme-16.png ├── icon-light-theme-19.png ├── icon-light-theme-32.png ├── icon-light-theme-38.png ├── icon-light-theme-48.png └── icon-light-theme-64.png ├── js ├── background.js ├── editredirect.js ├── importexport.js ├── organizemode.js ├── popup.js ├── redirect.js ├── redirectorpage.js ├── stub.js └── util.js ├── manifest.json ├── nex-build.sh ├── popup.html ├── privacy.md ├── promo ├── screenshot1.png ├── screenshot2.png ├── screenshot3.png ├── screenshot4.png ├── smalltile.png └── tiles.html └── redirector.html /.gitignore: -------------------------------------------------------------------------------- 1 | *.xpi 2 | *.DS_Store 3 | *.zip 4 | build/* 5 | ffox.sh 6 | devprofile/* 7 | debug.sh 8 | *.pem 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Einar Egilsson 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 | ## Description 2 | Web browser extension (Firefox, Vivaldi, Chrome, Opera, Edge) to redirect URLs based on regex or wildcard patterns. 3 | 4 | ## Tribute 5 | In loving memory of Einar Egilsson, who gave us Redirector and selflessly nurtured it for many years. We miss you Einar, and will always remember your kindness and generosity. 6 | 7 | ## Download Links 8 | * [Firefox](https://addons.mozilla.org/firefox/addon/redirector/) 9 | * [Google Chrome and Vivaldi](https://chrome.google.com/webstore/detail/redirector/ocgpenflpmgnfapjedencafcfakcekcd) 10 | 14 | 15 | ## Examples 16 | ### De-mobilizer 17 | - Example URL: `https://en.m.wikipedia.org/` 18 | - Include pattern: `^(https?://)([a-z0-9-]*\.)m(?:obile)?\.(.*)` 19 | - Redirect to: `$1$2$3` 20 | - Pattern type: Regular Expression 21 | - Description: Always show the desktop version of websites 22 | 23 | ### AMP redirect 24 | - Example URL: `https://www.google.com/amp/www.example.com/amp/document` 25 | - Include pattern: `^(?:https?://)www.(?:google|bing).com/amp/(?:s/)?(.*)` 26 | - Redirect to: `https://$1` 27 | - Pattern type: Regular Expression 28 | - Description: AMP is bad: 29 | 30 | ### Doubleclick escaper 31 | - Example URL: `https://ad.doubleclick.net/ddm/trackclk/N135005.2681608PRIVATENETWORK/B20244?https://www.example.com` 32 | - Include pattern: `^(?:https?://)ad.doubleclick.net/.*\?(http?s://.*)` 33 | - Redirect to: `$1` 34 | - Pattern type: Regular Expression 35 | - Description: Remove doubleclick link tracking / fix problems with doubleclick host-based blocking 36 | 37 | ### YouTube Shorts to YouTube 38 | - Example URL: `https://www.youtube.com/shorts/video-id` 39 | - Include pattern: `^(?:https?://)(?:www.)?youtube.com/shorts/([a-zA-Z0-9_-]+)(.*)` 40 | - Redirect to: `https://www.youtube.com/watch?v=$1$2` 41 | - Pattern type: Regular Expression 42 | - Description: Redirect YouTube Shorts to regular YouTube 43 | 44 | ### Fun with !bangs 45 | What are bangs?: 46 | 47 | #### Use DuckDuckGo.com !bangs on Google 48 | - Example URL: `https://www.google.com/search?&ei=-FvkXcOVMo6RRwW5p5DgBg&q=asdfasdf%21+sadfas&oq=%21asdfasdf+sadfas&gs_l=asdfsadfafsgaf` 49 | - Include pattern: `^(?:https?://)(?:www.)google\.(?:com|au|de|co\.uk)/search\?(?:.*)?(?:oq|q)=([^\&]*\+)?((?:%21|!)[^\&]*)` 50 | - Redirect to: `https://duckduckgo.com/?q=$1$2` 51 | - Pattern type: Regular Expression 52 | - Description: Redirect any Google query with a !bang to DDG 53 | 54 | ### Custom DuckDuckGo.com !bangs 55 | 56 | #### DDG !example Base 57 | - Example URL: `https://duckduckgo.com/?q=!`__example__`&get=other` 58 | - Include pattern: `^(?:https?://)(?:.*\.)?duckduckgo.com/\?q=(?:%21|!)`__example__`(?=[^\+]|$)(?=\W|$)` 59 | - Redirect to: `https://example.com/` 60 | - Pattern type: Regular Expression 61 | - Description: Redirect to the base site when !bang is the only search parameter 62 | 63 | #### DDG !example Search 64 | - Example URL: `https://duckduckgo.com/?q=searchterm+!`__example__`+searchterm2&get=other` 65 | - Include pattern: `^(?:https?://)(?:.*\.)?duckduckgo.com/\?q=(.*\+)?(?:(?:%21|!)`__example__`)(?:\+([^\&\?\#]*))?(?:\W|$)` 66 | - Redirect to: `https://example.com/?query=$1$2` 67 | - Pattern type: Regular Expression 68 | - Description: Redirect to custom site search 69 | 70 | #### DDG !ghh git-history 71 | - Example URL: `https://duckduckgo.com/?q=!ghh+https%3A%2F%2Fgithub.com%2Fbabel%2Fbabel%2Fblob%2Fmaster%2Fpackages%2Fbabel-core%2FREADME.md&adfasfasd` 72 | - Include pattern: `^(?:https?://)duckduckgo.com/\?q=(?:(?:%21|!)ghh\+)(?:.*)(github|gitlab|bitbucket)(?:\.org|\.com)(.*?(?=\&))` 73 | - Redirect to: `https://$1.githistory.xyz$2` 74 | - Pattern type: Regular Expression 75 | - Description: Create new !ghh bang that redirects to 76 | - Advanced: 77 | - Process matches: URL decode 78 | 79 | ### Fast DuckDuckGo.com !bangs 80 | 81 | Go directly to frequently used DuckDuckGo bangs to avoid intermediary network requests. 82 | 83 | - Example URL: `https://duckduckgo.com/?q=foo+bar+%21google+test+bar` 84 | - Include pattern: `^https://duckduckgo\.com/\?q=(.*)\+(?:%21|!)google\b\+(.*?)(?:&|$)` 85 | - Redirect to: `https://google.com/search?hl=en&q=$1+$2` 86 | - Pattern type: Regular Expression 87 | - Description: DuckDuckGo → Google !bang shortcut (prefix AND suffix) 88 | - Pattern Description: Avoid extraneous + in URL with two separate patterns 89 | ### 90 | 91 | - Example URL: `https://duckduckgo.com/?q=foo+bar+%21google` 92 | - Include pattern: `^https://duckduckgo\.com/\?q=(.*?)\+?(?:%21|!)google\b\+?(.*?)(?:&|$)` 93 | - Redirect to: `https://google.com/search?hl=en&q=$1$2` 94 | - Pattern type: Regular Expression 95 | - Description: DuckDuckGo → Google !bang shortcut (prefix OR suffix) 96 | - Pattern Description: Avoid extraneous + in URL with two separate patterns 97 | 98 | ## Dark Theme 99 | If you are a Firefox user and use a dark theme, you can add these lines to your `userChrome.css` file to make Redirector's extension button more visible: 100 | 101 | ```css 102 | /* Redirector button for dark Firefox themes */ 103 | toolbarbutton#toggle-button--redirectoreinaregilssoncom-redirector[image*="active"] { filter: invert(1) brightness(6); } 104 | toolbarbutton#toggle-button--redirectoreinaregilssoncom-redirector[image*="disabled"] { filter: invert(1) brightness(2.5); } 105 | ``` 106 | 107 | If you don't know what the `userChrome.css` file is, or how to edit it, please look it up on a Firefox forum instead of asking about it in this repository. Thanks! 108 | -------------------------------------------------------------------------------- /build.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import os, os.path, re, zipfile, json 4 | 5 | def get_files_to_zip(): 6 | #Exclude git stuff, build scripts etc. 7 | exclude = [ 8 | r'\.(py|sh|pem)$', #file endings 9 | r'(\\|/)\.', #hidden files 10 | r'package\.json|icon\.html', #file names 11 | r'(\\|/)(promo|unittest|build)(\\|/)' #folders 12 | ] 13 | 14 | zippable_files = [] 15 | for root, folders, files in os.walk('.'): 16 | print(root) 17 | for f in files: 18 | file = os.path.join(root,f) 19 | if not any(re.search(p, file) for p in exclude): 20 | zippable_files.append(file) 21 | return zippable_files 22 | 23 | 24 | def create_addon(files, browser): 25 | output_folder = 'build' 26 | if not os.path.isdir(output_folder): 27 | os.mkdir(output_folder) 28 | 29 | if browser == 'firefox': 30 | ext = 'xpi' 31 | else: 32 | ext = 'zip' 33 | 34 | output_file = os.path.join(output_folder, f'redirector-{browser}.{ext}') 35 | zf = zipfile.ZipFile(output_file, 'w', zipfile.ZIP_STORED) 36 | cert = 'extension-certificate.pem' 37 | 38 | print('') 39 | print(f'**** Creating addon for ${browser} ****') 40 | 41 | if browser == 'opera' and not os.path.exists(cert): 42 | print('Extension certificate does not exist, cannot create .nex file for Opera') 43 | return 44 | 45 | for f in files: 46 | print('Adding', f) 47 | if f.endswith('manifest.json'): 48 | manifest = json.load(open(f)) 49 | if browser != 'firefox': 50 | del manifest['applications'] #Firefox specific, and causes warnings in other browsers... 51 | 52 | 53 | if browser == 'firefox': 54 | del manifest['background']['persistent'] #Firefox chokes on this, is always persistent anyway 55 | 56 | if browser == 'opera': 57 | manifest['options_ui']['page'] = 'redirector.html' #Opera opens options in new tab, where the popup would look really ugly 58 | manifest['options_ui']['chrome_style'] = False 59 | 60 | zf.writestr(f[2:], json.dumps(manifest, indent=2)) 61 | else: 62 | zf.write(f[2:]) 63 | 64 | zf.close() 65 | 66 | if browser == 'opera': 67 | #Create .nex 68 | os.system('./nex-build.sh %s %s %s' % (output_file, output_file.replace('.zip', '.nex'), cert)) 69 | 70 | 71 | 72 | if __name__ == '__main__': 73 | #Make sure we can run this from anywhere 74 | folder = os.path.dirname(os.path.realpath(__file__)) 75 | os.chdir(folder) 76 | 77 | files = get_files_to_zip() 78 | 79 | print('******* REDIRECTOR BUILD SCRIPT *******') 80 | print('') 81 | 82 | create_addon(files, 'chrome') 83 | create_addon(files, 'edge') 84 | create_addon(files, 'opera') 85 | create_addon(files, 'firefox') 86 | 87 | -------------------------------------------------------------------------------- /css/help.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family:Arial, sans-serif; 3 | font-size:16px; 4 | line-height: 20px; 5 | max-width:740px; 6 | margin:20px auto; 7 | border:solid 1px #bbb; 8 | padding:50px; 9 | background: white; 10 | border-radius: 4px; 11 | color: #2b2626; 12 | } 13 | 14 | a, a:visited { 15 | color:rgb(21,90,233); 16 | text-decoration: none; 17 | } 18 | 19 | a:hover { 20 | color:black; 21 | text-decoration: underline; 22 | } 23 | 24 | html { 25 | background: #f8f8f8; 26 | } 27 | 28 | h1 { 29 | text-align: center; 30 | font-size:38px; 31 | margin-bottom:80px; 32 | } 33 | 34 | h4 { 35 | font-size:24px; 36 | margin-bottom:0px; 37 | padding-bottom:0px; 38 | } 39 | 40 | li { 41 | margin:2px; 42 | } 43 | 44 | .fields li { 45 | margin:10px 4px; 46 | } 47 | 48 | .url { 49 | font-style: italic; 50 | } 51 | 52 | td.pattern { 53 | font-weight: normal; 54 | background:transparent; 55 | } 56 | 57 | th { 58 | text-align:right; 59 | } 60 | 61 | table { 62 | margin:10px; 63 | border:solid 1px #bbb; 64 | padding:8px; 65 | margin-bottom:30px; 66 | border-radius: 3px; 67 | } 68 | 69 | .pattern { 70 | color:black; 71 | font-weight: bold; 72 | display: inline-block; 73 | padding-left:2px; 74 | padding-right:2px; 75 | border-radius:3px; 76 | background: #eee; 77 | } 78 | 79 | /* Dark mode support */ 80 | 81 | @media (prefers-color-scheme: dark) { 82 | 83 | html { 84 | background: rgb(32,33,36); 85 | } 86 | body { 87 | background: rgb(42,43,46); 88 | color: #ddd; 89 | border: solid 1px #888; 90 | } 91 | th { 92 | color: white; 93 | font-weight: normal; 94 | } 95 | a, a:visited, a:hover { 96 | color: rgb(138,179,241); 97 | } 98 | 99 | h1, h2, h3, h4, strong { 100 | color: white; 101 | } 102 | 103 | tr .pattern { 104 | color: rgb(53,180,75); 105 | } 106 | } -------------------------------------------------------------------------------- /css/popup.css: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | width: 180px; 4 | text-align: center; 5 | font-family: Arial, sans-serif; 6 | } 7 | 8 | h1 { 9 | margin-top: 4px; 10 | font-size: 22px; 11 | font-weight: bold; 12 | letter-spacing: 1.5px; 13 | } 14 | 15 | h1 span { 16 | position: relative; 17 | top: 1px; 18 | font-size: 38px; 19 | } 20 | 21 | button { 22 | margin: 5px auto !important; 23 | display: block; 24 | width: 90%; 25 | } 26 | 27 | .disabled { 28 | margin-top: -16px; 29 | height: 13px; 30 | color: red; 31 | font-size: 12px; 32 | } 33 | 34 | label { 35 | margin: 6px auto; 36 | display: block; 37 | width: 85%; 38 | text-align: left; 39 | font-size: 12px !important; 40 | } 41 | 42 | /* Firefox only */ 43 | @supports (-moz-appearance:none) { 44 | label input { 45 | position: relative; 46 | top: 1px; 47 | } 48 | } 49 | 50 | button { 51 | height: 20px; 52 | border: solid 1px #aaa !important; 53 | border-radius: 3px; 54 | color: #333; 55 | } 56 | 57 | label span { 58 | position: relative; 59 | top: 1px; 60 | } 61 | 62 | @media (prefers-color-scheme: dark) { 63 | 64 | html { 65 | background: rgb(52,53,56); 66 | color: #ddd; 67 | } 68 | 69 | h1, 70 | label { 71 | color: #eee; 72 | } 73 | 74 | th { 75 | color: #eee; 76 | font-weight: normal; 77 | } 78 | 79 | .disabled span { 80 | color: rgb(240,85,82); 81 | } 82 | 83 | button { 84 | border: solid 1px #777 !important; 85 | background-color: rgb(32,33,36) !important; 86 | color: #ddd; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /css/redirector.css: -------------------------------------------------------------------------------- 1 | 2 | /* Basic element styles */ 3 | 4 | body { 5 | font-family: Arial, sans-serif; 6 | font-size:14px; 7 | background:white; 8 | color:rgb(43, 38, 38); 9 | } 10 | 11 | h1, h2, h3, h5, h6 { 12 | text-align: center; 13 | } 14 | 15 | h1 { 16 | font-size:55px; 17 | margin-bottom:0px; 18 | cursor:default; 19 | padding:0px; 20 | letter-spacing: 2.1px; 21 | } 22 | 23 | h3 { 24 | font-size:18px; 25 | padding:0px; 26 | } 27 | 28 | h4 { 29 | text-align: left; 30 | font-size:14px; 31 | padding:0px; 32 | margin:4px; 33 | } 34 | 35 | h5 { 36 | font-size:20px; 37 | margin:-6px 0 0 0; 38 | color:#5e6163; 39 | } 40 | 41 | input[type='text'] { 42 | border-radius:2px; 43 | padding:3px; 44 | border:solid 1px #bbb; 45 | } 46 | 47 | input[type="radio"] { 48 | margin-right:5px; 49 | } 50 | 51 | /* Classes for buttons and other stuff */ 52 | 53 | .btn { 54 | background:white; 55 | border:solid 1px #bbb; 56 | border-radius:3px; 57 | cursor: pointer; 58 | font-weight: bold; 59 | display:inline-block; 60 | text-align: center; 61 | text-decoration: none; 62 | } 63 | 64 | .btn.large { 65 | font-size:18px; 66 | padding:4px 8px; 67 | 68 | } 69 | 70 | .btn.medium { 71 | font-size:14px; 72 | padding:2px 6px; 73 | } 74 | 75 | .btn:hover { 76 | color:white !important; 77 | text-decoration: none; 78 | } 79 | 80 | 81 | .btn.grey { 82 | color:#333; 83 | } 84 | 85 | .btn.grey:hover { 86 | background:#333; 87 | border:solid 1px #333; 88 | } 89 | 90 | .btn.red { 91 | color:rgb(208,52,37); 92 | } 93 | 94 | .btn.red:hover { 95 | background:rgb(208,52,37); 96 | border:solid 1px rgb(208,52,37); 97 | } 98 | 99 | .btn.blue { 100 | color:rgb(21,90,233); 101 | } 102 | 103 | .btn.blue:hover { 104 | background:rgb(21,90,233); 105 | border:solid 1px rgb(21,90,233); 106 | } 107 | 108 | .btn.green { 109 | color:green; 110 | } 111 | 112 | .btn.green:hover { 113 | background:green; 114 | border:solid 1px green; 115 | } 116 | 117 | .btn.blue.active { 118 | color: white; 119 | background:rgb(21,90,233); 120 | } 121 | 122 | #redirect-row-template { 123 | display: none; 124 | } 125 | 126 | /* Main menu with buttons */ 127 | 128 | #menu { 129 | margin: 30px auto 8px auto; 130 | max-width:90%; 131 | text-align: center; 132 | } 133 | 134 | #import-file { 135 | display:none; 136 | } 137 | 138 | /* Message box for success/failure messages */ 139 | #message-box { 140 | margin:10px auto; 141 | width:90%; 142 | color:white; 143 | max-width: 800px; 144 | border-radius:3px; 145 | height:0px; 146 | line-height: 30px; 147 | font-size:16px; 148 | text-align:center; 149 | overflow:hidden; 150 | transition: height 0.2s ease-out; 151 | position: relative; 152 | } 153 | 154 | #message-box.visible { 155 | height:30px; 156 | transition: height 0.2s ease-out; 157 | } 158 | 159 | #message-box.success { 160 | background-color: green; 161 | } 162 | 163 | #message-box.error { 164 | background-color: rgb(208,52,37);; 165 | } 166 | 167 | #message-box a { 168 | color:white; 169 | font-size:20px; 170 | position: absolute; 171 | right:6px; 172 | top:0px; 173 | cursor:pointer; 174 | } 175 | 176 | /* Table of redirects */ 177 | 178 | .redirect-table { 179 | max-width:800px; 180 | border:solid 1px #bbb; 181 | margin:0px auto; 182 | border-radius:3px; 183 | } 184 | 185 | .redirect-row { 186 | position: relative; 187 | font-size:14px; 188 | padding:8px; 189 | line-height:18px; 190 | border-bottom:solid 1px #bbb; 191 | } 192 | 193 | .redirect-info { 194 | display:table; 195 | } 196 | 197 | .redirect-info div { 198 | display:table-row; 199 | } 200 | 201 | .redirect-info div label { 202 | display:table-cell; 203 | padding:3px 5px; 204 | white-space: nowrap; 205 | } 206 | 207 | .redirect-info div p { 208 | display:table-cell; 209 | word-wrap:anywhere; 210 | max-width:700px; 211 | } 212 | 213 | .redirect-row:last-child { 214 | border-bottom: none !important; 215 | } 216 | 217 | .redirect-row:nth-child(odd) { 218 | background:#f8f8f8; 219 | } 220 | 221 | .redirect-info.disabled label, .redirect-info.disabled span, span.disabled, .redirect-info.disabled p, a.disabled, button[disabled] { 222 | color:#bbb !important; 223 | } 224 | 225 | button span { 226 | pointer-events:none; 227 | } 228 | 229 | /* Edit, Delete, Disable buttons */ 230 | .redirect-row button { 231 | font-size:13px; 232 | margin-top:5px; 233 | padding:2px; 234 | width:80px; 235 | display: inline-block; 236 | } 237 | 238 | .redirect-row h4 span.disabled-marker { 239 | color:red !important; 240 | } 241 | 242 | /* nav btns */ 243 | .move-up-btn, .move-down-btn, .move-downbottom-btn, .move-uptop-btn { 244 | width:45px !important; 245 | } 246 | 247 | .move-downbottom-btn, .move-uptop-btn { 248 | height:25px !important; 249 | } 250 | 251 | .redirect-row label { 252 | display:inline-block; 253 | width:80px; 254 | font-weight:bold; 255 | text-align: right; 256 | } 257 | 258 | .arrow { 259 | font-size:18px; 260 | } 261 | 262 | a.disabled:hover, button[disabled]:hover { 263 | cursor:default; 264 | color:#bbb !important; 265 | border:solid 1px #bbb !important; 266 | background:white !important; 267 | } 268 | 269 | /* Toggle Grouping Checkbox */ 270 | .toggle-container { 271 | display: block; 272 | position: absolute; 273 | top: 8%; 274 | right: 5%; 275 | cursor: pointer; 276 | font-size: 22px; 277 | -webkit-user-select: none; 278 | -moz-user-select: none; 279 | -ms-user-select: none; 280 | user-select: none; 281 | } 282 | 283 | .toggle-container input { 284 | position: absolute; 285 | opacity: 0; 286 | cursor: pointer; 287 | height: 0; 288 | width: 0; 289 | } 290 | 291 | .checkmark { 292 | position: absolute; 293 | height: 15px; 294 | width: 15px; 295 | background-color: #eee; 296 | } 297 | 298 | .toggle-container:hover input ~ .checkmark { 299 | background-color: #ccc; 300 | } 301 | 302 | .toggle-container input:checked ~ .checkMarked { 303 | background-color: #2196F3; 304 | } 305 | 306 | .checkmark:after { 307 | content: ""; 308 | position: absolute; 309 | display: none; 310 | } 311 | 312 | .toggle-container input:checked ~ .checkMarked:after { 313 | display: block; 314 | } 315 | 316 | .toggle-container .checkMarked:after { 317 | left: 4px; 318 | top: -1px; 319 | width: 4px; 320 | height: 9px; 321 | border: solid white; 322 | border-width: 0 3px 3px 0; 323 | -webkit-transform: rotate(45deg); 324 | -ms-transform: rotate(45deg); 325 | transform: rotate(45deg); 326 | } 327 | 328 | /* Popup form for deleting redirects */ 329 | 330 | #delete-redirect-form { 331 | position: fixed; 332 | padding:10px; 333 | width:500px; 334 | background:white; 335 | border:solid 1px #bbb; 336 | border-radius:3px; 337 | z-index: 6000; 338 | left:50%; 339 | margin-left:-260px; 340 | top:50%; 341 | margin-top:-220px; 342 | height:255px; 343 | display:none; 344 | } 345 | 346 | #delete-redirect-form div{ 347 | margin-bottom:7px; 348 | } 349 | 350 | #delete-redirect-form div label:first-child { 351 | width:130px; 352 | font-weight:bold; 353 | text-align: right; 354 | display:inline-block; 355 | vertical-align: top; 356 | } 357 | 358 | #delete-redirect-form div span { 359 | display:inline-block; 360 | width: 330px; 361 | text-overflow: ellipsis; 362 | overflow: hidden; 363 | white-space: nowrap; 364 | vertical-align: top; 365 | } 366 | 367 | /* Edit form */ 368 | 369 | #cover { 370 | position: fixed; 371 | top:0px; 372 | bottom:0px; 373 | left:0px; 374 | right:0px; 375 | z-index: 5000; 376 | background: #333; 377 | opacity: 0.5; 378 | display: none; 379 | } 380 | 381 | .blur { 382 | -webkit-filter:blur(3px); 383 | filter:blur(3px); 384 | } 385 | 386 | #edit-redirect-form { 387 | position: fixed; 388 | display:table; 389 | display:none; 390 | width:700px; 391 | background:white; 392 | border:solid 1px #bbb; 393 | border-radius:3px; 394 | z-index: 6000; 395 | left:50%; 396 | margin-left:-350px; 397 | top:50%; 398 | transform: translateY(-50%); 399 | max-height: 96vh; 400 | overflow: auto; 401 | } 402 | 403 | .form-grid { 404 | display:table; 405 | } 406 | 407 | .form-grid > div{ 408 | display:table-row; 409 | } 410 | 411 | .form-grid > div > label { 412 | display:table-cell; 413 | font-weight:bold; 414 | text-align: right; 415 | padding:6px; 416 | white-space: nowrap; 417 | width:140px; 418 | vertical-align:top; 419 | } 420 | 421 | .input-cell { 422 | padding-top:1px; 423 | } 424 | 425 | .form-grid div input[type='text'] { 426 | width:510px; 427 | font-size:14px; 428 | } 429 | 430 | .example-result { 431 | width:500px; 432 | display:inline-block; 433 | word-wrap:break-word; 434 | margin-top:5px; 435 | } 436 | 437 | .example-result-error { 438 | margin-top:5px; 439 | display:inline-block; 440 | } 441 | 442 | #unescape-matches, #escape-matches { 443 | margin-top:7px; 444 | margin-left:0px; 445 | } 446 | 447 | .input-cell label { 448 | display:block; 449 | } 450 | 451 | #apply-to { 452 | padding-top:3px; 453 | } 454 | 455 | #apply-to label span { 456 | position: relative; 457 | top:1px; 458 | } 459 | 460 | .input-cell label input { 461 | margin-left:0px; 462 | } 463 | 464 | .error { 465 | color:red; 466 | } 467 | 468 | .placeholder { 469 | color:#c0c0c0; 470 | font-size:11px; 471 | } 472 | 473 | ::-moz-placeholder { /* Firefox 19+ */ 474 | color: #c0c0c0; 475 | } 476 | 477 | .advanced { 478 | margin-top:8px; 479 | } 480 | 481 | .hidden { 482 | display:none; 483 | } 484 | 485 | #advanced-toggle { 486 | padding-top:3px; 487 | text-align: center; 488 | } 489 | 490 | #advanced-toggle a { 491 | color:rgb(21,90,233); 492 | cursor: pointer; 493 | } 494 | 495 | #advanced-toggle a:hover { 496 | text-decoration: underline; 497 | } 498 | 499 | .advanced div .input-cell label:first-child { 500 | margin-top:2px; 501 | } 502 | 503 | .advanced div .input-cell select { 504 | margin-top:4px; 505 | width:160px; 506 | } 507 | 508 | a[ng-click] { 509 | cursor:pointer; 510 | } 511 | 512 | #wildcardtype, #regextype { 513 | margin-right:10px; 514 | display:inline-block; 515 | margin-top:4px; 516 | } 517 | 518 | .button-container { 519 | margin-top:20px; 520 | text-align: center; 521 | padding-bottom:10px; 522 | } 523 | 524 | /* Footer with link */ 525 | footer { 526 | margin-top:30px; 527 | text-align: center; 528 | } 529 | 530 | footer small { 531 | font-size:10px; 532 | color:#555; 533 | } 534 | 535 | footer small a, footer small a:visited { 536 | text-decoration: none; 537 | color:rgb(21,90,233); 538 | } 539 | 540 | footer small a:hover { 541 | text-decoration: underline; 542 | } 543 | 544 | #storage-sync-option { 545 | border-top: solid 1px #bbb; 546 | display: none; 547 | } 548 | 549 | #storage-sync-option input { 550 | margin:10px; 551 | } 552 | 553 | /* Dark mode support */ 554 | 555 | @media (prefers-color-scheme: dark) { 556 | 557 | body { 558 | background: rgb(32,33,36); 559 | color: #bbb; 560 | } 561 | 562 | h1 { 563 | color: #eee; 564 | } 565 | h5, footer small { 566 | color: #aaa; 567 | } 568 | 569 | .redirect-row label { 570 | color: white; 571 | } 572 | 573 | footer small a, footer small a:visited { 574 | color: rgb(138,179,241); 575 | } 576 | 577 | .redirect-row:nth-child(odd) { 578 | background: rgb(31,32,35); 579 | } 580 | .redirect-row:nth-child(even) { 581 | background: rgb(41,42,45); 582 | } 583 | 584 | .btn { 585 | background-color: rgb(32,33,36); 586 | border: solid 1px #777; 587 | } 588 | 589 | .btn.grey { 590 | color: #ccc; 591 | } 592 | 593 | .btn.green { 594 | color: rgb(53,180,75); 595 | } 596 | 597 | .toggle { 598 | background-color: #ccc; 599 | } 600 | 601 | #message-box.success { 602 | background-color: rgb(53,203,75);; 603 | } 604 | 605 | #message-box.error { 606 | background-color: rgb(252,87,84); 607 | } 608 | 609 | .redirect-table, .redirect-row { 610 | border-color: #555 !important; 611 | } 612 | 613 | .redirect-row h4 span.disabled-marker { 614 | color:rgb(252,87,84) !important; 615 | } 616 | 617 | .btn.red { 618 | color: rgb(252,87,84); 619 | } 620 | 621 | .btn.grey[disabled]:hover { 622 | border: solid 1px #777 !important; 623 | background: rgb(32,33,36) !important; 624 | color:#555 !important;; 625 | } 626 | 627 | .btn.grey:hover { 628 | border: solid 1px white; 629 | background: black; 630 | color: white !important; 631 | } 632 | 633 | .btn.blue { 634 | color: rgb(138,179,241); 635 | } 636 | 637 | .redirect-row [data-bind="description"] { 638 | color: #eee; 639 | } 640 | 641 | .redirect-info.disabled label, .redirect-info.disabled span, span.disabled, .redirect-info.disabled p, a.disabled, button[disabled] { 642 | color:#555 !important; 643 | } 644 | 645 | #edit-redirect-form, #delete-redirect-form { 646 | background:rgb(41,42,45); 647 | border: solid 1px #888; 648 | } 649 | 650 | #advanced-toggle a, #advanced-toggle a:visited { 651 | color: rgb(138,179,241); 652 | } 653 | 654 | h3 { 655 | color: #eee; 656 | } 657 | 658 | #edit-redirect-form label, #delete-redirect-form label { 659 | color: white; 660 | font-weight: normal; 661 | } 662 | 663 | .example-result-error { 664 | color:rgb(252,87,84) !important; 665 | } 666 | 667 | #edit-redirect-form input { 668 | background: rgb(68,68,68); 669 | color: #ddd; 670 | border-color: rgb(68,68,68) !important ; 671 | border-radius: 2px; 672 | } 673 | } 674 | -------------------------------------------------------------------------------- /help.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | REDIRECTOR HELP 5 | 6 | 7 | 8 | 9 | 10 | 11 |

REDIRECTOR HELP

12 |

Table of contents

13 | 49 | 50 | 51 | 52 |

What is Redirector?

53 | 54 |

Redirector is a browser extension that allows you to automatically redirect from 55 | one webpage to another. For example, every time you visit http://abc.com you will automatically 56 | load http://def.com instead. This can be useful for instance to always redirect articles to printer friendly 57 | versions, redirect http:// to https:// for sites that support both, bypass advertising pages that appear before 58 | being able to view certain pages and more.

59 |

A new feature in v3.0 is that the result of a redirect will never be redirected again, even if it matches another include pattern. This is to prevent endless loops, for example if you have a pattern that redirects from a -> b and another that redirects from b -> a. This also removes the annoying message that the include pattern matches the result and therefore you can't create the redirect. That doesn't matter anymore because the result will never be redirected, even if it matches the include pattern again, so this should make it simpler for people to create redirects.

60 | 61 | 62 |

Basic usage

63 |

To add a new redirect you press the Redirector icon next to your address bar, and in the popup that comes up you choose the Edit Redirects button. 64 | On the settings page you can add, edit and delete redirect. Redirects will be checked in the same order as they are shown on that page, so you can move them 65 | up or down to give them higher or lower priority. The edit form will guide you by showing you an example result as you're typing in your patterns. A redirect 66 | contains the following fields: 67 |

68 |
    69 |
  • Description: The description is optional, it's only there for you to better keep track of your redirects 70 | and why they're there.
  • 71 | 72 |
  • Example url: This is an example of an url you want to redirect. It is used to help you create your redirect, and show you 73 | an example result while you're editing the redirect.
  • 74 | 75 |
  • Include pattern: This is pattern for the urls you want to redirect. In the simplest case, where you just want 76 | to redirect one specific url to another then this will just be the exact url you want to redirect. For instance, if you just want http://aaa.com to 77 | redirect to http://bbb.com then Include pattern will just be http://aaa.com. For more complex patterns that match many 78 | urls you can use either wildcards or regular expressions.
  • 79 | 80 |
  • Exclude pattern: Urls that match this pattern will never be redirected. This is not necessary to 81 | fill out, but can be useful when you want to redirect all urls that contain some text except if they contain some other text. 82 | Redirects like that can often be done with a complex regular expression, but using an exclude pattern makes it much simpler. The exclude 83 | patterns can use wildcard characters or regular expressions like the include patterns.
  • 84 | 85 |
  • Redirect to: This is the url that you will be redirected to when you open any page where the url matches the 86 | include pattern. You can use the special signs $1, $2, $3 etc. in the url, they will be replaced by the results of captures with regular 87 | expressions or stars with wildcards. For instance, if you have the include pattern http://google.com/*, redirect to http://froogle.com/$1 88 | and you open the page http://google.com/foobar, then you will be redirected to http://froogle.com/foobar, since 'foobar' was what the star replaced. $1 is for the 89 | first star in the pattern, $2 for the second and so on. For regular expression $1 is for the first parentheses, $2 for the second etc.
  • 90 | 91 |
  • Pattern type: This specifies how Redirector should interpret the patterns, either as 92 | wildcards or regular expressions.
  • 93 | 94 |
  • Process Matches: In some cases parameters in urls are encoded in different ways. The Process Matches option allows you to select a few 95 | different ways to process the Regular expression matches before using them. The decoding options available are: 96 |
      97 |
    • No Processing: This is the default. Just use the matches from the original url exactly as they are.
    • 98 |
    • URL Decode matches: A common usage of Redirector is to catch urls like 99 | http://foo.com/redirect.php?url=http%3A%2F%2Fbar%2Ecom%2Fpath and try to catch the url parameter and redirect to it. A pattern 100 | like http://foo.com/redirect.php?url=* might be used for that purpose. However, if the url parameter is escaped (also known 101 | as urlencoded) then that won't work. In the url above we see that it starts with http%3A%2F%2F instead of http://, and Firefox 102 | won't accept this as a new url to redirect to. So, in cases like these you can select the URL Decode matches option and then all 103 | matches will be URL decoded (turned from e.g. http%3A%2F%2Fbar%2Ecom to http://bar.com) before being inserted into the target url. 104 |
    • 105 |
    • Double URL Decode matches: Same as above except apply the decode function twice, if the url has been encoded twice.
    • 106 | 107 |
    • URL Encode matches: The opposite of URL Decode matches. Let's say you want to redirect all requests to 108 | a domain like http://example.com to some proxy site that took the url to proxy as an url parameter. Then you might do something like the regular expression pattern 109 | ^(http://example\.com/.*) and redirect it to http://proxysite.com?url=$1. If you used the Escape matches option then the 110 | final url would become http://proxysite.com?url=http%3A%2F%2Fexample.com%2Ffoo%2Fbar. 111 |
    • 112 | 113 |
    • Base64 Decode matches: Similar to URL Decoding, in some cases a parameter in a url might be Base64 encoded. This option will decode that parameter before using it in the target url. 114 |
    • 115 |
    116 |
  • 117 | 118 |
  • Apply to: The Apply to option is new in version 3.0 of Redirector. For 99% of cases you won't need this, so don't worry about it. 119 | By default Redirector only redirects requests from the address bar of your browser, the page you're viewing. It doesn't redirect requests for scripts, iframes, images 120 | or anything else. Now in version 3.0 it is possible to opt into that however, and redirect any type of request. Just beware that this might have performance implications 121 | if you're redirecting all types of requests and you have many redirects. 122 |
  • 123 | 124 | 125 |
126 | 127 | 128 |

Wildcards

129 | 130 |

Wildcards are the simplest way to specify include and exclude patterns. When you create a wildcard pattern there 131 | is just one special character, the asterisk *. An asterisk in your pattern will match zero or more characters and you can 132 | have more than one star in your pattern. Some examples: 133 |

    134 |
  • http://example.com/* matches http://example.com/, http://example.com/foo, http://example.com/bar and all other urls that start with http://example.com/.
  • 135 |
  • http://*.example.com matches all subdomains of example.com, like http://www.example.com, http://mail.example.com.
  • 136 |
  • http*://example.com matches both http://example.com and https://example.com.
  • 137 |
  • http://example.com/index.asp* matches http://example.com/index.asp, http://example.com/index.asp?a=b&c=d.
  • 138 |
139 | $1, $2, $3 in the redirect urls will match the text that the stars matched. Examples: 140 |
    141 |
  • http://example.com/* matches http://example.com/foobar, $1 is foobar.
  • 142 |
  • http://*.example.com/* matches http://www.example.com/foobar, $1 is www, $2 is foobar.
  • 143 |
144 |

145 | 146 | 147 |

Regular expressions

148 | 149 |

Regular expressions allow for more complicated patterns but they are a lot harder to learn than wildcards. I'm not gonna 150 | create a regex tutorial here but normal javascript regex syntax works, look at Regular-Expressions.info for 151 | an introduction to regular expressions. $1,$2 etc. can be used in the redirect url and will be replaced with contents of captures in 152 | the regular expressions. Captures are specified with parentheses. Example: http://example.com/index.asp\?id=(\d+) will match the url 153 | http://example.com/index.asp?id=12345 and $1 will be replaced by 12345. (A common mistake in regex patterns is to forget to escape 154 | the ? sign in the querystring of the url. ? is a special character in regular expressions so if you want to match an url with a querystring 155 | you should escape it as \?). To test your regular expressions, you may use any website or service. For example, RegExr.

156 | 157 | 158 |

Storage Area (Sync vs Local)

159 | 160 |

Storage Area, by default, is set to Local. If you wish to sync your redirector rules across devices, you may choose to enable Sync from Settings page. 161 | When you toggle to Sync, data will be copied over to Sync storage and local storage will be deleted. 162 | Similary, sync storage will be deleted if you disable sync and data will be moved to Local storage. 163 |

164 | Note:
  1. Google Chrome Sync and Mozilla Firefox Sync limits the storage size as per below. 165 | This limit is decided by browser vendors and Redirector addon cannot do anything about changing the below.
  2. 166 |
  3. You need to use chrome/firefox settings to setup a sync account for syncing to work. 167 | If that is not completed, Sync will just act like local storage - take note of the storage sizes below. 168 | If sync account is not setup in chrome/firefox browser settings, leave the storage area to LOCAL as it has much larger size than Sync storage size. 169 |
170 | 171 |
    172 |
  • Local Storage: 5 MB - Redirector uses this as Default upon its installation
  • 173 |
  • Sync Storage : 0.008192 MB to store "Redirects" (8192 bytes)
  • 174 |
175 |
176 |

177 | 178 | 179 |

Examples

180 | 181 |
    182 |
  1. 183 | Static redirect
    184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 |
    Example URL:http://example.com/foo
    Include pattern:http://example.com/foo
    Redirect to:http://example.com/bar
    Pattern type:Wildcard
    Example result:http://example.com/bar
    207 |
  2. 208 |
  3. 209 | Redirect using query string parameter and wildcards
    210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 |
    Example URL:http://example.com/index.php?id=12345&a=b
    Include pattern:http://example.com/index.php?id=*&a=b
    Redirect to:http://example.com/printerfriendly.php?id=$1&a=b
    Pattern type:Wildcard
    Example result:http://example.com/printerfriendly.php?id=12345&a=b
    232 |
  4. 233 |
  5. 234 | Redirect using query string parameter and regular expressions
    235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 |
    Example URL:http://example.com/index.php?id=12345&a=b
    Include pattern:http://example.com/index.php\?id=(\d+)&a=b
    Redirect to:http://example.com/printerfriendly.php?id=$1&a=b
    Pattern type:Regular Expression
    Example result:http://example.com/printerfriendly.php?id=12345&a=b
    258 |
  6. 259 |
  7. 260 | Redirect to a different folder using wildcards
    261 | The exclude pattern makes sure that there is only one folder there, so for instance 262 | http://example.com/category/fish/cat/mouse/index.php would not match. 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 |
    Example URL:http://example.com/category/fish/index.php
    Include pattern:http://example.com/category/*/index.php
    Exclude pattern:http://example.com/category/*/*/index.php
    Redirect to:http://example.com/category/cat/index.php
    Pattern type:Wildcard
    Example result:http://example.com/category/cat/index.php
    290 |
  8. 291 |
  9. 292 | Redirect http to https using wildcards
    293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 |
    Example URL:http://mail.google.com/randomcharacters
    Include pattern:http://mail.google.com*
    Redirect to:https://mail.google.com$1
    Pattern type:Wildcard
    Example result:https://mail.google.com/randomcharacters
    315 |
  10. 316 |
317 | 318 | 319 | -------------------------------------------------------------------------------- /icon.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 20 | 21 | 22 | 23 | 58 | 59 | -------------------------------------------------------------------------------- /images/icon-dark-theme-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/einaregilsson/Redirector/03f88b97520dbde505b7b5ec0cd14e7ff3877608/images/icon-dark-theme-128.png -------------------------------------------------------------------------------- /images/icon-dark-theme-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/einaregilsson/Redirector/03f88b97520dbde505b7b5ec0cd14e7ff3877608/images/icon-dark-theme-16.png -------------------------------------------------------------------------------- /images/icon-dark-theme-19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/einaregilsson/Redirector/03f88b97520dbde505b7b5ec0cd14e7ff3877608/images/icon-dark-theme-19.png -------------------------------------------------------------------------------- /images/icon-dark-theme-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/einaregilsson/Redirector/03f88b97520dbde505b7b5ec0cd14e7ff3877608/images/icon-dark-theme-32.png -------------------------------------------------------------------------------- /images/icon-dark-theme-38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/einaregilsson/Redirector/03f88b97520dbde505b7b5ec0cd14e7ff3877608/images/icon-dark-theme-38.png -------------------------------------------------------------------------------- /images/icon-dark-theme-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/einaregilsson/Redirector/03f88b97520dbde505b7b5ec0cd14e7ff3877608/images/icon-dark-theme-48.png -------------------------------------------------------------------------------- /images/icon-dark-theme-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/einaregilsson/Redirector/03f88b97520dbde505b7b5ec0cd14e7ff3877608/images/icon-dark-theme-64.png -------------------------------------------------------------------------------- /images/icon-light-theme-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/einaregilsson/Redirector/03f88b97520dbde505b7b5ec0cd14e7ff3877608/images/icon-light-theme-128.png -------------------------------------------------------------------------------- /images/icon-light-theme-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/einaregilsson/Redirector/03f88b97520dbde505b7b5ec0cd14e7ff3877608/images/icon-light-theme-16.png -------------------------------------------------------------------------------- /images/icon-light-theme-19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/einaregilsson/Redirector/03f88b97520dbde505b7b5ec0cd14e7ff3877608/images/icon-light-theme-19.png -------------------------------------------------------------------------------- /images/icon-light-theme-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/einaregilsson/Redirector/03f88b97520dbde505b7b5ec0cd14e7ff3877608/images/icon-light-theme-32.png -------------------------------------------------------------------------------- /images/icon-light-theme-38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/einaregilsson/Redirector/03f88b97520dbde505b7b5ec0cd14e7ff3877608/images/icon-light-theme-38.png -------------------------------------------------------------------------------- /images/icon-light-theme-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/einaregilsson/Redirector/03f88b97520dbde505b7b5ec0cd14e7ff3877608/images/icon-light-theme-48.png -------------------------------------------------------------------------------- /images/icon-light-theme-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/einaregilsson/Redirector/03f88b97520dbde505b7b5ec0cd14e7ff3877608/images/icon-light-theme-64.png -------------------------------------------------------------------------------- /js/background.js: -------------------------------------------------------------------------------- 1 | 2 | //This is the background script. It is responsible for actually redirecting requests, 3 | //as well as monitoring changes in the redirects and the disabled status and reacting to them. 4 | function log(msg, force) { 5 | if (log.enabled || force) { 6 | console.log('REDIRECTOR: ' + msg); 7 | } 8 | } 9 | log.enabled = false; 10 | var enableNotifications=false; 11 | 12 | function isDarkMode() { 13 | return window.matchMedia('(prefers-color-scheme: dark)').matches; 14 | } 15 | var isFirefox = !!navigator.userAgent.match(/Firefox/i); 16 | 17 | var storageArea = chrome.storage.local; 18 | //Redirects partitioned by request type, so we have to run through 19 | //the minimum number of redirects for each request. 20 | var partitionedRedirects = {}; 21 | 22 | //Cache of urls that have just been redirected to. They will not be redirected again, to 23 | //stop recursive redirects, and endless redirect chains. 24 | //Key is url, value is timestamp of redirect. 25 | var ignoreNextRequest = { 26 | 27 | }; 28 | 29 | //url => { timestamp:ms, count:1...n}; 30 | var justRedirected = { 31 | 32 | }; 33 | var redirectThreshold = 3; 34 | 35 | function setIcon(image) { 36 | var data = { 37 | path: {} 38 | }; 39 | 40 | for (let nr of [16,19,32,38,48,64,128]) { 41 | data.path[nr] = `images/${image}-${nr}.png`; 42 | } 43 | 44 | chrome.browserAction.setIcon(data, function() { 45 | var err = chrome.runtime.lastError; 46 | if (err) { 47 | //If not checked we will get unchecked errors in the background page console... 48 | log('Error in SetIcon: ' + err.message); 49 | } 50 | }); 51 | } 52 | 53 | //This is the actual function that gets called for each request and must 54 | //decide whether or not we want to redirect. 55 | function checkRedirects(details) { 56 | 57 | //We only allow GET request to be redirected, don't want to accidentally redirect 58 | //sensitive POST parameters 59 | if (details.method != 'GET') { 60 | return {}; 61 | } 62 | log('Checking: ' + details.type + ': ' + details.url); 63 | 64 | var list = partitionedRedirects[details.type]; 65 | if (!list) { 66 | log('No list for type: ' + details.type); 67 | return {}; 68 | } 69 | 70 | var timestamp = ignoreNextRequest[details.url]; 71 | if (timestamp) { 72 | log('Ignoring ' + details.url + ', was just redirected ' + (new Date().getTime()-timestamp) + 'ms ago'); 73 | delete ignoreNextRequest[details.url]; 74 | return {}; 75 | } 76 | 77 | 78 | for (var i = 0; i < list.length; i++) { 79 | var r = list[i]; 80 | var result = r.getMatch(details.url); 81 | 82 | if (result.isMatch) { 83 | 84 | //Check if we're stuck in a loop where we keep redirecting this, in that 85 | //case ignore! 86 | var data = justRedirected[details.url]; 87 | 88 | var threshold = 3000; 89 | if(!data || ((new Date().getTime()-data.timestamp) > threshold)) { //Obsolete after 3 seconds 90 | justRedirected[details.url] = { timestamp : new Date().getTime(), count: 1}; 91 | } else { 92 | data.count++; 93 | justRedirected[details.url] = data; 94 | if (data.count >= redirectThreshold) { 95 | log('Ignoring ' + details.url + ' because we have redirected it ' + data.count + ' times in the last ' + threshold + 'ms'); 96 | return {}; 97 | } 98 | } 99 | 100 | 101 | log('Redirecting ' + details.url + ' ===> ' + result.redirectTo + ', type: ' + details.type + ', pattern: ' + r.includePattern + ' which is in Rule : ' + r.description); 102 | if(enableNotifications){ 103 | sendNotifications(r, details.url, result.redirectTo); 104 | } 105 | ignoreNextRequest[result.redirectTo] = new Date().getTime(); 106 | 107 | return { redirectUrl: result.redirectTo }; 108 | } 109 | } 110 | 111 | return {}; 112 | } 113 | 114 | //Monitor changes in data, and setup everything again. 115 | //This could probably be optimized to not do everything on every change 116 | //but why bother? 117 | function monitorChanges(changes, namespace) { 118 | if (changes.disabled) { 119 | updateIcon(); 120 | 121 | if (changes.disabled.newValue == true) { 122 | log('Disabling Redirector, removing listener'); 123 | chrome.webRequest.onBeforeRequest.removeListener(checkRedirects); 124 | chrome.webNavigation.onHistoryStateUpdated.removeListener(checkHistoryStateRedirects); 125 | } else { 126 | log('Enabling Redirector, setting up listener'); 127 | setUpRedirectListener(); 128 | } 129 | } 130 | 131 | if (changes.redirects) { 132 | log('Redirects have changed, setting up listener again'); 133 | setUpRedirectListener(); 134 | } 135 | 136 | if (changes.logging) { 137 | log.enabled = changes.logging.newValue; 138 | log('Logging settings have changed to ' + changes.logging.newValue, true); //Always want this to be logged... 139 | } 140 | if (changes.enableNotifications){ 141 | log('notifications setting changed to ' + changes.enableNotifications.newValue); 142 | enableNotifications = changes.enableNotifications.newValue; 143 | } 144 | } 145 | chrome.storage.onChanged.addListener(monitorChanges); 146 | 147 | //Creates a filter to pass to the listener so we don't have to run through 148 | //all the redirects for all the request types we don't have any redirects for anyway. 149 | function createFilter(redirects) { 150 | var types = []; 151 | for (var i = 0; i < redirects.length; i++) { 152 | redirects[i].appliesTo.forEach(function(type) { 153 | // Added this condition below as part of fix for issue 115 https://github.com/einaregilsson/Redirector/issues/115 154 | // Firefox considers responsive web images request as imageset. Chrome doesn't. 155 | // Chrome throws an error for imageset type, so let's add to 'types' only for the values that chrome or firefox supports 156 | if(chrome.webRequest.ResourceType[type.toUpperCase()]!== undefined){ 157 | if (types.indexOf(type) == -1) { 158 | types.push(type); 159 | } 160 | } 161 | }); 162 | } 163 | types.sort(); 164 | 165 | return { 166 | urls: ["https://*/*", "http://*/*"], 167 | types : types 168 | }; 169 | } 170 | 171 | function createPartitionedRedirects(redirects) { 172 | var partitioned = {}; 173 | 174 | for (var i = 0; i < redirects.length; i++) { 175 | var redirect = new Redirect(redirects[i]); 176 | redirect.compile(); 177 | for (var j=0; j storageArea.QUOTA_BYTES_PER_ITEM) { 312 | log("size of redirects " + size + " is greater than allowed for Sync which is " + storageArea.QUOTA_BYTES_PER_ITEM); 313 | // Setting storageArea back to Local. 314 | storageArea = chrome.storage.local; 315 | sendResponse({ 316 | message: "Sync Not Possible - size of Redirects larger than what's allowed by Sync. Refer Help page" 317 | }); 318 | } else { 319 | chrome.storage.local.get({ 320 | redirects: [] 321 | }, function (obj) { 322 | //check if at least one rule is there. 323 | if (obj.redirects.length>0) { 324 | chrome.storage.sync.set(obj, function (a) { 325 | log('redirects moved from Local to Sync Storage Area'); 326 | //Remove Redirects from Local storage 327 | chrome.storage.local.remove("redirects"); 328 | // Call setupRedirectListener to setup the redirects 329 | setUpRedirectListener(); 330 | sendResponse({ 331 | message: "sync-enabled" 332 | }); 333 | }); 334 | } else { 335 | log('No redirects are setup currently in Local, just enabling Sync'); 336 | sendResponse({ 337 | message: "sync-enabled" 338 | }); 339 | } 340 | }); 341 | } 342 | }); 343 | } else { 344 | storageArea = chrome.storage.local; 345 | log('storageArea size for local is ' + storageArea.QUOTA_BYTES / 1000000 + ' MB, that is .. ' + storageArea.QUOTA_BYTES + " bytes"); 346 | chrome.storage.sync.get({ 347 | redirects: [] 348 | }, function (obj) { 349 | if (obj.redirects.length>0) { 350 | chrome.storage.local.set(obj, function (a) { 351 | log('redirects moved from Sync to Local Storage Area'); 352 | //Remove Redirects from sync storage 353 | chrome.storage.sync.remove("redirects"); 354 | // Call setupRedirectListener to setup the redirects 355 | setUpRedirectListener(); 356 | sendResponse({ 357 | message: "sync-disabled" 358 | }); 359 | }); 360 | } else { 361 | sendResponse({ 362 | message: "sync-disabled" 363 | }); 364 | } 365 | }); 366 | } 367 | }); 368 | 369 | } else { 370 | log('Unexpected message: ' + JSON.stringify(request)); 371 | return false; 372 | } 373 | 374 | return true; //This tells the browser to keep sendResponse alive because 375 | //we're sending the response asynchronously. 376 | } 377 | ); 378 | 379 | 380 | //First time setup 381 | updateIcon(); 382 | 383 | chrome.storage.local.get({logging:false}, function(obj) { 384 | log.enabled = obj.logging; 385 | }); 386 | 387 | chrome.storage.local.get({ 388 | isSyncEnabled: false 389 | }, function (obj) { 390 | if (obj.isSyncEnabled) { 391 | storageArea = chrome.storage.sync; 392 | } else { 393 | storageArea = chrome.storage.local; 394 | } 395 | // Now we know which storageArea to use, call setupInitial function 396 | setupInitial(); 397 | }); 398 | 399 | //wrapped the below inside a function so that we can call this once we know the value of storageArea from above. 400 | 401 | function setupInitial() { 402 | chrome.storage.local.get({enableNotifications:false},function(obj){ 403 | enableNotifications = obj.enableNotifications; 404 | }); 405 | 406 | chrome.storage.local.get({ 407 | disabled: false 408 | }, function (obj) { 409 | if (!obj.disabled) { 410 | setUpRedirectListener(); 411 | } else { 412 | log('Redirector is disabled'); 413 | } 414 | }); 415 | } 416 | log('Redirector starting up...'); 417 | 418 | 419 | // Below is a feature request by an user who wished to see visual indication for an Redirect rule being applied on URL 420 | // https://github.com/einaregilsson/Redirector/issues/72 421 | // By default, we will have it as false. If user wishes to enable it from settings page, we can make it true until user disables it (or browser is restarted) 422 | 423 | // Upon browser startup, just set enableNotifications to false. 424 | // Listen to a message from Settings page to change this to true. 425 | function sendNotifications(redirect, originalUrl, redirectedUrl ){ 426 | //var message = "Applied rule : " + redirect.description + " and redirected original page " + originalUrl + " to " + redirectedUrl; 427 | log("Showing redirect success notification"); 428 | //Firefox and other browsers does not yet support "list" type notification like in Chrome. 429 | // Console.log(JSON.stringify(chrome.notifications)); -- This will still show "list" as one option but it just won't work as it's not implemented by Firefox yet 430 | // Can't check if "chrome" typeof either, as Firefox supports both chrome and browser namespace. 431 | // So let's use useragent. 432 | // Opera UA has both chrome and OPR. So check against that ( Only chrome which supports list) - other browsers to get BASIC type notifications. 433 | 434 | let icon = isDarkMode() ? "images/icon-dark-theme-48.png": "images/icon-light-theme-48.png"; 435 | 436 | if(navigator.userAgent.toLowerCase().indexOf("chrome") > -1 && navigator.userAgent.toLowerCase().indexOf("opr")<0){ 437 | 438 | var items = [{title:"Original page: ", message: originalUrl},{title:"Redirected to: ",message: redirectedUrl}]; 439 | var head = "Redirector - Applied rule : " + redirect.description; 440 | chrome.notifications.create({ 441 | type : "list", 442 | items : items, 443 | title : head, 444 | message : head, 445 | iconUrl : icon 446 | }); 447 | } 448 | else{ 449 | var message = "Applied rule : " + redirect.description + " and redirected original page " + originalUrl + " to " + redirectedUrl; 450 | 451 | chrome.notifications.create({ 452 | type : "basic", 453 | title : "Redirector", 454 | message : message, 455 | iconUrl : icon 456 | }); 457 | } 458 | } 459 | 460 | chrome.runtime.onStartup.addListener(handleStartup); 461 | function handleStartup(){ 462 | enableNotifications=false; 463 | chrome.storage.local.set({ 464 | enableNotifications: false 465 | }); 466 | 467 | updateIcon(); //To set dark/light icon... 468 | 469 | //This doesn't work yet in Chrome, but we'll put it here anyway, in case it starts working... 470 | let darkModeMql = window.matchMedia('(prefers-color-scheme: dark)'); 471 | darkModeMql.onchange = updateIcon; 472 | } -------------------------------------------------------------------------------- /js/editredirect.js: -------------------------------------------------------------------------------- 1 | //Everything to do with the edit and delete forms is here... 2 | 3 | var activeRedirect = null; 4 | 5 | function createNewRedirect() { 6 | activeRedirect = new Redirect(); 7 | el('#edit-redirect-form h3').textContent = 'Create Redirect'; 8 | showForm('#edit-redirect-form', activeRedirect); 9 | el('#btn-save-redirect').setAttribute('disabled', 'disabled'); 10 | } 11 | 12 | function editRedirect(index) { 13 | el('#edit-redirect-form h3').textContent = 'Edit Redirect'; 14 | activeRedirect = new Redirect(REDIRECTS[index]); //Make a new one, which we can dump a bunch of stuff on... 15 | activeRedirect.existing = true; 16 | activeRedirect.index = index; 17 | showForm('#edit-redirect-form', activeRedirect); 18 | setTimeout(() => el('input[data-bind="description"]').focus(), 200); //Why not working...? 19 | } 20 | 21 | function cancelEdit() { 22 | activeRedirect = null; 23 | hideForm('#edit-redirect-form'); 24 | } 25 | 26 | function saveRedirect() { 27 | let savedRedirect = new Redirect(activeRedirect); 28 | if (activeRedirect.existing) { 29 | REDIRECTS[activeRedirect.index] = savedRedirect; //To strip out any extra crap we've added 30 | } else { 31 | REDIRECTS.push(savedRedirect); 32 | let newNode = template.cloneNode(true); 33 | newNode.removeAttribute('id'); 34 | el('.redirect-rows').appendChild(newNode); 35 | } 36 | 37 | updateBindings(); 38 | saveChanges(); 39 | hideForm('#edit-redirect-form'); 40 | } 41 | 42 | function toggleAdvancedOptions(ev) { 43 | ev.preventDefault(); 44 | let advancedOptions = el('.advanced'); 45 | if (advancedOptions.classList.contains('hidden')) { 46 | advancedOptions.classList.remove('hidden'); 47 | el('#advanced-toggle a').textContent = 'Hide advanced options...'; 48 | } else { 49 | advancedOptions.classList.add('hidden'); 50 | el('#advanced-toggle a').textContent = 'Show advanced options...'; 51 | } 52 | } 53 | 54 | 55 | function editFormChange() { 56 | //Now read values back from the form... 57 | for (let input of el('#edit-redirect-form').querySelectorAll('input[type="text"][data-bind]')) { 58 | let prop = input.getAttribute('data-bind'); 59 | activeRedirect[prop] = input.value; 60 | } 61 | activeRedirect.appliesTo = []; 62 | for (let input of el('#apply-to').querySelectorAll('input:checked')) { 63 | activeRedirect.appliesTo.push(input.value); 64 | } 65 | 66 | activeRedirect.processMatches = el('#process-matches option:checked').value; 67 | activeRedirect.patternType = el('[name="patterntype"]:checked').value; 68 | 69 | activeRedirect.updateExampleResult(); 70 | 71 | dataBind('#edit-redirect-form', activeRedirect); 72 | } 73 | 74 | 75 | 76 | var deleteIndex; 77 | function confirmDeleteRedirect(index) { 78 | deleteIndex = index; 79 | let redirect = REDIRECTS[deleteIndex]; 80 | showForm('#delete-redirect-form', redirect); 81 | } 82 | 83 | function deleteRedirect() { 84 | REDIRECTS.splice(deleteIndex, 1); 85 | let node = el(`.redirect-row[data-index="${deleteIndex}"]`); 86 | node.parentNode.removeChild(node); 87 | updateBindings(); 88 | saveChanges(); 89 | hideForm('#delete-redirect-form'); 90 | } 91 | 92 | function cancelDelete() { 93 | hideForm('#delete-redirect-form'); 94 | } 95 | 96 | 97 | function setupEditAndDeleteEventListeners() { 98 | 99 | el('#btn-save-redirect').addEventListener('click', saveRedirect); 100 | el('#btn-cancel-edit').addEventListener('click', cancelEdit); 101 | 102 | el('#confirm-delete').addEventListener('click', deleteRedirect); 103 | el('#cancel-delete').addEventListener('click', cancelDelete); 104 | 105 | el('#advanced-toggle a').addEventListener('click', toggleAdvancedOptions); 106 | 107 | el('#create-new-redirect').addEventListener('click', createNewRedirect); 108 | //Listen to any change from the edit form... 109 | el('#edit-redirect-form').addEventListener('input', editFormChange); 110 | } 111 | 112 | 113 | setupEditAndDeleteEventListeners(); -------------------------------------------------------------------------------- /js/importexport.js: -------------------------------------------------------------------------------- 1 | // Shows a message explaining how many redirects were imported. 2 | function showImportedMessage(imported, existing) { 3 | if (imported == 0 && existing == 0) { 4 | showMessage('No redirects existed in the file.'); 5 | } 6 | if (imported > 0 && existing == 0) { 7 | showMessage('Successfully imported ' + imported + ' redirect' + (imported > 1 ? 's.' : '.'), true); 8 | } 9 | if (imported == 0 && existing > 0) { 10 | showMessage('All redirects in the file already existed and were ignored.'); 11 | } 12 | if (imported > 0 && existing > 0) { 13 | var m = 'Successfully imported ' + imported + ' redirect' + (imported > 1 ? 's' : '') + '. '; 14 | if (existing == 1) { 15 | m += '1 redirect already existed and was ignored.'; 16 | } else { 17 | m += existing + ' redirects already existed and were ignored.'; 18 | } 19 | showMessage(m, true); 20 | } 21 | } 22 | 23 | function importRedirects(ev) { 24 | 25 | let file = ev.target.files[0]; 26 | if (!file) { 27 | return; 28 | } 29 | var reader = new FileReader(); 30 | 31 | reader.onload = function(e) { 32 | var data; 33 | try { 34 | data = JSON.parse(reader.result); 35 | } catch(e) { 36 | showMessage('Failed to parse JSON data, invalid JSON: ' + (e.message||'').substr(0,100)); 37 | return; 38 | } 39 | 40 | if (!data.redirects) { 41 | showMessage('Invalid JSON, missing "redirects" property'); 42 | return; 43 | } 44 | 45 | var imported = 0, existing = 0; 46 | for (var i = 0; i < data.redirects.length; i++) { 47 | var r = new Redirect(data.redirects[i]); 48 | r.updateExampleResult(); 49 | if (REDIRECTS.some(function(i) { return new Redirect(i).equals(r);})) { 50 | existing++; 51 | } else { 52 | REDIRECTS.push(r.toObject()); 53 | imported++; 54 | } 55 | } 56 | 57 | showImportedMessage(imported, existing); 58 | 59 | saveChanges(); 60 | renderRedirects(); 61 | }; 62 | 63 | try { 64 | reader.readAsText(file, 'utf-8'); 65 | } catch(e) { 66 | showMessage('Failed to read import file'); 67 | } 68 | } 69 | 70 | function updateExportLink() { 71 | var redirects = REDIRECTS.map(function(r) { 72 | return new Redirect(r).toObject(); 73 | }); 74 | 75 | let version = chrome.runtime.getManifest().version; 76 | 77 | var exportObj = { 78 | createdBy : 'Redirector v' + version, 79 | createdAt : new Date(), 80 | redirects : redirects 81 | }; 82 | 83 | var json = JSON.stringify(exportObj, null, 4); 84 | 85 | //Using encodeURIComponent here instead of base64 because base64 always messed up our encoding for some reason... 86 | el('#export-link').href = 'data:text/plain;charset=utf-8,' + encodeURIComponent(json); 87 | } 88 | 89 | updateExportLink(); 90 | 91 | function setupImportExportEventListeners() { 92 | el("#import-file").addEventListener('change', importRedirects); 93 | el("#export-link").addEventListener('mousedown', updateExportLink); 94 | } 95 | 96 | setupImportExportEventListeners(); -------------------------------------------------------------------------------- /js/organizemode.js: -------------------------------------------------------------------------------- 1 | 2 | function displayOrganizeModeMessage() { 3 | if(el('#message-box').classList.contains('visible')) { 4 | hideMessage(); 5 | } else { 6 | showMessage("Use ⟱ to move a redirect to the bottom, ⟰ to move to the top, and use the checkboxes to select multiple redirects.", true) 7 | } 8 | } 9 | 10 | function organizeModeToggle(ev) { 11 | ev.preventDefault(); 12 | let organizeModes = ['.groupings', '.arrows'] 13 | for (let mode of organizeModes) { 14 | let organizeModeElms = document.querySelectorAll(mode); 15 | for (i = 0; i < organizeModeElms.length; ++i) { 16 | let elm = organizeModeElms[i]; 17 | let isHidden = ''; 18 | if(mode === '.arrows') { 19 | // targeting parent span for arrows 20 | elm = elm.parentElement; 21 | } 22 | isHidden = elm.classList.contains('hidden'); 23 | isHidden ? elm.classList.remove('hidden') : elm.classList.add('hidden'); 24 | } 25 | } 26 | 27 | let buttonClasses = el('#organize-mode').classList; 28 | !buttonClasses.contains('active') ? el('#organize-mode').classList.add('active') : el('#organize-mode').classList.remove('active'); 29 | 30 | displayOrganizeModeMessage(); 31 | } 32 | 33 | 34 | function setupOrganizeModeToggleEventListener() { 35 | el('#organize-mode').addEventListener('click', organizeModeToggle); 36 | } 37 | 38 | setupOrganizeModeToggleEventListener(); -------------------------------------------------------------------------------- /js/popup.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | var storage = chrome.storage.local; 4 | var viewModel = {}; //Just an object for the databinding 5 | 6 | function applyBinding() { 7 | dataBind(document.body, viewModel); 8 | } 9 | 10 | function toggle(prop) { 11 | storage.get({[prop]: false}, function(obj) { 12 | storage.set({[prop] : !obj[prop]}); 13 | viewModel[prop] = !obj[prop]; 14 | applyBinding(); 15 | }); 16 | } 17 | 18 | 19 | 20 | function openRedirectorSettings() { 21 | 22 | //switch to open one if we have it to minimize conflicts 23 | var url = chrome.extension.getURL('redirector.html'); 24 | 25 | //FIREFOXBUG: Firefox chokes on url:url filter if the url is a moz-extension:// url 26 | //so we don't use that, do it the more manual way instead. 27 | chrome.tabs.query({currentWindow:true}, function(tabs) { 28 | for (var i=0; i < tabs.length; i++) { 29 | if (tabs[i].url == url) { 30 | chrome.tabs.update(tabs[i].id, {active:true}, function(tab) { 31 | close(); 32 | }); 33 | return; 34 | } 35 | } 36 | 37 | chrome.tabs.create({url:url, active:true}); 38 | }); 39 | return; 40 | }; 41 | 42 | 43 | function pageLoad() { 44 | storage.get({logging:false, enableNotifications:false, disabled: false}, function(obj) { 45 | viewModel = obj; 46 | applyBinding(); 47 | }) 48 | 49 | el('#enable-notifications').addEventListener('input', () => toggle('enableNotifications')); 50 | el('#enable-logging').addEventListener('input', () => toggle('logging')); 51 | el('#toggle-disabled').addEventListener('click', () => toggle('disabled')); 52 | el('#open-redirector-settings').addEventListener('click', openRedirectorSettings); 53 | } 54 | 55 | pageLoad(); 56 | //Setup page... 57 | -------------------------------------------------------------------------------- /js/redirect.js: -------------------------------------------------------------------------------- 1 | 2 | function Redirect(o) { 3 | this._init(o); 4 | } 5 | 6 | //temp, allow addon sdk to require this. 7 | if (typeof exports !== 'undefined') { 8 | exports.Redirect = Redirect; 9 | } 10 | 11 | //Static 12 | Redirect.WILDCARD = 'W'; 13 | Redirect.REGEX = 'R'; 14 | 15 | Redirect.requestTypes = { 16 | main_frame: "Main window (address bar)", 17 | sub_frame: "IFrames", 18 | stylesheet : "Stylesheets", 19 | font: "Fonts", 20 | script : "Scripts", 21 | image : "Images", 22 | imageset: "Responsive Images in Firefox", 23 | media : "Media (audio and video)", 24 | object : "Objects (e.g. Flash content, Java applets)", 25 | object_subrequest : "Object subrequests", 26 | xmlhttprequest : "XMLHttpRequests (Ajax)", 27 | history : "HistoryState", 28 | other : "Other" 29 | }; 30 | 31 | 32 | Redirect.prototype = { 33 | 34 | //attributes 35 | description : '', 36 | exampleUrl : '', 37 | exampleResult : '', 38 | error : null, 39 | includePattern : '', 40 | excludePattern : '', 41 | patternDesc:'', 42 | redirectUrl : '', 43 | patternType : '', 44 | processMatches : 'noProcessing', 45 | disabled : false, 46 | grouped: false, 47 | 48 | compile : function() { 49 | 50 | var incPattern = this._preparePattern(this.includePattern); 51 | var excPattern = this._preparePattern(this.excludePattern); 52 | 53 | if (incPattern) { 54 | this._rxInclude = new RegExp(incPattern, 'gi'); 55 | } 56 | if (excPattern) { 57 | this._rxExclude = new RegExp(excPattern, 'gi'); 58 | } 59 | }, 60 | 61 | equals : function(redirect) { 62 | return this.description == redirect.description 63 | && this.exampleUrl == redirect.exampleUrl 64 | && this.includePattern == redirect.includePattern 65 | && this.excludePattern == redirect.excludePattern 66 | && this.patternDesc == redirect.patternDesc 67 | && this.redirectUrl == redirect.redirectUrl 68 | && this.patternType == redirect.patternType 69 | && this.processMatches == redirect.processMatches 70 | && this.appliesTo.toString() == redirect.appliesTo.toString(); 71 | }, 72 | 73 | toObject : function() { 74 | return { 75 | description : this.description, 76 | exampleUrl : this.exampleUrl, 77 | exampleResult : this.exampleResult, 78 | error : this.error, 79 | includePattern : this.includePattern, 80 | excludePattern : this.excludePattern, 81 | patternDesc : this.patternDesc, 82 | redirectUrl : this.redirectUrl, 83 | patternType : this.patternType, 84 | processMatches : this.processMatches, 85 | disabled : this.disabled, 86 | grouped: this.grouped, 87 | appliesTo : this.appliesTo.slice(0) 88 | }; 89 | }, 90 | 91 | getMatch: function(url, forceIgnoreDisabled) { 92 | if (!this._rxInclude) { 93 | this.compile(); 94 | } 95 | var result = { 96 | isMatch : false, 97 | isExcludeMatch : false, 98 | isDisabledMatch : false, 99 | redirectTo : '', 100 | toString : function() { return JSON.stringify(this); } 101 | }; 102 | var redirectTo = this._includeMatch(url); 103 | 104 | if (redirectTo !== null) { 105 | if (this.disabled && !forceIgnoreDisabled) { 106 | result.isDisabledMatch = true; 107 | } else if (this._excludeMatch(url)) { 108 | result.isExcludeMatch = true; 109 | } else { 110 | result.isMatch = true; 111 | result.redirectTo = redirectTo; 112 | } 113 | } 114 | return result; 115 | }, 116 | 117 | //Updates the .exampleResult field or the .error 118 | //field depending on if the example url and patterns match 119 | //and make a good redirect 120 | updateExampleResult : function() { 121 | 122 | //Default values 123 | this.error = null; 124 | this.exampleResult = ''; 125 | 126 | 127 | if (!this.exampleUrl) { 128 | this.error = 'No example URL defined.'; 129 | return; 130 | } 131 | 132 | if (this.patternType == Redirect.REGEX && this.includePattern) { 133 | try { 134 | new RegExp(this.includePattern, 'gi'); 135 | } catch(e) { 136 | this.error = 'Invalid regular expression in Include pattern.'; 137 | return; 138 | } 139 | } 140 | 141 | if (this.patternType == Redirect.REGEX && this.excludePattern) { 142 | try { 143 | new RegExp(this.excludePattern, 'gi'); 144 | } catch(e) { 145 | this.error = 'Invalid regular expression in Exclude pattern.'; 146 | return; 147 | } 148 | } 149 | 150 | if (!this.appliesTo || this.appliesTo.length == 0) { 151 | this.error = 'At least one request type must be chosen.'; 152 | return; 153 | } 154 | 155 | this.compile(); 156 | 157 | var match = this.getMatch(this.exampleUrl, true); 158 | 159 | if (match.isExcludeMatch) { 160 | this.error = 'The exclude pattern excludes the example url.' 161 | return; 162 | } 163 | 164 | //Commented out because this code prevents saving many types of valid redirects. 165 | //if (match.isMatch && !match.redirectTo.match(/^https?\:\/\//)) { 166 | // this.error = 'The redirect result must start with http:// or https://, current result is: "' + match.redirectTo; 167 | // return; 168 | //} 169 | 170 | if (!match.isMatch) { 171 | this.error = 'The include pattern does not match the example url.'; 172 | return; 173 | } 174 | 175 | this.exampleResult = match.redirectTo; 176 | }, 177 | 178 | isRegex: function() { 179 | return this.patternType == Redirect.REGEX; 180 | }, 181 | 182 | isWildcard : function() { 183 | return this.patternType == Redirect.WILDCARD; 184 | }, 185 | 186 | test : function() { 187 | return this.getMatch(this.exampleUrl); 188 | }, 189 | 190 | //Private functions below 191 | _rxInclude : null, 192 | _rxExclude : null, 193 | 194 | _preparePattern : function(pattern) { 195 | if (!pattern) { 196 | return null; 197 | } 198 | if (this.patternType == Redirect.REGEX) { 199 | return pattern; 200 | } else { //Convert wildcard to regex pattern 201 | var converted = '^'; 202 | for (var i = 0; i < pattern.length; i++) { 203 | var ch = pattern.charAt(i); 204 | if ('()[]{}?.^$\\+'.indexOf(ch) != -1) { 205 | converted += '\\' + ch; 206 | } else if (ch == '*') { 207 | converted += '(.*?)'; 208 | } else { 209 | converted += ch; 210 | } 211 | } 212 | converted += '$'; 213 | return converted; 214 | } 215 | }, 216 | 217 | _init : function(o) { 218 | o = o || {}; 219 | this.description = o.description || ''; 220 | this.exampleUrl = o.exampleUrl || ''; 221 | this.exampleResult = o.exampleResult || ''; 222 | this.error = o.error || null; 223 | this.includePattern = o.includePattern || ''; 224 | this.excludePattern = o.excludePattern || ''; 225 | this.redirectUrl = o.redirectUrl || ''; 226 | this.patternType = o.patternType || Redirect.WILDCARD; 227 | 228 | this.patternTypeText = this.patternType == 'W' ? 'Wildcard' : 'Regular Expression' 229 | 230 | this.patternDesc = o.patternDesc || ''; 231 | this.processMatches = o.processMatches || 'noProcessing'; 232 | if (!o.processMatches && o.unescapeMatches) { 233 | this.processMatches = 'urlDecode'; 234 | } 235 | if (!o.processMatches && o.escapeMatches) { 236 | this.processMatches = 'urlEncode'; 237 | } 238 | 239 | this.disabled = !!o.disabled; 240 | if (o.appliesTo && o.appliesTo.length) { 241 | this.appliesTo = o.appliesTo.slice(0); 242 | } else { 243 | this.appliesTo = ['main_frame']; 244 | } 245 | }, 246 | 247 | get appliesToText() { 248 | return this.appliesTo.map(type => Redirect.requestTypes[type]).join(', '); 249 | }, 250 | 251 | get processMatchesExampleText() { 252 | let examples = { 253 | noProcessing : 'Use matches as they are', 254 | urlEncode : 'E.g. turn /bar/foo?x=2 into %2Fbar%2Ffoo%3Fx%3D2', 255 | urlDecode : 'E.g. turn %2Fbar%2Ffoo%3Fx%3D2 into /bar/foo?x=2', 256 | doubleUrlDecode : 'E.g. turn %252Fbar%252Ffoo%253Fx%253D2 into /bar/foo?x=2', 257 | base64Decode : 'E.g. turn aHR0cDovL2Nubi5jb20= into http://cnn.com' 258 | }; 259 | 260 | return examples[this.processMatches]; 261 | }, 262 | 263 | toString : function() { 264 | return JSON.stringify(this.toObject(), null, 2); 265 | }, 266 | 267 | _includeMatch : function(url) { 268 | if (!this._rxInclude) { 269 | return null; 270 | } 271 | var matches = this._rxInclude.exec(url); 272 | if (!matches) { 273 | return null; 274 | } 275 | var resultUrl = this.redirectUrl; 276 | for (var i = matches.length - 1; i > 0; i--) { 277 | var repl = matches[i] || ''; 278 | if (this.processMatches == 'urlDecode') { 279 | repl = unescape(repl); 280 | } else if (this.processMatches == 'doubleUrlDecode') { 281 | repl = unescape(unescape(repl)); 282 | } else if (this.processMatches == 'urlEncode') { 283 | repl = encodeURIComponent(repl); 284 | } else if (this.processMatches == 'base64decode') { 285 | if (repl.indexOf('%') > -1) { 286 | repl = unescape(repl); 287 | } 288 | repl = atob(repl); 289 | } 290 | resultUrl = resultUrl.replace(new RegExp('\\$' + i, 'gi'), repl); 291 | } 292 | this._rxInclude.lastIndex = 0; 293 | return resultUrl; 294 | }, 295 | 296 | _excludeMatch : function(url) { 297 | if (!this._rxExclude) { 298 | return false; 299 | } 300 | var shouldExclude = this._rxExclude.test(url); 301 | this._rxExclude.lastIndex = 0; 302 | return shouldExclude; 303 | } 304 | }; 305 | -------------------------------------------------------------------------------- /js/redirectorpage.js: -------------------------------------------------------------------------------- 1 | var REDIRECTS = []; // The global redirects list... 2 | var options = { 3 | isSyncEnabled : false 4 | }; 5 | var template; 6 | 7 | function normalize(r) { 8 | return new Redirect(r).toObject(); //Cleans out any extra props, and adds default values for missing ones. 9 | } 10 | 11 | // Saves the entire list of redirects to storage. 12 | function saveChanges() { 13 | 14 | // Clean them up so angular $$hash things and stuff don't get serialized. 15 | let arr = REDIRECTS.map(normalize); 16 | 17 | chrome.runtime.sendMessage({type:"save-redirects", redirects:arr}, function(response) { 18 | console.log(response.message); 19 | if(response.message.indexOf("Redirects failed to save") > -1){ 20 | showMessage(response.message, false); 21 | } else{ 22 | console.log('Saved ' + arr.length + ' redirects at ' + new Date() + '. Message from background page:' + response.message); 23 | } 24 | }); 25 | } 26 | 27 | function toggleSyncSetting() { 28 | chrome.runtime.sendMessage({type:"toggle-sync", isSyncEnabled: !options.isSyncEnabled}, function(response) { 29 | if(response.message === "sync-enabled"){ 30 | options.isSyncEnabled = true; 31 | showMessage('Sync is enabled!', true); 32 | } else if(response.message === "sync-disabled"){ 33 | options.isSyncEnabled = false; 34 | showMessage('Sync is disabled - local storage will be used!', true); 35 | } else if(response.message.indexOf("Sync Not Possible")>-1){ 36 | options.isSyncEnabled = false; 37 | chrome.storage.local.set({isSyncEnabled: $s.isSyncEnabled}, function(){ 38 | // console.log("set back to false"); 39 | }); 40 | showMessage(response.message, false); 41 | } 42 | else { 43 | alert(response.message) 44 | showMessage('Error occured when trying to change Sync settings. Look at the logs and raise an issue',false); 45 | } 46 | el('#storage-sync-option input').checked = options.isSyncEnabled; 47 | }); 48 | } 49 | 50 | function renderRedirects() { 51 | el('.redirect-rows').textContent = ''; 52 | for (let i=0; i < REDIRECTS.length; i++) { 53 | let r = REDIRECTS[i]; 54 | let node = template.cloneNode(true); 55 | node.removeAttribute('id'); 56 | 57 | renderSingleRedirect(node, r, i); 58 | el('.redirect-rows').appendChild(node); 59 | } 60 | } 61 | 62 | function renderSingleRedirect(node, redirect, index) { 63 | 64 | //Add extra props to help with rendering... 65 | if (index === 0) { 66 | redirect.$first = true; 67 | } 68 | if (index === REDIRECTS.length - 1) { 69 | redirect.$last = true; 70 | } 71 | redirect.$index = index; 72 | 73 | dataBind(node, redirect); 74 | 75 | node.setAttribute('data-index', index); 76 | for (let btn of node.querySelectorAll('.btn')) { 77 | btn.setAttribute('data-index', index); 78 | } 79 | 80 | let checkmark = node.querySelectorAll('.checkmark'); 81 | 82 | if(checkmark.length == 1) { 83 | checkmark[0].setAttribute('data-index', index); 84 | } 85 | 86 | //Remove extra props... 87 | delete redirect.$first; 88 | delete redirect.$last; 89 | delete redirect.$index; 90 | } 91 | 92 | 93 | function updateBindings() { 94 | 95 | let nodes = document.querySelectorAll('.redirect-row'); 96 | 97 | if (nodes.length !== REDIRECTS.length) { 98 | throw new Error('Mismatch in lengths, Redirects are ' + REDIRECTS.length + ', nodes are ' + nodes.length) 99 | } 100 | 101 | for (let i=0; i < nodes.length; i++) { 102 | let node = nodes[i]; 103 | let redirect = REDIRECTS[i]; 104 | renderSingleRedirect(node, redirect, i); 105 | } 106 | } 107 | 108 | function duplicateRedirect(index) { 109 | let redirect = new Redirect(REDIRECTS[index]); 110 | REDIRECTS.splice(index, 0, redirect); 111 | 112 | let newNode = template.cloneNode(true); 113 | newNode.removeAttribute('id'); 114 | el('.redirect-rows').appendChild(newNode); 115 | updateBindings(); 116 | saveChanges(); 117 | } 118 | 119 | function checkIfGroupingExists() { 120 | let grouping = REDIRECTS.map((row, i) => { return { row, index: i}}) 121 | .filter(result => result.row.grouped) 122 | .sort((a, b) => a.index - b.index); 123 | return grouping; 124 | } 125 | 126 | function toggleDisabled(index) { 127 | let grouping = checkIfGroupingExists(); 128 | 129 | if(grouping && grouping.length > 1) { 130 | for (let redirect of grouping) { 131 | let redirectDom = REDIRECTS[redirect.index]; 132 | redirectDom.disabled = !redirectDom.disabled 133 | redirectDom.grouped = !redirectDom.grouped 134 | let elm = document.querySelector("[data-index='" + (redirect.index).toString() + "']"); 135 | clearGrouping(elm); 136 | } 137 | } else { 138 | let redirect = REDIRECTS[index]; 139 | redirect.disabled = !redirect.disabled 140 | } 141 | 142 | updateBindings(); 143 | saveChanges(); 144 | } 145 | 146 | function clearGrouping(elm) { 147 | elm.classList.remove('grouped'); 148 | let checkMarkElm = elm.querySelector("label > .groupings"); 149 | let toggleBoxElm = elm.querySelector("input"); 150 | checkMarkElm.classList.remove("checkMarked"); 151 | toggleBoxElm.classList.remove("checked"); 152 | } 153 | 154 | function swap(node1, node2) { 155 | const afterNode2 = node2.nextElementSibling; 156 | const parent = node2.parentNode; 157 | node1.replaceWith(node2); 158 | parent.insertBefore(node1, afterNode2); 159 | } 160 | 161 | function groupedMoveDown(group) { 162 | var jumpLength = 1; 163 | 164 | if(isGroupAdjacent(group)) { 165 | jumpLength = group.length; 166 | } 167 | 168 | for(let rule of group) { 169 | let elm = document.querySelector("[data-index='" + (rule.index).toString() + "']"); 170 | let prev = document.querySelector("[data-index='" + (rule.index + jumpLength).toString() + "']"); 171 | clearGrouping(elm); 172 | clearGrouping(prev); 173 | swap(elm,prev); 174 | } 175 | 176 | for(let rule of group) { 177 | rule.row.grouped = false; 178 | let prevRedir = REDIRECTS[rule.index + jumpLength]; 179 | REDIRECTS[rule.index + jumpLength] = REDIRECTS[rule.index]; 180 | REDIRECTS[rule.index] = prevRedir; 181 | } 182 | 183 | updateBindings(); 184 | saveChanges(); 185 | } 186 | 187 | function isGroupAdjacent(grouping) { 188 | let distances = []; 189 | for(let i = grouping.length - 1; i >= 0; i--) { 190 | if(i != 0) { 191 | distances.push(grouping[i].index - grouping[i - 1].index); 192 | } 193 | } 194 | return distances.every(distance => distance === 1); 195 | } 196 | 197 | function groupedMoveUp(group) { 198 | var jumpLength = 1; 199 | 200 | if(isGroupAdjacent(group)) { 201 | jumpLength = group.length; 202 | } 203 | 204 | for(let rule of group) { 205 | let elm = document.querySelector("[data-index='" + (rule.index).toString() + "']"); 206 | let prev = document.querySelector("[data-index='" + (rule.index - jumpLength).toString() + "']"); 207 | clearGrouping(elm); 208 | clearGrouping(prev); 209 | 210 | if(jumpLength > 1) { 211 | swap(elm,prev); 212 | } 213 | } 214 | 215 | for(let rule of group) { 216 | rule.row.grouped = false; 217 | let prevRedir = REDIRECTS[rule.index - jumpLength]; 218 | REDIRECTS[rule.index - jumpLength] = REDIRECTS[rule.index]; 219 | REDIRECTS[rule.index] = prevRedir; 220 | } 221 | 222 | updateBindings(); 223 | saveChanges(); 224 | } 225 | function moveUp(index) { 226 | let grouping = checkIfGroupingExists(); 227 | 228 | if(grouping.length > 1) { 229 | groupedMoveUp(grouping); 230 | } else { 231 | let prev = REDIRECTS[index-1]; 232 | REDIRECTS[index-1] = REDIRECTS[index]; 233 | REDIRECTS[index] = prev; 234 | } 235 | 236 | updateBindings(); 237 | saveChanges(); 238 | } 239 | 240 | function moveDown(index) { 241 | let grouping = checkIfGroupingExists(); 242 | 243 | if(grouping.length > 1) { 244 | groupedMoveDown(grouping); 245 | } else { 246 | let next = REDIRECTS[index+1]; 247 | REDIRECTS[index+1] = REDIRECTS[index]; 248 | REDIRECTS[index] = next; 249 | } 250 | updateBindings(); 251 | saveChanges(); 252 | } 253 | 254 | function moveUpTop(index) { 255 | let top = REDIRECTS[0]; 256 | move(REDIRECTS, index, top); 257 | updateBindings(); 258 | saveChanges(); 259 | } 260 | 261 | function moveDownBottom(index) { 262 | let bottom = REDIRECTS.length - 1; 263 | move(REDIRECTS, index, bottom); 264 | updateBindings(); 265 | saveChanges(); 266 | } 267 | 268 | //All the setup stuff for the page 269 | function pageLoad() { 270 | template = el('#redirect-row-template'); 271 | template.parentNode.removeChild(template); 272 | 273 | //Need to proxy this through the background page, because Firefox gives us dead objects 274 | //nonsense when accessing chrome.storage directly. 275 | chrome.runtime.sendMessage({type: "get-redirects"}, function(response) { 276 | console.log('Received redirects message, count=' + response.redirects.length); 277 | for (var i=0; i < response.redirects.length; i++) { 278 | REDIRECTS.push(new Redirect(response.redirects[i])); 279 | } 280 | 281 | if (response.redirects.length === 0) { 282 | //Add example redirect for first time users... 283 | REDIRECTS.push(new Redirect( 284 | { 285 | "description": "Example redirect, try going to http://example.com/anywordhere", 286 | "exampleUrl": "http://example.com/some-word-that-matches-wildcard", 287 | "exampleResult": "https://google.com/search?q=some-word-that-matches-wildcard", 288 | "error": null, 289 | "includePattern": "http://example.com/*", 290 | "excludePattern": "", 291 | "patternDesc": "Any word after example.com leads to google search for that word.", 292 | "redirectUrl": "https://google.com/search?q=$1", 293 | "patternType": "W", 294 | "processMatches": "noProcessing", 295 | "disabled": false, 296 | "appliesTo": [ 297 | "main_frame" 298 | ] 299 | } 300 | )); 301 | } 302 | renderRedirects(); 303 | }); 304 | 305 | chrome.storage.local.get({isSyncEnabled:false}, function(obj){ 306 | options.isSyncEnabled = obj.isSyncEnabled; 307 | el('#storage-sync-option input').checked = options.isSyncEnabled; 308 | }); 309 | 310 | if(navigator.userAgent.toLowerCase().indexOf("chrome") > -1){ 311 | show('#storage-sync-option'); 312 | } 313 | 314 | 315 | //Setup event listeners 316 | el('#hide-message').addEventListener('click', hideMessage); 317 | el('#storage-sync-option input').addEventListener('click', toggleSyncSetting); 318 | el('.redirect-rows').addEventListener('click', function(ev) { 319 | if(ev.target.type == 'checkbox') { 320 | ev.target.nextElementSibling.classList.add("checkMarked"); 321 | ev.target.parentElement.parentElement.classList.add('grouped'); 322 | toggleGrouping(ev.target.index); 323 | } 324 | 325 | let action = ev.target.getAttribute('data-action'); 326 | 327 | //We clone and re-use nodes all the time, so instead of attaching and removing event handlers endlessly we just put 328 | //a data-action attribute on them with the name of the function that should be called... 329 | if (!action) { 330 | return; 331 | } 332 | 333 | let handler = window[action]; 334 | 335 | let index = parseInt(ev.target.getAttribute('data-index')); 336 | 337 | handler(index); 338 | }); 339 | } 340 | 341 | function updateFavicon(e) { 342 | let type = e.matches ? 'dark' : 'light' 343 | el('link[rel="shortcut icon"]').href = `images/icon-${type}-theme-32.png`; 344 | chrome.runtime.sendMessage({type: "update-icon"}); //Only works if this page is open, but still, better than nothing... 345 | } 346 | 347 | let mql = window.matchMedia('(prefers-color-scheme:dark)'); 348 | 349 | mql.onchange = updateFavicon; 350 | updateFavicon(mql); 351 | 352 | function toggleGrouping(index) { 353 | if(REDIRECTS[index]) { 354 | REDIRECTS[index].grouped = !REDIRECTS[index].grouped; 355 | } 356 | } 357 | 358 | pageLoad(); 359 | -------------------------------------------------------------------------------- /js/stub.js: -------------------------------------------------------------------------------- 1 | 2 | //Dummy file to use while developing the UI. This way we can just develop it on a local fileserver, and don't have to reload 3 | //an extension for every tiny change! 4 | 5 | if (!chrome || !chrome.storage || !chrome.storage.local) { 6 | 7 | let testData = { 8 | "createdBy": "Redirector v3.2", 9 | "createdAt": "2019-12-09T12:54:13.391Z", 10 | "redirects": [ 11 | { 12 | "description": "Mbl test", 13 | "exampleUrl": "https://mbl.is", 14 | "exampleResult": "http://foo.is", 15 | "error": null, 16 | "includePattern": "*mbl*", 17 | "excludePattern": "", 18 | "patternDesc": "My description", 19 | "redirectUrl": "http://foo.is", 20 | "patternType": "R", 21 | "processMatches": "noProcessing", 22 | "disabled": false, 23 | "appliesTo": [ 24 | "main_frame", 25 | "script" 26 | ] 27 | }, 28 | { 29 | "description": "Msdfsdfbl test", 30 | "exampleUrl": "https://mbssfdsl.is", 31 | "exampleResult": "http://foo.is", 32 | "error": null, 33 | "includePattern": "*mbl*", 34 | "excludePattern": "", 35 | "patternDesc": "My description", 36 | "redirectUrl": "http://foo.is", 37 | "patternType": "W", 38 | "processMatches": "urlEncode", 39 | "disabled": false, 40 | "appliesTo": [ 41 | "main_frame", 42 | "sub_frame" 43 | ] 44 | }, { 45 | "description": "https://foo.is?s=joh", 46 | "exampleUrl": "https://foo.is?s=joh", 47 | "exampleResult": "https://foo.is", 48 | "error": null, 49 | "includePattern": "(.*)(\\?s=)(.*)", 50 | "excludePattern": "", 51 | "patternDesc": "Test error", 52 | "redirectUrl": "$1", 53 | "patternType": "R", 54 | "processMatches": "noProcessing", 55 | "disabled": false, 56 | "appliesTo": [ 57 | "main_frame" 58 | ] 59 | } 60 | ] 61 | }; 62 | 63 | localStorage.redirector = JSON.stringify(testData); 64 | 65 | 66 | //Make dummy for testing... 67 | window.chrome = window.chrome || {}; 68 | chrome.storage = { 69 | local : { 70 | get : function(defaults, callback) { 71 | let data = JSON.parse(localStorage.redirector || '{}'); 72 | 73 | let result = {}; 74 | for (let key in defaults) { 75 | if (typeof data[key] !== 'undefined') { 76 | result[key] = data[key]; 77 | } else { 78 | result[key] = defaults[key]; 79 | } 80 | } 81 | callback(result); 82 | }, 83 | 84 | set : function(obj) { 85 | let data = JSON.parse(localStorage.redirector || '{}'); 86 | 87 | for (let k in obj) { 88 | data[k] = obj[k]; 89 | } 90 | localStorage.redirector = JSON.stringify(data); 91 | } 92 | } 93 | }; 94 | 95 | chrome.runtime = { 96 | sendMessage : function(params, callback) { 97 | let data = JSON.parse(localStorage.redirector || '{}'); 98 | if (params.type === 'get-redirects') { 99 | chrome.storage.local.get({redirects:[]}, callback); 100 | } else if (params.type === 'toggle-sync') { 101 | if (params.isSyncEnabled) { 102 | callback({message:'sync-enabled'}); 103 | } else { 104 | callback({message:'sync-disabled'}); 105 | } 106 | } 107 | }, 108 | getManifest : function() { 109 | return { version: '0-dev' }; 110 | } 111 | }; 112 | } -------------------------------------------------------------------------------- /js/util.js: -------------------------------------------------------------------------------- 1 | function dataBind(el, dataObject) { 2 | 3 | function boolValue(prop) { 4 | return prop.charAt(0) === '!' ? !dataObject[prop.substr(1)] : dataObject[prop]; 5 | } 6 | 7 | if (typeof el === 'string') { 8 | el = document.querySelector(el) 9 | } 10 | for (let tag of el.querySelectorAll('[data-bind]')) { 11 | let prop = tag.getAttribute('data-bind'); 12 | if (tag.tagName.toLowerCase() === 'input') { 13 | if (tag.getAttribute('type').toLowerCase() === 'radio') { 14 | tag.checked = dataObject[prop] === tag.getAttribute('value'); 15 | } else if (tag.getAttribute('type').toLowerCase() === 'checkbox') { 16 | tag.checked = dataObject[prop]; 17 | } else { 18 | tag.value = dataObject[prop]; 19 | } 20 | } else if (tag.tagName.toLowerCase() === 'select') { 21 | for (let opt of tag.querySelectorAll('option')) { 22 | if (opt.getAttribute('value') === dataObject[prop]) { 23 | opt.setAttribute('selected', 'selected'); 24 | } else { 25 | opt.removeAttribute('selected'); 26 | } 27 | } 28 | } else if (Array.isArray(dataObject[prop])) { 29 | //Array of values, check any checkboxes in child elements 30 | for (let checkbox of tag.querySelectorAll('input[type="checkbox"')) { 31 | checkbox.checked = dataObject[prop].includes(checkbox.getAttribute('value')); 32 | } 33 | 34 | } else { 35 | tag.textContent = dataObject[prop]; 36 | } 37 | } 38 | for (let tag of el.querySelectorAll('[data-show]')) { 39 | let shouldShow = boolValue(tag.getAttribute('data-show')); 40 | tag.style.display = shouldShow ? '' : 'none'; 41 | } 42 | for (let tag of el.querySelectorAll('[data-disabled]')) { 43 | let isDisabled = boolValue(tag.getAttribute('data-disabled')); 44 | 45 | if (isDisabled) { 46 | tag.classList.add('disabled'); 47 | tag.setAttribute('disabled', 'disabled'); 48 | } else { 49 | tag.classList.remove('disabled'); 50 | tag.removeAttribute('disabled'); 51 | } 52 | } 53 | for (let tag of el.querySelectorAll('[data-class]')) { 54 | let [className, prop] = tag.getAttribute('data-class').split(':'); 55 | let shouldHaveClass = boolValue(prop); 56 | if (shouldHaveClass) { 57 | tag.classList.add(className); 58 | } else { 59 | tag.classList.remove(className); 60 | } 61 | } 62 | } 63 | 64 | function show(id) { 65 | let el = document.querySelector(id); 66 | el.style.display = 'block'; 67 | } 68 | 69 | function hide(id) { 70 | let el = document.querySelector(id); 71 | el.style.display = 'none'; 72 | } 73 | 74 | function el(query) { 75 | return document.querySelector(query); 76 | } 77 | 78 | function showForm(selector, dataObject) { 79 | dataBind(selector, dataObject); 80 | el('#blur-wrapper').classList.add('blur'); 81 | show('#cover'); 82 | show(selector); 83 | } 84 | 85 | function move(arr, from, to) { 86 | arr.splice(to, 0, arr.splice(from, 1)[0]); 87 | } 88 | 89 | function hideForm(selector) { 90 | hide('#cover'); 91 | hide(selector); 92 | el('#blur-wrapper').classList.remove('blur'); 93 | } 94 | 95 | // Shows a message bar above the list of redirects. 96 | function showMessage(message, success) { 97 | let messageBox = document.getElementById('message-box'); 98 | dataBind('#message-box', {message}); 99 | if (success) { 100 | messageBox.className = 'visible success'; 101 | } else { 102 | messageBox.className = 'visible error'; 103 | } 104 | 105 | let timer = 20; 106 | 107 | //Remove the message in 20 seconds if it hasn't been changed... 108 | setTimeout(function() { 109 | if (el('#message').textContent === message) { 110 | messageBox.className = ''; //Removing .visible removes the box... 111 | } 112 | }, timer * 1000); 113 | } 114 | 115 | function hideMessage() { 116 | el('#message-box').className = ''; 117 | } -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "Redirector", 4 | "description": "Automatically redirect content based on user-defined rules.", 5 | "version": "3.5.4", 6 | "icons": { 7 | "16": "images/icon-light-theme-16.png", 8 | "19": "images/icon-light-theme-19.png", 9 | "32": "images/icon-light-theme-32.png", 10 | "38": "images/icon-light-theme-38.png", 11 | "48": "images/icon-light-theme-48.png", 12 | "64": "images/icon-light-theme-64.png", 13 | "128": "images/icon-light-theme-128.png" 14 | }, 15 | 16 | "permissions": [ 17 | "webRequest", 18 | "webRequestBlocking", 19 | "webNavigation", 20 | "storage", 21 | "tabs", 22 | "http://*/*", 23 | "https://*/*", 24 | "notifications" 25 | ], 26 | "applications": { 27 | "gecko": { 28 | "id": "redirector@einaregilsson.com" 29 | } 30 | }, 31 | "background": { 32 | "scripts": [ 33 | "js/redirect.js", 34 | "js/background.js" 35 | ], 36 | "persistent": true 37 | }, 38 | "options_ui": { 39 | "page": "popup.html", 40 | "chrome_style": true 41 | }, 42 | "browser_action": { 43 | "default_icon": { 44 | "16": "images/icon-light-theme-16.png", 45 | "19": "images/icon-light-theme-19.png", 46 | "32": "images/icon-light-theme-32.png", 47 | "38": "images/icon-light-theme-38.png", 48 | "48": "images/icon-light-theme-48.png", 49 | "64": "images/icon-light-theme-64.png", 50 | "128": "images/icon-light-theme-128.png" 51 | }, 52 | "default_title": "Redirector", 53 | "default_popup": "popup.html", 54 | "theme_icons": [ 55 | { 56 | "dark": "images/icon-light-theme-16.png", 57 | "light": "images/icon-dark-theme-16.png", 58 | "size": 16 59 | }, 60 | { 61 | "dark": "images/icon-light-theme-19.png", 62 | "light": "images/icon-dark-theme-19.png", 63 | "size": 19 64 | }, 65 | { 66 | "dark": "images/icon-light-theme-32.png", 67 | "light": "images/icon-dark-theme-32.png", 68 | "size": 32 69 | }, 70 | { 71 | "dark": "images/icon-light-theme-38.png", 72 | "light": "images/icon-dark-theme-38.png", 73 | "size": 38 74 | }, 75 | { 76 | "dark": "images/icon-light-theme-48.png", 77 | "light": "images/icon-dark-theme-48.png", 78 | "size": 48 79 | }, 80 | { 81 | "dark": "images/icon-light-theme-64.png", 82 | "light": "images/icon-dark-theme-64.png", 83 | "size": 64 84 | }, 85 | { 86 | "dark": "images/icon-light-theme-128.png", 87 | "light": "images/icon-dark-theme-128.png", 88 | "size": 128 89 | } 90 | ] 91 | } 92 | } -------------------------------------------------------------------------------- /nex-build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | # 3 | # Purpose: Convert a .zip file into a .nex file for Opera 4 | # Adapted from 5 | 6 | dir=$PWD 7 | zip="$PWD/$1" 8 | nex="$PWD/$2" 9 | key=$3 10 | pub="tmp.pub" 11 | sig="tmp.sig" 12 | trap 'rm -f "$pub" "$sig" "$zip"' EXIT 13 | 14 | # signature 15 | openssl sha1 -sha1 -binary -sign "$key" < "$zip" > "$sig" 16 | 17 | # public key 18 | openssl rsa -pubout -outform DER < "$key" > "$pub" 2>/dev/null 19 | 20 | byte_swap () { 21 | # Take "abcdefgh" and return it as "ghefcdab" 22 | echo "${1:6:2}${1:4:2}${1:2:2}${1:0:2}" 23 | } 24 | 25 | crmagic_hex="4372 3234" # Cr24 26 | version_hex="0200 0000" # 2 27 | pub_len_hex=$(byte_swap $(printf '%08x\n' $(ls -l "$pub" | awk '{print $5}'))) 28 | sig_len_hex=$(byte_swap $(printf '%08x\n' $(ls -l "$sig" | awk '{print $5}'))) 29 | ( 30 | echo "$crmagic_hex $version_hex $pub_len_hex $sig_len_hex" | xxd -r -p 31 | cat "$pub" "$sig" "$zip" 32 | ) > "$nex" 33 | echo "Wrote $nex" -------------------------------------------------------------------------------- /popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | REDIRECTOR 5 | 6 | 7 | 8 | 9 | 10 |

REDIRECTOR

11 |
Disabled
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /privacy.md: -------------------------------------------------------------------------------- 1 | ## Redirector Privacy Policy 2 | 3 | Redirector does not collect any data at all about its users, or send any data back to any central server. 4 | -------------------------------------------------------------------------------- /promo/screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/einaregilsson/Redirector/03f88b97520dbde505b7b5ec0cd14e7ff3877608/promo/screenshot1.png -------------------------------------------------------------------------------- /promo/screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/einaregilsson/Redirector/03f88b97520dbde505b7b5ec0cd14e7ff3877608/promo/screenshot2.png -------------------------------------------------------------------------------- /promo/screenshot3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/einaregilsson/Redirector/03f88b97520dbde505b7b5ec0cd14e7ff3877608/promo/screenshot3.png -------------------------------------------------------------------------------- /promo/screenshot4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/einaregilsson/Redirector/03f88b97520dbde505b7b5ec0cd14e7ff3877608/promo/screenshot4.png -------------------------------------------------------------------------------- /promo/smalltile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/einaregilsson/Redirector/03f88b97520dbde505b7b5ec0cd14e7ff3877608/promo/smalltile.png -------------------------------------------------------------------------------- /promo/tiles.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | REDIRECTOR TILES FOR WEB STORE 5 | 6 | 38 | 39 | 40 |
41 |
42 |

REDIRECTOR

43 | Go where YOU want! 44 |
45 | 46 | -------------------------------------------------------------------------------- /redirector.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | REDIRECTOR 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 | 15 | 16 |
17 |

Are you sure you want to delete this redirect?

18 |
19 | 20 | 21 |
22 |
23 | 24 | 25 |
26 |
27 | 28 | 29 |
30 |
31 | 32 | 33 |
34 |
35 | 36 | 37 |
38 |
39 | 40 | 41 |
42 |
43 | 44 | 45 | 46 |
47 |

Create Redirect

48 |
49 |
50 | 51 |
52 |
53 |
54 | 55 |
56 |
57 |
58 | 59 |
60 |
61 |
62 | 63 |
64 |
65 |
66 | 67 |
68 | 70 | 72 |
73 |
74 |
75 | 76 |
77 |
78 |
79 | 80 |
81 |
82 |
83 | 86 | 123 |
124 | 125 | 126 |
127 |
128 | 129 | 130 |
131 | 132 |

REDIRECTOR

133 |
Go where YOU want!
134 | 135 | 148 | 149 | 150 |
151 | 152 | 153 |
154 | 155 | 156 |
157 |
158 |
159 |

[Disabled]

160 |
161 |
162 | 163 |

164 |
165 |
166 |

167 |
168 |
169 |

170 |
171 |
172 |

173 |
174 |
175 |

176 |
177 |
178 |

179 |
180 |
181 |
182 | 183 | 184 | 185 | 188 | 189 | 190 | 191 | 194 | 195 |
196 | 200 |
201 |
202 | 203 |
204 | 205 | 208 | 209 |
210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | --------------------------------------------------------------------------------