├── .gitignore ├── CHANGES.md ├── LICENSE ├── Makefile ├── README.md ├── icon.svg ├── manifest.json ├── screenshot.png └── src ├── bg.js ├── content.js ├── popup.css ├── popup.html ├── popup.js ├── settings.css ├── settings.html ├── settings.js ├── shared.js └── storage.js /.gitignore: -------------------------------------------------------------------------------- 1 | icon-128.png 2 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # 0.10.0 (2025-04-25) 2 | 3 | - update to manifest v3 4 | - client side JSON validation for rules 5 | 6 | 7 | # 0.9.0 (2023-12-09) 8 | 9 | - Performance improvements 10 | 11 | 12 | # 0.8.0 (2023-12-08) 13 | 14 | - Store requests to session storage instead of local storage 15 | - Add experimental support for domain patterns 16 | 17 | 18 | # 0.7.1 (2023-10-08) 19 | 20 | - Fix detection of inline scripts and CSS in firefox 115 21 | 22 | 23 | # 0.7.0 (2023-07-06) 24 | 25 | - fix: do not count main request as "other" 26 | - add dark mode 27 | 28 | 29 | # 0.6.0 (2023-05-18) 30 | 31 | - allow to block cookies 32 | 33 | 34 | # 0.5.0 (2023-05-03) 35 | 36 | - add reset button 37 | - flow-relative CSS 38 | - settings: add labels 39 | - add lang attribute in HTML 40 | 41 | 42 | # 0.4.0 (2023-02-25) 43 | 44 | - allow to have temporary and permanent rules 45 | - fix: add title for settings page 46 | 47 | 48 | # 0.3.1 (2022-11-27) 49 | 50 | - fix: do not clear request counts on navigation inside frame 51 | 52 | 53 | # 0.3.0 (2022-11-27) 54 | 55 | - fix concurrency issue when recording requests 56 | - allow to disable recording of requests 57 | - move edit raw rules to settings page 58 | - add explicit extension ID 59 | 60 | 61 | # 0.2.1 (2022-10-15) 62 | 63 | - fix compatibility with event pages 64 | - fix: add DOCTYPE for popup 65 | 66 | 67 | # 0.2.0 (2022-08-08) 68 | 69 | - allow to edit rules as JSON 70 | - fix inline counts if allowed 71 | - fix outdated help text 72 | 73 | 74 | # 0.1.0 (2022-08-07) 75 | 76 | - close popup on reload 77 | - detect counts of blocked inline code 78 | - display URLs of blocked subframes 79 | - fix layout warnings 80 | 81 | 82 | # 0.0.1 (2022-08-01) 83 | 84 | - fix extension icon 85 | - fix: include subdomains in first-party rule 86 | - fix hostname sorting for first party subdomains 87 | 88 | 89 | # 0.0.0 (2022-07-30) 90 | 91 | - initial release 92 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Tobias Bengfort 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | bundle.zip: manifest.json icon.svg src/* 2 | zip $@ $^ 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | xiMatrix - block requests based on domain and type 2 | 3 | This extension is heavily inspired by 4 | [uMatrix](https://github.com/gorhill/uMatrix), but much simpler. By default, 5 | all web requests as well as inline code are blocked. You can then define rules 6 | to allow only those requests you want. Definitely for advanced users. 7 | 8 | Available for Firefox here: https://addons.mozilla.org/firefox/addon/ximatrix/ 9 | 10 | ![screenshot](screenshot.png) 11 | 12 | ## Differences to uMatrix 13 | 14 | - advantages 15 | - uMatrix is no longer maintained. The last release was in July 2021. 16 | - keyboard navigation 17 | - simpler code 18 | - rules are encoded as JSON 19 | - recording requests can be disabled to improve performance 20 | - it is possible to control inline scripts, styles, and images 21 | - there is a separate column for fonts 22 | - disadvantages / simplifications 23 | - no settings at all 24 | - less flexible rules 25 | - there are no block rules, so it is not possible to overwrite an extensive 26 | allow rule with a more specific block rule 27 | - images and media have been combined into a single column 28 | - I am sure some of the details of the rule inheritance are different. I 29 | tried to produce something that works for me rather than copying every 30 | detail. 31 | - the popup is not updated while it is open. You have to close and open it 32 | again to refresh the data. 33 | - the icon does not show the amount of blocked requests 34 | - blocked images are not replaced by a placeholder 35 | 36 | ## Known issues 37 | 38 | - cached requests are not included in the request counts 39 | -------------------------------------------------------------------------------- /icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "xiMatrix", 4 | "author": "Tobias Bengfort", 5 | "homepage_url": "https://github.com/xi/xiMatrix", 6 | "description": "block requests based on domain and type", 7 | "version": "0.10.0", 8 | "action": { 9 | "default_title": "xiMatrix", 10 | "default_popup": "src/popup.html" 11 | }, 12 | "icons": { 13 | "32": "icon.svg", 14 | "64": "icon.svg" 15 | }, 16 | "background": { 17 | "scripts": ["src/bg.js"], 18 | "type": "module" 19 | }, 20 | "content_scripts": [{ 21 | "js": ["src/content.js"], 22 | "matches": [""], 23 | "run_at": "document_start" 24 | }], 25 | "options_ui": { 26 | "page": "src/settings.html", 27 | "open_in_tab": true 28 | }, 29 | "permissions": [ 30 | "storage", 31 | "tabs", 32 | "webNavigation", 33 | "webRequest", 34 | "webRequestBlocking" 35 | ], 36 | "host_permissions": [ 37 | "" 38 | ], 39 | "browser_specific_settings": { 40 | "gecko": { 41 | "id": "{936cea12-8e61-4929-b589-caece971bbd7}" 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xi/xiMatrix/d170cb7368cc6aca1490181a014020d4855e8f0d/screenshot.png -------------------------------------------------------------------------------- /src/bg.js: -------------------------------------------------------------------------------- 1 | /* global browser */ 2 | 3 | import * as shared from './shared.js'; 4 | import * as storage from './storage.js'; 5 | 6 | var glob = function(s, pattern) { 7 | var p = pattern.split('*'); 8 | return s.startsWith(p[0]) && s.endsWith(p.at(-1)); 9 | }; 10 | 11 | var getHostname = function(url, patterns) { 12 | var u = new URL(url); 13 | 14 | for (var pattern of patterns) { 15 | if (glob(u.hostname, pattern)) { 16 | return pattern; 17 | } 18 | } 19 | 20 | return u.hostname; 21 | }; 22 | 23 | var setRule = async function(context, hostname, type, rule) { 24 | var savedRules = await storage.get('savedRules'); 25 | await storage.change('rules', rules => { 26 | if (hostname === 'first-party') { 27 | context = '*'; 28 | } 29 | if (!rules[context]) { 30 | rules[context] = savedRules[context] || {}; 31 | } 32 | if (!rules[context][hostname]) { 33 | rules[context][hostname] = {}; 34 | } 35 | if (rule) { 36 | rules[context][hostname][type] = rule; 37 | } else { 38 | delete rules[context][hostname][type]; 39 | if (Object.keys(rules[context][hostname]).length === 0) { 40 | delete rules[context][hostname]; 41 | } 42 | if (Object.keys(rules[context]).length === 0 && !savedRules[context]) { 43 | delete rules[context]; 44 | } 45 | } 46 | return rules; 47 | }); 48 | }; 49 | 50 | var getPatterns = async function() { 51 | var savedRules = await storage.get('savedRules'); 52 | return savedRules._patterns || []; 53 | }; 54 | 55 | var getRules = async function(context) { 56 | var [rules, savedRules] = await Promise.all([ 57 | storage.get('rules'), 58 | storage.get('savedRules'), 59 | ]); 60 | var restricted = {}; 61 | restricted['*'] = rules['*'] || savedRules['*'] || {}; 62 | restricted[context] = rules[context] || savedRules[context] || {}; 63 | restricted.dirty = !!rules[context]; 64 | return restricted; 65 | }; 66 | 67 | var pushRequest = async function(tabId, hostname, type) { 68 | var recording = await storage.get('recording'); 69 | if (recording) { 70 | await storage.change('requests', requests => { 71 | if (!requests[tabId]) { 72 | requests[tabId] = {}; 73 | } 74 | if (!requests[tabId][hostname]) { 75 | requests[tabId][hostname] = {}; 76 | } 77 | if (!requests[tabId][hostname][type]) { 78 | requests[tabId][hostname][type] = 0; 79 | } 80 | requests[tabId][hostname][type] += 1; 81 | return requests; 82 | }); 83 | } 84 | }; 85 | 86 | var clearRequests = async function(tabId) { 87 | await storage.change('requests', requests => { 88 | if (requests[tabId]) { 89 | delete requests[tabId]; 90 | } 91 | return requests; 92 | }); 93 | }; 94 | 95 | var getCurrentTab = async function() { 96 | var tabs = await browser.tabs.query({ 97 | active: true, 98 | currentWindow: true, 99 | }); 100 | return tabs[0]; 101 | }; 102 | 103 | browser.runtime.onMessage.addListener(async (msg, sender) => { 104 | if (msg.type === 'get') { 105 | const [tab, patterns] = await Promise.all([ 106 | getCurrentTab(), 107 | getPatterns(), 108 | ]); 109 | const context = getHostname(tab.url, patterns); 110 | const [rules, requests, recording] = await Promise.all([ 111 | getRules(context), 112 | storage.get('requests'), 113 | storage.get('recording'), 114 | ]); 115 | return { 116 | context: context, 117 | rules: rules, 118 | requests: requests[tab.id] || {}, 119 | recording: recording, 120 | }; 121 | } else if (msg.type === 'setRule') { 122 | await setRule( 123 | msg.data.context, 124 | msg.data.hostname, 125 | msg.data.type, 126 | msg.data.value, 127 | ); 128 | return await getRules(msg.data.context); 129 | } else if (msg.type === 'commit') { 130 | let r; 131 | await storage.change('rules', rules => { 132 | r = rules[msg.data]; 133 | delete rules[msg.data]; 134 | return rules; 135 | }); 136 | await storage.change('savedRules', savedRules => { 137 | if (Object.keys(r).length === 0) { 138 | delete savedRules[msg.data]; 139 | } else { 140 | savedRules[msg.data] = r; 141 | } 142 | return savedRules; 143 | }); 144 | } else if (msg.type === 'reset') { 145 | await storage.change('rules', rules => { 146 | delete rules[msg.data]; 147 | return rules; 148 | }); 149 | } else if (msg.type === 'securitypolicyviolation') { 150 | await pushRequest(sender.tab.id, 'inline', msg.data); 151 | } else if (msg.type === 'toggleRecording') { 152 | await storage.change('recording', recording => !recording); 153 | } 154 | }); 155 | 156 | browser.tabs.onRemoved.addListener(clearRequests); 157 | browser.webNavigation.onBeforeNavigate.addListener(async details => { 158 | if (details.frameId === 0) { 159 | await clearRequests(details.tabId); 160 | } 161 | }); 162 | 163 | browser.webRequest.onBeforeSendHeaders.addListener(async details => { 164 | var patterns = await getPatterns(); 165 | var context = getHostname(details.documentUrl || details.url, patterns); 166 | if (details.frameAncestors && details.frameAncestors.length) { 167 | var last = details.frameAncestors.length - 1; 168 | context = getHostname(details.frameAncestors[last].url, patterns); 169 | } 170 | var hostname = getHostname(details.url, patterns); 171 | var type = shared.TYPE_MAP[details.type] || 'other'; 172 | 173 | var promises = [ 174 | getRules(context), 175 | ]; 176 | 177 | if (details.type !== 'main_frame') { 178 | promises.push(pushRequest(details.tabId, hostname, type)); 179 | } 180 | 181 | var isCookie = h => h.name.toLowerCase() === 'cookie'; 182 | if (details.requestHeaders.some(isCookie)) { 183 | promises.push(pushRequest(details.tabId, hostname, 'cookie')); 184 | } 185 | 186 | var [rules, ..._rest] = await Promise.all(promises); 187 | if ( 188 | details.type !== 'main_frame' 189 | && !shared.shouldAllow(rules, context, hostname, type) 190 | ) { 191 | if (details.type === 'sub_frame') { 192 | // this can in turn be blocked by a local CSP 193 | return {redirectUrl: 'data:,' + encodeURIComponent(details.url)}; 194 | } else { 195 | return {cancel: true}; 196 | } 197 | } 198 | 199 | if (shared.shouldAllow(rules, context, hostname, 'cookie')) { 200 | return {requestHeaders: details.requestHeaders}; 201 | } else { 202 | var filtered = details.requestHeaders.filter(h => !isCookie(h)); 203 | return {requestHeaders: filtered}; 204 | } 205 | }, {urls: ['']}, ['blocking', 'requestHeaders']); 206 | 207 | browser.webRequest.onHeadersReceived.addListener(async details => { 208 | var patterns = await getPatterns(); 209 | var context = getHostname(details.url, patterns); 210 | var [rules, recording] = await Promise.all([ 211 | getRules(context), 212 | storage.get('recording'), 213 | ]); 214 | var csp = (type, value) => { 215 | var name = 'Content-Security-Policy'; 216 | if (shared.shouldAllow(rules, context, 'inline', type)) { 217 | if (recording) { 218 | name = 'Content-Security-Policy-Report-Only'; 219 | } else { 220 | return; 221 | } 222 | } 223 | details.responseHeaders.push({ 224 | name: name, 225 | value: value, 226 | }); 227 | }; 228 | 229 | csp('css', "style-src 'self' *"); 230 | csp('script', "script-src 'self' *"); 231 | csp('media', "img-src 'self' *"); 232 | 233 | return {responseHeaders: details.responseHeaders}; 234 | }, { 235 | urls: [''], 236 | types: ['main_frame'], 237 | }, ['blocking', 'responseHeaders']); 238 | -------------------------------------------------------------------------------- /src/content.js: -------------------------------------------------------------------------------- 1 | /* global browser */ 2 | 3 | const TYPE_MAP = { 4 | 'style-src': 'css', 5 | 'style-src-elem': 'css', 6 | 'style-src-attr': 'css', 7 | 'script-src': 'script', 8 | 'script-src-elem': 'script', 9 | 'script-src-attr': 'script', 10 | 'img-src': 'media', 11 | 'media-src': 'media', 12 | }; 13 | 14 | document.addEventListener('securitypolicyviolation', event => { 15 | var type = TYPE_MAP[event.effectiveDirective]; 16 | if (type) { 17 | browser.runtime.sendMessage({ 18 | type: 'securitypolicyviolation', 19 | data: type, 20 | }); 21 | } 22 | }); 23 | -------------------------------------------------------------------------------- /src/popup.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --green-light: #d0f0d0; 3 | --green-dark: #080; 4 | --red-light: #f8d0d0; 5 | --grey: #ccc; 6 | --text-on-light: #000; 7 | --text-on-dark: #fff; 8 | } 9 | 10 | .toolbar { 11 | display: flex; 12 | gap: 0.5em; 13 | } 14 | .toolbar label { 15 | flex-grow: 1; 16 | } 17 | 18 | table { 19 | background: var(--red-light); 20 | color: var(--text-on-light); 21 | border-spacing: 0; 22 | margin-block-end: 0.2em; 23 | } 24 | th, td { 25 | position: relative; 26 | border: 1px solid #fff; 27 | min-inline-size: 2.4em; 28 | line-height: 1.8; 29 | text-align: center; 30 | font-weight: normal; 31 | } 32 | th:first-child { 33 | text-align: right; 34 | } 35 | td.disabled { 36 | background-color: var(--grey); 37 | } 38 | 39 | table input { 40 | appearance: none; 41 | position: absolute; 42 | top: 0; 43 | bottom: 0; 44 | left: 0; 45 | right: 0; 46 | margin: 0; 47 | cursor: pointer; 48 | } 49 | table input:focus-visible { 50 | outline: 2px solid; 51 | z-index: 1; 52 | } 53 | .inherit-allow { 54 | background: var(--green-light); 55 | } 56 | table input:checked { 57 | background: var(--green-dark); 58 | } 59 | table input ~ span { 60 | pointer-events: none; 61 | position: relative; 62 | z-index: 1; 63 | } 64 | table input:checked ~ span { 65 | color: var(--text-on-dark); 66 | } 67 | 68 | @media (prefers-color-scheme: dark) { 69 | :root { 70 | color-scheme: dark; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 |
10 | 14 | 15 | 16 | 17 |
18 |
19 | Help 20 |

In the table above, the columns represent different types of requests. The rows represent domains. Numbers (if recording is enabled) show how many requests of a given type the current tab tries to make to the given domain. Red cells are blocked, green cells are allowed. Light green cells are allowed indirectly, e.g. because they represent a sub-domain of an allowed domain. Grey cells are disabled.

21 |

By default, everything is blocked. You can click on a cell to allow it. You can also click on the domain or type to allow the complete row or column. There are also some special rows:

22 |
    23 |
  • inline: This row is in charge of blocking inline code, e.g. <script>-elements that are directly embedded into the HTML code. Note that some cells are disabled, e.g. there is no such thing as an "inline XHR".
  • 24 |
  • first-party: This row allows you to set global defaults for requests to the same domain as the page itself.
  • 25 |
  • sub-domains: If you allow a domain, all of its sub-domains are allowed along with it.
  • 26 |
27 |
28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/popup.js: -------------------------------------------------------------------------------- 1 | /* global browser */ 2 | 3 | import * as shared from './shared.js'; 4 | 5 | var context; 6 | var requests; 7 | var rules; 8 | 9 | var table = document.querySelector('table'); 10 | var recording = document.querySelector('[name="recording"]'); 11 | var commitButton = document.querySelector('[name="commit"]'); 12 | var resetButton = document.querySelector('[name="reset"]'); 13 | 14 | var sendMessage = async function(type, data) { 15 | return await browser.runtime.sendMessage({type: type, data: data}); 16 | }; 17 | 18 | var getHostnames = function() { 19 | var hostnames = []; 20 | 21 | var addSubdomains = function(h) { 22 | if (['inline', 'first-party', '*'].includes(h)) { 23 | return; 24 | } 25 | hostnames.unshift(h); 26 | var parts = h.split('.'); 27 | while (parts.length > 2) { 28 | parts.shift(); 29 | hostnames.unshift(parts.join('.')); 30 | } 31 | }; 32 | 33 | for (const hostname in rules[context]) { 34 | addSubdomains(hostname); 35 | } 36 | for (const hostname in requests) { 37 | addSubdomains(hostname); 38 | } 39 | 40 | addSubdomains(context); 41 | 42 | var contextRoot = context.split('.').slice(-2).join('.'); 43 | hostnames = hostnames 44 | .map(h => { 45 | var parts = h.split('.'); 46 | var root = parts.slice(-2).join('.'); 47 | var isContext = root === contextRoot ? 0 : 1; 48 | return [isContext, parts.reverse()]; 49 | }) 50 | .sort() 51 | .map(a => a[1].reverse().join('.')); 52 | 53 | return hostnames.filter((value, i) => hostnames.indexOf(value) === i); 54 | }; 55 | 56 | var updateInherit = function(type) { 57 | var selector = 'input'; 58 | if (type !== '*') { 59 | selector += `[data-type="${type}"]`; 60 | } 61 | table.querySelectorAll(selector).forEach(input => { 62 | input.classList.toggle('inherit-allow', shared.shouldAllow( 63 | rules, 64 | context, 65 | input.dataset.hostname, 66 | input.dataset.type, 67 | )); 68 | }); 69 | }; 70 | 71 | var createCheckbox = function(hostname, type) { 72 | var input = document.createElement('input'); 73 | input.type = 'checkbox'; 74 | input.dataset.hostname = hostname; 75 | input.dataset.type = type; 76 | 77 | var c = (hostname === 'first-party') ? '*' : context; 78 | input.checked = (rules[c][hostname] || {})[type]; 79 | 80 | input.onchange = async () => { 81 | var newRules = await sendMessage('setRule', { 82 | context: context, 83 | hostname: hostname, 84 | type: type, 85 | value: input.checked, 86 | }); 87 | rules = newRules; 88 | commitButton.disabled = !rules.dirty; 89 | resetButton.disabled = !rules.dirty; 90 | updateInherit(type); 91 | }; 92 | 93 | return input; 94 | }; 95 | 96 | var createCell = function(tag, hostname, type, text) { 97 | const cell = document.createElement(tag); 98 | cell.append(createCheckbox(hostname, type)); 99 | 100 | const span = document.createElement('span'); 101 | span.textContent = text; 102 | cell.append(span); 103 | 104 | return cell; 105 | }; 106 | 107 | var createHeader = function() { 108 | var tr = document.createElement('tr'); 109 | 110 | var th = document.createElement('th'); 111 | th.textContent = context; 112 | tr.append(th); 113 | 114 | for (const type of shared.TYPES) { 115 | tr.append(createCell('th', '*', type, type)); 116 | } 117 | return tr; 118 | }; 119 | 120 | var createRow = function(hostname) { 121 | var tr = document.createElement('tr'); 122 | tr.append(createCell('th', hostname, '*', hostname)); 123 | for (const type of shared.TYPES) { 124 | const count = (requests[hostname] || {})[type]; 125 | 126 | if (hostname !== 'inline' || ['css', 'script', 'media'].includes(type)) { 127 | tr.append(createCell('td', hostname, type, count)); 128 | } else { 129 | const td = document.createElement('td'); 130 | td.className = 'disabled'; 131 | tr.append(td); 132 | } 133 | } 134 | return tr; 135 | }; 136 | 137 | var loadContext = async function() { 138 | var data = await sendMessage('get'); 139 | context = data.context; 140 | requests = data.requests; 141 | rules = data.rules; 142 | recording.checked = data.recording; 143 | commitButton.disabled = !rules.dirty; 144 | resetButton.disabled = !rules.dirty; 145 | 146 | table.innerHTML = ''; 147 | table.append(createHeader()); 148 | table.append(createRow('inline')); 149 | table.append(createRow('first-party')); 150 | 151 | for (const hostname of getHostnames()) { 152 | table.append(createRow(hostname)); 153 | } 154 | 155 | updateInherit('*'); 156 | }; 157 | 158 | browser.webNavigation.onBeforeNavigate.addListener(window.close); 159 | 160 | document.querySelector('[name="settings"]').addEventListener('click', () => { 161 | browser.runtime.openOptionsPage(); 162 | }); 163 | 164 | document.addEventListener('DOMContentLoaded', async () => { 165 | await loadContext(); 166 | }); 167 | 168 | recording.addEventListener('change', async () => { 169 | await sendMessage('toggleRecording'); 170 | }); 171 | 172 | commitButton.addEventListener('click', async () => { 173 | await sendMessage('commit', context); 174 | commitButton.disabled = true; 175 | resetButton.disabled = true; 176 | }); 177 | 178 | resetButton.addEventListener('click', async () => { 179 | await sendMessage('reset', context); 180 | await loadContext(); 181 | }); 182 | -------------------------------------------------------------------------------- /src/settings.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | html, 6 | body { 7 | margin: 0; 8 | padding: 0; 9 | } 10 | 11 | form { 12 | block-size: 100vb; 13 | display: grid; 14 | grid-template-rows: 1fr min-content; 15 | grid-template-columns: 1fr 1fr; 16 | grid-gap: 0.5em; 17 | padding: 0.5em; 18 | } 19 | 20 | label { 21 | display: flex; 22 | flex-direction: column; 23 | } 24 | 25 | textarea { 26 | block-size: 100%; 27 | resize: none; 28 | } 29 | 30 | button { 31 | padding-block: 0.5em; 32 | padding-inline: 2em; 33 | justify-self: end; 34 | grid-column: 1 / 3; 35 | } 36 | 37 | @media (prefers-color-scheme: dark) { 38 | :root { 39 | color-scheme: dark; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/settings.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | xiMatrix — edit rules 7 | 8 | 9 |
10 | 14 | 18 | 19 |
20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/settings.js: -------------------------------------------------------------------------------- 1 | /* global browser */ 2 | 3 | var form = document.querySelector('form'); 4 | var textarea1 = document.querySelector('textarea.rules'); 5 | var textarea2 = document.querySelector('textarea.savedRules'); 6 | 7 | browser.storage.local.get(['rules', 'savedRules']).then(data => { 8 | var rules = data.rules || {}; 9 | var savedRules = data.savedRules || {}; 10 | textarea1.value = JSON.stringify(rules, null, 2); 11 | textarea2.value = JSON.stringify(savedRules, null, 2); 12 | }); 13 | 14 | form.addEventListener('change', event => { 15 | try { 16 | JSON.parse(event.target.value); 17 | event.target.setCustomValidity(''); 18 | } catch (e) { 19 | event.target.setCustomValidity(e); 20 | event.target.reportValidity(); 21 | } 22 | }); 23 | 24 | form.addEventListener('submit', event => { 25 | event.preventDefault(); 26 | var rules = JSON.parse(textarea1.value); 27 | var savedRules = JSON.parse(textarea2.value); 28 | browser.storage.local.set({ 29 | 'rules': rules, 30 | 'savedRules': savedRules, 31 | }).then(() => { 32 | location.reload(); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/shared.js: -------------------------------------------------------------------------------- 1 | export const TYPES = ['cookie', 'font', 'css', 'media', 'script', 'xhr', 'frame', 'other']; 2 | export const TYPE_MAP = { 3 | 'stylesheet': 'css', 4 | 'font': 'font', 5 | 'image': 'media', 6 | 'imageset': 'media', 7 | 'media': 'media', 8 | 'script': 'script', 9 | 'beacon': 'xhr', 10 | 'xmlhttprequest': 'xhr', 11 | 'websocket': 'xhr', 12 | 'sub_frame': 'frame', 13 | }; 14 | 15 | export var shouldAllow = function(rules, context, hostname, type) { 16 | var hostnames = ['*', hostname]; 17 | var parts = hostname.split('.'); 18 | while (parts.length > 2) { 19 | parts.shift(); 20 | hostnames.push(parts.join('.')); 21 | } 22 | if (context !== '*' && hostnames.some(h => h === context)) { 23 | hostnames.push('first-party'); 24 | } 25 | 26 | return [context, '*'].some(c => { 27 | return rules[c] && hostnames.some(h => { 28 | return rules[c][h] && [type, '*'].some(t => { 29 | return !!rules[c][h][t]; 30 | }); 31 | }); 32 | }); 33 | }; 34 | -------------------------------------------------------------------------------- /src/storage.js: -------------------------------------------------------------------------------- 1 | /* global browser */ 2 | 3 | var STORAGE_DEFAULTS = { 4 | 'rules': {}, 5 | 'savedRules': {}, 6 | 'requests': {}, 7 | 'recording': true, 8 | }; 9 | var STORAGE_AREAS = { 10 | 'rules': browser.storage.local, 11 | 'savedRules': browser.storage.local, 12 | 'requests': browser.storage.session, 13 | 'recording': browser.storage.local, 14 | }; 15 | 16 | var lock = Promise.resolve(); 17 | var cache = {}; 18 | 19 | var _get = async function(key) { 20 | var data = await STORAGE_AREAS[key].get(key); 21 | return data[key] ?? STORAGE_DEFAULTS[key]; 22 | }; 23 | 24 | export var get = function(key) { 25 | if (!cache[key]) { 26 | cache[key] = _get(key); 27 | } 28 | return cache[key]; 29 | }; 30 | 31 | var _change = async function(key, fn) { 32 | var oldValue = await get(key); 33 | var data = {}; 34 | data[key] = fn(oldValue); 35 | delete cache[key]; 36 | await STORAGE_AREAS[key].set(data); 37 | }; 38 | 39 | export var change = async function(key, fn) { 40 | lock = lock.then(() => _change(key, fn)); 41 | await lock; 42 | }; 43 | 44 | var invalidateCache = function(changes) { 45 | for (var key in changes) { 46 | delete cache[key]; 47 | } 48 | }; 49 | 50 | browser.storage.local.onChanged.addListener(invalidateCache); 51 | 52 | // migrations 53 | browser.runtime.onInstalled.addListener(() => { 54 | // 0.8.0: store requests to session storage 55 | lock = lock.then(() => browser.storage.local.remove('requests')); 56 | }); 57 | --------------------------------------------------------------------------------