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 |