├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── _locales ├── de │ └── messages.json ├── en │ └── messages.json ├── en_US │ └── messages.json ├── it │ └── messages.json ├── pt │ └── messages.json └── pt_BR │ └── messages.json ├── background.js ├── default-prefs.js ├── images ├── GitHub-Octocat.svg ├── GitHub-Typography.svg ├── allowall19.png ├── allowall38.png ├── allowonce-off.svg ├── allowonce-on.svg ├── blacklist38.png ├── blockall19.png ├── blockall38.png ├── delete.svg ├── delete38.png ├── delete38.svg ├── filtered19.png ├── filtered38.png ├── hasframe.svg ├── jaegerhut128.png ├── jaegerhut16.png ├── jaegerhut48.png ├── preferences.svg ├── relaxed19.png ├── relaxed38.png ├── undefined19.png ├── undefined38.png ├── websocket.svg └── whitelist38.png ├── manifest.json ├── popup.css ├── popup.html ├── popup.js ├── prefs.css ├── prefs.html ├── prefs.js └── theme.css /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ############# 2 | ## OS crap ## 3 | ############# 4 | 5 | # Windows image file caches 6 | Thumbs.db 7 | ehthumbs.db 8 | 9 | # Folder config file 10 | Desktop.ini 11 | 12 | # Recycle Bin used on file shares 13 | $RECYCLE.BIN/ 14 | 15 | # Mac crap 16 | .DS_Store 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The BSD 2-Clause License 2 | 3 | Copyright (c) 2016 André Zanghelini 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ScriptJäger 2 | __Der ScriptJäger - die Erweiterung für den Jägermeister!__ 3 | 4 | ScriptJäger is a script, frame and websocket connection management extension, it allows you to control which domains are allowed to load those resources for each page. 5 | 6 | ScriptJäger is inspired by [ScriptWeeder](https://github.com/lemonsqueeze/scriptweeder) by lemonsqueeze. 7 | 8 | ## How it works 9 | ScriptJäger only checks for scripts, frames and websocket connections on the http, https and file protocols, traffic from other resource types or protocols are not intercepted. 10 | 11 | ScriptJäger was created to be lightweigth, fast, powerful & easy-to-use. Anything is only loaded when really required. 12 | 13 | ScriptJäger is tested only under the [Vivaldi browser](https://vivaldi.com/), sometimes on Firefox and very sporadically on bare Chromium, though it should work on any browser built on Chromium, and very likely to work on any Gecko-based browser. No matter which browser you are using remember to include this info when reporting a bug. 14 | 15 | Check the [wiki](https://github.com/An-dz/ScriptJaeger/wiki) for info on how to use it. 16 | 17 | ## Download 18 | A _'stable'_ version of the extension as a CRX file can be downloaded on the [releases](https://github.com/An-dz/ScriptJaeger/releases) page. 19 | 20 | The extension automatically checks for updates every 24 hours and will notify you of updates, if you cancel the notification a new one will be shown in the next 24 hours. The update check will send a normal GET request to GitHub to download the latest manifest file here, no other information is sent. 21 | 22 | After downloading the latest CRX open the page, enable _developer mode_ at the top right, refresh the page and drop the CRX file on the page. 23 | 24 | This extension is not available at Chrome Web Store, if it ever becomes available on it this notice will be removed and a link to the store will be added. There are two reasons for this: 25 | 26 | * You must pay a fee to be there 27 | Yes, I must pay to help them make their browser more popular and track users 28 | * This fee is their verification method 29 | Yes, you pay and you are verified, this gives no protection to the users 30 | 31 | ## Permissions 32 | This extension requests some permissions, here's why it requires each one: 33 | 34 | * tabs 35 | To be able to access the tabs url and so apply the rules 36 | * storage 37 | To save & load preferences 38 | * webNavigation 39 | To know which kind of redirection it is to update the popup accordingly, preventing resetting of information on dynamically loaded pages like YouTube, Facebook & Twitter 40 | * webRequest & webRequestBlocking 41 | To be able to intercept the scripts before requesting them and then block if necessary 42 | * \ 43 | To work on any page 44 | * alarms 45 | To check for updates 46 | * notifications 47 | To notify you about an update 48 | -------------------------------------------------------------------------------- /_locales/de/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "sjDescription": { 3 | "message": "Legen Sie fest, welche Scripts, Frames und Websocket-Verbindungen pro Webseite geladen werden sollen.", 4 | "description": "Description in the Extensions page" 5 | }, 6 | "scopePage": { 7 | "message": "Seite", 8 | "description": "Scope where the rules will be applied. Shown on popup first line." 9 | }, 10 | "scopeSite": { 11 | "message": "Website", 12 | "description": "Scope where the rules will be applied. Shown on popup first line." 13 | }, 14 | "scopeDomain": { 15 | "message": "Domain", 16 | "description": "Scope where the rules will be applied. Shown on popup first line." 17 | }, 18 | "scopeGlobal": { 19 | "message": "Global", 20 | "description": "Scope where the rules will be applied. Shown on popup first line." 21 | }, 22 | "policyBlockAll": { 23 | "message": "Blockiert", 24 | "description": "Policy applied on level. Shown in popup second line & multiple other areas in settings." 25 | }, 26 | "policyFiltered": { 27 | "message": "Gefiltert", 28 | "description": "Policy applied on level. Shown in popup second line & multiple other areas in settings." 29 | }, 30 | "policyRelaxed": { 31 | "message": "Gelockert", 32 | "description": "Policy applied on level. Shown in popup second line & multiple other areas in settings." 33 | }, 34 | "policyAllowAll": { 35 | "message": "Erlaubt", 36 | "description": "Policy applied on level. Shown in popup second line & multiple other areas in settings." 37 | }, 38 | "policyAllowOnce": { 39 | "message": "Neuladen & Alle ein Mal zulassen", 40 | "description": "Tooltip in popup second line, last button." 41 | }, 42 | "policyAllowOnceDisable": { 43 | "message": "Neuladen & Wiederherstellen der normalen Einstellungen", 44 | "description": "Tooltip in popup second line, last button when Allow Once is enabled." 45 | }, 46 | "errorFTP": { 47 | "message": "FTP-Seiten werden nicht gescannt", 48 | "description": "Error message when opening the popup from an invalid page. FTP is a protocol, like http, and so this error tells that pages generated from FTP are ignored. Reason: FTP pages are generated by Chromium and so can't contain any of those files" 49 | }, 50 | "errorFile": { 51 | "message": "Lokale Dateien werden nicht gescannt", 52 | "description": "Error message when opening the popup from an invalid page. Reason: No support yet." 53 | }, 54 | "errorInternal": { 55 | "message": "Interne Seiten werden nicht gescannt", 56 | "description": "Error message when opening the popup from an invalid page. Reason: Internal pages can't be accessed by the API." 57 | }, 58 | "seeResources": { 59 | "message": "Klicken Sie hier, um die Liste der Skripte, Frames oder Websocket-Verbindungen anzuzeigen.", 60 | "description": "Hover number at the right of domain name in popup." 61 | }, 62 | "tooltipSettings": { 63 | "message": "Einstellungen", 64 | "description": "Hover tooltip for last button of the first line in popup." 65 | }, 66 | "settingsProject": { 67 | "message": "Projektseite aufrufen", 68 | "description": "Hover tooltip for GitHub button at top right. Opens project homepage." 69 | }, 70 | "settingsPolicy": { 71 | "message": "Standard-Richtlinie", 72 | "description": "Settings title." 73 | }, 74 | "settingsPolicyPrivate": { 75 | "message": "Standardrichtlinie für das private Surfen", 76 | "description": "Settings title." 77 | }, 78 | "settingsScripts": { 79 | "message": "Whitelist & Schwarze Liste", 80 | "description": "Settings title." 81 | }, 82 | "settingsRules": { 83 | "message": "Regeln", 84 | "description": "Settings title." 85 | }, 86 | "settingsRulesNone": { 87 | "message": "Keine Regel", 88 | "description": "When a rule has no rule, rule is under a lower level." 89 | }, 90 | "settingsRulesWhite": { 91 | "message": "Erlaubt", 92 | "description": "When a rule is to whitelist." 93 | }, 94 | "settingsRulesBlack": { 95 | "message": "Verboten", 96 | "description": "When a rule is blacklist." 97 | }, 98 | "settingsManage": { 99 | "message": "Verwaltung der Einstellungen", 100 | "description": "Settings title. Keep consistent with tooltipSettings." 101 | }, 102 | "settingsManageExport": { 103 | "message": "Exportieren", 104 | "description": "Button." 105 | }, 106 | "settingsManageReset": { 107 | "message": "Zurücksetzen", 108 | "description": "Button." 109 | }, 110 | "settingsManageImport": { 111 | "message": "Importieren", 112 | "description": "Button." 113 | }, 114 | "settingsManageExportTooltip": { 115 | "message": "Die Einstellungen werden in den unteren Textbereich exportiert.", 116 | "description": "Button tooltip." 117 | }, 118 | "settingsManageResetTooltip": { 119 | "message": "Es gibt keinen Bestätigungsdialog und kein Zurück! Exportieren Sie Ihre Einstellungen, wenn Sie sie behalten wollen!", 120 | "description": "Button tooltip." 121 | }, 122 | "settingsManageImportTooltip": { 123 | "message": "Fügen Sie Ihre alten Einstellungen in das Textfeld ein und drücken Sie dann diese Schaltfläche.", 124 | "description": "Button tooltip." 125 | }, 126 | "settingsManagePlaceholder": { 127 | "message": "(Einstellungen zum Importieren hier einfügen)", 128 | "description": "Text in text box to explain how to import." 129 | }, 130 | "settingsDelete": { 131 | "message": "Löschen", 132 | "description": "Delete button." 133 | }, 134 | "settingsNumScripts": { 135 | "message": "Skript-Regeln in dieser Ebene", 136 | "description": "Tooltip in number square. Shows how many scripts are allowed or blocked in this level." 137 | }, 138 | "settingsNumLevels": { 139 | "message": "Untergeordnete Regeln", 140 | "description": "Tooltip in number square. Shows how many rules for a more specific scope exist under that level. E.g. number of sub-domain specific rules exist under a domain rule." 141 | }, 142 | "settingsTitle": { 143 | "message": "ScriptJäger Einstellungen", 144 | "description": "Page/tab title. Keep consistent with tooltipSettings" 145 | }, 146 | "tooltipWebsockets": { 147 | "message": "Websockets: $1", 148 | "description": "Hover number or icon in popup." 149 | }, 150 | "tooltipFrames": { 151 | "message": "Frames: $1", 152 | "description": "Hover number or icon in popup." 153 | }, 154 | "tooltipScripts": { 155 | "message": "Scripte: $1", 156 | "description": "Hover number or icon in popup." 157 | }, 158 | "updateTitle": { 159 | "message": "ScriptJäger Update", 160 | "description": "Title of the notification when an update is available." 161 | }, 162 | "updateMessage": { 163 | "message": "Version $1 ist verfügbar\nSie verwenden die Version $2.", 164 | "description": "Message of the notification when an update is available." 165 | }, 166 | "updateContext": { 167 | "message": "Klicken Sie hier, um die Download-Seite zu öffnen", 168 | "description": "Presentation varies according to OS, but purpose is to explain what action will happen when clicking on the notification." 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "sjDescription": { 3 | "message": "Control which scripts, frames & websocket connections to load per webpage.", 4 | "description": "Description in the Extensions page" 5 | }, 6 | "scopePage": { 7 | "message": "Page", 8 | "description": "Scope where the rules will be applied. Shown on popup first line." 9 | }, 10 | "scopeSite": { 11 | "message": "Site", 12 | "description": "Scope where the rules will be applied. Shown on popup first line." 13 | }, 14 | "scopeDomain": { 15 | "message": "Domain", 16 | "description": "Scope where the rules will be applied. Shown on popup first line." 17 | }, 18 | "scopeGlobal": { 19 | "message": "Global", 20 | "description": "Scope where the rules will be applied. Shown on popup first line." 21 | }, 22 | "policyBlockAll": { 23 | "message": "Block All", 24 | "description": "Policy applied on level. Shown in popup second line & multiple other areas in settings." 25 | }, 26 | "policyFiltered": { 27 | "message": "Filtered", 28 | "description": "Policy applied on level. Shown in popup second line & multiple other areas in settings." 29 | }, 30 | "policyRelaxed": { 31 | "message": "Relaxed", 32 | "description": "Policy applied on level. Shown in popup second line & multiple other areas in settings." 33 | }, 34 | "policyAllowAll": { 35 | "message": "Allow All", 36 | "description": "Policy applied on level. Shown in popup second line & multiple other areas in settings." 37 | }, 38 | "policyAllowOnce": { 39 | "message": "Reload & Allow all once", 40 | "description": "Tooltip in popup second line, last button." 41 | }, 42 | "policyAllowOnceDisable": { 43 | "message": "Reload & Go back to normal settings", 44 | "description": "Tooltip in popup second line, last button when Allow Once is enabled." 45 | }, 46 | "errorFTP": { 47 | "message": "FTP pages are not scanned", 48 | "description": "Error message when opening the popup from an invalid page. FTP is a protocol, like http, and so this error tells that pages generated from FTP are ignored. Reason: FTP pages are generated by Chromium and so can't contain any of those files" 49 | }, 50 | "errorFile": { 51 | "message": "Local files are not scanned", 52 | "description": "Error message when opening the popup from an invalid page. Reason: No support yet." 53 | }, 54 | "errorInternal": { 55 | "message": "Internal pages cannot be scanned", 56 | "description": "Error message when opening the popup from an invalid page. Reason: Internal pages can't be accessed by the API." 57 | }, 58 | "seeResources": { 59 | "message": "Click to see the list of scripts, frames or websocket connections", 60 | "description": "Hover number at the right of domain name in popup." 61 | }, 62 | "tooltipSettings": { 63 | "message": "Settings", 64 | "description": "Hover tooltip for last button of the first line in popup." 65 | }, 66 | "settingsProject": { 67 | "message": "Open project page", 68 | "description": "Hover tooltip for GitHub button at top right. Opens project homepage." 69 | }, 70 | "settingsPolicy": { 71 | "message": "Default policy", 72 | "description": "Settings title." 73 | }, 74 | "settingsPolicyPrivate": { 75 | "message": "Default policy on private browsing", 76 | "description": "Settings title." 77 | }, 78 | "settingsScripts": { 79 | "message": "Whitelist & Blacklist", 80 | "description": "Settings title." 81 | }, 82 | "settingsRules": { 83 | "message": "Rules", 84 | "description": "Settings title." 85 | }, 86 | "settingsRulesNone": { 87 | "message": "No Rule", 88 | "description": "When a rule has no rule, rule is under a lower level." 89 | }, 90 | "settingsRulesWhite": { 91 | "message": "Whitelisted", 92 | "description": "When a rule is to whitelist." 93 | }, 94 | "settingsRulesBlack": { 95 | "message": "Blacklisted", 96 | "description": "When a rule is blacklist." 97 | }, 98 | "settingsOthers": { 99 | "message": "Other Settings", 100 | "description": "Settings title." 101 | }, 102 | "settingsPing": { 103 | "message": "Disable ping", 104 | "description": "Checkbox text. Ping is a W3C HTML feature, you probably don't want to translate it." 105 | }, 106 | "settingsPingDesc": { 107 | "message": "Ping is a feature used by some sites for tracking user clicks. There's no reason to actually disable this.", 108 | "description": "Text below ping disabling checkbox explaining the setting." 109 | }, 110 | "settingsManage": { 111 | "message": "Settings Management", 112 | "description": "Settings title. Keep consistent with tooltipSettings." 113 | }, 114 | "settingsManageExport": { 115 | "message": "Export", 116 | "description": "Button." 117 | }, 118 | "settingsManageReset": { 119 | "message": "Reset", 120 | "description": "Button." 121 | }, 122 | "settingsManageImport": { 123 | "message": "Import", 124 | "description": "Button." 125 | }, 126 | "settingsManageMerge": { 127 | "message": "Merge", 128 | "description": "Button." 129 | }, 130 | "settingsManageExportTooltip": { 131 | "message": "Export your preferences for backup.", 132 | "description": "Button tooltip." 133 | }, 134 | "settingsManageExportTitle": { 135 | "message": "Export Preferences", 136 | "description": "Title for the export preferences dialogue." 137 | }, 138 | "settingsManageExportText": { 139 | "message": "Your current preferences were exported into the text area below.", 140 | "description": "Export preferences dialogue text." 141 | }, 142 | "settingsManageResetTooltip": { 143 | "message": "Reset preferences to default.", 144 | "description": "Button tooltip." 145 | }, 146 | "settingsManageResetTitle": { 147 | "message": "Reset Preferences", 148 | "description": "Title for the reset preferences confirmation dialogue." 149 | }, 150 | "settingsManageResetText": { 151 | "message": "Are you sure you want to reset preferences to the default? There's no going back! Export your settings if you want them!", 152 | "description": "Reset preferences confirmation dialogue." 153 | }, 154 | "settingsManageImportTooltip": { 155 | "message": "Replace your preferences from a backup.", 156 | "description": "Button tooltip." 157 | }, 158 | "settingsManageImportTitle": { 159 | "message": "Import Preferences", 160 | "description": "Title for the import preferences confirmation dialogue." 161 | }, 162 | "settingsManageImportText": { 163 | "message": "Paste a preferences file in the text area below and press the $1 button to import.", 164 | "description": "Import preferences confirmation dialogue. $1 is replaced by the text of `buttonOk`." 165 | }, 166 | "settingsManageMergeTooltip": { 167 | "message": "Merge new rules into your preferences.", 168 | "description": "Button tooltip." 169 | }, 170 | "settingsManageMergeTitle": { 171 | "message": "Merge Preferences", 172 | "description": "Title for the merge preferences confirmation dialogue." 173 | }, 174 | "settingsManageMergeText": { 175 | "message": "Paste a rules object in the text area below and press the $1 button to begin the merge process. You'll be able to see the differences and choose what to merge.", 176 | "description": "Merge preferences confirmation dialogue. $1 is replaced by the text of `buttonOk`." 177 | }, 178 | "settingsManagePlaceholder": { 179 | "message": "(Paste settings here to import)", 180 | "description": "Text in text box to explain how to import." 181 | }, 182 | "settingsDelete": { 183 | "message": "Delete", 184 | "description": "Delete button." 185 | }, 186 | "settingsNumScripts": { 187 | "message": "Script rules in this level", 188 | "description": "Tooltip in number square. Shows how many scripts are allowed or blocked in this level." 189 | }, 190 | "settingsNumLevels": { 191 | "message": "Sub-level rules", 192 | "description": "Tooltip in number square. Shows how many rules for a more specific scope exist under that level. E.g. number of sub-domain specific rules exist under a domain rule." 193 | }, 194 | "settingsTitle": { 195 | "message": "ScriptJäger Settings", 196 | "description": "Page/tab title. Keep consistent with tooltipSettings" 197 | }, 198 | "settingsMergeTitle": { 199 | "message": "Merge Preferences", 200 | "description": "Title for the popup that allows merging preferences" 201 | }, 202 | "settingsMergeMsg": { 203 | "message": "Verify below what will change in your preferences. You can also choose to not apply some of the changes by ticking the checkboxes.", 204 | "description": "Text in popup explaining what merging preferences mean." 205 | }, 206 | "settingsMergeFailMsg": { 207 | "message": "There's nothing to merge, your preferences already match what you are trying to merge.", 208 | "description": "Sometimes nothing can be merged so we tell the user about that." 209 | }, 210 | "settingsWarningTitle": { 211 | "message": "Unknown Keys Ignored", 212 | "description": "Title for alert box warning user that some of the keys in the imported file were ignored. Key can also be translated as Parameter." 213 | }, 214 | "settingsWarningText": { 215 | "message": "Key '$1' is unknown and was ignored", 216 | "description": "When a key from an imported file is found to be useless it's entirely ignored by the imported and so won't make into the final imported preferences. Key can also be translated as Parameter." 217 | }, 218 | "settingsWarningText2": { 219 | "message": "Key '$1' contains children but it's already the lowest possible level. All children were ignored.", 220 | "description": "Policy rules can be 3 levels deep – main domain (o.it), sub-domain (d.o.it) and page (d.o.it/now) – and script blocking lists can be 2 levels deep, this warning is raised when there are more levels. $1 is replaced by the key name. Key can also be translated as Parameter." 221 | }, 222 | "settingsInvalidPrefs": { 223 | "message": "Invalid Preferences Object", 224 | "description": "Title for an alert box telling the preferences file being imported is invalid" 225 | }, 226 | "settingsInvalidData": { 227 | "message": "The data you are trying to import is invalid. Preferences from versions before 1.0.0 can't be imported.", 228 | "description": "tells the user the data is not valid and also alerts that preferences from older versions are unsupported" 229 | }, 230 | "settingsInvalidDelete": { 231 | "message": "'$1' key should not exist in your preferences file, remove it to prevent possible data loss.", 232 | "description": "Some keys should not exist in some places and their existence may lead to catastrophic data loss of the preferences. Key can also be translated as Parameter." 233 | }, 234 | "settingsInvalidLevel": { 235 | "message": "'$1' key should not be at this level", 236 | "description": "tells the user the key should not be there. Key can also be translated as Parameter." 237 | }, 238 | "settingsInvalidObject": { 239 | "message": "'$1' key is not an Object", 240 | "description": "tells the user the key is not a JavaScript Object. Key can also be translated as Parameter." 241 | }, 242 | "settingsInvalidNumber": { 243 | "message": "'$1' key is not a Number", 244 | "description": "tells the user the key is not a JavaScript Number. Key can also be translated as Parameter." 245 | }, 246 | "settingsInvalidNumberNull": { 247 | "message": "'$1' key is not a Number or null", 248 | "description": "tells the user the key is not a JavaScript Number or its value is null, don't translate null. Key can also be translated as Parameter." 249 | }, 250 | "settingsInvalidBooleanNull": { 251 | "message": "'$1' key is not a Boolean or null", 252 | "description": "tells the user the key is not a JavaScript Boolean or its value is null, don't translate null. Key can also be translated as Parameter." 253 | }, 254 | "settingsInvalidRange": { 255 | "message": "'$1' key is not under the range 0-3", 256 | "description": "tells the user the key value is not between 0 and 3. Key can also be translated as Parameter." 257 | }, 258 | "settingsInvalidRangeNull": { 259 | "message": "'$1' key is not under the range 0-3 or null", 260 | "description": "tells the user the key value is not between 0 and 3 or its value is null, don't translate null. Key can also be translated as Parameter." 261 | }, 262 | "settingsInvalidUnder": { 263 | "message": "under $1", 264 | "description": "Used on settings validation. Tells under which level the problem is, $1 is replaced with the key name. It gets repeated to show the person where's the problem in the JSON." 265 | }, 266 | "settingsInvalidValue": { 267 | "message": "'$1' key must have a value of '$2'", 268 | "description": "Tells that a value can only be a specific one. Key can also be translated as Parameter." 269 | }, 270 | "settingsMergeCurrent": { 271 | "message": "Current: $1", 272 | "description": "When merging preferences, tooltip when hovering left hat. $1 is replaced by rule name." 273 | }, 274 | "settingsMergeNew": { 275 | "message": "New: $1", 276 | "description": "When merging preferences, tooltip when hovering right hat. $1 is replaced by rule name." 277 | }, 278 | "settingsEnabledCheckbox": { 279 | "message": "Checked: Will be merged\nUnchecked: Will not be merged", 280 | "description": "When merging preferences this is the tooltip for the checkboxes." 281 | }, 282 | "settingsDisabledCheckbox": { 283 | "message": "This rule is being deleted on a top level and this is only showning what's in your current settings.", 284 | "description": "When merging preferences some rules can tell to delete a level, anything below the level is shown in the UI to let the user know what's happening, but they can't choose to delete some and keep others." 285 | }, 286 | "buttonOk": { 287 | "message": "OK", 288 | "description": "An OK button to accept some dialogue box" 289 | }, 290 | "buttonCancel": { 291 | "message": "Cancel", 292 | "description": "A Cancel button to cancel some dialogue box" 293 | }, 294 | "tooltipWebsockets": { 295 | "message": "Websockets: $1", 296 | "description": "Hover number or icon in popup." 297 | }, 298 | "tooltipFrames": { 299 | "message": "Frames: $1", 300 | "description": "Hover number or icon in popup." 301 | }, 302 | "tooltipScripts": { 303 | "message": "Scripts: $1", 304 | "description": "Hover number or icon in popup." 305 | }, 306 | "updateTitle": { 307 | "message": "ScriptJäger Update", 308 | "description": "Title of the notification when an update is available." 309 | }, 310 | "updateMessage": { 311 | "message": "Version $1 is available\nYou are using version $2", 312 | "description": "Message of the notification when an update is available." 313 | }, 314 | "updateContext": { 315 | "message": "Click here to open the download page", 316 | "description": "Presentation varies according to OS, but purpose is to explain what action will happen when clicking on the notification." 317 | } 318 | } 319 | -------------------------------------------------------------------------------- /_locales/en_US/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "settingsManageResetTooltip": { 3 | "message": "There's no confirmation dialog and no going back! Export your settings if you want them!", 4 | "description": "Button tooltip." 5 | }, 6 | "settingsNumLevels": { 7 | "message": "Sublevel rules", 8 | "description": "Tooltip in number square. Shows how many rules for a more specific scope exist under that level. E.g. number of sub-domain specific rules exist under a domain rule." 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /_locales/it/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "sjDescription": { 3 | "message": "Controlla quali script, frame e connessioni websocket i siti possono caricare.", 4 | "description": "Description in the Extensions page" 5 | }, 6 | "scopePage": { 7 | "message": "Pagina", 8 | "description": "Scope where the rules will be applied. Shown on popup first line." 9 | }, 10 | "scopeSite": { 11 | "message": "Sito", 12 | "description": "Scope where the rules will be applied. Shown on popup first line." 13 | }, 14 | "scopeDomain": { 15 | "message": "Dominio", 16 | "description": "Scope where the rules will be applied. Shown on popup first line." 17 | }, 18 | "scopeGlobal": { 19 | "message": "Globale", 20 | "description": "Scope where the rules will be applied. Shown on popup first line." 21 | }, 22 | "policyBlockAll": { 23 | "message": "Blocca", 24 | "description": "Policy applied on level. Shown in popup second line & multiple other areas in settings." 25 | }, 26 | "policyFiltered": { 27 | "message": "Filtrato", 28 | "description": "Policy applied on level. Shown in popup second line & multiple other areas in settings." 29 | }, 30 | "policyRelaxed": { 31 | "message": "Rilassato", 32 | "description": "Policy applied on level. Shown in popup second line & multiple other areas in settings." 33 | }, 34 | "policyAllowAll": { 35 | "message": "Permetti", 36 | "description": "Policy applied on level. Shown in popup second line & multiple other areas in settings." 37 | }, 38 | "policyAllowOnce": { 39 | "message": "Ricarica e Permetti tutto questa volta", 40 | "description": "Tooltip in popup second line, last button." 41 | }, 42 | "policyAllowOnceDisable": { 43 | "message": "Ricarica e Torna alle impostazioni normali", 44 | "description": "Tooltip in popup second line, last button when Allow Once is enabled." 45 | }, 46 | "errorFTP": { 47 | "message": "Le pagine FTP non sono scansionate", 48 | "description": "Error message when opening the popup from an invalid page. FTP is a protocol, like http, and so this error tells that pages generated from FTP are ignored. Reason: FTP pages are generated by Chromium and so can't contain any of those files" 49 | }, 50 | "errorFile": { 51 | "message": "I file locali non sono scansionati", 52 | "description": "Error message when opening the popup from an invalid page. Reason: No support yet." 53 | }, 54 | "errorInternal": { 55 | "message": "Le pagine interne non sono scansionate", 56 | "description": "Error message when opening the popup from an invalid page. Reason: Internal pages can't be accessed by the API." 57 | }, 58 | "seeResources": { 59 | "message": "Clicca per vedere l'elenco degli script, frame o delle connessioni websocket", 60 | "description": "Hover number at the right of domain name in popup." 61 | }, 62 | "tooltipSettings": { 63 | "message": "Impostazioni", 64 | "description": "Hover tooltip for last button of the first line in popup." 65 | }, 66 | "settingsProject": { 67 | "message": "Apri la pagina del progetto", 68 | "description": "Hover tooltip for GitHub button at top right. Opens project homepage." 69 | }, 70 | "settingsPolicy": { 71 | "message": "Informativa predefinita", 72 | "description": "Settings title." 73 | }, 74 | "settingsPolicyPrivate": { 75 | "message": "Informativa predefinita nella navigazione riservata", 76 | "description": "Settings title." 77 | }, 78 | "settingsScripts": { 79 | "message": "Whitelist e Blacklist", 80 | "description": "Settings title." 81 | }, 82 | "settingsRules": { 83 | "message": "Regole", 84 | "description": "Settings title." 85 | }, 86 | "settingsRulesNone": { 87 | "message": "Nessuna Regola", 88 | "description": "When a rule has no rule, rule is under a lower level." 89 | }, 90 | "settingsRulesWhite": { 91 | "message": "Permesso", 92 | "description": "When a rule is to whitelist." 93 | }, 94 | "settingsRulesBlack": { 95 | "message": "Bloccato", 96 | "description": "When a rule is blacklist." 97 | }, 98 | "settingsManage": { 99 | "message": "Controllo impostazioni", 100 | "description": "Settings title. Keep consistent with tooltipSettings." 101 | }, 102 | "settingsManageExport": { 103 | "message": "Esporta", 104 | "description": "Button." 105 | }, 106 | "settingsManageReset": { 107 | "message": "Reimposta", 108 | "description": "Button." 109 | }, 110 | "settingsManageImport": { 111 | "message": "Importa", 112 | "description": "Button." 113 | }, 114 | "settingsManageExportTooltip": { 115 | "message": "Le impostazioni saranno esportate nell'area di testo in basso.", 116 | "description": "Button tooltip." 117 | }, 118 | "settingsManageResetTooltip": { 119 | "message": "Non c'è nessun dialogo di conferma e non si torna indietro! Esportare le tue impostazioni, se lo vuoi!", 120 | "description": "Button tooltip." 121 | }, 122 | "settingsManageImportTooltip": { 123 | "message": "Incolla le vecchie impostazioni nell'area di testo in basso e dopo premi questo pulsante.", 124 | "description": "Button tooltip." 125 | }, 126 | "settingsManagePlaceholder": { 127 | "message": "(Incolla le impostazioni qui per importarle)", 128 | "description": "Text in text box to explain how to import." 129 | }, 130 | "settingsDelete": { 131 | "message": "Elimina", 132 | "description": "Delete button." 133 | }, 134 | "settingsNumScripts": { 135 | "message": "Regole dello script in questo livello", 136 | "description": "Tooltip in number square. Shows how many scripts are allowed or blocked in this level." 137 | }, 138 | "settingsNumLevels": { 139 | "message": "Regole di sottolivello", 140 | "description": "Tooltip in number square. Shows how many rules for a more specific scope exist under that level. E.g. number of sub-domain specific rules exist under a domain rule." 141 | }, 142 | "settingsTitle": { 143 | "message": "ScriptJäger - Impostazioni", 144 | "description": "Page/tab title. Keep consistent with tooltipSettings" 145 | }, 146 | "tooltipWebsockets": { 147 | "message": "Websocket: $1", 148 | "description": "Hover number or icon in popup." 149 | }, 150 | "tooltipFrames": { 151 | "message": "Frame: $1", 152 | "description": "Hover number or icon in popup." 153 | }, 154 | "tooltipScripts": { 155 | "message": "Script: $1", 156 | "description": "Hover number or icon in popup." 157 | }, 158 | "updateTitle": { 159 | "message": "Aggiornamento per ScriptJäger", 160 | "description": "Title of the notification when an update is available." 161 | }, 162 | "updateMessage": { 163 | "message": "La versione $1 è disponibile\nStai usando la versione $2", 164 | "description": "Message of the notification when an update is available." 165 | }, 166 | "updateContext": { 167 | "message": "Clicca qui per aprire la pagina da cui scaricarlo", 168 | "description": "Presentation varies according to OS, but purpose is to explain what action will happen when clicking on the notification." 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /_locales/pt/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "sjDescription": { 3 | "message": "Controle quais os scripts, frames e conexões por websocket que serão carregados por página.", 4 | "description": "Description in the Extensions page" 5 | }, 6 | "scopePage": { 7 | "message": "Página", 8 | "description": "Scope where the rules will be applied. Shown on popup first line." 9 | }, 10 | "scopeSite": { 11 | "message": "Site", 12 | "description": "Scope where the rules will be applied. Shown on popup first line." 13 | }, 14 | "scopeDomain": { 15 | "message": "Domínio", 16 | "description": "Scope where the rules will be applied. Shown on popup first line." 17 | }, 18 | "scopeGlobal": { 19 | "message": "Global", 20 | "description": "Scope where the rules will be applied. Shown on popup first line." 21 | }, 22 | "policyBlockAll": { 23 | "message": "Bloquear", 24 | "description": "Policy applied on level. Shown in popup second line & multiple other areas in settings." 25 | }, 26 | "policyFiltered": { 27 | "message": "Filtrado", 28 | "description": "Policy applied on level. Shown in popup second line & multiple other areas in settings." 29 | }, 30 | "policyRelaxed": { 31 | "message": "Relaxado", 32 | "description": "Policy applied on level. Shown in popup second line & multiple other areas in settings." 33 | }, 34 | "policyAllowAll": { 35 | "message": "Permitir", 36 | "description": "Policy applied on level. Shown in popup second line & multiple other areas in settings." 37 | }, 38 | "policyAllowOnce": { 39 | "message": "Atualizar a página e permitir tudo apenas esta vez", 40 | "description": "Tooltip in popup second line, last button." 41 | }, 42 | "policyAllowOnceDisable": { 43 | "message": "Atualizar a página e voltar às definições normais", 44 | "description": "Tooltip in popup second line, last button when Allow Once is enabled." 45 | }, 46 | "errorFTP": { 47 | "message": "As páginas FTP não são analisadas", 48 | "description": "Error message when opening the popup from an invalid page. FTP is a protocol, like http, and so this error tells that pages generated from FTP are ignored. Reason: FTP pages are generated by Chromium and so can't contain any of those files" 49 | }, 50 | "errorFile": { 51 | "message": "Os ficheiros locais não são analisados", 52 | "description": "Error message when opening the popup from an invalid page. Reason: No support yet." 53 | }, 54 | "errorInternal": { 55 | "message": "As páginas internas não são analisadas", 56 | "description": "Error message when opening the popup from an invalid page. Reason: Internal pages can't be accessed by the API." 57 | }, 58 | "seeResources": { 59 | "message": "Clique para ver a lista de scripts, frames ou conexões websocket", 60 | "description": "Hover number at the right of domain name in popup." 61 | }, 62 | "tooltipSettings": { 63 | "message": "Definições", 64 | "description": "Hover tooltip for last button of the first line in popup." 65 | }, 66 | "settingsProject": { 67 | "message": "Abrir página do projeto", 68 | "description": "Hover tooltip for GitHub button at top right. Opens project homepage." 69 | }, 70 | "settingsPolicy": { 71 | "message": "Política padrão", 72 | "description": "Settings title." 73 | }, 74 | "settingsPolicyPrivate": { 75 | "message": "Política padrão em navegação privada", 76 | "description": "Settings title." 77 | }, 78 | "settingsScripts": { 79 | "message": "Lista negra", 80 | "description": "Settings title." 81 | }, 82 | "settingsRules": { 83 | "message": "Regras", 84 | "description": "Settings title." 85 | }, 86 | "settingsRulesNone": { 87 | "message": "Sem regras", 88 | "description": "When a rule has no rule, rule is under a lower level." 89 | }, 90 | "settingsRulesWhite": { 91 | "message": "Permitido", 92 | "description": "When a rule is to whitelist." 93 | }, 94 | "settingsRulesBlack": { 95 | "message": "Bloqueado", 96 | "description": "When a rule is blacklist." 97 | }, 98 | "settingsManage": { 99 | "message": "Gestão de definições", 100 | "description": "Settings title. Keep consistent with tooltipSettings." 101 | }, 102 | "settingsManageExport": { 103 | "message": "Exportar", 104 | "description": "Button." 105 | }, 106 | "settingsManageReset": { 107 | "message": "Repor", 108 | "description": "Button." 109 | }, 110 | "settingsManageImport": { 111 | "message": "Importar", 112 | "description": "Button." 113 | }, 114 | "settingsManageExportTooltip": { 115 | "message": "As definições serão exportadas para o campo de texto abaixo.", 116 | "description": "Button tooltip." 117 | }, 118 | "settingsManageResetTooltip": { 119 | "message": "Não existe nenhuma janela de confirmação nem forma de voltar atrás! Exporte as suas definições se as deseja manter!", 120 | "description": "Button tooltip." 121 | }, 122 | "settingsManageImportTooltip": { 123 | "message": "Cole as suas antigas definições no campo de texto abaixo e, depois, pressione este botão.", 124 | "description": "Button tooltip." 125 | }, 126 | "settingsManagePlaceholder": { 127 | "message": "(Cole as definições aqui para importar)", 128 | "description": "Text in text box to explain how to import." 129 | }, 130 | "settingsDelete": { 131 | "message": "Apagar", 132 | "description": "Delete button." 133 | }, 134 | "settingsNumScripts": { 135 | "message": "Regras de script neste nível", 136 | "description": "Tooltip in number square. Shows how many scripts are allowed or blocked in this level." 137 | }, 138 | "settingsNumLevels": { 139 | "message": "Sub-níveis", 140 | "description": "Tooltip in number square. Shows how many rules for a more specific scope exist under that level. E.g. number of sub-domain specific rules exist under a domain rule." 141 | }, 142 | "settingsTitle": { 143 | "message": "Definições do ScriptJäger", 144 | "description": "Page/tab title. Keep consistent with tooltipSettings" 145 | }, 146 | "tooltipWebsockets": { 147 | "message": "Websockets: $1", 148 | "description": "Hover number or icon in popup." 149 | }, 150 | "tooltipFrames": { 151 | "message": "Frames: $1", 152 | "description": "Hover number or icon in popup." 153 | }, 154 | "tooltipScripts": { 155 | "message": "Scripts: $1", 156 | "description": "Hover number or icon in popup." 157 | }, 158 | "updateTitle": { 159 | "message": "Atualização do ScriptJäger", 160 | "description": "Title of the notification when an update is available." 161 | }, 162 | "updateMessage": { 163 | "message": "Está disponível a versão $1\nEstá a utilizar a versão $2", 164 | "description": "Message of the notification when an update is available." 165 | }, 166 | "updateContext": { 167 | "message": "Clique aqui para abrir a página de transferências", 168 | "description": "Presentation varies according to OS, but purpose is to explain what action will happen when clicking on the notification." 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /_locales/pt_BR/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "sjDescription": { 3 | "message": "Controle quais scripts, frames & conexões por websocket os sites podem fazer.", 4 | "description": "Description in the Extensions page" 5 | }, 6 | "scopePage": { 7 | "message": "Página", 8 | "description": "Scope where the rules will be applied. Shown on popup first line." 9 | }, 10 | "scopeSite": { 11 | "message": "Site", 12 | "description": "Scope where the rules will be applied. Shown on popup first line." 13 | }, 14 | "scopeDomain": { 15 | "message": "Domínio", 16 | "description": "Scope where the rules will be applied. Shown on popup first line." 17 | }, 18 | "scopeGlobal": { 19 | "message": "Global", 20 | "description": "Scope where the rules will be applied. Shown on popup first line." 21 | }, 22 | "policyBlockAll": { 23 | "message": "Bloquear", 24 | "description": "Policy applied on level. Shown in popup second line & multiple other areas in settings." 25 | }, 26 | "policyFiltered": { 27 | "message": "Filtrado", 28 | "description": "Policy applied on level. Shown in popup second line & multiple other areas in settings." 29 | }, 30 | "policyRelaxed": { 31 | "message": "Relaxado", 32 | "description": "Policy applied on level. Shown in popup second line & multiple other areas in settings." 33 | }, 34 | "policyAllowAll": { 35 | "message": "Permitir", 36 | "description": "Policy applied on level. Shown in popup second line & multiple other areas in settings." 37 | }, 38 | "policyAllowOnce": { 39 | "message": "Atualizar página & Permitir tudo apenas esta vez", 40 | "description": "Tooltip in popup second line, last button." 41 | }, 42 | "policyAllowOnceDisable": { 43 | "message": "Atualizar página & Voltar às configurações padrões", 44 | "description": "Tooltip in popup second line, last button when Allow Once is enabled." 45 | }, 46 | "errorFTP": { 47 | "message": "Páginas FTP não são analisadas", 48 | "description": "Error message when opening the popup from an invalid page. FTP is a protocol, like http, and so this error tells that pages generated from FTP are ignored. Reason: FTP pages are generated by Chromium and so can't contain any of those files" 49 | }, 50 | "errorFile": { 51 | "message": "Arquivos locais não são analisados", 52 | "description": "Error message when opening the popup from an invalid page. Reason: No support yet." 53 | }, 54 | "errorInternal": { 55 | "message": "Páginas internas não são analisadas", 56 | "description": "Error message when opening the popup from an invalid page. Reason: Internal pages can't be accessed by the API." 57 | }, 58 | "seeResources": { 59 | "message": "Clique para ver a lista de scripts, frames ou conexões websocket", 60 | "description": "Hover number at the right of domain name in popup." 61 | }, 62 | "tooltipSettings": { 63 | "message": "Configurações", 64 | "description": "Hover tooltip for last button of the first line in popup." 65 | }, 66 | "settingsProject": { 67 | "message": "Abrir página do projeto", 68 | "description": "Hover tooltip for GitHub button at top right. Opens project homepage." 69 | }, 70 | "settingsPolicy": { 71 | "message": "Política padrão", 72 | "description": "Settings title." 73 | }, 74 | "settingsPolicyPrivate": { 75 | "message": "Política padrão na navegação privada", 76 | "description": "Settings title." 77 | }, 78 | "settingsScripts": { 79 | "message": "Lista negra", 80 | "description": "Settings title." 81 | }, 82 | "settingsRules": { 83 | "message": "Regras", 84 | "description": "Settings title." 85 | }, 86 | "settingsRulesNone": { 87 | "message": "Sem Regra", 88 | "description": "When a rule has no rule, rule is under a lower level." 89 | }, 90 | "settingsRulesWhite": { 91 | "message": "Permitido", 92 | "description": "When a rule is to whitelist." 93 | }, 94 | "settingsRulesBlack": { 95 | "message": "Bloqueado", 96 | "description": "When a rule is blacklist." 97 | }, 98 | "settingsManage": { 99 | "message": "Gestão das configurações", 100 | "description": "Settings title. Keep consistent with tooltipSettings." 101 | }, 102 | "settingsManageExport": { 103 | "message": "Exportar", 104 | "description": "Button." 105 | }, 106 | "settingsManageReset": { 107 | "message": "Reset", 108 | "description": "Button." 109 | }, 110 | "settingsManageImport": { 111 | "message": "Importar", 112 | "description": "Button." 113 | }, 114 | "settingsManageExportTooltip": { 115 | "message": "As configurações serão exportadas na caixa de texto abaixo.", 116 | "description": "Button tooltip." 117 | }, 118 | "settingsManageResetTooltip": { 119 | "message": "Não há confirmação e nem como voltar atrás! Exporte suas configurações se deseja mantê-las!", 120 | "description": "Button tooltip." 121 | }, 122 | "settingsManageImportTooltip": { 123 | "message": "Cole suas configurações antigas na caixa de texto abaixo e então pressione este botão.", 124 | "description": "Button tooltip." 125 | }, 126 | "settingsManagePlaceholder": { 127 | "message": "(Cole as configurações aqui para importar)", 128 | "description": "Text in text box to explain how to import." 129 | }, 130 | "settingsDelete": { 131 | "message": "Excluir", 132 | "description": "Delete button." 133 | }, 134 | "settingsNumScripts": { 135 | "message": "Regras de script neste nível", 136 | "description": "Tooltip in number square. Shows how many scripts are allowed or blocked in this level." 137 | }, 138 | "settingsNumLevels": { 139 | "message": "Sub-níveis", 140 | "description": "Tooltip in number square. Shows how many rules for a more specific scope exist under that level. E.g. number of sub-domain specific rules exist under a domain rule." 141 | }, 142 | "settingsTitle": { 143 | "message": "ScriptJäger - Configurações", 144 | "description": "Page/tab title. Keep consistent with tooltipSettings" 145 | }, 146 | "tooltipWebsockets": { 147 | "message": "Websockets: $1", 148 | "description": "Hover number or icon in popup." 149 | }, 150 | "tooltipFrames": { 151 | "message": "Frames: $1", 152 | "description": "Hover number or icon in popup." 153 | }, 154 | "tooltipScripts": { 155 | "message": "Scripts: $1", 156 | "description": "Hover number or icon in popup." 157 | }, 158 | "updateTitle": { 159 | "message": "Atualização do ScriptJäger", 160 | "description": "Title of the notification when an update is available." 161 | }, 162 | "updateMessage": { 163 | "message": "A versão $1 está disponível\nVocê está usando a $2", 164 | "description": "Message of the notification when an update is available." 165 | }, 166 | "updateContext": { 167 | "message": "Clique aqui para abrir a página de download", 168 | "description": "Presentation varies according to OS, but purpose is to explain what action will happen when clicking on the notification." 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /background.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * @var preferences [Object] JSON that holds preferences 5 | * 6 | * @see default-prefs.js 7 | * @see https://github.com/An-dz/ScriptJaeger/wiki/Dev:-Preferences 8 | * 9 | * --- 10 | * 11 | * Block policy (rule key) 12 | * 13 | * 0 Allowed - All scripts allowed, white(black)list doesn't run 14 | * 1 Filtered - Only from the same domain allowed 15 | * 2 Relaxed - From same domain & helpers are allowed 16 | * 3 Blocked - All scripts blocked, white(black)list doesn't run 17 | * 4 Block injected - Inline script is blocked 18 | * 5 Pretend disabled - Pretend scripts are blocked (load noscript tags) 19 | * 20 | * --- 21 | * 22 | * Whitelist & Blacklist (rules key) 23 | * 24 | * true blocked/blacklist 25 | * false allowed/whitelist 26 | */ 27 | let preferences = {}; 28 | 29 | /** 30 | * @var tabStorage [Object] Holds info about the open tabs 31 | * 32 | * Each tab is represented as a child object with key as tab ID 33 | * 34 | * Children: 35 | * - protocol [String] Protocol part of the url 36 | * - domain [String] Domain/Host part of the url 37 | * - subdomain [String] Subdomains part of the url 38 | * - page [String] Page path part of the url 39 | * - query [String] Query of the url 40 | * - policy [Number] Policy to be applied 41 | * - rules [Object] Blackwhitelist to be applied 42 | * - private [Boolean] If tab is in a private window 43 | * - blocked [Number] Number of blocked scripts 44 | * - scripts [Object] List of all scripts from the page 45 | * - frames [Object] Info about sub-frames in the tab 46 | */ 47 | const tabStorage = {}; 48 | 49 | /** 50 | * @var ports [Object] This object keeps track of all open 51 | * communication ports with popup interfaces. 52 | * 53 | * The keys are the ID of the window the popup belongs to and the 54 | * values are the runtime.Port interfaces. 55 | * 56 | * @note These ports might be used on many parts of the code to 57 | * update the popup in realtime. 58 | */ 59 | const ports = {}; 60 | 61 | /** 62 | * @var privatum [Object] Holds all preferences on private browsing 63 | * 64 | * It's only filled when required and is cleared when no longer 65 | * 66 | * - windows [Object] Holds the number of open private windows 67 | * in `length` and their ids as keys with `true` values 68 | * - preferences [Object] copy of normal policy 69 | * 70 | * @see createPrivatePrefs() 71 | */ 72 | let privatum = { 73 | windows: {length: 0}, 74 | preferences: {} 75 | }; 76 | 77 | /** 78 | * @var jaegerhut [Object] Badge icons, one for each policy 79 | */ 80 | const jaegerhut = { 81 | 0: { 82 | name: "allowall", 83 | colour: "#D84A4A" 84 | }, 85 | 1: { 86 | name: "relaxed", 87 | colour: "#559FE6" 88 | }, 89 | 2: { 90 | name: "filtered", 91 | colour: "#73AB55" 92 | }, 93 | 3: { 94 | name: "blockall", 95 | colour: "#26272A" 96 | }, 97 | undefined: { 98 | colour: "#6F7072" 99 | } 100 | }; 101 | 102 | /* ====================================================================== */ 103 | 104 | /** 105 | * @brief Called when default preferences are loaded 106 | * 107 | * Saves the default preferences in storage 108 | * 109 | * @see default-prefs.js 110 | */ 111 | function defaultPreferencesLoaded() { 112 | chrome.storage.local.set({preferences: preferences}); 113 | 114 | // remove the injected script 115 | document.head.removeChild(document.getElementById("default")); 116 | } 117 | 118 | /** 119 | * @brief Open download page 120 | * 121 | * Clicking on the update notification will open the GitHub releases 122 | * 123 | * @param notification [String] An ID of the clicked notification 124 | */ 125 | chrome.notifications.onClicked.addListener((notification) => { 126 | chrome.tabs.create({ 127 | url: "https://github.com/An-dz/ScriptJaeger/releases" 128 | }); 129 | chrome.notifications.clear(notification); 130 | }); 131 | 132 | /** 133 | * @brief Check for updates 134 | * 135 | * A created alarm will check for updates regularly. 136 | * The manifest.json in the repository will contain the version number 137 | */ 138 | chrome.alarms.onAlarm.addListener(function checkUpdates() { 139 | const xhr = new XMLHttpRequest(); 140 | 141 | xhr.onreadystatechange = function processUpdate() { 142 | if (xhr.readyState !== 4) { 143 | return; 144 | } 145 | 146 | const currentVersion = chrome.runtime.getManifest().version; 147 | const version = JSON.parse(xhr.responseText).version; 148 | 149 | // if version is equal then we are up-to-date 150 | if (version === currentVersion) { 151 | return; 152 | } 153 | 154 | chrome.notifications.create({ 155 | type: "basic", 156 | title: chrome.i18n.getMessage("updateTitle"), 157 | iconUrl: "images/jaegerhut128.png", 158 | message: chrome.i18n.getMessage("updateMessage", [version, currentVersion]), 159 | contextMessage: chrome.i18n.getMessage("updateContext"), 160 | isClickable: true, 161 | requireInteraction: true 162 | }); 163 | }; 164 | 165 | xhr.open("GET", `https://raw.githubusercontent.com/An-dz/ScriptJaeger/master/manifest.json?time=${Date.now()}`); 166 | xhr.send(); 167 | }); 168 | 169 | /* ====================================================================== 170 | * Relaxed mode functions and variables, default values based from 171 | * ScriptWeeder https://github.com/lemonsqueeze/scriptweeder 172 | * ====================================================================== */ 173 | 174 | /** 175 | * @var standardSecondLevelDomains [Object] List of common standard 176 | * second level domains 177 | * 178 | * This is a list of top-level domains for identifying hosts and 179 | * subdomains from URLs. 180 | * 181 | * @note This smaller list is for performance as most domains will 182 | * fall under these rules. 183 | */ 184 | const standardSecondLevelDomains = { "com": 1, "org": 1, "net": 1, "gov": 1, "co": 1, "info": 1, "ac": 1, "or": 1, "tm": 1, "edu": 1, "mil": 1, "sch": 1, "int": 1, "nom": 1, "biz": 1, "gob": 1, "asso": 1 }; 185 | 186 | /** 187 | * @var otherSecondLevelDomains [Object] List of top-level domains 188 | * 189 | * This is a list of top-level domains for identifying hosts and 190 | * subdomains from URLs. 191 | * 192 | * @note From http://publicsuffix.org 193 | * 194 | * @note The standard above and long constructions are omitted 195 | * 196 | * @note Those are only tested if the above failed 197 | */ 198 | const otherSecondLevelDomains = { 199 | "aero": { "caa": 1, "club": 1, "crew": 1, "dgca": 1, "fuel": 1, "res": 1, "show": 1, "taxi": 1}, 200 | "ai": { "off": 1}, 201 | "ao": { "ed": 1, "gv": 1, "it": 1, "og": 1, "pb": 1}, 202 | "ar": { "tur": 1}, 203 | "arpa": { "e164": 1, "ip6": 1, "iris": 1, "uri": 1, "urn": 1}, 204 | "at": { "gv": 1, "priv": 1}, 205 | "au": { "act": 1, "asn": 1, "conf": 1, "id": 1, "nsw": 1, "nt": 1, "oz": 1, "qld": 1, "sa": 1, "tas": 1, "vic": 1, "wa": 1}, 206 | "az": { "name": 1, "pp": 1, "pro": 1}, 207 | "ba": { "rs": 1, "unbi": 1, "unsa": 1}, 208 | "bb": { "store": 1, "tv": 1}, 209 | "bg": { "0": 1, "1": 1, "2": 1, "3": 1, "4": 1, "5": 1, "6": 1, "7": 1, "8": 1, "9": 1, "a": 1, "b": 1, "c": 1, "d": 1, "e": 1, "f": 1, "g": 1, "h": 1, "i": 1, "j": 1, "k": 1, "l": 1, "m": 1, "n": 1, "o": 1, "p": 1, "q": 1, "r": 1, "s": 1, "t": 1, "u": 1, "v": 1, "w": 1, "x": 1, "y": 1, "z": 1}, 210 | "bj": { "barreau": 1, "gouv": 1}, 211 | "bo": { "tv": 1}, 212 | "br": { "adm": 1, "adv": 1, "agr": 1, "am": 1, "arq": 1, "art": 1, "ato": 1, "b": 1, "bio": 1, "blog": 1, "bmd": 1, "cim": 1, "cng": 1, "cnt": 1, "coop": 1, "ecn": 1, "eco": 1, "emp": 1, "eng": 1, "esp": 1, "etc": 1, "eti": 1, "far": 1, "flog": 1, "fm": 1, "fnd": 1, "fot": 1, "fst": 1, "g12": 1, "ggf": 1, "imb": 1, "ind": 1, "inf": 1, "jor": 1, "jus": 1, "leg": 1, "lel": 1, "mat": 1, "med": 1, "mp": 1, "mus": 1, "nom": 1, "not": 1, "ntr": 1, "odo": 1, "ppg": 1, "pro": 1, "psc": 1, "psi": 1, "qsl": 1, "radio": 1, "rec": 1, "slg": 1, "srv": 1, "taxi": 1, "teo": 1, "tmp": 1, "trd": 1, "tur": 1, "tv": 1, "vet": 1, "vlog": 1, "wiki": 1, "zlg": 1}, 213 | "by": { "of": 1}, 214 | "ca": { "ab": 1, "bc": 1, "gc": 1, "mb": 1, "nb": 1, "nf": 1, "nl": 1, "ns": 1, "nt": 1, "nu": 1, "on": 1, "pe": 1, "qc": 1, "sk": 1, "yk": 1}, 215 | "ci": { "ed": 1, "go": 1, "gouv": 1, "md": 1, "presse": 1}, 216 | "cn": { "ah": 1, "bj": 1, "cq": 1, "fj": 1, "gd": 1, "gs": 1, "gx": 1, "gz": 1, "ha": 1, "hb": 1, "he": 1, "hi": 1, "hk": 1, "hl": 1, "hn": 1, "jl": 1, "js": 1, "jx": 1, "ln": 1, "mo": 1, "nm": 1, "nx": 1, "qh": 1, "sc": 1, "sd": 1, "sh": 1, "sn": 1, "sx": 1, "tj": 1, "tw": 1, "xj": 1, "xz": 1, "yn": 1, "zj": 1}, 217 | "co": { "arts": 1, "firm": 1, "rec": 1, "web": 1}, 218 | "cr": { "ed": 1, "fi": 1, "go": 1, "sa": 1}, 219 | "cu": { "inf": 1}, 220 | "cx": { "ath": 1}, 221 | "cy": { "ltd": 1, "name": 1, "press": 1, "pro": 1}, 222 | "do": { "art": 1, "sld": 1, "web": 1}, 223 | "dz": { "art": 1, "pol": 1}, 224 | "ec": { "fin": 1, "k12": 1, "med": 1, "pro": 1}, 225 | "ee": { "aip": 1, "fie": 1, "lib": 1, "med": 1, "pri": 1, "riik": 1}, 226 | "eg": { "eun": 1, "name": 1, "sci": 1}, 227 | "et": { "name": 1}, 228 | "fi": { "iki": 1, "aland": 1}, 229 | "fr": { "cci": 1, "gouv": 1, "port": 1, "prd": 1}, 230 | "ge": { "pvt": 1}, 231 | "gi": { "ltd": 1, "mod": 1}, 232 | "gp": { "mobi": 1}, 233 | "gt": { "ind": 1}, 234 | "hk": { "idv": 1}, 235 | "hr": { "from": 1, "iz": 1, "name": 1}, 236 | "ht": { "art": 1, "coop": 1, "firm": 1, "gouv": 1, "med": 1, "pol": 1, "pro": 1, "rel": 1, "shop": 1}, 237 | "hu": { "2000": 1, "bolt": 1, "city": 1, "film": 1, "news": 1, "priv": 1, "sex": 1, "shop": 1, "suli": 1, "szex": 1}, 238 | "id": { "go": 1, "my": 1, "web": 1}, 239 | "im": { "nic": 1}, 240 | "in": { "firm": 1, "gen": 1, "ind": 1, "nic": 1, "res": 1}, 241 | "int": { "eu": 1}, 242 | "ir": { "id": 1}, 243 | "it": { "ag": 1, "al": 1, "an": 1, "ao": 1, "ap": 1, "aq": 1, "ar": 1, "asti": 1, "at": 1, "av": 1, "ba": 1, "bari": 1, "bg": 1, "bi": 1, "bl": 1, "bn": 1, "bo": 1, "br": 1, "bs": 1, "bt": 1, "bz": 1, "ca": 1, "cb": 1, "ce": 1, "ch": 1, "ci": 1, "cl": 1, "cn": 1, "como": 1, "cr": 1, "cs": 1, "ct": 1, "cz": 1, "en": 1, "enna": 1, "fc": 1, "fe": 1, "fg": 1, "fi": 1, "fm": 1, "fr": 1, "ge": 1, "go": 1, "gr": 1, "im": 1, "is": 1, "kr": 1, "lc": 1, "le": 1, "li": 1, "lo": 1, "lodi": 1, "lt": 1, "lu": 1, "mb": 1, "mc": 1, "me": 1, "mi": 1, "mn": 1, "mo": 1, "ms": 1, "mt": 1, "na": 1, "no": 1, "nu": 1, "og": 1, "ot": 1, "pa": 1, "pc": 1, "pd": 1, "pe": 1, "pg": 1, "pi": 1, "pisa": 1, "pn": 1, "po": 1, "pr": 1, "pt": 1, "pu": 1, "pv": 1, "pz": 1, "ra": 1, "rc": 1, "re": 1, "rg": 1, "ri": 1, "rm": 1, "rn": 1, "ro": 1, "roma": 1, "rome": 1, "sa": 1, "si": 1, "so": 1, "sp": 1, "sr": 1, "ss": 1, "sv": 1, "ta": 1, "te": 1, "tn": 1, "to": 1, "tp": 1, "tr": 1, "ts": 1, "tv": 1, "ud": 1, "va": 1, "vb": 1, "vc": 1, "ve": 1, "vi": 1, "vr": 1, "vs": 1, "vt": 1, "vv": 1}, 244 | "jo": { "name": 1}, 245 | "jp": { "ad": 1, "ed": 1, "gifu": 1, "go": 1, "gr": 1, "lg": 1, "mie": 1, "nara": 1, "ne": 1, "oita": 1, "saga": 1}, 246 | "km": { "ass": 1, "coop": 1, "gouv": 1, "prd": 1}, 247 | "kp": { "rep": 1, "tra": 1}, 248 | "kr": { "es": 1, "go": 1, "hs": 1, "jeju": 1, "kg": 1, "ms": 1, "ne": 1, "pe": 1, "re": 1, "sc": 1}, 249 | "la": { "c": 1, "per": 1}, 250 | "lk": { "assn": 1, "grp": 1, "ltd": 1, "ngo": 1, "soc": 1, "web": 1}, 251 | "lv": { "asn": 1, "conf": 1, "id": 1}, 252 | "ly": { "id": 1, "med": 1, "plc": 1}, 253 | "me": { "its": 1, "priv": 1}, 254 | "mg": { "prd": 1}, 255 | "mk": { "inf": 1, "name": 1}, 256 | "ml": { "gouv": 1}, 257 | "mn": { "nyc": 1}, 258 | "museum": { "air": 1, "and": 1, "art": 1, "arts": 1, "axis": 1, "bahn": 1, "bale": 1, "bern": 1, "bill": 1, "bonn": 1, "bus": 1, "can": 1, "coal": 1, "cody": 1, "dali": 1, "ddr": 1, "farm": 1, "film": 1, "frog": 1, "glas": 1, "graz": 1, "iraq": 1, "iron": 1, "jfk": 1, "juif": 1, "kids": 1, "lans": 1, "linz": 1, "mad": 1, "manx": 1, "mill": 1, "moma": 1, "nrw": 1, "nyc": 1, "nyny": 1, "roma": 1, "satx": 1, "silk": 1, "ski": 1, "spy": 1, "tank": 1, "tcm": 1, "time": 1, "town": 1, "tree": 1, "ulm": 1, "usa": 1, "utah": 1, "uvic": 1, "war": 1, "york": 1}, 259 | "mv": { "aero": 1, "coop": 1, "name": 1, "pro": 1}, 260 | "mw": { "coop": 1}, 261 | "my": { "name": 1}, 262 | "na": { "ca": 1, "cc": 1, "dr": 1, "in": 1, "mobi": 1, "mx": 1, "name": 1, "pro": 1, "tv": 1, "us": 1, "ws": 1}, 263 | "net": { "gb": 1, "hu": 1, "jp": 1, "se": 1, "uk": 1, "za": 1}, 264 | "nf": { "arts": 1, "firm": 1, "per": 1, "rec": 1, "web": 1}, 265 | "nl": { "bv": 1}, 266 | "no": { "aa": 1, "ah": 1, "al": 1, "alta": 1, "amli": 1, "amot": 1, "arna": 1, "aure": 1, "berg": 1, "bodo": 1, "bokn": 1, "bu": 1, "dep": 1, "eid": 1, "etne": 1, "fet": 1, "fhs": 1, "fla": 1, "flå": 1, "fm": 1, "frei": 1, "fusa": 1, "gol": 1, "gran": 1, "grue": 1, "ha": 1, "hl": 1, "hm": 1, "hof": 1, "hol": 1, "hole": 1, "hå": 1, "ivgu": 1, "kvam": 1, "leka": 1, "lier": 1, "lom": 1, "lund": 1, "moss": 1, "mr": 1, "nl": 1, "nt": 1, "odda": 1, "of": 1, "ol": 1, "osen": 1, "oslo": 1, "oyer": 1, "priv": 1, "rade": 1, "rana": 1, "rl": 1, "roan": 1, "rost": 1, "sel": 1, "sf": 1, "ski": 1, "sola": 1, "st": 1, "stat": 1, "sula": 1, "sund": 1, "tana": 1, "time": 1, "tinn": 1, "tr": 1, "va": 1, "vaga": 1, "vang": 1, "vega": 1, "vf": 1, "vgs": 1, "vik": 1, "voss": 1, "ål": 1, "ås": 1}, 267 | "nu": { "mine": 1}, 268 | "org": { "ae": 1, "us": 1, "za": 1}, 269 | "pa": { "abo": 1, "ing": 1, "med": 1, "sld": 1}, 270 | "ph": { "i": 1, "ngo": 1}, 271 | "pk": { "fam": 1, "gok": 1, "gon": 1, "gop": 1, "gos": 1, "web": 1}, 272 | "pl": { "agro": 1, "aid": 1, "art": 1, "atm": 1, "auto": 1, "elk": 1, "gda": 1, "gsm": 1, "irc": 1, "lapy": 1, "mail": 1, "med": 1, "ngo": 1, "nysa": 1, "pc": 1, "pila": 1, "pisz": 1, "priv": 1, "rel": 1, "sex": 1, "shop": 1, "sos": 1, "waw": 1, "wroc": 1}, 273 | "pr": { "est": 1, "isla": 1, "name": 1, "pro": 1, "prof": 1}, 274 | "pro": { "aca": 1, "bar": 1, "cpa": 1, "eng": 1, "jur": 1, "law": 1, "med": 1}, 275 | "ps": { "plo": 1, "sec": 1}, 276 | "pt": { "nome": 1, "publ": 1}, 277 | "pw": { "ed": 1, "go": 1, "ne": 1}, 278 | "py": { "coop": 1}, 279 | "qa": { "name": 1}, 280 | "ro": { "arts": 1, "firm": 1, "nt": 1, "rec": 1, "www": 1}, 281 | "rs": { "in": 1}, 282 | "ru": { "amur": 1, "bir": 1, "cbg": 1, "chel": 1, "cmw": 1, "jar": 1, "kchr": 1, "khv": 1, "kms": 1, "komi": 1, "mari": 1, "msk": 1, "nkz": 1, "nnov": 1, "nov": 1, "nsk": 1, "omsk": 1, "perm": 1, "pp": 1, "ptz": 1, "rnd": 1, "snz": 1, "spb": 1, "stv": 1, "test": 1, "tom": 1, "tsk": 1, "tula": 1, "tuva": 1, "tver": 1, "udm": 1, "vrn": 1}, 283 | "rw": { "gouv": 1}, 284 | "sa": { "med": 1, "pub": 1}, 285 | "sd": { "med": 1, "tv": 1}, 286 | "se": { "a": 1, "b": 1, "bd": 1, "c": 1, "d": 1, "e": 1, "f": 1, "fh": 1, "fhsk": 1, "fhv": 1, "g": 1, "h": 1, "i": 1, "k": 1, "l": 1, "m": 1, "n": 1, "o": 1, "p": 1, "pp": 1, "r": 1, "s": 1, "sshn": 1, "t": 1, "u": 1, "w": 1, "x": 1, "y": 1, "z": 1}, 287 | "sg": { "per": 1}, 288 | "sn": { "art": 1, "gouv": 1, "univ": 1}, 289 | "th": { "go": 1, "in": 1, "mi": 1}, 290 | "tj": { "go": 1, "name": 1, "nic": 1, "test": 1, "web": 1}, 291 | "tn": { "ens": 1, "fin": 1, "ind": 1, "intl": 1, "nat": 1, "rnrt": 1, "rns": 1, "rnu": 1}, 292 | "tt": { "aero": 1, "coop": 1, "jobs": 1, "mobi": 1, "name": 1, "pro": 1}, 293 | "tw": { "club": 1, "ebiz": 1, "game": 1, "idv": 1}, 294 | "tz": { "go": 1, "me": 1, "mobi": 1, "ne": 1, "sc": 1, "tv": 1}, 295 | "ua": { "ck": 1, "cn": 1, "cr": 1, "cv": 1, "dn": 1, "dp": 1, "if": 1, "in": 1, "kh": 1, "kiev": 1, "km": 1, "kr": 1, "krym": 1, "ks": 1, "kv": 1, "kyiv": 1, "lg": 1, "lt": 1, "lv": 1, "lviv": 1, "mk": 1, "od": 1, "pl": 1, "pp": 1, "rv": 1, "sb": 1, "sm": 1, "sumy": 1, "te": 1, "uz": 1, "vn": 1, "zp": 1, "zt": 1}, 296 | "ug": { "go": 1, "ne": 1, "sc": 1}, 297 | "us": { "ak": 1, "al": 1, "ar": 1, "as": 1, "az": 1, "ca": 1, "ct": 1, "dc": 1, "de": 1, "dni": 1, "fed": 1, "fl": 1, "ga": 1, "gu": 1, "hi": 1, "ia": 1, "id": 1, "il": 1, "in": 1, "isa": 1, "kids": 1, "ks": 1, "ky": 1, "la": 1, "ma": 1, "md": 1, "me": 1, "mi": 1, "mn": 1, "mo": 1, "ms": 1, "mt": 1, "nc": 1, "nd": 1, "ne": 1, "nh": 1, "nj": 1, "nm": 1, "nsn": 1, "nv": 1, "ny": 1, "oh": 1, "ok": 1, "pa": 1, "pr": 1, "ri": 1, "sc": 1, "sd": 1, "tn": 1, "tx": 1, "ut": 1, "va": 1, "vi": 1, "vt": 1, "wa": 1, "wi": 1, "wv": 1, "wy": 1}, 298 | "uy": { "gub": 1}, 299 | "ve": { "e12": 1, "web": 1}, 300 | "vi": { "k12": 1}, 301 | "vn": { "name": 1, "pro": 1} 302 | }; 303 | 304 | /** 305 | * @brief Check if URL looks 'useful' 306 | * 307 | * Checks if we can allow from some common patterns in the url 308 | * 309 | * @param site [Object] url object obtained from `extractUrl()` 310 | * 311 | * @return [Boolean] identifying if it must be allowed 312 | * 313 | * @note url object must contain `domain` and `subdomain` keys 314 | */ 315 | function isCommonHelpers(site) { 316 | return ( 317 | site.subdomain !== "s" && 318 | site.subdomain.indexOf("tag") === -1 && ( 319 | site.domain.indexOf("cdn") > -1 || 320 | site.domain.indexOf("img") > -1 || 321 | site.domain.indexOf("static") > -1 || 322 | site.domain.indexOf("auth") > -1 || 323 | site.subdomain.indexOf("login") > -1 || 324 | site.subdomain.indexOf("auth") > -1 || 325 | site.subdomain.indexOf("code") === 0 || 326 | site.domain === "google.com" 327 | ) 328 | ); 329 | } 330 | 331 | /** 332 | * @brief Check if script url looks related to site url 333 | * 334 | * Checks if the domain name of the site is in the domain of the 335 | * script. If tab domain is bigger, search for the inverse. 336 | * 337 | * @param js [String] domain obtained from `extractUrl()` 338 | * @param tab [String] domain obtained from `extractUrl()` 339 | * 340 | * @return [Boolean] identifying if it must be allowed 341 | */ 342 | function isRelated(js, tab) { 343 | if (tab.length > js.length) { 344 | return isRelated(tab, js); 345 | } 346 | 347 | const domain = tab.substring(0, tab.indexOf(".")); 348 | 349 | if (js.indexOf(domain) > -1 || (domain.length > 2 && js.slice(0, 3) === domain.slice(0, 3))) { 350 | return true; 351 | } 352 | 353 | return false; 354 | } 355 | 356 | /** 357 | * @brief Get object with url parts 358 | * 359 | * Extracts the important parts of the url and returns an object with 360 | * each of them in a key. 361 | * 362 | * @param url [String] Full url 363 | * 364 | * @return [Object] containing the parts of the url 365 | * 366 | * @note Return object children: 367 | * - protocol [String] contains the protocol (e.g. http://) 368 | * - subdomain [String] contains the subdomain name (e.g. www) 369 | * - domain [String] contains the host name (e.g. github.com) 370 | * - page [String] contains the dir & file name (e.g. /index.htm) 371 | * - query [String] contains query information (e.g. ?p=a) 372 | * - fragment [String] contains fragment information (e.g. #main) 373 | */ 374 | function extractUrl(url) { 375 | /* 376 | * Obtain the important parts of the url to load settings 377 | * 0 contains the full url (because it's the match of the full regexp) 378 | * 1 contains the protocol 379 | * 2 contains the full domain (subdomain + domain/host) 380 | * 3 contains the directory + filename 381 | * 4 contains the query 382 | * 5 contains the fragment 383 | */ 384 | url = url.match(/^([^:]+:\/\/)(\/?[^/]+)([^?#]+)([^#]*)(.*)$/); 385 | const domains = url[2].split("."); 386 | 387 | // less than three levels everything is domain 388 | if (domains.length < 3) { 389 | url[0] = ""; 390 | } 391 | else { 392 | // let's keep it simple, no more than two levels 393 | let levels = 2; 394 | const tld = domains[domains.length - 1]; 395 | const sld = domains[domains.length - 2]; 396 | 397 | if (tld !== "com" && (standardSecondLevelDomains[sld] || (otherSecondLevelDomains[tld] && otherSecondLevelDomains[tld][sld]))) { 398 | levels = 3; 399 | } 400 | 401 | url[0] = domains.slice(0, domains.length - levels).join("."); 402 | url[2] = domains.slice(domains.length - levels).join("."); 403 | } 404 | 405 | return { 406 | protocol: url[1], 407 | subdomain: url[0], 408 | domain: url[2], 409 | page: url[3], 410 | query: url[4], 411 | fragment: url[5] 412 | }; 413 | } 414 | 415 | /* ====================================================================== */ 416 | 417 | /** 418 | * @brief Merge `rules` keys 419 | * 420 | * Merges blackwhitelist rules to get correct inherit behaviour 421 | * 422 | * @param[in] from [Object] To copy rules from 423 | * 424 | * @param[out] to [Object] To copy rules into 425 | */ 426 | function mergeRules(from, to) { 427 | for (const key in from) { 428 | to[key] = { 429 | rule: (from[key].rule !== null ? from[key].rule : (to[key] ? to[key].rule : null)), 430 | urls: (to[key] ? to[key].urls : {}) 431 | }; 432 | 433 | mergeRules(from[key].urls, to[key].urls); 434 | } 435 | } 436 | 437 | /** 438 | * @brief Saves the rule in the passed location 439 | * 440 | * Saves the rule in `tosave` at the specified site 441 | * 442 | * @param level [Object] Current level in the preferences object 443 | * @param sites [Array] Url parts, order is the order it's 444 | * saved in preferences (domain, subdomain, page) 445 | * @param tosave [any] Rule to save 446 | * 447 | * @note It replaces whatever is passed in `tosave` 448 | */ 449 | function saveRule(level, sites, tosave) { 450 | const address = sites.shift(); 451 | 452 | // while there's an address we go on opening it 453 | if (address) { 454 | // create level if it does not exist 455 | if (!level.urls[address]) { 456 | level.urls[address] = { 457 | rule: null, 458 | rules: {urls: {}}, 459 | urls: {} 460 | }; 461 | } 462 | 463 | // move inside level 464 | saveRule(level.urls[address], sites, tosave); 465 | return; 466 | } 467 | 468 | // object is `rules` key, others (boolean/number) is `rule` 469 | if (typeof tosave !== "object") { 470 | level.rule = tosave; 471 | } 472 | else { 473 | mergeRules(tosave, level.rules.urls); 474 | } 475 | } 476 | 477 | /** 478 | * @brief Load settings in the passed location 479 | * 480 | * Loads the policy and script rules to apply into `applyRules` 481 | * 482 | * @param[in] level [Object] Current level in the preferences object 483 | * @param[in] sites [Array] Url parts, order is the order it's 484 | * saved in preferences (domain, subdomain, page) 485 | * 486 | * @param[out] applyRules [Object] Contains rules to apply, 487 | * childs include `policy` containing policy rule and 488 | * `rules` containing blackwhitelist object. 489 | */ 490 | function loadRule(level, sites, applyRules) { 491 | if (level.rule !== null) { 492 | applyRules.policy = level.rule; 493 | } 494 | 495 | mergeRules(level.rules.urls, applyRules.rules); 496 | 497 | const address = sites.shift(); 498 | 499 | if (address && level.urls[address]) { 500 | loadRule(level.urls[address], sites, applyRules); 501 | } 502 | } 503 | 504 | /** 505 | * @brief Get rules to apply 506 | * 507 | * Returns the blocking policy and blackwhitelist rules to be used 508 | * 509 | * @param site [Object] url object obtained from extractUrl() 510 | * 511 | * @return [Object] Contains rules to apply, child keys include 512 | * `policy` containing policy rule and `rules` containing 513 | * blackwhitelist 514 | */ 515 | function getRules(site) { 516 | const urls = [ 517 | site.subdomain, 518 | site.page 519 | ]; 520 | 521 | // private windows must read from other object 522 | const rulesList = (site.private ? privatum.preferences : preferences); 523 | 524 | const applyRules = { 525 | policy: rulesList.rule, 526 | rules: {} 527 | }; 528 | 529 | if (rulesList.urls[site.domain]) { 530 | loadRule(rulesList.urls[site.domain], urls, applyRules); 531 | } 532 | 533 | return applyRules; 534 | } 535 | 536 | /* ====================================================================== */ 537 | 538 | /** 539 | * @brief Deep clone JSON 540 | * 541 | * Deep clones JSON for preferences in private windows 542 | * 543 | * @param object [Object] JSON object to deep clone 544 | * 545 | * @return [Object] Cloned object 546 | * 547 | * @warning This does not deep clone any JS Object or JSON. 548 | * This function is optimised for the specifics of the 549 | * `preferences` JSON used in this project. 550 | */ 551 | function deepClone(object) { 552 | // if not an object then it's a value 553 | // just return it for the `for` below 554 | if (object === null || typeof (object) !== "object") { 555 | return object; 556 | } 557 | 558 | // if it's an object we need to initialise and copy keys 559 | const clone = object.constructor(); 560 | 561 | for (const key in object) { 562 | clone[key] = deepClone(object[key]); 563 | } 564 | 565 | // returns cloned object 566 | return clone; 567 | } 568 | 569 | /** 570 | * @brief Create preferences for private browsing 571 | * 572 | * When a windows is created we check if it's a private one. 573 | * If it is and there is no other private windows we create a 574 | * private preferences object separated from the main preferences. 575 | * 576 | * @param details, [Object] Details about the new window 577 | */ 578 | function createPrivatePrefs(details) { 579 | if (details.incognito === true) { 580 | privatum.windows[details.id] = true; 581 | privatum.windows.length++; 582 | 583 | if (privatum.windows.length === 1) { 584 | privatum.preferences = deepClone(preferences); 585 | // put private rule into effective rule 586 | privatum.preferences.rule = privatum.preferences.private; 587 | } 588 | } 589 | } 590 | 591 | /** 592 | * @brief Listener for window creation event 593 | * 594 | * Fired whenever a new window opens 595 | */ 596 | chrome.windows.onCreated.addListener(createPrivatePrefs); 597 | 598 | /** 599 | * @brief Check if private rules should be deleted 600 | * 601 | * When a window is closed we check if it's the last private 602 | * window, if it is we delete the private preferences object 603 | * 604 | * @param windowid [Number] ID of the closed window 605 | */ 606 | chrome.windows.onRemoved.addListener((windowid) => { 607 | if (privatum.windows[windowid] === true) { 608 | delete privatum.windows[windowid]; 609 | privatum.windows.length--; 610 | 611 | if (privatum.windows.length === 0) { 612 | privatum = { 613 | windows: {length: 0}, 614 | preferences: {} 615 | }; 616 | } 617 | } 618 | }); 619 | 620 | /* ====================================================================== */ 621 | 622 | /** 623 | * @brief Add info about tab 624 | * 625 | * Adds info about the tab into `tabStorage`. Can also be used to 626 | * update information for pages loaded dynamically. 627 | * 628 | * @param tab [Object] Holds information about the tab 629 | */ 630 | function addTab(tab) { 631 | if (tab.id === -1) { 632 | return; 633 | } 634 | 635 | const tabData = tabStorage[tab.id]; 636 | const site = extractUrl(tab.url); 637 | site.private = tab.incognito; 638 | site.window = tab.windowId; 639 | site.tabid = tab.id; 640 | 641 | const block = getRules(site); 642 | site.policy = block.policy; 643 | site.rules = block.rules; 644 | 645 | // pages without history.pushState fall here 646 | // allowonce does not remove the whole thing so check page url 647 | if (tabData === undefined || tabData.page === undefined) { 648 | site.blocked = 0; 649 | site.scripts = {}; 650 | site.frames = {}; 651 | } 652 | // if page uses history.pushState the old scripts are still loaded 653 | else { 654 | site.blocked = tabData.blocked; 655 | site.scripts = tabData.scripts; 656 | site.frames = tabData.frames; 657 | } 658 | 659 | // allow all once 660 | if (tabData !== undefined && tabData.allowonce === true && tabData.subdomain === site.subdomain && tabData.domain === site.domain) { 661 | site.policy = 0; 662 | site.allowonce = true; 663 | 664 | chrome.browserAction.setBadgeText({ 665 | text: "T", 666 | tabId: tab.id 667 | }); 668 | } 669 | 670 | tabStorage[tab.id] = site; 671 | } 672 | 673 | /** 674 | * @brief Remove info about tab 675 | * 676 | * Removes all information about the tab from `tabStorage` 677 | * 678 | * @param tabid [Number] id of the removed tab 679 | * @param allowonce [Boolean] if Allow Once policy is set 680 | */ 681 | function removeTab(tabid, allowonce) { 682 | if (allowonce === true) { 683 | tabStorage[tabid] = { 684 | allowonce: true, 685 | domain: tabStorage[tabid].domain, 686 | subdomain: tabStorage[tabid].subdomain 687 | }; 688 | return; 689 | } 690 | 691 | delete tabStorage[tabid]; 692 | } 693 | 694 | /** 695 | * @brief Remove tab info on close 696 | * 697 | * When a tab is closed stop monitoring it. 698 | * 699 | * @param tabid [Number] ID of the tab being closed 700 | */ 701 | chrome.tabs.onRemoved.addListener((tabid) => { 702 | removeTab(tabid, false); 703 | }); 704 | 705 | /** 706 | * @brief Obtain info about tabs and windows 707 | * 708 | * Goes through all windows and tabs to create the appropriate 709 | * data for the extension. 710 | */ 711 | function getTabsAndWindows() { 712 | // check if private preferences must be created 713 | chrome.windows.getAll({populate: false}, (windows) => { 714 | windows.forEach((details) => { 715 | createPrivatePrefs(details); 716 | }); 717 | }); 718 | 719 | // get info and rules about open tabs 720 | chrome.tabs.query({}, (tabs) => { 721 | tabs.forEach((tab) => { 722 | addTab(tab); 723 | }); 724 | }); 725 | } 726 | 727 | /** 728 | * @brief Converts a single level to the new format 729 | * 730 | * Recursively calls itself if necessary until the whole tree 731 | * is converted. Data is not deleted, just created. 732 | * 733 | * @param[in] domain [Object] The current level to convert 734 | * 735 | * @param[out] this [Array] Array containing two children: 736 | * - [Object] Level to write the converted object 737 | * - [Boolean] If `rules` key must be included 738 | */ 739 | function convertLevel(domain) { 740 | const level = domain.sites || domain.pages; 741 | 742 | this[0].urls[domain.name] = { 743 | rule: (domain.rule !== undefined ? domain.rule : null), 744 | urls: {} 745 | }; 746 | 747 | if (level) { 748 | level.forEach(convertLevel, [this[0].urls[domain.name], this[1]]); 749 | } 750 | 751 | if (this[1]) { 752 | this[0].urls[domain.name].rules = {urls: {}}; 753 | 754 | if (domain.rules) { 755 | domain.rules.domains.forEach(convertLevel, [this[0].urls[domain.name].rules, false]); 756 | } 757 | } 758 | } 759 | 760 | /** 761 | * @brief Convert old preferences to new format 762 | * 763 | * Converts the old preferences format to the new unified format. 764 | * The Blackwhitelist object has already been moved inside the 765 | * `rules` key of the top level `preferences` object. 766 | */ 767 | function convertPreferences() { 768 | preferences.urls = {}; 769 | preferences.domains.forEach(convertLevel, [preferences, true]); 770 | delete preferences.domains; 771 | 772 | preferences.rules.urls = {}; 773 | preferences.rules.domains.forEach(convertLevel, [preferences.rules, false]); 774 | delete preferences.rules.domains; 775 | 776 | preferences.ping = false; 777 | 778 | chrome.storage.local.clear(); 779 | chrome.storage.local.set({preferences: preferences}); 780 | } 781 | 782 | /** 783 | * @brief Load default preferences 784 | * 785 | * Loads the default preferences and sets an update check. 786 | * Settings 787 | */ 788 | function loadDefaultPreferences() { 789 | // the default preferences are in an external file for reducing the size and overhead of this background page 790 | const script = document.createElement("script"); 791 | script.src = "default-prefs.js"; 792 | script.type = "text/javascript"; 793 | script.id = "default"; 794 | document.head.appendChild(script); 795 | 796 | // alarm creates a permanent update check across restarts 797 | chrome.alarms.create("updateCheck", { 798 | delayInMinutes: 1, // first check after 1 minute 799 | periodInMinutes: 1440 // check for updates every 24 hours 800 | }); 801 | } 802 | 803 | /** 804 | * @brief Load preferences 805 | * 806 | * Loads the preferences and convert them if necessary. 807 | * If no preferences exist it loads defaults. 808 | * 809 | * @param pref [Object] Loaded preferences 810 | * 811 | * @note This is fired on load 812 | */ 813 | chrome.storage.local.get((pref) => { 814 | // just load prefs if not first run and already converted 815 | if (pref.preferences !== undefined) { 816 | preferences = pref.preferences; 817 | } 818 | 819 | // this key only exists in the old preferences 820 | // if not here then it's first run 821 | else if (pref.firstRun === undefined) { 822 | loadDefaultPreferences(); 823 | } 824 | 825 | // if not converted and there's a firstRun key we begin upgrade 826 | else { 827 | preferences = pref.policy; 828 | preferences.rules = pref.blackwhitelist; 829 | convertPreferences(); 830 | } 831 | 832 | getTabsAndWindows(); 833 | }); 834 | 835 | /** 836 | * @brief Rename tabid on replacement 837 | * 838 | * Under Chromium tab processes can be replaced, 839 | * this moves the tab info to another id 840 | * 841 | * @param newId [Number] id of the new process 842 | * @param oldId [Number] id of the old process 843 | * 844 | * Based on https://github.com/Christoph142/Pin-Sites/ 845 | */ 846 | chrome.tabs.onReplaced.addListener((newId, oldId) => { 847 | if (newId === oldId || tabStorage[oldId] === undefined) { 848 | return; 849 | } 850 | 851 | tabStorage[newId] = tabStorage[oldId]; 852 | removeTab(oldId, false); 853 | }); 854 | 855 | /** 856 | * @brief Update info on navigation 857 | * 858 | * This is only run when the content is not loaded using 859 | * history.pushState 860 | * 861 | * We reset the info when this occurs because the page was not 862 | * dynamically loaded and so the whole content was reloaded. 863 | * 864 | * This event is fired right before tabs.onUpdated 865 | * 866 | * @param details [Object] of the navigation 867 | */ 868 | chrome.webNavigation.onBeforeNavigate.addListener((details) => { 869 | const tabid = details.tabId; 870 | const frameid = details.frameId; 871 | 872 | // we should not continue if we don't know from which tab the frame is coming 873 | if (tabid === -1) { 874 | return; 875 | } 876 | 877 | if (frameid === 0) { 878 | // delete anything about the tab because tabs.onUpdate will re-add 879 | removeTab(tabid, (tabStorage[tabid] ? tabStorage[tabid].allowonce : false)); 880 | } 881 | // if frameId > 0 & url is about:blank 882 | else if (details.url === "about:blank") { 883 | let pframeid = details.parentFrameId; 884 | 885 | // if not loaded from main frame, check where it was 886 | if (pframeid > 0) { 887 | const use = tabStorage[tabid].frames[pframeid].use; 888 | 889 | if (use !== undefined) { 890 | pframeid = use; 891 | } 892 | } 893 | 894 | // save frame information 895 | tabStorage[tabid].frames[frameid] = { 896 | use: pframeid 897 | }; 898 | } 899 | }); 900 | 901 | /** 902 | * @brief Update tab info and extension icon on tab loading 903 | * 904 | * Pages that have content loaded dynamically like Facebook or YouTube 905 | * can't have the counter and script list reset 906 | * 907 | * onUpdated updates all info about the page 908 | * 909 | * @param tabId [Number] id of the tab 910 | * @param changeInfo [Object] changes to the state of the tab that was updated 911 | * @param tab [Object] details about the tab 912 | */ 913 | chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { 914 | if (changeInfo.status === "loading") { 915 | // set info 916 | addTab(tab); 917 | 918 | // set icon according to policy 919 | const policy = tabStorage[tabId].policy; 920 | 921 | chrome.browserAction.setIcon({ 922 | path: { 923 | "19": `images/${jaegerhut[policy].name}19.png`, 924 | "38": `images/${jaegerhut[policy].name}38.png` 925 | }, 926 | tabId: tabId 927 | }); 928 | 929 | chrome.browserAction.setBadgeBackgroundColor({ 930 | color: jaegerhut[policy].colour, 931 | tabId: tab.id 932 | }); 933 | } 934 | }); 935 | 936 | // ========================================================================= 937 | 938 | /** 939 | * @brief Returns the frame that is loading the object 940 | * 941 | * Frames don't follow the rules of the tab site, but the rules 942 | * that match the frame site. This function will return that 943 | * info so the resource can be analysed with the correct urls 944 | * 945 | * @note `use` key contains which frameid to use, if it does 946 | * not exist then we already are in the correct place 947 | * 948 | * @param frameid [Number] id of the frame 949 | * @param tabsite [Object] info about tab from `tabStorage` 950 | * 951 | * @return [Object] info about frame from `tabStorage` 952 | */ 953 | function getLoadingFrame(frameid, tabsite) { 954 | const framesite = tabsite.frames[frameid]; 955 | 956 | if (framesite.use === undefined) { 957 | return framesite; 958 | } 959 | 960 | if (framesite.use === 0) { 961 | return tabsite; 962 | } 963 | 964 | return tabsite.frames[framesite.use]; 965 | } 966 | 967 | /** 968 | * @brief Get if script is blocked 969 | * 970 | * Check if there's a blackwhitelist rule for that site and 971 | * returns the rule. 972 | * 973 | * @param level [Object] where to check for the rule 974 | * @param site [Object] object containinig domain and subdomain keys 975 | * 976 | * @return [Null/Boolean] Rule under that level 977 | */ 978 | function getScriptRule(level, site) { 979 | let block = null; 980 | level = level[site.domain]; 981 | 982 | if (level) { 983 | block = level.rule; 984 | level = level.urls[site.subdomain]; 985 | 986 | if (level && level.rule !== null) { 987 | block = level.rule; 988 | } 989 | } 990 | 991 | return block; 992 | } 993 | 994 | /** 995 | * @brief Check if resource is blocked 996 | * 997 | * Checks if script is blocked according to policy and rules 998 | * 999 | * @param tabsite [Object] urls of the tab 1000 | * @param scriptsite [Object] urls of the loading resource 1001 | * @param policy [Number] policy being applied 1002 | * 1003 | * @return [Boolean] If resource is blocked 1004 | */ 1005 | function isScriptAllowed(tabsite, scriptsite, policy) { 1006 | // allow all policy 1007 | if (policy === 0) { 1008 | return false; 1009 | } 1010 | 1011 | // block all policy 1012 | if (policy === 3) { 1013 | return true; 1014 | } 1015 | 1016 | // we start blocking to then check if we allow 1017 | let block = true; 1018 | 1019 | // allow same domain 1020 | if (scriptsite.domain === tabsite.domain) { 1021 | block = false; 1022 | } 1023 | // relaxed policy - helper scripts also allowed 1024 | else if (policy === 1 && (isCommonHelpers(scriptsite) || isRelated(scriptsite.domain, tabsite.domain))) { 1025 | block = false; 1026 | } 1027 | 1028 | // global blackwhitelist 1029 | const level = (tabsite.private ? privatum.preferences : preferences); 1030 | let blocked = getScriptRule(level.rules.urls, scriptsite); 1031 | 1032 | if (blocked !== null) { 1033 | block = blocked; 1034 | } 1035 | 1036 | // custom rules for the domain/site/page 1037 | blocked = getScriptRule(tabsite.rules, scriptsite); 1038 | 1039 | if (blocked !== null) { 1040 | block = blocked; 1041 | } 1042 | 1043 | return block; 1044 | } 1045 | 1046 | /** 1047 | * @brief Update the icon number 1048 | * 1049 | * Updates the number in the extension icon and keeps track of all 1050 | * resources blocked and allowed to populate the popup when needed. 1051 | * 1052 | * @param block [Boolean] if resource was blocked 1053 | * @param tabsite [Object] urls of the tab 1054 | * @param scriptsite [Object] urls of the loading resource 1055 | * @param tabid [Number] ID of the tab 1056 | * @param frameid [Number] ID of the frame 1057 | * @param subframe [Boolean] if resource is a subframe 1058 | */ 1059 | function updateUI(block, tabsite, scriptsite, tabid, frameid, subframe) { 1060 | // set badge icon 1061 | if (block) { 1062 | tabsite.blocked++; 1063 | 1064 | // no frame contains this key 1065 | if (tabsite.private === undefined) { 1066 | tabStorage[tabid].blocked++; 1067 | } 1068 | 1069 | chrome.browserAction.setBadgeText({ 1070 | text: tabStorage[tabid].blocked.toString(), 1071 | tabId: tabid 1072 | }); 1073 | } 1074 | 1075 | // send info about the scripts to the popup 1076 | /*chrome.runtime.sendMessage({ 1077 | site: scriptsite, 1078 | blocked: block, 1079 | tabid: tabid 1080 | });*/ 1081 | 1082 | // save info about loaded scripts or frame 1083 | const script = tabsite.scripts; 1084 | const scriptInfo = { 1085 | name: scriptsite.page, 1086 | query: scriptsite.query, 1087 | protocol: scriptsite.protocol, 1088 | blocked: block 1089 | }; 1090 | 1091 | // save info about frame 1092 | if (subframe) { 1093 | // add frameId on sub_frame 1094 | scriptInfo.frameid = frameid; 1095 | 1096 | // save frame info in another area 1097 | const frameInfo = scriptsite; 1098 | scriptsite.private = tabsite.private; 1099 | const sitePolicies = getRules(scriptsite); 1100 | frameInfo.policy = sitePolicies.policy; 1101 | frameInfo.rules = sitePolicies.rules; 1102 | frameInfo.blocked = 0; 1103 | frameInfo.scripts = {}; 1104 | tabStorage[tabid].frames[frameid] = frameInfo; 1105 | } 1106 | 1107 | // console.log("@scriptweeder, Saving domain", script[scriptsite.domain]) 1108 | if (script[scriptsite.domain] === undefined) { 1109 | script[scriptsite.domain] = {}; 1110 | } 1111 | 1112 | if (script[scriptsite.domain][scriptsite.subdomain] === undefined) { 1113 | script[scriptsite.domain][scriptsite.subdomain] = [scriptInfo]; 1114 | } 1115 | else { 1116 | script[scriptsite.domain][scriptsite.subdomain].push(scriptInfo); 1117 | } 1118 | } 1119 | 1120 | /** 1121 | * @brief The Script Weeder - Evaluate if resource can be downloaded 1122 | * 1123 | * This is the main function that is run on every request made. 1124 | * 1125 | * @param details [Object] Info about the resource 1126 | * 1127 | * @return [Object] If resource must be blocked 1128 | */ 1129 | function scriptweeder(details) { 1130 | const tabid = details.tabId; 1131 | 1132 | if (tabStorage[tabid] === undefined) { 1133 | if (tabid !== -1) { 1134 | console.warn("@scriptweeder, tabStorage was not found!", tabid); 1135 | } 1136 | return {cancel: false}; 1137 | } 1138 | 1139 | const scriptsite = extractUrl(details.url); 1140 | let tabsite = tabStorage[tabid]; 1141 | const frameid = details.frameId; 1142 | let subframe = false; 1143 | 1144 | // if request comes from sub_frame or is a sub_frame 1145 | if (frameid > 0) { 1146 | // if request is a sub_frame 1147 | if (details.type === "sub_frame") { 1148 | subframe = true; 1149 | const pframeid = details.parentFrameId; 1150 | 1151 | if (pframeid > 0) { 1152 | tabsite = getLoadingFrame(pframeid, tabsite); 1153 | } 1154 | } 1155 | 1156 | // if request comes from a sub_frame we apply the rules from the frame site 1157 | if (!subframe) { 1158 | tabsite = getLoadingFrame(frameid, tabsite); 1159 | } 1160 | } 1161 | 1162 | // get if resource must be blocked or not 1163 | const block = isScriptAllowed(tabsite, scriptsite, tabsite.policy); 1164 | 1165 | updateUI(block, tabsite, scriptsite, tabid, frameid, subframe); 1166 | 1167 | // cancel: true - blocks loading, false - allows loading 1168 | return {cancel: block}; 1169 | } 1170 | 1171 | /** 1172 | * @brief Resources and protocols to be evaluated 1173 | * 1174 | * Here we tell the webRequest API which protocols and resource types 1175 | * need to be evaluated by the scriptweeder before the request is made 1176 | */ 1177 | chrome.webRequest.onBeforeRequest.addListener( 1178 | scriptweeder, 1179 | { 1180 | urls: ["http://*/*", "https://*/*", "ws://*/*", "wss://*/*", "file://*/*"], 1181 | types: ["script", "sub_frame", "websocket"] 1182 | }, 1183 | ["blocking"] 1184 | ); 1185 | 1186 | /** 1187 | * @brief Block ping requests 1188 | * 1189 | * Block ping requests to reduce tracking. 1190 | * 1191 | * @note Ping requests are global, less branches in scriptweeder. 1192 | */ 1193 | chrome.webRequest.onBeforeRequest.addListener( 1194 | () => { 1195 | return {cancel: preferences.ping !== true}; 1196 | }, 1197 | { 1198 | urls: ["http://*/*", "https://*/*"], 1199 | types: ["ping"] 1200 | }, 1201 | ["blocking"] 1202 | ); 1203 | 1204 | /** 1205 | * @brief Redirect http://ScriptJäger urls to preferences merging 1206 | * 1207 | * Gets URLs that match `https?://ScriptJ(ä|ae)ger/.*` and redirects 1208 | * them to the preferences page for merging preferences. 1209 | * 1210 | * The snippet to be merged is added as the path of the URL. 1211 | * 1212 | * This redirection allows sharing preferences between any browser. 1213 | */ 1214 | chrome.webRequest.onBeforeRequest.addListener( 1215 | (details) => { 1216 | const url = `${chrome.runtime.getURL("prefs.html")}?${details.urls.substring(details.urls.search(/\w\//) + 2)}`; 1217 | return {redirectUrl: url}; 1218 | }, 1219 | { 1220 | urls: ["http://ScriptJäger/*", "http://ScriptJaeger/*", "https://ScriptJäger/*", "https://ScriptJaeger/*"], 1221 | types: ["main_frame"] 1222 | }, 1223 | ["blocking"] 1224 | ); 1225 | 1226 | // ========================================================================= 1227 | 1228 | /** 1229 | * @brief Perform actions acording to message 1230 | * 1231 | * The popup and the preferences page might require info or things 1232 | * to be executed. 1233 | * 1234 | * Child 'type' will contain the type of the request 1235 | * 1236 | * @param msg [Object] Contains type and data for the action 1237 | * 1238 | * @note Each request has different msg children/info 1239 | * 1240 | * 0 Change blackwhitelist 1241 | * - private [Boolean] Is private browsing? 1242 | * - rule [Boolean] If script is blocked or allowed 1243 | * - rule [Number] New policy rule 1244 | * - script [Array] Contains urls of the script to apply rule 1245 | * - domain [String] Host script being allowed or blocked 1246 | * - subdomain [String] host script being allowed or blocked 1247 | * - site [Array] Contains urls where to apply, all optional 1248 | * - domain [String] Host to save policy 1249 | * - subdomain [String] Subdomain to save policy 1250 | * - page [String] Dir+filename to save policy 1251 | * 1252 | * 1 Allow all scripts once (do not save) 1253 | * - tabId [Number] id of the tab to allow once 1254 | * - allow [Boolean] Enable/disable allow once 1255 | * 1256 | * 2 Popup requesting allowed/blocked list for relaxed/filtered 1257 | * - policy [Number] Block policy 1258 | * - tabid [Number] id of the requested tab 1259 | * - frameid [Number] id of frame that had its policy changed 1260 | * - window [Number] id of the window the tab is from 1261 | * 1262 | * 3 New preferences from preferences page 1263 | * - prefs [Object] New preferences 1264 | */ 1265 | function processMessage(msg) { 1266 | switch (msg.type) { 1267 | // save a rule 1268 | case 0: { 1269 | const level = (msg.private ? privatum.preferences : preferences); 1270 | 1271 | saveRule(level, msg.site, msg.rule); 1272 | 1273 | if (!msg.private) { 1274 | chrome.storage.local.set({preferences: preferences}); 1275 | } 1276 | break; 1277 | } 1278 | 1279 | // allow once 1280 | case 1: { 1281 | tabStorage[msg.tabId].allowonce = msg.allow; 1282 | chrome.tabs.reload(msg.tabId, {bypassCache: !msg.allow}); 1283 | break; 1284 | } 1285 | 1286 | // popup requests to check which scripts are blocked when on filtered or relaxed 1287 | case 2: { 1288 | const scriptslist = []; 1289 | let frame = tabStorage[msg.tabid]; 1290 | 1291 | if (msg.frameid > 0) { 1292 | frame = frame.frames[msg.frameid]; 1293 | } 1294 | 1295 | Object.entries(frame.scripts).forEach((domain) => { 1296 | Object.keys(domain[1]).forEach((subdomain) => { 1297 | scriptslist.push({ 1298 | name: `${subdomain}${domain[0]}${msg.frameid}`, 1299 | blocked: isScriptAllowed(frame, {domain: domain[0], subdomain: subdomain}, msg.policy) 1300 | }); 1301 | }); 1302 | }); 1303 | 1304 | ports[msg.window].postMessage({ 1305 | type: 2, 1306 | tabid: msg.tabid, 1307 | scripts: scriptslist 1308 | }); 1309 | break; 1310 | } 1311 | 1312 | case 3: { 1313 | preferences = msg.prefs; 1314 | break; 1315 | } 1316 | 1317 | default: 1318 | } 1319 | } 1320 | 1321 | /** 1322 | * @brief Listener for normal messages 1323 | * 1324 | * Fired only from preferences page sending the new modified settings 1325 | */ 1326 | chrome.runtime.onMessage.addListener(processMessage); 1327 | 1328 | /** 1329 | * @brief Exchange information with popup page 1330 | * 1331 | * We keep a channel open with the popup page so we can update it on 1332 | * realtime. 1333 | */ 1334 | chrome.runtime.onConnect.addListener((port) => { 1335 | port.postMessage({ 1336 | type: 0, 1337 | data: tabStorage[port.name] 1338 | }); 1339 | 1340 | const windowid = tabStorage[port.name].window; 1341 | ports[windowid] = port; 1342 | 1343 | port.onDisconnect.addListener(() => { 1344 | delete ports[windowid]; 1345 | }); 1346 | 1347 | port.onMessage.addListener(processMessage); 1348 | }); 1349 | 1350 | /** 1351 | * @brief Update popup on changing tabs 1352 | * 1353 | * When the active tab is changed this function is fired. 1354 | * If a port is open it means some popup interface is running, 1355 | * in this case we send the information about the tab to it. 1356 | * 1357 | * The message sent has a key `type` with value `0`, key `data` 1358 | * contains `tabStorage` data. 1359 | */ 1360 | chrome.tabs.onActivated.addListener((active) => { 1361 | const port = ports[active.windowId]; 1362 | 1363 | // don't send if no ports are open 1364 | if (port === undefined) { 1365 | return; 1366 | } 1367 | 1368 | port.postMessage({ 1369 | type: 0, 1370 | data: tabStorage[active.tabId] 1371 | }); 1372 | }); 1373 | -------------------------------------------------------------------------------- /default-prefs.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * @var preferences [Object] These are the default settings after 5 | * first run 6 | * 7 | * This file is injected on demand in files that already contain 8 | * this object. 9 | */ 10 | preferences = { 11 | rule: 1, 12 | private: 1, 13 | ping: false, 14 | urls: { 15 | // block ads 16 | "amazon.com": { 17 | rule: null, 18 | rules: { 19 | urls: { 20 | "amazon-adsystem.com": { 21 | rule: true, 22 | urls: {} 23 | }, 24 | "media-amazon.com": { 25 | rule: true, 26 | urls: {} 27 | } 28 | } 29 | }, 30 | urls: {} 31 | }, 32 | // private search 33 | "duckduckgo.com": { 34 | rule: 0, 35 | rules: {urls: {}}, 36 | urls: {} 37 | }, 38 | // language learning site 39 | "duolingo.com": { 40 | rule: 1, 41 | rules: { 42 | urls: { 43 | "cloudfront.net": { 44 | rule: null, 45 | urls: { 46 | "d35aaqx5ub95lt": { 47 | rule: false, 48 | urls: {} 49 | } 50 | } 51 | } 52 | } 53 | }, 54 | urls: {} 55 | }, 56 | // allow their tracker in their site 57 | "facebook.com": { 58 | rule: null, 59 | rules: { 60 | urls: { 61 | "fbcdn.net": { 62 | rule: false, 63 | urls: {} 64 | } 65 | } 66 | }, 67 | urls: {} 68 | }, 69 | // fan wikis 70 | "fandom.com": { 71 | rule: null, 72 | rules: { 73 | urls: { 74 | "nocookie.net": { 75 | rule: false, 76 | urls: {} 77 | } 78 | } 79 | }, 80 | urls: {} 81 | }, 82 | "jsfiddle.net": { 83 | rule: null, 84 | rules: { 85 | urls: { 86 | "jshell.net": { 87 | rule: false, 88 | urls: {} 89 | } 90 | } 91 | }, 92 | urls: {} 93 | }, 94 | "linkedin.com": { 95 | rule: null, 96 | rules: { 97 | urls: { 98 | "linkedin.com": { 99 | rule: true, 100 | urls: {} 101 | } 102 | } 103 | }, 104 | urls: {} 105 | }, 106 | "live.com": { 107 | rule: 2, 108 | rules: { 109 | urls: { 110 | // onedrive 111 | "1drv.com": { 112 | rule: false, 113 | urls: {} 114 | }, 115 | "gfx.ms": { 116 | rule: false, 117 | urls: {} 118 | }, 119 | // outlook contacts 120 | "office.com": { 121 | rule: false, 122 | urls: {} 123 | }, 124 | // outlook 125 | "office365.com": { 126 | rule: false, 127 | urls: {} 128 | }, 129 | // office apps 130 | "office.net": { 131 | rule: false, 132 | urls: {} 133 | } 134 | } 135 | }, 136 | urls: { 137 | "onedrive": { 138 | rule: null, 139 | rules: { 140 | urls: { 141 | // download data 142 | "1drv.com": { 143 | rule: false, 144 | urls: {} 145 | }, 146 | "akamaihd.net": { 147 | rule: false, 148 | urls: {} 149 | }, 150 | // loads the main bar 151 | "outlook.com": { 152 | rule: false, 153 | urls: {} 154 | }, 155 | // loads free space statistics 156 | "sfx.ms": { 157 | rule: false, 158 | urls: {} 159 | } 160 | } 161 | }, 162 | urls: {} 163 | } 164 | } 165 | }, 166 | "microsoft.com": { 167 | rule: null, 168 | rules: { 169 | urls: { 170 | "gfx.ms": { 171 | rule: false, 172 | urls: {} 173 | }, 174 | "microsoft.com": { 175 | rule: null, 176 | urls: { 177 | "c": { 178 | rule: true, 179 | urls: {} 180 | }, 181 | "fpt": { 182 | rule: true, 183 | urls: {} 184 | }, 185 | "web.vortex.data": { 186 | rule: true, 187 | urls: {} 188 | } 189 | } 190 | }, 191 | "onestore.ms": { 192 | rule: false, 193 | urls: {} 194 | } 195 | } 196 | }, 197 | urls: { 198 | "support": { 199 | rule: null, 200 | rules: { 201 | urls: { 202 | "akamaized.net": { 203 | rule: false, 204 | urls: {} 205 | } 206 | } 207 | }, 208 | urls: {} 209 | } 210 | } 211 | }, 212 | "netflix.com": { 213 | rule: null, 214 | rules: { 215 | urls: { 216 | "gstatic.com": { 217 | rule: true, 218 | urls: {} 219 | }, 220 | "netflix.com": { 221 | rule: true, 222 | urls: {} 223 | }, 224 | "nflxext.com": { 225 | rule: false, 226 | urls: {} 227 | }, 228 | "nflximg.net": { 229 | rule: true, 230 | urls: {} 231 | } 232 | } 233 | }, 234 | urls: {} 235 | }, 236 | "sina.com.cn": { 237 | rule: null, 238 | rules: { 239 | urls: { 240 | "sina.com.cn": { 241 | rule: null, 242 | urls: { 243 | "d0": { 244 | rule: true, 245 | urls: {} 246 | }, 247 | "d1": { 248 | rule: true, 249 | urls: {} 250 | }, 251 | "d2": { 252 | rule: true, 253 | urls: {} 254 | }, 255 | "d3": { 256 | rule: true, 257 | urls: {} 258 | }, 259 | "d4": { 260 | rule: true, 261 | urls: {} 262 | }, 263 | "d5": { 264 | rule: true, 265 | urls: {} 266 | }, 267 | "d6": { 268 | rule: true, 269 | urls: {} 270 | }, 271 | "d7": { 272 | rule: true, 273 | urls: {} 274 | }, 275 | "d8": { 276 | rule: true, 277 | urls: {} 278 | }, 279 | "interest.mix": { 280 | rule: true, 281 | urls: {} 282 | }, 283 | } 284 | } 285 | } 286 | }, 287 | urls: {} 288 | }, 289 | "skype.com": { 290 | rule: null, 291 | rules: { 292 | urls: { 293 | "akamaized.net": { 294 | rule: false, 295 | urls: {} 296 | } 297 | } 298 | }, 299 | urls: {} 300 | }, 301 | // private search 302 | "startpage.com": { 303 | rule: 0, 304 | rules: {urls: {}}, 305 | urls: {} 306 | }, 307 | // fan wikis 308 | "wikia.com": { 309 | rule: null, 310 | rules: { 311 | urls: { 312 | "nocookie.net": { 313 | rule: false, 314 | urls: {} 315 | } 316 | } 317 | }, 318 | urls: {} 319 | }, 320 | "xbox.com": { 321 | rule: null, 322 | rules: { 323 | urls: { 324 | "akamaized.net": { 325 | rule: false, 326 | urls: {} 327 | }, 328 | "gfx.ms": { 329 | rule: false, 330 | urls: {} 331 | }, 332 | "microsoft.com": { 333 | rule: false, 334 | urls: {} 335 | } 336 | } 337 | }, 338 | urls: {} 339 | } 340 | }, 341 | rules: { 342 | urls: { 343 | // your local sites 344 | "localhost": { 345 | rule: false, 346 | urls: {} 347 | }, 348 | // ads on Chinese sites 349 | "360buyimg.com": { 350 | rule: true, 351 | urls: {} 352 | }, 353 | // ads on Chinese sites 354 | "alicdn.com": { 355 | rule: null, 356 | urls: { 357 | "tbip": { 358 | rule: true, 359 | urls: {} 360 | } 361 | } 362 | }, 363 | // advertising 364 | "aolcdn.com": { 365 | rule: true, 366 | urls: {} 367 | }, 368 | // known good cdn 369 | "cloudflare.com": { 370 | rule: null, 371 | urls: { 372 | "ajax": { 373 | rule: false, 374 | urls: {} 375 | }, 376 | "cdnjs": { 377 | rule: false, 378 | urls: {} 379 | } 380 | } 381 | }, 382 | // gaming tracker 383 | "cursecdn.com": { 384 | rule: true, 385 | urls: {} 386 | }, 387 | // tracker 388 | "dotomi.com": { 389 | rule: true, 390 | urls: {} 391 | }, 392 | // Facebook (blacklisted here, but whitelisted on facebook.com) 393 | "fbcdn.net": { 394 | rule: true, 395 | urls: {} 396 | }, 397 | "google.com": { 398 | rule: null, 399 | urls: { 400 | // +1 stuff 401 | "apis": { 402 | rule: true, 403 | urls: {} 404 | }, 405 | "maps": { 406 | rule: false, 407 | urls: {} 408 | } 409 | } 410 | }, 411 | // javascript libraries 412 | "googleapis.com": { 413 | rule: null, 414 | urls: { 415 | "ajax": { 416 | rule: false, 417 | urls: {} 418 | }, 419 | "maps": { 420 | rule: false, 421 | urls: {} 422 | } 423 | } 424 | }, 425 | // linkedin tracker 426 | "licdn.com": { 427 | rule: true, 428 | urls: {} 429 | }, 430 | // sites built with wix 431 | "parastorage.com": { 432 | rule: false, 433 | urls: {} 434 | }, 435 | // Tumblr sites 436 | "tumblr.com": { 437 | rule: null, 438 | urls: { 439 | "assets": { 440 | rule: false, 441 | urls: {} 442 | } 443 | } 444 | }, 445 | // Twitter widgets (tracker) 446 | "twimg.com": { 447 | rule: null, 448 | urls: { 449 | "widgets": { 450 | rule: true, 451 | urls: {} 452 | } 453 | } 454 | }, 455 | // Allow Node modules CDN 456 | "unpkg.com": { 457 | rule: false, 458 | urls: {} 459 | }, 460 | // Allow Vimeo frames 461 | "vimeo.com": { 462 | rule: false, 463 | urls: {} 464 | }, 465 | // Allow YouTube frames 466 | "youtube.com": { 467 | rule: false, 468 | urls: {} 469 | }, 470 | // Allow no cookie version of YouTube frames 471 | "youtube-nocookie.com": { 472 | rule: false, 473 | urls: {} 474 | }, 475 | // Youtube images 476 | "ytimg.com": { 477 | rule: null, 478 | urls: { 479 | "s": { 480 | rule: false, 481 | urls: {} 482 | } 483 | } 484 | } 485 | } 486 | } 487 | }; 488 | 489 | /** 490 | * @brief Callee to update preferences 491 | * 492 | * Call a function when the file has loaded to let the 493 | * caller know when the preferences has been replaced. 494 | * 495 | * @note This function must exist in the script that injects this file 496 | */ 497 | defaultPreferencesLoaded(); 498 | -------------------------------------------------------------------------------- /images/GitHub-Octocat.svg: -------------------------------------------------------------------------------- 1 | GitHub-Octocat -------------------------------------------------------------------------------- /images/GitHub-Typography.svg: -------------------------------------------------------------------------------- 1 | Ativo 1 -------------------------------------------------------------------------------- /images/allowall19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/An-dz/ScriptJaeger/be0369edb3962d5312ab8cfc56c9a65800891eb0/images/allowall19.png -------------------------------------------------------------------------------- /images/allowall38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/An-dz/ScriptJaeger/be0369edb3962d5312ab8cfc56c9a65800891eb0/images/allowall38.png -------------------------------------------------------------------------------- /images/allowonce-off.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /images/allowonce-on.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /images/blacklist38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/An-dz/ScriptJaeger/be0369edb3962d5312ab8cfc56c9a65800891eb0/images/blacklist38.png -------------------------------------------------------------------------------- /images/blockall19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/An-dz/ScriptJaeger/be0369edb3962d5312ab8cfc56c9a65800891eb0/images/blockall19.png -------------------------------------------------------------------------------- /images/blockall38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/An-dz/ScriptJaeger/be0369edb3962d5312ab8cfc56c9a65800891eb0/images/blockall38.png -------------------------------------------------------------------------------- /images/delete.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /images/delete38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/An-dz/ScriptJaeger/be0369edb3962d5312ab8cfc56c9a65800891eb0/images/delete38.png -------------------------------------------------------------------------------- /images/delete38.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /images/filtered19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/An-dz/ScriptJaeger/be0369edb3962d5312ab8cfc56c9a65800891eb0/images/filtered19.png -------------------------------------------------------------------------------- /images/filtered38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/An-dz/ScriptJaeger/be0369edb3962d5312ab8cfc56c9a65800891eb0/images/filtered38.png -------------------------------------------------------------------------------- /images/hasframe.svg: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /images/jaegerhut128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/An-dz/ScriptJaeger/be0369edb3962d5312ab8cfc56c9a65800891eb0/images/jaegerhut128.png -------------------------------------------------------------------------------- /images/jaegerhut16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/An-dz/ScriptJaeger/be0369edb3962d5312ab8cfc56c9a65800891eb0/images/jaegerhut16.png -------------------------------------------------------------------------------- /images/jaegerhut48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/An-dz/ScriptJaeger/be0369edb3962d5312ab8cfc56c9a65800891eb0/images/jaegerhut48.png -------------------------------------------------------------------------------- /images/preferences.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /images/relaxed19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/An-dz/ScriptJaeger/be0369edb3962d5312ab8cfc56c9a65800891eb0/images/relaxed19.png -------------------------------------------------------------------------------- /images/relaxed38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/An-dz/ScriptJaeger/be0369edb3962d5312ab8cfc56c9a65800891eb0/images/relaxed38.png -------------------------------------------------------------------------------- /images/undefined19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/An-dz/ScriptJaeger/be0369edb3962d5312ab8cfc56c9a65800891eb0/images/undefined19.png -------------------------------------------------------------------------------- /images/undefined38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/An-dz/ScriptJaeger/be0369edb3962d5312ab8cfc56c9a65800891eb0/images/undefined38.png -------------------------------------------------------------------------------- /images/websocket.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /images/whitelist38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/An-dz/ScriptJaeger/be0369edb3962d5312ab8cfc56c9a65800891eb0/images/whitelist38.png -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ScriptJäger", 3 | "version": "0.5.1", 4 | "author": "André Zanghelini (An_dz)", 5 | "description": "Die Erweiterung für den Jägermeister! __MSG_sjDescription__", 6 | "homepage_url": "https://github.com/An-dz/ScriptJaeger", 7 | "applications": { 8 | "gecko": { 9 | "id": "scriptjaeger@zanghelini.com", 10 | "strict_min_version": "58.0" 11 | } 12 | }, 13 | 14 | "icons": { 15 | "128": "images/jaegerhut128.png", 16 | "48": "images/jaegerhut48.png", 17 | "16": "images/jaegerhut16.png" 18 | }, 19 | "options_ui": { 20 | "page": "prefs.html", 21 | "open_in_tab": true, 22 | "browser_style": false, 23 | "chrome_style": false 24 | }, 25 | 26 | "background": { 27 | "scripts": [ 28 | "background.js" 29 | ] 30 | }, 31 | 32 | "browser_action": { 33 | "default_icon": { 34 | "19": "images/undefined19.png", 35 | "38": "images/undefined38.png" 36 | }, 37 | "default_title": "ScriptJäger", 38 | "default_popup": "popup.html" 39 | }, 40 | 41 | "web_accessible_resources": [ 42 | "prefs.html" 43 | ], 44 | 45 | "permissions": [ 46 | "alarms", 47 | "notifications", 48 | "storage", 49 | "tabs", 50 | "webNavigation", 51 | "webRequest", 52 | "webRequestBlocking", 53 | "" 54 | ], 55 | 56 | "default_locale": "en", 57 | 58 | "manifest_version": 2 59 | } 60 | -------------------------------------------------------------------------------- /popup.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | width: 380px; 4 | font-size: 11pt; 5 | background: var(--colorBgDark); 6 | } 7 | div, 8 | body, 9 | span, 10 | input, 11 | label { 12 | cursor: pointer; 13 | transition: background .2s ease-in-out; 14 | } 15 | input { 16 | margin: 0 5px; 17 | height: 30px; 18 | display: none; 19 | } 20 | .host > input { 21 | display: block; 22 | } 23 | p { 24 | cursor: default; 25 | text-align: center; 26 | } 27 | .scopes, 28 | .policies, 29 | .host, 30 | .details, 31 | .frame { 32 | display: flex; 33 | overflow: hidden; 34 | } 35 | .scopes div, 36 | .policies div { 37 | text-align: center; 38 | flex: 1; 39 | padding: 5px; 40 | } 41 | #f0 > div:nth-child(even) { 42 | background-color: rgba(255, 255, 255, .2); 43 | } 44 | .scopes div:hover, 45 | .policies div:hover, 46 | .host:hover, 47 | .frame:hover, 48 | .resources > .resource:hover, 49 | #f0 > div:nth-child(even):hover { 50 | background-color: rgba(255, 255, 255, 0.4); 51 | } 52 | .details { 53 | flex: 1; 54 | } 55 | span.domain, 56 | .subdomain, 57 | .resource { 58 | text-overflow: ellipsis; 59 | overflow: hidden; 60 | white-space: nowrap; 61 | } 62 | .subdomain { 63 | flex: 1; 64 | opacity: .5; 65 | padding: 5px 0; 66 | text-align: right; 67 | } 68 | span.domain { 69 | flex: 1.3; 70 | padding: 5px 0; 71 | text-align: left; 72 | } 73 | .number { 74 | cursor: pointer; 75 | width: 18px; 76 | padding: 5px; 77 | text-align: right; 78 | } 79 | .resources { 80 | max-height: 0; 81 | overflow-y: auto; 82 | overflow-x: hidden; 83 | transition: max-height 500ms ease-out; 84 | } 85 | input:checked + .resources { 86 | max-height: 200px; 87 | } 88 | .resource { 89 | color: #000; 90 | padding: 5px 10px 5px 20px; 91 | display: block; 92 | text-align: right; 93 | } 94 | #allowonce, 95 | #preferences { 96 | height: 30px; 97 | width: 30px; 98 | padding: 0; 99 | flex: none; 100 | } 101 | #preferences { 102 | background-image: url("/images/preferences.svg"); 103 | } 104 | #allowonce { 105 | background-image: url("/images/allowonce-off.svg"); 106 | } 107 | #allowonce.allowonce { 108 | background-image: url("/images/allowonce-on.svg"); 109 | } 110 | .frames { 111 | width: 16px; 112 | margin-right: 5px; 113 | } 114 | .websocket { 115 | width: 16px; 116 | margin-right: -16px; 117 | } 118 | .hasframe { 119 | background: url("/images/hasframe.svg") center/100% no-repeat; 120 | } 121 | .haswebsocket { 122 | background: url("/images/websocket.svg") center/100% no-repeat; 123 | } 124 | .resource.haswebsocket { 125 | background-size: 16px; 126 | background-position: 3px center; 127 | } 128 | .frame .resource { 129 | flex: 1; 130 | padding-left: 0; 131 | padding-right: 0; 132 | } 133 | .frame-policy { 134 | height: 20px; 135 | padding: 5px; 136 | transition: transform 200ms ease; 137 | } 138 | .frame-policy:hover { 139 | transform: scale(1.3); 140 | } 141 | .blocked .domain, 142 | .blocked .number, 143 | .blocked + input + .resources { 144 | opacity: .6; 145 | } 146 | /* Global selected */ 147 | [data-scope="0"] > .scopes > [data-value="0"], 148 | [data-scope="0"] > .policies { 149 | background: var(--colorScopeGlobal); 150 | } 151 | [data-scope="0"] > .scopes { 152 | background: var(--colorScopeGlobalLight); 153 | } 154 | /* Domain selected */ 155 | [data-scope="1"] > .scopes > [data-value="1"], 156 | [data-scope="1"] > .policies, 157 | [data-scope="1"] > .triangle > div { 158 | background: var(--colorScopeDomain); 159 | } 160 | [data-scope="1"] > .scopes { 161 | background: var(--colorScopeDomainLight); 162 | } 163 | /* Site selected */ 164 | [data-scope="2"] > .scopes > [data-value="2"], 165 | [data-scope="2"] > .policies, 166 | [data-scope="2"] > .triangle > div { 167 | background: var(--colorScopeSite); 168 | } 169 | [data-scope="2"] > .scopes { 170 | background: var(--colorScopeSiteLight); 171 | } 172 | /* Page selected */ 173 | [data-scope="3"] > .scopes > [data-value="3"], 174 | [data-scope="3"] > .policies, 175 | [data-scope="3"] > .triangle > div { 176 | background: var(--colorScopePage); 177 | } 178 | [data-scope="3"] > .scopes { 179 | background: var(--colorScopePageLight); 180 | } 181 | /* Block All selected */ 182 | .blockall, 183 | [data-policy="3"] > .policies > [data-value="3"] { 184 | background: var(--colorPolicyBlock); 185 | } 186 | [data-policy="3"] .hosts, 187 | [data-policy="3"] > .policies > [data-value="3"] { 188 | color: #fff; 189 | } 190 | /* Filtered selected */ 191 | .filtered, 192 | [data-policy="2"] > .policies > [data-value="2"] { 193 | background: var(--colorPolicyFiltered); 194 | } 195 | /* Relaxed selected */ 196 | .relaxed, 197 | [data-policy="1"] > .policies > [data-value="1"] { 198 | background: var(--colorPolicyRelaxed); 199 | } 200 | /* Allow All selected */ 201 | .allowonce, 202 | .allowall, 203 | [data-policy="0"] > .policies > [data-value="0"] { 204 | background: var(--colorPolicyAllow); 205 | } 206 | #frame-edit { 207 | position: absolute; 208 | width: 270px; 209 | left: 40px; 210 | top: -50px; 211 | box-shadow: 0 0 25px #444; 212 | transition: 500ms ease-out; 213 | transition-property: opacity, visibility; 214 | visibility: visible; 215 | opacity: 1; 216 | } 217 | #frame-edit[data-hidden="true"] { 218 | visibility: hidden; 219 | opacity: 0; 220 | } 221 | #frame-edit .policies label { 222 | padding-bottom: 0; 223 | } 224 | #frame-edit .policies img { 225 | width: 25px; 226 | } 227 | #frame-edit .triangle { 228 | overflow: hidden; 229 | position: absolute; 230 | top: 32px; 231 | left: -20px; 232 | width: 20px; 233 | height: 30px; 234 | } 235 | #frame-edit .triangle div { 236 | transform: rotate(45deg); 237 | height: 30px; 238 | width: 30px; 239 | right: -21px; 240 | position: absolute; 241 | box-shadow: 0 0 10px #666; 242 | } 243 | #cancel { 244 | background: url(images/delete.svg) center/20px var(--colorFg); 245 | border-radius: 200px; 246 | width: 24px; 247 | height: 24px; 248 | position: absolute; 249 | left: -12px; 250 | top: -12px; 251 | transition: all 200ms ease; 252 | } 253 | #cancel:hover { 254 | background-size: 26px; 255 | background-color: var(--colorBgDarker) 256 | } 257 | -------------------------------------------------------------------------------- /popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ScriptJäger 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | __MSG_scopePage__ 17 | __MSG_scopeSite__ 18 | __MSG_scopeDomain__ 19 | __MSG_scopeGlobal__ 20 | 21 | 22 | 23 | __MSG_policyBlockAll__ 24 | __MSG_policyFiltered__ 25 | __MSG_policyRelaxed__ 26 | __MSG_policyAllowAll__ 27 | 28 | 29 | 30 | 31 | 32 | 33 | __MSG_scopePage__ 34 | __MSG_scopeSite__ 35 | __MSG_scopeDomain__ 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /popup.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * @var jaegerhut [Object] Badge icons, one for each policy 5 | */ 6 | const jaegerhut = { 7 | 0: { 8 | name: "allowall", 9 | colour: "#D84A4A", 10 | text: chrome.i18n.getMessage("policyAllowAll") 11 | }, 12 | 1: { 13 | name: "relaxed", 14 | colour: "#559FE6", 15 | text: chrome.i18n.getMessage("policyRelaxed") 16 | }, 17 | 2: { 18 | name: "filtered", 19 | colour: "#73AB55", 20 | text: chrome.i18n.getMessage("policyFiltered") 21 | }, 22 | 3: { 23 | name: "blockall", 24 | colour: "#26272A", 25 | text: chrome.i18n.getMessage("policyBlockAll") 26 | }, 27 | undefined: { 28 | name: "undefined", 29 | colour: "#6F7072" 30 | } 31 | }; 32 | 33 | /** 34 | * @var tabInfo [Object] Holds data about the tab 35 | * obtained from the background process 36 | */ 37 | let tabInfo = {}; 38 | 39 | /** 40 | * @var port [Object] A connection Port that allows message exchanging 41 | */ 42 | let port; 43 | 44 | /* 45 | * Basic nodes for building the interface 46 | */ 47 | const nodeHost = document.createElement("div"); 48 | const nodeCheckbox = document.createElement("input"); 49 | const nodeDetails = document.createElement("label"); 50 | const nodeWebsocket = document.createElement("div"); 51 | const nodeFrames = document.createElement("div"); 52 | const nodeSubdomain = document.createElement("span"); 53 | const nodeDomain = document.createElement("span"); 54 | const nodeNumber = document.createElement("label"); 55 | const nodeResource = document.createElement("a"); 56 | const nodeHostsList = document.createElement("div"); 57 | 58 | nodeHost.className = "host blocked"; 59 | nodeDetails.className = "details"; 60 | nodeWebsocket.className = "websocket"; 61 | nodeFrames.className = "frames"; 62 | nodeSubdomain.className = "subdomain"; 63 | nodeDomain.className = "domain"; 64 | nodeNumber.className = "number"; 65 | nodeResource.className = "resource"; 66 | nodeHostsList.className = "hosts"; 67 | 68 | nodeHostsList.id = "f0"; 69 | 70 | nodeCheckbox.type = "checkbox"; 71 | 72 | nodeNumber.title = chrome.i18n.getMessage("seeResources"); 73 | 74 | nodeResource.target = "_blank"; 75 | 76 | /** 77 | * @brief Set and save an exception rule for that script 78 | * 79 | * Fired whenever the user changes the checkbox of a rule. 80 | * This will set and save the rule according to what the 81 | * user has chosen. 82 | * 83 | * @param e [Event] Event interface on the checkbox change event 84 | */ 85 | function setScriptRule(e) { 86 | const input = e.target; 87 | 88 | let info = tabInfo; 89 | const frameID = parseInt(input.dataset.frameid, 10); 90 | 91 | if (frameID > 0) { 92 | info = tabInfo.frames[frameID]; 93 | } 94 | 95 | const msg = { 96 | type: 0, 97 | private: tabInfo.private, 98 | site: [], 99 | rule: {} 100 | }; 101 | 102 | switch (parseInt(document.getElementById("settings").dataset.scope, 10)) { 103 | // page 104 | case 3: msg.site[2] = info.page; 105 | // site - fallthrough 106 | case 2: msg.site[1] = info.subdomain; 107 | // domain - fallthrough 108 | case 1: msg.site[0] = info.domain; 109 | // global - fallthrough 110 | default: 111 | } 112 | 113 | const domain = input.dataset.domain; 114 | const subdomain = input.dataset.subdomain; 115 | 116 | msg.rule[domain] = { 117 | rule: null, 118 | urls: {} 119 | }; 120 | 121 | msg.rule[domain].urls[subdomain] = { 122 | // in the DOM true means checked which means allow 123 | // in the settings true means block 124 | rule: !input.checked, 125 | urls: {} 126 | }; 127 | 128 | // The background script deals with it because the popup process will die on close 129 | port.postMessage(msg); 130 | } 131 | 132 | /** 133 | * @brief Open dropdown to choose frame policy 134 | * 135 | * Opens an overlay div to choose a new policy for a frame. 136 | * This is fired when clicking on a hat (Jaegerhut) 137 | * 138 | * @param e [Event] Event interface on the clicked Jaegerhut 139 | */ 140 | function openFramePolicy(e) { 141 | const frameid = e.target.parentNode.dataset.frameid; 142 | const policy = parseInt(e.target.dataset.policy, 10); 143 | const dropdown = document.getElementById("frame-edit"); 144 | const pos = e.target.getBoundingClientRect().y - 30; 145 | 146 | dropdown.dataset.frameid = frameid; 147 | dropdown.dataset.hidden = false; 148 | dropdown.style = `top:${pos}px`; 149 | 150 | dropdown.dataset.scope = 1; 151 | dropdown.dataset.policy = policy; 152 | } 153 | 154 | /** 155 | * @brief Close frame policy dropdown 156 | * 157 | * Closes the overlay div where you choose a new policy for a frame. 158 | */ 159 | function closeFramePolicy() { 160 | document.getElementById("frame-edit").dataset.hidden = true; 161 | } 162 | 163 | /** 164 | * @brief Build resource list in the DOM 165 | * 166 | * Injects nodes to display the list of resources that the page 167 | * contains. Also attaches events to the elements to allow 168 | * manipulation of the settings. 169 | * 170 | * @param frameid [Number] id of the frame being built 171 | */ 172 | function buildList(frameID) { 173 | const elemMainNode = document.getElementById(`f${frameID}`); 174 | let frame = tabInfo; 175 | 176 | if (frameID > 0) { 177 | frame = tabInfo.frames[frameID]; 178 | } 179 | 180 | Object.entries(frame.scripts).sort().forEach((domainData) => { 181 | const domain = domainData[0]; 182 | 183 | Object.entries(domainData[1]).sort().forEach((subdomainData) => { 184 | const subdomain = subdomainData[0]; 185 | const resources = subdomainData[1]; 186 | 187 | const elemHost = nodeHost.cloneNode(false); 188 | const elemCheckbox = nodeCheckbox.cloneNode(false); 189 | const elemDetails = nodeDetails.cloneNode(false); 190 | const elemWebsocket = nodeWebsocket.cloneNode(false); 191 | const elemFrames = nodeFrames.cloneNode(false); 192 | const elemSubdomain = nodeSubdomain.cloneNode(false); 193 | const elemDomain = nodeDomain.cloneNode(false); 194 | const elemNumber = nodeNumber.cloneNode(false); 195 | 196 | elemDetails.appendChild(elemWebsocket); 197 | elemDetails.appendChild(elemFrames); 198 | elemDetails.appendChild(elemSubdomain); 199 | elemDetails.appendChild(elemDomain); 200 | elemHost.appendChild(elemCheckbox); 201 | elemHost.appendChild(elemDetails); 202 | elemHost.appendChild(elemNumber); 203 | elemMainNode.appendChild(elemHost); 204 | 205 | const hostID = `${subdomain}${domain}${frameID}`; 206 | 207 | elemCheckbox.id = hostID; 208 | elemDetails.htmlFor = hostID; 209 | 210 | elemSubdomain.innerHTML = `${subdomain}${((subdomain.length > 0) ? "." : "")}`; 211 | elemDomain.innerHTML = `${domain}`; 212 | elemNumber.innerText = resources.length; 213 | 214 | // if the text is larger than the area, we display a tooltip 215 | if (elemSubdomain.scrollWidth > elemSubdomain.clientWidth || elemDomain.scrollWidth > elemDomain.clientWidth) { 216 | elemHost.title = `${elemSubdomain.textContent}${domain}`; 217 | } 218 | 219 | // save script exception 220 | elemCheckbox.addEventListener("change", setScriptRule, false); 221 | // add data to checkbox 222 | elemCheckbox.dataset.frameid = frameID; 223 | elemCheckbox.dataset.domain = domain; 224 | elemCheckbox.dataset.subdomain = subdomain; 225 | 226 | // input that controls the script list visibility 227 | const openList = nodeCheckbox.cloneNode(false); 228 | openList.id = `list_${hostID}`; 229 | elemNumber.htmlFor = `list_${hostID}`; 230 | elemMainNode.appendChild(openList); 231 | 232 | // element that holds the list of elements from that host 233 | const resourcesList = document.createElement("div"); 234 | resourcesList.className = "resources"; 235 | elemMainNode.appendChild(resourcesList); 236 | 237 | let frames = 0; 238 | let websockets = 0; 239 | 240 | // populate scripts list 241 | // script can be a websocket or frame 242 | resources.forEach((script) => { 243 | if (!script.blocked) { 244 | elemCheckbox.checked = true; 245 | // remove blocked class 246 | elemHost.className = "host"; 247 | } 248 | 249 | const url = `${script.protocol}${elemSubdomain.textContent}${domain}${script.name}${script.query}`; 250 | const elemResource = nodeResource.cloneNode(false); 251 | elemResource.innerText = script.name.match(/[^/]*.$/); 252 | elemResource.title = url; 253 | elemResource.href = url; 254 | 255 | // websocket 256 | if (script.protocol === "wss://" || script.protocol === "ws://") { 257 | elemResource.className = "resource haswebsocket"; 258 | elemWebsocket.className = "websocket haswebsocket"; 259 | elemWebsocket.title = `\n${chrome.i18n.getMessage("tooltipWebsockets", (++websockets).toString())}`; 260 | } 261 | 262 | // if frameid exists it's a frame 263 | // otherwise it's a normal script/websocket 264 | if (script.frameid === undefined) { 265 | resourcesList.appendChild(elemResource); 266 | } 267 | else { 268 | const policy = jaegerhut[tabInfo.frames[script.frameid].policy]; 269 | const elemFrameDiv = document.createElement("div"); 270 | elemFrameDiv.className = "frame"; 271 | elemFrameDiv.dataset.frameid = script.frameid; 272 | 273 | elemFrames.className = "frames hasframe"; 274 | elemFrames.title = chrome.i18n.getMessage("tooltipFrames", (++frames).toString()); 275 | 276 | const elemPolicy = document.createElement("img"); 277 | elemPolicy.src = `/images/${policy.name}38.png`; 278 | elemPolicy.className = "frame-policy"; 279 | elemPolicy.title = policy.text; 280 | elemPolicy.dataset.policy = tabInfo.frames[script.frameid].policy; 281 | elemPolicy.addEventListener("click", openFramePolicy); 282 | 283 | const elemNumberFrame = nodeNumber.cloneNode(false); 284 | elemNumberFrame.htmlFor = `frame${script.frameid}`; 285 | elemNumberFrame.innerText = Object.keys(tabInfo.frames[script.frameid].scripts).length; 286 | 287 | elemFrameDiv.appendChild(elemPolicy); 288 | elemFrameDiv.appendChild(elemResource); 289 | elemFrameDiv.appendChild(elemNumberFrame); 290 | resourcesList.appendChild(elemFrameDiv); 291 | 292 | const elemCheckboxFrame = nodeCheckbox.cloneNode(false); 293 | elemCheckboxFrame.id = `frame${script.frameid}`; 294 | resourcesList.appendChild(elemCheckboxFrame); 295 | 296 | const resourcesListFrame = document.createElement("div"); 297 | resourcesListFrame.className = `resources ${policy.name}`; 298 | resourcesListFrame.id = `f${script.frameid}`; 299 | resourcesList.appendChild(resourcesListFrame); 300 | 301 | buildList(script.frameid); 302 | } 303 | 304 | elemFrames.title = `${elemFrames.title}${elemWebsocket.title}`; 305 | }); 306 | }); 307 | }); 308 | } 309 | 310 | /** 311 | * @brief Sets and build the popup UI 312 | * 313 | * Define main classes and then call the script list builder. 314 | */ 315 | function startUI() { 316 | const error = document.getElementById("error"); 317 | const settings = document.getElementById("settings"); 318 | 319 | settings.replaceChild(nodeHostsList.cloneNode(false), document.getElementById("f0")); 320 | settings.removeAttribute("hidden"); 321 | error.hidden = true; 322 | 323 | const blocked = tabInfo.policy ? tabInfo.allowonce ? "(T) " : `(${tabInfo.blocked}) ` : ""; 324 | 325 | document.title = `${blocked}ScriptJäger`; 326 | document.getElementById("jaegerhut").href = `images/${jaegerhut[tabInfo.policy].name}38.png`; 327 | document.getElementById("jaegerfarbe").content = jaegerhut[tabInfo.policy].colour; 328 | 329 | let skip = false; 330 | 331 | switch (tabInfo.protocol) { 332 | case "https://": 333 | case "http://": 334 | break; 335 | case "chrome://": 336 | case "chrome-extension://": 337 | skip = "errorInternal"; 338 | break; 339 | case "file://": 340 | if (!tabInfo.policy) { 341 | skip = "errorFile"; 342 | } 343 | break; 344 | default: 345 | skip = "errorInternal"; 346 | } 347 | 348 | document.body.className = jaegerhut[tabInfo.policy].name; 349 | 350 | if (skip !== false) { 351 | error.innerText = chrome.i18n.getMessage(skip); 352 | error.removeAttribute("hidden"); 353 | settings.hidden = true; 354 | return; 355 | } 356 | 357 | // policy button reflects current policy 358 | settings.dataset.policy = tabInfo.policy; 359 | 360 | const allowonce = document.getElementById("allowonce"); 361 | 362 | // Allow once is turned on 363 | if (tabInfo.allowonce === true) { 364 | allowonce.title = chrome.i18n.getMessage("policyAllowOnceDisable"); 365 | allowonce.className = "allowonce"; 366 | } 367 | // Allow once is turned off 368 | else { 369 | allowonce.title = chrome.i18n.getMessage("policyAllowOnce"); 370 | allowonce.className = ""; 371 | } 372 | 373 | buildList(0); 374 | } 375 | 376 | /** 377 | * @brief Get info about tab 378 | * 379 | * When opening the popup we request the info about the 380 | * page scripts and create the DOM nodes with this info 381 | * 382 | * @param tabs [Array] Contains info about the current tab 383 | */ 384 | chrome.tabs.query({currentWindow: true, active: true}, (tabs) => { 385 | port = chrome.runtime.connect({name: tabs[0].id.toString(10)}); 386 | 387 | /** 388 | * @brief Perform actions acording to message 389 | * 390 | * The background script will send the info we need 391 | * 392 | * Child 'type' will contain the type of the request 393 | * 394 | * @param msg [Object] Contains type and data for the action 395 | * 396 | * @note Each request has different msg children/info 397 | * 398 | * 0 (Re)Build UI - Whenever the UI has to be completely updated 399 | * - data [Object] Tab info, a children of background tabStorage 400 | * 401 | * 1 Update interface 402 | * 403 | * 2 Response of allowed/blocked list for relaxed/filtered 404 | * - tabid [Number] id of the requested tab 405 | * - scripts [Array] Contains the url and the rule 406 | * - name [String] DOM ID of the script 407 | * - blocked [Boolean] Whether that level will be blocked 408 | */ 409 | port.onMessage.addListener((msg) => { 410 | console.log(msg); 411 | 412 | if (msg.type === 0) { 413 | // save tab info in variable 414 | tabInfo = msg.data; 415 | startUI(); 416 | return; 417 | } 418 | 419 | if (msg.type === 1) { 420 | return; 421 | } 422 | 423 | // msg.type === 2 424 | // check if the user has not changed tab 425 | if (msg.tabid === tabInfo.tabid) { 426 | msg.scripts.forEach((domain) => { 427 | document.getElementById(domain.name).checked = !domain.blocked; 428 | }); 429 | } 430 | }); 431 | }); 432 | 433 | /** 434 | * @brief Save new policy 435 | * 436 | * Send to background process to save new policy for the specific scope 437 | * 438 | * @param policy [Number] Policy to save 439 | * @param scope [Number] Where to change rule, e.g. domain, global 440 | * @param frameid [Number] Frame where the policy change is being done 441 | */ 442 | function changePolicy(policy, scope, frameid) { 443 | const msg = { 444 | type: 0, 445 | private: tabInfo.private, 446 | site: [], 447 | rule: policy 448 | }; 449 | 450 | let frame = tabInfo; 451 | 452 | if (frameid > 0) { 453 | frame = tabInfo.frames[frameid]; 454 | } 455 | 456 | switch (scope) { 457 | // page 458 | case 3: msg.site[2] = frame.page; 459 | // site - fallthrough 460 | case 2: msg.site[1] = frame.subdomain; 461 | // domain - fallthrough 462 | case 1: msg.site[0] = frame.domain; 463 | // global - fallthrough 464 | default: 465 | } 466 | 467 | port.postMessage(msg); 468 | } 469 | 470 | /** 471 | * @brief Enable listeners when the DOM has loaded 472 | * 473 | * When the DOM is loaded we can attach the events to manipulate 474 | * the preferences. 475 | */ 476 | function enableListeners() { 477 | document.getElementById("settings").addEventListener("click", closeFramePolicy, true); 478 | 479 | document.getElementById("cancel").addEventListener("click", closeFramePolicy); 480 | 481 | document.getElementById("preferences").addEventListener("click", (e) => { 482 | e.stopPropagation(); 483 | chrome.runtime.openOptionsPage(); 484 | }); 485 | 486 | // allow once 487 | document.getElementById("allowonce").addEventListener("click", (e) => { 488 | e.stopPropagation(); 489 | port.postMessage({ 490 | type: 1, 491 | tabId: tabInfo.tabid, 492 | allow: !tabInfo.allowonce 493 | }); 494 | }); 495 | 496 | document.querySelectorAll(".scopes").forEach((scopes) => { 497 | scopes.addEventListener("click", (e) => { 498 | e.target.parentNode.parentNode.dataset.scope = e.target.dataset.value; 499 | }); 500 | }); 501 | 502 | document.querySelectorAll(".policies").forEach((policies) => { 503 | policies.addEventListener("click", (e) => { 504 | const target = (e.target.tagName === "IMG") ? e.target.parentNode : e.target; 505 | 506 | const frame = target.parentNode.parentNode.dataset; 507 | const policy = parseInt(target.dataset.value, 10); 508 | const scope = parseInt(frame.scope, 10); 509 | 510 | frame.policy = policy; 511 | 512 | changePolicy(policy, scope, frame.frameid); 513 | 514 | if (frame.frameid > 0) { 515 | document.getElementById(`f${frame.frameid}`).className = `resources ${jaegerhut[frame.policy].name}`; 516 | } else { 517 | document.body.className = jaegerhut[frame.policy].name; 518 | } 519 | 520 | // change all inputs to checked (allowed) or unchecked (blocked) 521 | if (policy === 0 || policy === 3) { 522 | document.querySelectorAll(`#f${frame.frameid} > .script > input`).forEach((checkbox) => { 523 | checkbox.checked = !policy; 524 | }); 525 | 526 | return; 527 | } 528 | 529 | // request list of blocked and allowed scripts from background script 530 | port.postMessage({ 531 | type: 2, 532 | policy: policy, 533 | tabid: tabInfo.tabid, 534 | frameid: frame.frameid, 535 | window: tabInfo.window, 536 | }); 537 | }); 538 | }); 539 | } 540 | 541 | /** 542 | * @brief Translate and attach events 543 | * 544 | * This will translate the page and attach the events to the nodes. 545 | */ 546 | document.addEventListener("DOMContentLoaded", () => { 547 | const template = document.body.innerHTML; 548 | 549 | // translate the page 550 | document.body.innerHTML = template.replace(/__MSG_(\w+)__/g, (a, b) => { 551 | return chrome.i18n.getMessage(b); 552 | }); 553 | 554 | // allow resizable width on webpanel 555 | if (document.location.search === "?webpanel") { 556 | document.body.style = "width: 100%"; 557 | } 558 | 559 | enableListeners(); 560 | }); 561 | -------------------------------------------------------------------------------- /prefs.css: -------------------------------------------------------------------------------- 1 | html { 2 | background: var(--colorBg); 3 | } 4 | body { 5 | color: var(--colorFg); 6 | margin: 0 80px 50px; 7 | } 8 | h1, 9 | h2, 10 | h3, 11 | p, 12 | span:not(.site) { 13 | cursor: default; 14 | } 15 | h1 { 16 | margin-left: -20px; 17 | border-bottom: 1px solid var(--colorBorder); 18 | padding-bottom: .2em; 19 | } 20 | body, 21 | h2, 22 | .by { 23 | font-size: 13px; 24 | } 25 | .by { 26 | margin-left: .5em; 27 | } 28 | .logo { 29 | vertical-align: bottom; 30 | height: 35px; 31 | margin-right: 7px; 32 | } 33 | .github { 34 | display: inline-block; 35 | float: right; 36 | height: 35px; 37 | margin-top: 5px; 38 | } 39 | .github img { 40 | height: 60%; 41 | margin-left: 5px; 42 | } 43 | input[type=radio] { 44 | background-color: var(--colorBgIntense); 45 | border: 1px solid var(--colorBorder); 46 | border-radius: var(--radiusRound); 47 | box-shadow: inset 0 1px 2px rgba(0,0,0,.1); 48 | position: relative; 49 | margin: 0 10px 4px 0; 50 | outline: none; 51 | height: 16px; 52 | width: 16px; 53 | -webkit-appearance: none; 54 | vertical-align: middle; 55 | } 56 | input[type=radio]:focus, input[type=checkbox]:focus { 57 | border-color: var(--colorBorderHighlight); 58 | box-shadow: 0 0 0 1px var(--colorBorderHighlight); 59 | } 60 | input[type=radio]:checked::before { 61 | content: ""; 62 | position: absolute; 63 | background-color: var(--colorHighlightBg); 64 | border-radius: var(--radiusRound); 65 | top: 3px; 66 | left: 3px; 67 | right: 3px; 68 | bottom: 3px; 69 | } 70 | input[type=checkbox] { 71 | background-color: var(--colorBgIntense); 72 | border: 1px solid var(--colorBorder); 73 | border-radius: calc(var(--radius) * 0.33); 74 | box-shadow: inset 0 1px 2px rgba(0,0,0,.1); 75 | position: relative; 76 | margin: 0 10px 4px 0; 77 | outline: none; 78 | height: 16px; 79 | width: 16px; 80 | -webkit-appearance: none; 81 | vertical-align: middle; 82 | } 83 | input[type=checkbox]:before, 84 | input[type=checkbox]:after { 85 | content: ''; 86 | position: absolute; 87 | top: 0; 88 | left: 0; 89 | right: 0; 90 | bottom: 0; 91 | background-color: var(--colorHighlightBg); 92 | transform-origin: 0 0; 93 | transition: transform 0ms linear 0ms; 94 | } 95 | input[type=checkbox]:before { 96 | transform: rotate(-45deg) translateY(45%) translateX(-30%) scaleX(0.25) scaleY(0); 97 | } 98 | input[type=checkbox]:after { 99 | transform: rotate(225deg) translateY(-30%) translateX(-95%) scaleX(0.25) scaleY(0); 100 | transition-delay: 0ms; 101 | } 102 | input[type=checkbox]:indeterminate:before { 103 | transform: rotate(0) translateY(37.5%) translateX(10%) scaleX(0.8) scaleY(0.25); 104 | transition-duration: 33.33333333ms; 105 | } 106 | input[type=checkbox]:checked:before { 107 | transform: rotate(-45deg) translateY(45%) translateX(-30%) scaleX(0.25) scaleY(0.4); 108 | transition-duration: 33.33333333ms; 109 | } 110 | input[type=checkbox]:checked:after { 111 | transform: rotate(225deg) translateY(-30%) translateX(-95%) scaleX(0.25) scaleY(1.2); 112 | transition-duration: 100ms; 113 | transition-delay: 33.33333333ms; 114 | } 115 | input[type=checkbox]:disabled { 116 | background-color: var(--colorBgDark); 117 | border-color: var(--colorDisabled); 118 | } 119 | input[type=checkbox]:disabled:before, 120 | input[type=checkbox]:disabled:after { 121 | background-color: var(--colorDisabled); 122 | } 123 | .flex { 124 | display: flex; 125 | } 126 | .half { 127 | flex: 1; 128 | margin-right: 20px; 129 | } 130 | .rule-box { 131 | background: var(--colorBgDark); 132 | border: 1px solid var(--colorBorder); 133 | border-radius: var(--radius); 134 | padding: 0; 135 | height: 350px; 136 | width: 100%; 137 | overflow-y: scroll; 138 | overflow-x: hidden; 139 | } 140 | .pointer { 141 | cursor: pointer; 142 | } 143 | .description { 144 | font-size: 85%; 145 | margin: 0 26px; 146 | } 147 | ul.scripts { 148 | background: var(--colorAccentBgLight); 149 | outline: 1px solid var(--colorAccentBg); 150 | border-radius: var(--radius); 151 | padding-inline-start: 0; 152 | } 153 | ul.subrules { 154 | background: var(--colorSecondaryLight); 155 | outline: 1px solid var(--colorSecondary); 156 | border-radius: var(--radius); 157 | padding-inline-start: 0; 158 | } 159 | li { 160 | list-style-type: none; 161 | } 162 | li.show::after { 163 | content: ""; 164 | height: 5px; 165 | display: block; 166 | } 167 | li div { 168 | padding: 5px 10px; 169 | } 170 | ul li:nth-child(even) { 171 | background-color: var(--colorBgAlpha); 172 | } 173 | li ul { 174 | display: none; 175 | margin: 5px 0 0 20px; 176 | padding: 0; 177 | } 178 | .show > ul { 179 | display: block; 180 | } 181 | .show > ul:empty { 182 | display: none; 183 | } 184 | .rule { 185 | cursor: pointer; 186 | margin-right: 10px; 187 | vertical-align: middle; 188 | width: 22px; 189 | height: 100%; 190 | transition: transform 200ms ease; 191 | } 192 | a:focus, 193 | ul:focus, 194 | button:focus, 195 | textarea:focus, 196 | [tabIndex]:focus { 197 | outline-color: var(--colorAccentBg); 198 | } 199 | .rule:hover, 200 | .rule:focus, 201 | #dropdown label:hover .rule { 202 | transform: scale(1.3); 203 | } 204 | #dropdown { 205 | background: #fff; 206 | border: 1px solid var(--colorBorder); 207 | box-shadow: 0 0 5px rgba(0,0,0,0.5); 208 | margin-top: -1px; /* Compensate top border */ 209 | margin-left: -11px; /* Compensate left border + div padding */ 210 | position: absolute; 211 | top: 0; 212 | left: -200px; 213 | opacity: 0; 214 | visibility: hidden; 215 | transition: opacity 200ms ease-out; 216 | } 217 | #dropdown input { 218 | display: none; 219 | } 220 | #dropdown label { 221 | cursor: pointer; 222 | display: block; 223 | padding: 5px 10px; 224 | } 225 | #dropdown input:checked + label { 226 | background: var(--colorHighlightBgLight); 227 | } 228 | #dropdown label:hover { 229 | background: var(--colorHighlightBgDark) !important; 230 | } 231 | #dropdown input:checked + label:hover{ 232 | background: var(--colorHighlightBg) !important; 233 | color: var(--colorHighlightFg); 234 | } 235 | #dropdown.bwl, 236 | #dropdown.policy { 237 | opacity: 1; 238 | visibility: visible; 239 | } 240 | #dropdown.bwl .policy, 241 | #dropdown.policy .bwl { 242 | display: none; 243 | } 244 | .number { 245 | background-color: var(--colorSecondary); 246 | color: white; 247 | border-radius: var(--radiusRounded); 248 | margin-left: 5px; 249 | padding: 0 4px; 250 | font-size: 9pt; 251 | font-weight: bold; 252 | } 253 | .number.scripts { 254 | background-color: var(--colorAccentBg); 255 | } 256 | .number:empty { 257 | display: none; 258 | } 259 | .delete { 260 | opacity: 0; 261 | float: right; 262 | background-color: var(--colorBgDarker); 263 | background-image: url("images/delete.svg") !important; 264 | border-radius: 100%; 265 | border: none; 266 | height: 19px; 267 | width: 19px; 268 | transition: 200ms ease-in; 269 | transform: scale(0); 270 | padding: 0; 271 | } 272 | .delete:hover { 273 | transform: scale(1.25) !important; 274 | background-color: var(--colorAccentBgDark); 275 | } 276 | .delete:active { 277 | background-color: var(--colorAccentBgDarkDark) 278 | } 279 | .delete:focus { 280 | border: 1px solid var(--colorBorderHighlight); 281 | box-shadow: 0 0 0 1px var(--colorBorderHighlight); 282 | outline: none; 283 | } 284 | div:hover > .delete, 285 | .delete:focus { 286 | transform: scale(1); 287 | opacity: 1; 288 | } 289 | .site { 290 | position: relative; 291 | padding-right: 2px; 292 | } 293 | .rule-edit { 294 | position: absolute; 295 | font: inherit; 296 | top: 0; 297 | left: -6px; 298 | bottom: 0; 299 | padding: 0 5px; 300 | border: 1px solid var(--colorBorder); 301 | width: 100%; 302 | min-width: 50px; 303 | outline: none; 304 | } 305 | button { 306 | cursor: pointer; 307 | background: linear-gradient(var(--colorBgLightIntense), var(--colorBg)); 308 | color: var(--colorFg); 309 | border: 1px solid var(--colorBorder); 310 | box-shadow: 0 1px var(--colorBg); 311 | padding: 6px 12px; 312 | } 313 | body > button + button { 314 | border-left: 0; 315 | } 316 | button:hover { 317 | background: var(--colorBg); 318 | } 319 | button:active { 320 | background: var(--colorBgDark); 321 | } 322 | textarea { 323 | border: 1px solid var(--colorBorder); 324 | border-radius: var(--border); 325 | background: var(--colorBgIntense); 326 | color: var(--colorFg); 327 | width: 100%; 328 | margin-bottom: 20px; 329 | min-height: 7em; 330 | } 331 | #alertbox { 332 | visibility: hidden; 333 | opacity: 0; 334 | transition: opacity 200ms ease-in; 335 | background-color: var(--colorBgAlpha); 336 | position: fixed; 337 | display: grid; 338 | top: 0; 339 | left: 0; 340 | right: 0; 341 | bottom: 0; 342 | grid-template-rows: 1fr 1fr 1fr; 343 | grid-template-columns: 1fr 1fr 1fr; 344 | grid-template-areas: ". . ." 345 | ". a ." 346 | ". . ."; 347 | } 348 | #alertbox .grid { 349 | background-color: var(--colorBg); 350 | border: 1px solid var(--colorBorder); 351 | border-top: 2px solid var(--colorBorderHighlight); 352 | border-radius: var(--radiusRounded); 353 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.2); 354 | padding: 20px; 355 | grid-area: a; 356 | display: grid; 357 | grid-template-rows: 1fr; 358 | } 359 | #alertbox h3 { 360 | margin-top: 0; 361 | } 362 | #alertbox p { 363 | max-height: 40vh; 364 | overflow: auto; 365 | } 366 | #alertbox p > span { 367 | display: block; 368 | padding-left: 20px; 369 | } 370 | #alertbox .rule-box { 371 | height: auto; 372 | max-height: 40vh; 373 | } 374 | #alertbox .rule-box > li { 375 | font-weight: bold; 376 | padding: 10px; 377 | } 378 | #alertbox .rule-box label { 379 | padding: 5px 10px; 380 | cursor: pointer; 381 | } 382 | #alertbox .rule-box label input { 383 | cursor: pointer; 384 | margin-top: 3px; 385 | } 386 | #alertbox .rule-box li ul { 387 | display: block; 388 | } 389 | #alertbox label span { 390 | cursor: pointer; 391 | flex: 1; 392 | } 393 | #alertbox button:last-child { 394 | margin: 0 10px; 395 | } 396 | .visible { 397 | visibility: visible !important; 398 | opacity: 1 !important; 399 | } 400 | @media all and (max-width: 660px) { 401 | .flex { 402 | display: block; 403 | } 404 | } 405 | -------------------------------------------------------------------------------- /prefs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ScriptJäger 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | ScriptJägerby An_dz 15 | 16 | 17 | __MSG_settingsPolicy__ 18 | __MSG_policyAllowAll__ 19 | __MSG_policyRelaxed__ 20 | __MSG_policyFiltered__ 21 | __MSG_policyBlockAll__ 22 | 23 | 24 | __MSG_settingsPolicyPrivate__ 25 | __MSG_policyAllowAll__ 26 | __MSG_policyRelaxed__ 27 | __MSG_policyFiltered__ 28 | __MSG_policyBlockAll__ 29 | 30 | 31 | 32 | 33 | __MSG_settingsScripts__ 34 | 35 | 36 | 37 | __MSG_settingsRules__ 38 | 39 | 40 | 41 | __MSG_settingsOthers__ 42 | __MSG_settingsPing__ 43 | __MSG_settingsPingDesc__ 44 | __MSG_settingsManage__ 45 | __MSG_settingsManageReset____MSG_settingsManageExport____MSG_settingsManageImport____MSG_settingsManageMerge__ 46 | 47 | 48 | 49 | __MSG_settingsRulesNone__ 50 | 51 | 52 | 53 | __MSG_settingsRulesWhite__ 54 | 55 | 56 | 57 | __MSG_settingsRulesBlack__ 58 | 59 | 60 | 61 | __MSG_policyAllowAll__ 62 | 63 | 64 | 65 | __MSG_policyRelaxed__ 66 | 67 | 68 | 69 | __MSG_policyFiltered__ 70 | 71 | 72 | 73 | __MSG_policyBlockAll__ 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | __MSG_buttonOk____MSG_buttonCancel__ 86 | 87 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /prefs.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * @var preferences [Object] JSON that holds preferences 5 | * 6 | * @see background.js 7 | * @see default-prefs.js 8 | * @see https://github.com/An-dz/ScriptJaeger/wiki/Dev:-Preferences 9 | */ 10 | let preferences = {}; 11 | 12 | /** 13 | * @var mergePreferences [Object] Rules to be merged 14 | * 15 | * This is used when merging new preferences so it 16 | * can keep the stuff without costly operations. 17 | */ 18 | let mergingPref = {}; 19 | 20 | /** 21 | * @var jaegerhut [Object] Badge icons, one for each policy 22 | */ 23 | const jaegerhut = { 24 | 0: { 25 | offset: 1, 26 | name: "allowall", 27 | text: chrome.i18n.getMessage("policyAllowAll") 28 | }, 29 | 1: { 30 | offset: 2, 31 | name: "relaxed", 32 | text: chrome.i18n.getMessage("policyRelaxed") 33 | }, 34 | 2: { 35 | offset: 3, 36 | name: "filtered", 37 | text: chrome.i18n.getMessage("policyFiltered") 38 | }, 39 | 3: { 40 | offset: 4, 41 | name: "blockall", 42 | text: chrome.i18n.getMessage("policyBlockAll") 43 | }, 44 | true: { 45 | offset: 2, 46 | name: "blacklist", 47 | text: chrome.i18n.getMessage("settingsRulesBlack") 48 | }, 49 | false: { 50 | offset: 1, 51 | name: "whitelist", 52 | text: chrome.i18n.getMessage("settingsRulesWhite") 53 | }, 54 | null: { 55 | offset: 0, 56 | name: "undefined", 57 | text: chrome.i18n.getMessage("settingsRulesNone") 58 | }, 59 | delete: { 60 | name: "delete", 61 | text: chrome.i18n.getMessage("settingsDelete") 62 | } 63 | }; 64 | 65 | /** 66 | * @brief Save settings and tell background script about it 67 | * 68 | * Saves the new settings and then send them to the background 69 | * script so it's aware of changes and can replace them right away 70 | * 71 | * As the message API is async the saving might not be in the same 72 | * order as the functions are called, so a timeout is implemented to 73 | * reduce the calls to the background page and the storage API. 74 | */ 75 | function saveAndAlertBackground() { 76 | clearTimeout(window.saveDelay); 77 | 78 | window.saveDelay = setTimeout(() => { 79 | // send message to background 80 | chrome.runtime.sendMessage({ 81 | type: 3, // alert of preferences changes 82 | prefs: preferences 83 | }); 84 | 85 | // save preferences 86 | chrome.storage.local.set({preferences: preferences}); 87 | }, 1000); 88 | } 89 | 90 | /* ====================================================================== */ 91 | 92 | /** 93 | * @brief Display an alert box 94 | * 95 | * Shows an alert box in the middle of the page 96 | * 97 | * @param okaction [String] Action to execute when clicking the OK Button 98 | * @param title [String] Title of the alert box 99 | * @param msg [String] Message of the alert box (can be html) 100 | * @param text [String] Textbox content, send null to hide 101 | * @param cancelBtn [Boolean] If the cancel button must be shown 102 | */ 103 | function showAlert(okaction, title, msg, text, cancelBtn) { 104 | const alertbox = document.getElementById("alertbox"); 105 | const textarea = document.getElementById("textarea"); 106 | 107 | textarea.hidden = text === null; 108 | textarea.value = text; 109 | 110 | alertbox.className = "visible"; 111 | alertbox.dataset.okaction = okaction; 112 | alertbox.querySelector("h3").textContent = title; 113 | alertbox.querySelector("p").innerHTML = msg; 114 | alertbox.querySelector("button:last-child").hidden = !cancelBtn; 115 | } 116 | 117 | /** 118 | * @brief Open rule selector 119 | * 120 | * Opens a 'dropdown' to allow changing the policy on the specified 121 | * level 122 | * 123 | * @param e [Event] Event interface on the clicked DIV 124 | */ 125 | function openRuleSelector(e) { 126 | const ruleNode = e.target; 127 | 128 | const dropdown = document.getElementById("dropdown"); 129 | const rule = jaegerhut[ruleNode.dataset.rule]; 130 | const div = ruleNode.parentNode; 131 | const li = div.parentNode; 132 | const pos = div.getBoundingClientRect(); 133 | 134 | // position is relative to visible portion, need to add scrolled distance 135 | pos.y = pos.y + window.scrollY - 136 | // open at the position of the current rule 137 | (rule.offset * document.querySelector("li").offsetHeight); 138 | 139 | dropdown.style.top = `${pos.y}px`; 140 | dropdown.style.left = `${ruleNode.offsetLeft}px`; 141 | 142 | // copy domains and scripts url parts to dropdown element 143 | dropdown.dataset.domains = li.dataset.domains; 144 | dropdown.dataset.scripts = li.dataset.scripts; 145 | 146 | // highlight selected 147 | document.getElementById(rule.name).checked = true; 148 | 149 | // hide options that are not possible 150 | if (li.dataset.scripts === "[]") { 151 | dropdown.className = "policy"; 152 | } 153 | else { 154 | dropdown.className = "bwl"; 155 | } 156 | 157 | ruleNode.id = "active"; 158 | } 159 | 160 | /** 161 | * @brief Toggle sublevel rules 162 | * 163 | * Opens or closes the viewing of sublevel rules, like scripts or 164 | * subdomain rules. 165 | * 166 | * @param e [Event] Event interface on the clicked DIV 167 | */ 168 | function toggleSubLevel(e) { 169 | const li = e.target.parentNode; 170 | 171 | if (!li.hasAttribute("class")) { 172 | li.className = "show"; 173 | return; 174 | } 175 | 176 | li.removeAttribute("class"); 177 | li.querySelectorAll("li.show").forEach(node => { 178 | node.removeAttribute("class"); 179 | }); 180 | } 181 | 182 | /** 183 | * @brief Delete rule 184 | * 185 | * Deletes the rules where the X button was pressed 186 | * 187 | * @param e [Event] Event interface on the clicked X button 188 | */ 189 | function deleteRule(e) { 190 | // nodes 191 | const div = e.target.parentNode; 192 | const li = div.parentNode; 193 | const ul = li.parentNode; 194 | const liP = ul.parentNode; 195 | 196 | const subUrls = JSON.parse(li.dataset.domains); 197 | const scriptUrls = JSON.parse(li.dataset.scripts); 198 | 199 | let level = preferences; 200 | const isRules = scriptUrls.length > 0; 201 | 202 | subUrls.forEach((url, index) => { 203 | if (!isRules && subUrls.length - 1 === index) { 204 | delete level.urls[url]; 205 | return; 206 | } 207 | 208 | level = level.urls[url]; 209 | }); 210 | 211 | if (isRules) { 212 | level = level.rules; 213 | } 214 | 215 | scriptUrls.forEach((url, index) => { 216 | if (scriptUrls.length - 1 === index) { 217 | delete level.urls[url]; 218 | return; 219 | } 220 | 221 | level = level.urls[url]; 222 | }); 223 | 224 | // now that the rule has been deleted time to deal with the DOM 225 | const subNumber = liP.querySelector(".number:not(.scripts)"); 226 | const scriptNumber = liP.querySelector(".number.scripts"); 227 | 228 | const number = isRules ? scriptNumber : subNumber; 229 | 230 | // update numbers 231 | if (number) { 232 | --number.textContent; 233 | 234 | if (number.textContent === "0") { 235 | number.textContent = ""; 236 | } 237 | } 238 | 239 | // remove elements 240 | li.remove(); 241 | 242 | // if no more children exists remove clicking event 243 | if (subNumber.textContent === 0 && scriptNumber.textContent === 0) { 244 | liP.removeAttribute("class"); 245 | const divP = liP.firstElementChild; 246 | divP.removeAttribute("class"); 247 | divP.removeEventListener("click", toggleSubLevel); 248 | } 249 | 250 | // save preferences and alert the background page 251 | saveAndAlertBackground(); 252 | } 253 | 254 | /** 255 | * @brief Change the rule 256 | * 257 | * Changes the rule for the specified level on the dropdown 258 | * 259 | * @param e [Event] Event interface on the clicked rule 260 | */ 261 | function changeRule(e) { 262 | const dropdown = e.target.parentNode; 263 | 264 | const subUrls = JSON.parse(dropdown.dataset.domains); 265 | const scriptUrls = JSON.parse(dropdown.dataset.scripts); 266 | 267 | let level = preferences; 268 | const isRules = scriptUrls.length > 0; 269 | 270 | subUrls.forEach(url => { 271 | level = level.urls[url]; 272 | }); 273 | 274 | if (isRules) { 275 | level = level.rules; 276 | } 277 | 278 | scriptUrls.forEach(url => { 279 | level = level.urls[url]; 280 | }); 281 | 282 | let rule = parseInt(e.target.value, 10); 283 | 284 | // -1 is for null 285 | if (rule < 0) { 286 | rule = null; 287 | } 288 | // rules is boolean 289 | else if (isRules) { 290 | rule = Boolean(rule); 291 | } 292 | 293 | // save rule key 294 | level.rule = rule; 295 | 296 | // change icon to reflect new rule 297 | const active = document.getElementById("active"); 298 | active.src = `images/${e.target.id}38.png`; 299 | active.alt = jaegerhut[rule].text[0]; 300 | active.title = jaegerhut[rule].text; 301 | active.dataset.rule = rule; 302 | active.removeAttribute("id"); 303 | 304 | saveAndAlertBackground(); 305 | } 306 | 307 | /** 308 | * @brief Change toplevel policy 309 | * 310 | * Changes the global policy or global private policy 311 | * 312 | * @param e [Event] Event interface on input change 313 | */ 314 | function changePolicy(e) { 315 | preferences[e.target.name] = parseInt(e.target.value, 10); 316 | saveAndAlertBackground(); 317 | } 318 | 319 | /* ====================================================================== */ 320 | 321 | const ruleNodes = Object.freeze({ 322 | rule: document.createElement("img"), 323 | url: document.createElement("span"), 324 | subRules: document.createElement("span"), 325 | scriptRules: document.createElement("span"), 326 | delete: document.createElement("button"), 327 | subRulesUl: document.createElement("ul"), 328 | scriptRulesUl: document.createElement("ul"), 329 | }); 330 | 331 | ruleNodes.rule.className = "rule"; 332 | ruleNodes.url.className = "site"; 333 | ruleNodes.subRules.className = "number"; 334 | ruleNodes.scriptRules.className = "number scripts"; 335 | ruleNodes.delete.className = "delete"; 336 | ruleNodes.subRulesUl.className = "subrules"; 337 | ruleNodes.scriptRulesUl.className = "scripts"; 338 | 339 | ruleNodes.subRules.title = chrome.i18n.getMessage("settingsNumLevels"); 340 | ruleNodes.scriptRules.title = chrome.i18n.getMessage("settingsNumScripts"); 341 | ruleNodes.delete.title = chrome.i18n.getMessage("settingsDelete"); 342 | 343 | ruleNodes.rule.tabIndex = 0; 344 | 345 | /* ====================================================================== */ 346 | 347 | /** 348 | * @brief Build an LI DOM Element for the rule 349 | * 350 | * Creates the LI for the preferences list for a single rule. 351 | * 352 | * @param url [String] The URL of the rule 353 | * @param rule [Number/Boolean] Rule index for Jaegerhut index 354 | * @param subRules [Number] number of sub rules 355 | * @param scriptRules [Number] number of script rules 356 | * @param subUrls [Array] List of url parts of the rule level 357 | * @param scriptUrls [Array] List of url parts of the script rule 358 | * 359 | * @return [Element] An LI DOM Element for the rule 360 | */ 361 | function createRuleLi(url, rule, subRules, scriptRules, subUrls, scriptUrls) { 362 | const ruleLi = document.createElement("li"); 363 | const ruleDiv = document.createElement("div"); 364 | 365 | ruleLi.dataset.domains = JSON.stringify(subUrls); 366 | ruleLi.dataset.scripts = JSON.stringify(scriptUrls); 367 | 368 | const nodes = Object.freeze({ 369 | rule: ruleNodes.rule.cloneNode(false), 370 | url: ruleNodes.url.cloneNode(false), 371 | subRules: ruleNodes.subRules.cloneNode(false), 372 | scriptRules: ruleNodes.scriptRules.cloneNode(false), 373 | delete: ruleNodes.delete.cloneNode(false), 374 | }); 375 | 376 | // the URL 377 | nodes.url.textContent = url; 378 | 379 | // Jaegerhut: Rule applied 380 | nodes.rule.src = `images/${jaegerhut[rule].name}38.png`; 381 | nodes.rule.alt = jaegerhut[rule].text[0]; 382 | nodes.rule.title = jaegerhut[rule].text; 383 | nodes.rule.dataset.rule = rule; 384 | nodes.rule.addEventListener("click", openRuleSelector); 385 | 386 | // add icons with the number of sub and script rules 387 | if (subRules > 0) { 388 | nodes.subRules.textContent = subRules; 389 | } 390 | 391 | if (scriptRules > 0) { 392 | nodes.scriptRules.textContent = scriptRules; 393 | } 394 | 395 | // add listener for toggling subrules view 396 | if (subRules > 0 || scriptRules > 0) { 397 | ruleDiv.className = "pointer"; 398 | ruleDiv.tabIndex = 0; 399 | ruleDiv.addEventListener("click", toggleSubLevel); 400 | } 401 | 402 | // delete button listener 403 | nodes.delete.addEventListener("click", deleteRule); 404 | 405 | // append nodes 406 | ruleDiv.appendChild(nodes.rule); 407 | ruleDiv.appendChild(nodes.url); 408 | ruleDiv.appendChild(nodes.subRules); 409 | ruleDiv.appendChild(nodes.scriptRules); 410 | ruleDiv.appendChild(nodes.delete); 411 | 412 | ruleLi.appendChild(ruleDiv); 413 | 414 | return ruleLi; 415 | } 416 | 417 | /** 418 | * @brief Fill the list with the rules 419 | * 420 | * Creates and injects the DOM elements to represent the rules 421 | * 422 | * @param rulesList [Array] Rules objects to be printed 423 | * @param node [Element] Node where to inject the generated DOMs 424 | * 425 | * @note Sorting must be done beforehand 426 | */ 427 | function fillList(rulesList, node) { 428 | rulesList.forEach(item => { 429 | // get url parts, these are useful for knowing the rule we must change 430 | const subUrls = JSON.parse(node.parentNode.dataset.domains); 431 | const scriptUrls = JSON.parse(node.parentNode.dataset.scripts); 432 | 433 | // we need to append the new url part in the correct part 434 | const useScript = scriptUrls.length > 0 || node.className === "scripts"; 435 | const urlParts = useScript ? scriptUrls : subUrls; 436 | 437 | urlParts.push(item[0]); 438 | 439 | // print the url as a whole to make it easier for the user 440 | const url = `${ 441 | urlParts[1] === undefined ? "" : ( 442 | urlParts[1].length > 0 ? `${urlParts[1]}.` : "*://" 443 | ) 444 | }${ 445 | urlParts[0] || "" 446 | }${ 447 | urlParts[2] || "" 448 | }`; 449 | 450 | const data = item[1]; 451 | 452 | // get the sub-levels 453 | const subRules = Object.entries(data.urls); 454 | const scriptRules = data.rules ? Object.entries(data.rules.urls) : []; 455 | 456 | // build the DOM Element for the item 457 | const ruleLi = createRuleLi( 458 | url, 459 | data.rule, 460 | subRules.length, 461 | scriptRules.length, 462 | subUrls, 463 | scriptUrls 464 | ); 465 | 466 | const subRulesUl = ruleNodes.subRulesUl.cloneNode(false); 467 | const scriptRulesUl = ruleNodes.scriptRulesUl.cloneNode(false); 468 | 469 | // append everything together 470 | ruleLi.appendChild(subRulesUl); 471 | ruleLi.appendChild(scriptRulesUl); 472 | 473 | // call recursively 474 | fillList(subRules.sort(), subRulesUl); 475 | fillList(scriptRules.sort(), scriptRulesUl); 476 | 477 | node.appendChild(ruleLi); 478 | }); 479 | } 480 | 481 | /** 482 | * @brief Display loaded preferences on screen 483 | * 484 | * Gets the current loaded settings and show them on the screen 485 | */ 486 | function showPreferences() { 487 | // fill policy preferences 488 | document.querySelector( 489 | `input[name='rule'][value='${preferences.rule}']` 490 | ).checked = true; 491 | 492 | document.querySelector( 493 | `input[name='private'][value='${preferences.private}']` 494 | ).checked = true; 495 | 496 | // ping pref 497 | document.querySelector( 498 | "input[type='checkbox']" 499 | ).checked = !preferences.ping; 500 | 501 | // fill global blackwhitelist 502 | fillList( 503 | Object.entries(preferences.rules.urls).sort(), 504 | document.getElementById("bwl") 505 | ); 506 | 507 | // fill site specific settings 508 | fillList( 509 | Object.entries(preferences.urls).sort(), 510 | document.getElementById("rules") 511 | ); 512 | } 513 | 514 | /** 515 | * @brief Save preferences and rebuild UI 516 | * 517 | * When lots of changes in the preferences happen, this will 518 | * save the new preferences and will rebuild the rules lists. 519 | */ 520 | function newPreferences() { 521 | // clear the tables to allow the new rules to fill 522 | document.getElementById("bwl").innerText = ""; 523 | document.getElementById("rules").innerText = ""; 524 | 525 | showPreferences(); 526 | saveAndAlertBackground(); 527 | } 528 | 529 | /* ====================================================================== */ 530 | 531 | /** 532 | * @brief Update merge checkboxes 533 | * 534 | * If a lower level is checked, upper levels should be checked too. 535 | * If an upper level is unchecked all sublevel should too. 536 | * 537 | * @param event [Event] raised by the checked listener 538 | */ 539 | function checkboxMerge(event) { 540 | const checked = event.target.checked; 541 | const li = event.target.parentNode.parentNode; 542 | 543 | // if enabling a level, all parent levels must be enabled too 544 | if (checked) { 545 | const upperLi = li.parentNode.parentNode; 546 | 547 | // check upper level 548 | if (upperLi.tagName === "LI") { 549 | upperLi.querySelector("input").checked = true; 550 | } 551 | 552 | // rules that delete a point must check its children 553 | // but others should not 554 | if (!event.target.dataset.deleted) { 555 | return; 556 | } 557 | } 558 | 559 | // if disabling a level, all sublevels must be disabled too 560 | li.querySelectorAll("ul > li > label > input").forEach(input => { 561 | input.checked = checked; 562 | }); 563 | } 564 | 565 | /** 566 | * @brief Appends an item to the merge settings UI 567 | * 568 | * This appends a single line (LI DOM) for a single rule in the 569 | * merging settings UI (UL DOM). 570 | * 571 | * @param[in] url [String] Name of the rule level 572 | * @param[in] current [String] Name of the current/old rule 573 | * @param[in] change [String] Name of the merging/new rule 574 | * 575 | * @return [Element] constructed li html element 576 | */ 577 | function createMergeItem(url, current, change) { 578 | const li = document.createElement("li"); 579 | 580 | const label = document.createElement("label"); 581 | label.className = "flex"; 582 | 583 | const checkbox = document.createElement("input"); 584 | checkbox.type = "checkbox"; 585 | checkbox.checked = true; 586 | // add event that controls selection of parents and children 587 | checkbox.addEventListener("change", checkboxMerge); 588 | 589 | if (change === "deletes") { 590 | change = "delete"; 591 | checkbox.dataset.deleted = true; 592 | checkbox.title = chrome.i18n.getMessage("settingsEnabledCheckbox"); 593 | } 594 | else if (change === "delete") { 595 | checkbox.dataset.deleted = true; 596 | checkbox.disabled = true; 597 | checkbox.title = chrome.i18n.getMessage("settingsDisabledCheckbox"); 598 | } 599 | 600 | const span = document.createElement("span"); 601 | span.innerText = url; 602 | 603 | const currentImg = document.createElement("img"); 604 | currentImg.className = "rule"; 605 | currentImg.src = `images/${jaegerhut[current].name}38.png`; 606 | currentImg.alt = jaegerhut[current].text[0]; 607 | currentImg.title = chrome.i18n.getMessage( 608 | "settingsMergeCurrent", jaegerhut[current].text 609 | ); 610 | 611 | const changeImg = document.createElement("img"); 612 | changeImg.className = "rule"; 613 | changeImg.src = `images/${jaegerhut[change].name}38.png`; 614 | changeImg.alt = jaegerhut[change].text[0]; 615 | changeImg.title = chrome.i18n.getMessage( 616 | "settingsMergeNew", jaegerhut[change].text 617 | ); 618 | 619 | label.appendChild(checkbox); 620 | label.appendChild(span); 621 | label.appendChild(currentImg); 622 | label.appendChild(changeImg); 623 | li.appendChild(label); 624 | 625 | return li; 626 | } 627 | 628 | /** 629 | * @brief Builds the UI for merging preferences 630 | * 631 | * The preferences object that will be merged into the other does not 632 | * need to have all keys. 633 | * 634 | * @note The `this` argument is sent by the function itself when a 635 | * branch of the preferences is being deleted so that all subitems 636 | * are added on the UI as being deleted as well. 637 | * 638 | * @param[in] this [Boolean] If this whole rule is being deleted 639 | * @param[in] node [Element] DOM Element to inject element 640 | * @param[in] from [Array] List of preferences to be merged 641 | * @param[in] to [Object] Where the preferences must be merged into 642 | */ 643 | function checkMerge(node, from, to) { 644 | from.forEach(site => { 645 | const key = site[0]; 646 | const value = site[1]; 647 | 648 | // move rule to variable to prevent reference 649 | let rule = value.rule; 650 | let urls = value.urls ? Object.entries(value.urls) : []; 651 | let rules = value.rules && value.rules.urls ? 652 | Object.entries(value.rules.urls) : []; 653 | 654 | const oldValue = { 655 | rule: (to[key] && to[key].rule !== undefined) ? 656 | to[key].rule : "delete", 657 | 658 | rules: (to[key] && to[key].rules) ? 659 | to[key].rules : {urls:{}}, 660 | 661 | urls: (to[key] && to[key].urls) ? 662 | to[key].urls : {} 663 | }; 664 | 665 | const toDelete = this || value.delete; 666 | 667 | if (toDelete) { 668 | rule = `delete${value.delete ? "s" : ""}`; 669 | urls = Object.entries(oldValue.urls); 670 | rules = Object.entries(oldValue.rules.urls); 671 | } 672 | else if (rule === undefined) { 673 | rule = oldValue.rule; 674 | } 675 | 676 | const ulUrls = document.createElement("ul"); 677 | const ulRules = document.createElement("ul"); 678 | ulUrls.className = node.className; 679 | ulRules.className = "scripts"; 680 | 681 | checkMerge.call(toDelete, ulUrls, urls, oldValue.urls); 682 | checkMerge.call(toDelete, ulRules, rules, oldValue.rules.urls); 683 | 684 | // we only show it on the UI if the new `rule` is different 685 | // or if the other keys are present 686 | if ( 687 | rule !== oldValue.rule || 688 | ulUrls.childElementCount > 0 || 689 | ulRules.childElementCount > 0 690 | ) { 691 | const li = createMergeItem(key, oldValue.rule, rule); 692 | 693 | if (!this) { 694 | value.checkbox = li.firstElementChild.firstElementChild; 695 | } 696 | 697 | if (ulUrls.childElementCount > 0) { 698 | li.className = "show"; 699 | li.appendChild(ulUrls); 700 | } 701 | 702 | if (ulRules.childElementCount > 0) { 703 | li.className = "show"; 704 | li.appendChild(ulRules); 705 | } 706 | 707 | node.appendChild(li); 708 | } 709 | }); 710 | } 711 | 712 | /** 713 | * @brief Builds the UI for merging preferences 714 | * 715 | * The preferences object that will be merged into the other does not 716 | * need to have all keys. 717 | * 718 | * @note The `this` argument is sent by the function itself when a 719 | * branch of the preferences is being deleted so that all subitems 720 | * are added on the UI as being deleted as well. 721 | * 722 | * @param[in] this [Boolean] If this whole rule is being deleted 723 | * @param[in] node [Element] DOM Element to inject element 724 | * @param[in] from [Object] Preferences to be merged 725 | * 726 | * @param[out] to [Object] Where the preferences must be merged into 727 | */ 728 | function buildMergeUI(mergingRules) { 729 | const ul = document.querySelector("#alertbox ul"); 730 | const ulUrls = document.createElement("ul"); 731 | const ulRules = document.createElement("ul"); 732 | const liUrls = document.createElement("li"); 733 | const liRules = document.createElement("li"); 734 | 735 | ulUrls.className = "subrules"; 736 | ulRules.className = "scripts"; 737 | liUrls.textContent = chrome.i18n.getMessage("settingsRules"); 738 | liRules.textContent = chrome.i18n.getMessage("settingsScripts"); 739 | 740 | // it's already valid so no need to check type or content 741 | const urls = mergingRules.urls ? Object.entries(mergingRules.urls) : []; 742 | const rules = mergingRules.rules && mergingRules.rules.urls ? 743 | Object.entries(mergingRules.rules.urls) : []; 744 | 745 | checkMerge(ulUrls, urls, preferences.urls); 746 | checkMerge(ulRules, rules, preferences.rules.urls); 747 | 748 | ul.appendChild(liUrls); 749 | ul.appendChild(ulUrls); 750 | ul.appendChild(liRules); 751 | ul.appendChild(ulRules); 752 | } 753 | 754 | /* ====================================================================== */ 755 | 756 | /** 757 | * @brief Merges two preferences file 758 | * 759 | * The preferences object that will be merged into the other does 760 | * not need to have all keys. 761 | * 762 | * @param[in] from [Object] Preferences to be merged 763 | * 764 | * @param[out] to [Object] Where the preferences must be merged into 765 | */ 766 | function mergePreferences(from, to) { 767 | Object.entries(from).forEach(site => { 768 | const key = site[0]; 769 | const value = site[1]; 770 | 771 | if (value.checkbox.checked === false) { 772 | return; 773 | } 774 | 775 | if (value.delete === true) { 776 | if (to[key] !== undefined) { 777 | delete to[key]; 778 | } 779 | 780 | return; 781 | } 782 | 783 | if (to[key] === undefined) { 784 | to[key] = { 785 | rule: null, 786 | rules: {urls:{}}, 787 | urls: {} 788 | }; 789 | } 790 | 791 | if (value.rule !== undefined) { 792 | to[key].rule = value.rule; 793 | } 794 | 795 | if (value.rules !== undefined) { 796 | mergePreferences(value.rules.urls, to[key].rules.urls); 797 | } 798 | 799 | if (value.urls !== undefined) { 800 | mergePreferences(value.urls, to[key].urls); 801 | } 802 | }); 803 | } 804 | 805 | /** 806 | * @brief Prints all levels the rule is under 807 | * 808 | * Builds an HTML with all the levels the problem is under. 809 | * 810 | * @param at [Array] Each level is an entry 811 | * 812 | * @return [String] HTML string 813 | */ 814 | function validatePrintLevels(at) { 815 | return at.map( 816 | level => chrome.i18n.getMessage("settingsInvalidUnder", level) 817 | ).join(""); 818 | } 819 | 820 | /** 821 | * @brief Validates a preferences object 822 | * 823 | * Validation can be done at multiple levels as long as the level 824 | * contains a 'rule' key. Validation is recursive and checks the 825 | * entire object. 826 | * 827 | * @note Unknown keys are deleted 828 | * 829 | * @note Full validation check if the whole object is a valid 830 | * preferences object, it will raise errors if any object is missing. 831 | * Partial only checks if the values are correct and ignores if some 832 | * keys are missing, this check is for validating merging operations. 833 | * 834 | * @param[in] obj [Object] The level in the object to validate 835 | * @param[in] isFull [Boolean] If validation is full or partial 836 | * @param[in] isRules [Boolean] If the level is a blacklist 837 | * @param[in] at [Array] Array identifying the levels 838 | * 839 | * @param[out] warn [Array] Array that will contain warnings 840 | */ 841 | function validate(obj, isFull, isRules, at, warn) { 842 | // this check is to ensure that first all necessary entries exist 843 | if (isFull) { 844 | if (obj.rule === undefined) { 845 | throw new TypeError( 846 | `${chrome.i18n.getMessage( 847 | "settingsInvalidBooleanNull", "rule" 848 | )}${ 849 | validatePrintLevels(at) 850 | }` 851 | ); 852 | } 853 | 854 | if (obj.urls === undefined) { 855 | throw new TypeError( 856 | `${chrome.i18n.getMessage( 857 | "settingsInvalidObject", "urls" 858 | )}${ 859 | validatePrintLevels(at) 860 | }` 861 | ); 862 | } 863 | 864 | if (!isRules && obj.rules === undefined) { 865 | throw new TypeError( 866 | `${chrome.i18n.getMessage( 867 | "settingsInvalidObject", "rules" 868 | )}${ 869 | validatePrintLevels(at) 870 | }` 871 | ); 872 | } 873 | } 874 | 875 | // validate each entry 876 | Object.entries(obj).forEach(entry => { 877 | const key = entry[0]; 878 | const value = entry[1]; 879 | 880 | if (key === "rule") { 881 | // if rule is null then it's correct 882 | if (value === null) { 883 | return; 884 | } 885 | // otherwise more checks are necessary 886 | 887 | // blacklist (rules key) uses boolean 888 | if (isRules) { 889 | if (typeof value !== "boolean") { 890 | throw new TypeError( 891 | `${chrome.i18n.getMessage( 892 | "settingsInvalidBooleanNull", "rule" 893 | )}${ 894 | validatePrintLevels(at) 895 | }` 896 | ); 897 | } 898 | 899 | return; 900 | } 901 | 902 | // policy uses a number 903 | if (typeof value !== "number") { 904 | throw new TypeError( 905 | `${chrome.i18n.getMessage( 906 | "settingsInvalidNumberNull", "rule" 907 | )}${ 908 | validatePrintLevels(at) 909 | }` 910 | ); 911 | } 912 | 913 | if (value < 0 || value > 3) { 914 | throw new RangeError( 915 | `${chrome.i18n.getMessage( 916 | "settingsInvalidRangeNull", "rule" 917 | )}${ 918 | validatePrintLevels(at) 919 | }` 920 | ); 921 | } 922 | 923 | return; 924 | } 925 | 926 | if (key === "rules") { 927 | // blacklist (rules key) doesn't contain rules subkeys 928 | if (isRules) { 929 | throw new SyntaxError( 930 | `${chrome.i18n.getMessage( 931 | "settingsInvalidLevel", "rules" 932 | )}${ 933 | validatePrintLevels(at) 934 | }` 935 | ); 936 | } 937 | 938 | // policy objects must contain a rules key/object 939 | if (typeof value !== "object") { 940 | throw new TypeError( 941 | `${chrome.i18n.getMessage( 942 | "settingsInvalidObject", "rules" 943 | )}${ 944 | validatePrintLevels(at) 945 | }` 946 | ); 947 | } 948 | 949 | const atNew = [...at, "rules"]; 950 | 951 | // rules key/object must contain a urls key/object 952 | if (typeof value.urls !== "object") { 953 | throw new TypeError( 954 | `${chrome.i18n.getMessage( 955 | "settingsInvalidObject", "urls" 956 | )}${ 957 | validatePrintLevels(atNew) 958 | }` 959 | ); 960 | } 961 | 962 | // rules key should only have urls key 963 | // this is here because of the full check 964 | Object.keys(value).forEach(subkey => { 965 | if (subkey !== "urls") { 966 | warn.push( 967 | `${chrome.i18n.getMessage( 968 | "settingsWarningText", subkey 969 | )}${ 970 | validatePrintLevels(atNew) 971 | }` 972 | ); 973 | 974 | delete value[subkey]; 975 | } 976 | }); 977 | 978 | // validate children 979 | Object.entries(value.urls).forEach(object => { 980 | validate( 981 | object[1], 982 | isFull, 983 | true, 984 | [...at, "urls", object[0]], 985 | warn 986 | ); 987 | }); 988 | 989 | return; 990 | } 991 | 992 | if (key === "urls") { 993 | // all policy and blacklist objects must contain a urls object 994 | if (typeof value !== "object") { 995 | throw new TypeError( 996 | `${chrome.i18n.getMessage( 997 | "settingsInvalidObject", "urls" 998 | )}${ 999 | validatePrintLevels(at) 1000 | }` 1001 | ); 1002 | } 1003 | 1004 | const children = Object.entries(value); 1005 | 1006 | // limit to 3 levels of policy rules or 2 of script block rules 1007 | if ( 1008 | children.length > 0 && ( 1009 | (!isRules && at.length > 7) || 1010 | (isRules && at.length - at.indexOf("rules") > 5) 1011 | ) 1012 | ) { 1013 | warn.push( 1014 | `${chrome.i18n.getMessage( 1015 | "settingsWarningText2", key 1016 | )}${ 1017 | validatePrintLevels(at) 1018 | }` 1019 | ); 1020 | 1021 | value.urls = {}; 1022 | return; 1023 | } 1024 | 1025 | // validate children 1026 | children.forEach(object => { 1027 | validate( 1028 | object[1], 1029 | isFull, 1030 | isRules, 1031 | [...at, "urls", object[0]], 1032 | warn 1033 | ); 1034 | }); 1035 | 1036 | return; 1037 | } 1038 | 1039 | if (key === "delete") { 1040 | if (isFull) { 1041 | throw new SyntaxError( 1042 | `${chrome.i18n.getMessage( 1043 | "settingsInvalidDelete", "delete" 1044 | )}${ 1045 | validatePrintLevels(at) 1046 | }` 1047 | ); 1048 | } 1049 | 1050 | if (value !== true) { 1051 | throw new TypeError( 1052 | `${chrome.i18n.getMessage( 1053 | "settingsInvalidValue", ["delete", "true"] 1054 | )}${ 1055 | validatePrintLevels(at) 1056 | }` 1057 | ); 1058 | } 1059 | 1060 | return; 1061 | } 1062 | 1063 | // private key is only used under root 1064 | if (key === "private" && at.length === 1) { 1065 | if (typeof value !== "number") { 1066 | throw new TypeError( 1067 | `${chrome.i18n.getMessage( 1068 | "settingsInvalidNumber", "private" 1069 | )}${ 1070 | validatePrintLevels(at) 1071 | }` 1072 | ); 1073 | } 1074 | 1075 | if (value < 0 || value > 3) { 1076 | throw new RangeError( 1077 | `${chrome.i18n.getMessage( 1078 | "settingsInvalidRange", "private" 1079 | )}${ 1080 | validatePrintLevels(at) 1081 | }` 1082 | ); 1083 | } 1084 | 1085 | return; 1086 | } 1087 | 1088 | warn.push( 1089 | `${chrome.i18n.getMessage( 1090 | "settingsWarningText", key 1091 | )}${ 1092 | validatePrintLevels(at) 1093 | }` 1094 | ); 1095 | 1096 | delete obj[key]; 1097 | }); 1098 | } 1099 | 1100 | /** 1101 | * @brief Merge imported file into current preferences 1102 | * 1103 | * A special preferences object can be merged into the preferences. 1104 | * This special object allows to add, delete, modify and ignore rules. 1105 | * 1106 | * @param[in] importedRules [String] Preferences to be merged 1107 | * 1108 | * @warn Side effect: mergingPref receives the mergingRules object 1109 | */ 1110 | function askMergePreferences(mergingRules) { 1111 | try { 1112 | mergingRules = JSON.parse(mergingRules); 1113 | 1114 | if (typeof mergingRules !== "object") { 1115 | throw new SyntaxError( 1116 | chrome.i18n.getMessage("settingsInvalidData") 1117 | ); 1118 | } 1119 | 1120 | const warn = []; 1121 | validate(mergingRules, false, false, ["root"], warn); 1122 | 1123 | if (warn.length > 0) { 1124 | // show warning to let user know some stuff was removed 1125 | showAlert( 1126 | "askmerge", 1127 | chrome.i18n.getMessage("settingsWarningTitle"), 1128 | warn, 1129 | JSON.stringify(mergingRules, null, " "), 1130 | false 1131 | ); 1132 | 1133 | return; 1134 | } 1135 | 1136 | showAlert( 1137 | "merge", 1138 | chrome.i18n.getMessage("settingsMergeTitle"), 1139 | chrome.i18n.getMessage("settingsMergeMsg"), 1140 | null, 1141 | true 1142 | ); 1143 | 1144 | buildMergeUI(mergingRules); 1145 | mergingPref = mergingRules; 1146 | } 1147 | catch (error) { 1148 | showAlert( 1149 | "none", 1150 | chrome.i18n.getMessage("settingsInvalidPrefs"), 1151 | error.message, 1152 | null, 1153 | false 1154 | ); 1155 | } 1156 | } 1157 | 1158 | /** 1159 | * @brief Replace current preferences with a new one 1160 | * 1161 | * Will try to replace the current preferences with a new one 1162 | * provided in the textarea of the import dialogue. The new file 1163 | * is validated before importing is allowed and will raise error 1164 | * or warning dialogues. 1165 | */ 1166 | function importPreferences() { 1167 | try { 1168 | const prefs = JSON.parse(document.getElementById("textarea").value); 1169 | 1170 | if (typeof prefs !== "object") { 1171 | throw new TypeError( 1172 | chrome.i18n.getMessage("settingsInvalidData") 1173 | ); 1174 | } 1175 | 1176 | // null is not allowed at root 1177 | if (typeof prefs.rule !== "number") { 1178 | throw new TypeError( 1179 | `${chrome.i18n.getMessage( 1180 | "settingsInvalidNumber", "rule" 1181 | )}${chrome.i18n.getMessage( 1182 | "settingsInvalidUnder", "root" 1183 | )}` 1184 | ); 1185 | } 1186 | 1187 | const warn = []; 1188 | 1189 | // validate preferences 1190 | validate(prefs, true, false, ["root"], warn); 1191 | // will only be reached if no errors occured 1192 | preferences = prefs; 1193 | 1194 | if (warn.length > 0) { 1195 | showAlert( 1196 | "none", 1197 | chrome.i18n.getMessage("settingsWarningTitle"), 1198 | warn, 1199 | null, 1200 | false 1201 | ); 1202 | } 1203 | } 1204 | catch (error) { 1205 | showAlert( 1206 | "none", 1207 | chrome.i18n.getMessage("settingsInvalidPrefs"), 1208 | error.message, 1209 | null, 1210 | false 1211 | ); 1212 | return; 1213 | } 1214 | 1215 | newPreferences(); 1216 | } 1217 | 1218 | /* ====================================================================== */ 1219 | 1220 | /** 1221 | * @brief Close the alert box 1222 | * 1223 | * Closes the alert box clearing all content in the textbox 1224 | */ 1225 | function closeAlert() { 1226 | const alertbox = document.getElementById("alertbox"); 1227 | const textarea = document.getElementById("textarea"); 1228 | 1229 | if (alertbox.dataset.okaction === "merge") { 1230 | history.pushState(null, document.title, "prefs.html"); 1231 | } 1232 | 1233 | textarea.hidden = true; 1234 | textarea.value = null; 1235 | 1236 | alertbox.className = null; 1237 | alertbox.dataset.okaction = "none"; 1238 | } 1239 | 1240 | /** 1241 | * @brief Alertbox OK button action 1242 | * 1243 | * Execute the correct action when clicking the OK button in the alert 1244 | */ 1245 | function okClick() { 1246 | const okaction = document.getElementById("alertbox").dataset.okaction; 1247 | 1248 | if (okaction === "import") { 1249 | importPreferences(); 1250 | } 1251 | else if (okaction === "reset") { 1252 | document.querySelector("#alertbox button:last-child").click(); 1253 | 1254 | const script = document.createElement("script"); 1255 | script.src = "default-prefs.js"; 1256 | script.type = "text/javascript"; 1257 | script.id = "default"; 1258 | document.head.appendChild(script); 1259 | } 1260 | else if (okaction === "merge") { 1261 | // it's been already validated 1262 | mergePreferences(mergingPref.urls, preferences.urls); 1263 | mergePreferences(mergingPref.rules.urls, preferences.rules.urls); 1264 | newPreferences(); 1265 | } 1266 | else if (okaction === "askmerge") { 1267 | const merge = document.getElementById("textarea").value; 1268 | askMergePreferences(merge); 1269 | return; 1270 | } 1271 | 1272 | closeAlert(); 1273 | } 1274 | 1275 | /** 1276 | * @brief Reset preferences 1277 | * 1278 | * Click listener for the reset preferences button 1279 | */ 1280 | function resetPreferencesButton() { 1281 | showAlert( 1282 | "reset", 1283 | chrome.i18n.getMessage("settingsManageResetTitle"), 1284 | chrome.i18n.getMessage("settingsManageResetText"), 1285 | null, 1286 | true 1287 | ); 1288 | } 1289 | 1290 | /** 1291 | * @brief Export preferences 1292 | * 1293 | * Click listener for the export preferences button 1294 | */ 1295 | function exportPreferencesButton() { 1296 | showAlert( 1297 | "none", 1298 | chrome.i18n.getMessage("settingsManageExportTitle"), 1299 | chrome.i18n.getMessage("settingsManageExportText"), 1300 | JSON.stringify(preferences, null, " "), 1301 | false 1302 | ); 1303 | } 1304 | 1305 | /** 1306 | * @brief Import preferences 1307 | * 1308 | * Click listener for the import preferences button 1309 | */ 1310 | function importPreferencesButton() { 1311 | showAlert( 1312 | "import", 1313 | chrome.i18n.getMessage("settingsManageImportTitle"), 1314 | chrome.i18n.getMessage("settingsManageImportText"), 1315 | "", 1316 | true 1317 | ); 1318 | } 1319 | 1320 | /** 1321 | * @brief Merge preferences 1322 | * 1323 | * Click listener for the merge preferences button 1324 | */ 1325 | function mergePreferencesButton() { 1326 | showAlert( 1327 | "askmerge", 1328 | chrome.i18n.getMessage("settingsManageMergeTitle"), 1329 | chrome.i18n.getMessage("settingsManageMergeText"), 1330 | "", 1331 | true 1332 | ); 1333 | } 1334 | 1335 | /* ====================================================================== */ 1336 | 1337 | /** 1338 | * @brief Load preferences 1339 | * 1340 | * Loads saved preferences from storage API 1341 | * 1342 | * @note Runs immediately on page load 1343 | */ 1344 | chrome.storage.local.get((pref) => { 1345 | preferences = pref.preferences; 1346 | showPreferences(); 1347 | 1348 | // now we can try merging preferences 1349 | if (document.location.search.length > 0) { 1350 | askMergePreferences( 1351 | decodeURIComponent(document.location.search.substring(1)) 1352 | ); 1353 | } 1354 | }); 1355 | 1356 | /** 1357 | * @brief Translate and attach events 1358 | * 1359 | * This will translate the page and attach the events to the nodes. 1360 | */ 1361 | document.addEventListener("DOMContentLoaded", () => { 1362 | // translate 1363 | document.title = chrome.i18n.getMessage("settingsTitle"); 1364 | 1365 | const template = document.body.innerHTML; 1366 | 1367 | document.body.innerHTML = template.replace( 1368 | /__MSG_(\w+)__/g, 1369 | (a, b) => chrome.i18n.getMessage(b) 1370 | ); 1371 | 1372 | // Hides the rule selector when you click anywhere on the page 1373 | document.addEventListener("click", () => { 1374 | const dropdown = document.getElementById("dropdown"); 1375 | dropdown.className = ""; 1376 | 1377 | for (const key in dropdown.dataset) { 1378 | delete dropdown.dataset[key]; 1379 | } 1380 | }, true); 1381 | 1382 | // first two preferences 1383 | document.querySelectorAll("input[name='rule']").forEach(input => { 1384 | input.addEventListener("change", changeRule); 1385 | }); 1386 | 1387 | document.querySelectorAll("input[name='private']").forEach(input => { 1388 | input.addEventListener("change", changePolicy); 1389 | }); 1390 | 1391 | // ping preferences 1392 | document.querySelector("input[name='ping']").addEventListener( 1393 | "change", e => { 1394 | preferences.ping = !e.target.checked; 1395 | saveAndAlertBackground(); 1396 | } 1397 | ); 1398 | 1399 | // preferences management buttons 1400 | document.getElementById("r").addEventListener( 1401 | "click", resetPreferencesButton 1402 | ); 1403 | document.getElementById("e").addEventListener( 1404 | "click", exportPreferencesButton 1405 | ); 1406 | document.getElementById("i").addEventListener( 1407 | "click", importPreferencesButton 1408 | ); 1409 | document.getElementById("m").addEventListener( 1410 | "click", mergePreferencesButton 1411 | ); 1412 | 1413 | // alert buttons 1414 | const buttons = document.querySelectorAll("#alertbox button"); 1415 | buttons[0].addEventListener("click", okClick); 1416 | buttons[1].addEventListener("click", closeAlert); 1417 | }); 1418 | 1419 | /** 1420 | * @brief Called when default preferences are loaded 1421 | * 1422 | * Updates the interface to display the new preferences and saves 1423 | * them. 1424 | * 1425 | * @see default-prefs.js 1426 | */ 1427 | function defaultPreferencesLoaded() { 1428 | newPreferences(); 1429 | // remove the injected script 1430 | document.getElementById("default").remove(); 1431 | } 1432 | -------------------------------------------------------------------------------- /theme.css: -------------------------------------------------------------------------------- 1 | html { 2 | --colorBorder: #d1d0d2; 3 | --colorBorderHighlight: #c83838; 4 | --colorAccentBg: #c83838; 5 | --colorAccentBgDark: #b22128; 6 | --colorAccentBgLight: #ffb7b7; 7 | --colorSecondary: #73ab55; 8 | --colorSecondaryLight: #c2d8b7; 9 | --colorFg: #1e1e1e; 10 | --colorBg: #fafafa; 11 | --colorBgAlpha: rgba(255, 255, 255, 0.5); 12 | --colorBgLightIntense: #fff; 13 | --colorBgIntense: #fff; 14 | --colorBgDark: #dedede; 15 | --colorBgDarker: rgba(0, 0, 0, 0.3); 16 | --colorHighlightBg: #c83838; 17 | --colorHighlightBgDark: #dedede; 18 | --colorHighlightBgLight: #ffb7b7; 19 | --colorHighlightFg: #fff; 20 | --colorDisabled: #8e8e8e; 21 | --radius: 0; 22 | --radiusRound: 100px; 23 | --radiusRounded: 2px; 24 | --colorScopeGlobal: #ab74c2; 25 | --colorScopeDomain: #f8b11b; 26 | --colorScopeSite: #18c5a1; 27 | --colorScopePage: #e5d817; 28 | --colorPolicyAllow: #d84a4a; 29 | --colorPolicyRelaxed: #559fe6; 30 | --colorPolicyFiltered: #73ab55; 31 | --colorPolicyBlock: #26272a; 32 | --colorScopeGlobalLight: #d8c1e2; 33 | --colorScopeDomainLight: #fcde9c; 34 | --colorScopeSiteLight: #9ce8d9; 35 | --colorScopePageLight: #f9f3a3; 36 | font-family: sans-serif; 37 | } 38 | --------------------------------------------------------------------------------
__MSG_settingsPingDesc__