Rebase to xiMatrix 0.9.0

このコミットが含まれているのは:
Wrongthink 2023-12-11 23:09:57 -05:00
コミット a08923eea4
12個のファイルの変更327行の追加262行の削除

ファイルの表示

@ -19,7 +19,7 @@ He has clearly stated in his project that it is a personal tool and will not lik
- Open to being extended
- Separate image and media columns
- Reload page button in-panel
- Dark theme!
- ~~Dark theme!~~ *(Mainlined, mostly)*
- Dialog clarifications
- More to come

ファイルの表示

@ -2,9 +2,9 @@
"manifest_version": 2,
"name": "paraMatrix",
"author": "Wrongthink",
"homepage_url": "https://gitler.moe/Wrongthink/paraMatrix",
"homepage_url": "https://gitler.moe/wrongthink/paramatrix",
"description": "block requests based on domain and type",
"version": "🐢.🐎",
"version": "🐢.🦍",
"browser_action": {
"default_title": "paraMatrix",
"default_popup": "src/popup.html",
@ -14,8 +14,9 @@
"48": "icon.svg"
},
"background": {
"scripts": ["src/shared.js", "src/bg.js"],
"persistent": false
"scripts": ["src/bg.js"],
"persistent": false,
"type": "module"
},
"content_scripts": [{
"js": ["src/content.js"],

334
src/bg.js
ファイルの表示

@ -1,101 +1,90 @@
/* global browser shared */
/* global browser */
var lock = Promise.resolve();
import * as shared from './shared.js';
import * as storage from './storage.js';
var STORAGE_DEFAULTS = {
'rules': {},
'savedRules': {},
'requests': {},
'recording': true,
var glob = function(s, pattern) {
var p = pattern.split('*');
return s.startsWith(p[0]) && s.endsWith(p.at(-1));
};
var getHostname = function(url) {
var getHostname = function(url, patterns) {
var u = new URL(url);
for (var pattern of patterns) {
if (glob(u.hostname, pattern)) {
return pattern;
}
}
return u.hostname;
};
var storageGet = function(key) {
return browser.storage.local.get(key).then(data => {
return data[key] ?? STORAGE_DEFAULTS[key];
});
};
var _storageChange = function(key, fn) {
return storageGet(key).then(oldValue => {
var data = {};
data[key] = fn(oldValue);
return browser.storage.local.set(data);
});
};
var storageChange = function(key, fn) {
lock = lock.then(() => _storageChange(key, fn));
return lock;
};
var setRule = function(context, hostname, type, rule) {
return storageGet('savedRules').then(savedRules => {
return storageChange('rules', rules => {
if (hostname === 'first-party') {
context = '*';
}
if (!rules[context]) {
rules[context] = savedRules[context] || {};
}
if (!rules[context][hostname]) {
rules[context][hostname] = {};
}
if (rule) {
rules[context][hostname][type] = rule;
} else {
delete rules[context][hostname][type];
if (Object.keys(rules[context][hostname]).length === 0) {
delete rules[context][hostname];
}
if (Object.keys(rules[context]).length === 0 && !savedRules[context]) {
delete rules[context];
}
}
return rules;
});
});
};
var getRules = function(context) {
return Promise.all([
storageGet('rules'),
storageGet('savedRules'),
]).then(([rules, savedRules]) => {
var restricted = {};
restricted['*'] = rules['*'] || savedRules['*'] || {};
restricted[context] = rules[context] || savedRules[context] || {};
restricted.dirty = !!rules[context];
return restricted;
});
};
var pushRequest = function(tabId, hostname, type) {
return storageGet('recording').then(recording => {
if (recording) {
return storageChange('requests', requests => {
if (!requests[tabId]) {
requests[tabId] = {};
}
if (!requests[tabId][hostname]) {
requests[tabId][hostname] = {};
}
if (!requests[tabId][hostname][type]) {
requests[tabId][hostname][type] = 0;
}
requests[tabId][hostname][type] += 1;
return requests;
});
var setRule = async function(context, hostname, type, rule) {
var savedRules = await storage.get('savedRules');
await storage.change('rules', rules => {
if (hostname === 'first-party') {
context = '*';
}
if (!rules[context]) {
rules[context] = savedRules[context] || {};
}
if (!rules[context][hostname]) {
rules[context][hostname] = {};
}
if (rule) {
rules[context][hostname][type] = rule;
} else {
delete rules[context][hostname][type];
if (Object.keys(rules[context][hostname]).length === 0) {
delete rules[context][hostname];
}
if (Object.keys(rules[context]).length === 0 && !savedRules[context]) {
delete rules[context];
}
}
return rules;
});
};
var clearRequests = function(tabId) {
return storageChange('requests', requests => {
var getPatterns = async function() {
var savedRules = await storage.get('savedRules');
return savedRules._patterns || [];
};
var getRules = async function(context) {
var [rules, savedRules] = await Promise.all([
storage.get('rules'),
storage.get('savedRules'),
]);
var restricted = {};
restricted['*'] = rules['*'] || savedRules['*'] || {};
restricted[context] = rules[context] || savedRules[context] || {};
restricted.dirty = !!rules[context];
return restricted;
};
var pushRequest = async function(tabId, hostname, type) {
var recording = await storage.get('recording');
if (recording) {
await storage.change('requests', requests => {
if (!requests[tabId]) {
requests[tabId] = {};
}
if (!requests[tabId][hostname]) {
requests[tabId][hostname] = {};
}
if (!requests[tabId][hostname][type]) {
requests[tabId][hostname][type] = 0;
}
requests[tabId][hostname][type] += 1;
return requests;
});
}
};
var clearRequests = async function(tabId) {
await storage.change('requests', requests => {
if (requests[tabId]) {
delete requests[tabId];
}
@ -103,138 +92,145 @@ var clearRequests = function(tabId) {
});
};
var getCurrentTab = function() {
return browser.tabs.query({
var getCurrentTab = async function() {
var tabs = await browser.tabs.query({
active: true,
currentWindow: true,
}).then(tabs => tabs[0]);
});
return tabs[0];
};
browser.runtime.onMessage.addListener((msg, sender) => {
browser.runtime.onMessage.addListener(async (msg, sender) => {
if (msg.type === 'get') {
return getCurrentTab().then(tab => {
var context = getHostname(tab.url);
return Promise.all([
getRules(context),
storageGet('requests'),
storageGet('recording'),
]).then(([rules, requests, recording]) => {
return {
context: context,
rules: rules,
requests: requests[tab.id] || {},
recording: recording,
};
});
});
const [tab, patterns] = await Promise.all([
getCurrentTab(),
getPatterns(),
]);
const context = getHostname(tab.url, patterns);
const [rules, requests, recording] = await Promise.all([
getRules(context),
storage.get('requests'),
storage.get('recording'),
]);
return {
context: context,
rules: rules,
requests: requests[tab.id] || {},
recording: recording,
};
} else if (msg.type === 'setRule') {
return setRule(
await setRule(
msg.data.context,
msg.data.hostname,
msg.data.type,
msg.data.value,
).then(() => getRules(msg.data.context));
);
return await getRules(msg.data.context);
} else if (msg.type === 'commit') {
var r;
return storageChange('rules', rules => {
let r;
await storage.change('rules', rules => {
r = rules[msg.data];
delete rules[msg.data];
return rules;
}).then(() => storageChange('savedRules', savedRules => {
});
await storage.change('savedRules', savedRules => {
if (Object.keys(r).length === 0) {
delete savedRules[msg.data];
} else {
savedRules[msg.data] = r;
}
return savedRules;
}));
});
} else if (msg.type === 'reset') {
return storageChange('rules', rules => {
await storage.change('rules', rules => {
delete rules[msg.data];
return rules;
});
} else if (msg.type === 'securitypolicyviolation') {
return pushRequest(sender.tab.id, 'inline', msg.data);
await pushRequest(sender.tab.id, 'inline', msg.data);
} else if (msg.type === 'toggleRecording') {
return storageChange('recording', recording => !recording);
await storage.change('recording', recording => !recording);
}
});
browser.tabs.onRemoved.addListener(clearRequests);
browser.webNavigation.onBeforeNavigate.addListener(details => {
browser.webNavigation.onBeforeNavigate.addListener(async details => {
if (details.frameId === 0) {
return clearRequests(details.tabId);
await clearRequests(details.tabId);
}
});
browser.webRequest.onBeforeSendHeaders.addListener(details => {
var context = getHostname(details.documentUrl || details.url);
if (details.frameAncestors.length) {
browser.webRequest.onBeforeSendHeaders.addListener(async details => {
var patterns = await getPatterns();
var context = getHostname(details.documentUrl || details.url, patterns);
if (details.frameAncestors && details.frameAncestors.length) {
var last = details.frameAncestors.length - 1;
context = getHostname(details.frameAncestors[last].url);
context = getHostname(details.frameAncestors[last].url, patterns);
}
var hostname = getHostname(details.url);
var hostname = getHostname(details.url, patterns);
var type = shared.TYPE_MAP[details.type] || 'other';
let isCookie = h => h.name.toLowerCase() === 'cookie';
var cookiePromise = Promise.resolve();
if (details.requestHeaders.some(isCookie)) {
cookiePromise = pushRequest(details.tabId, hostname, 'cookie');
var promises = [
getRules(context),
];
if (details.type !== 'main_frame') {
promises.push(pushRequest(details.tabId, hostname, type));
}
return Promise.all([
pushRequest(details.tabId, hostname, type),
cookiePromise,
getRules(context),
]).then(([_, _2, rules]) => {
if (
details.type !== 'main_frame'
&& !shared.shouldAllow(rules, context, hostname, type)
) {
if (details.type === 'sub_frame') {
// this can in turn be blocked by a local CSP
return {redirectUrl: 'data:,' + encodeURIComponent(details.url)};
} else {
return {cancel: true};
}
}
var isCookie = h => h.name.toLowerCase() === 'cookie';
if (details.requestHeaders.some(isCookie)) {
promises.push(pushRequest(details.tabId, hostname, 'cookie'));
}
if (shared.shouldAllow(rules, context, hostname, 'cookie')) {
return {requestHeaders: details.requestHeaders};
var [rules, ..._rest] = await Promise.all(promises);
if (
details.type !== 'main_frame'
&& !shared.shouldAllow(rules, context, hostname, type)
) {
if (details.type === 'sub_frame') {
// this can in turn be blocked by a local CSP
return {redirectUrl: 'data:,' + encodeURIComponent(details.url)};
} else {
var filtered = details.requestHeaders.filter(h => !isCookie(h));
return {requestHeaders: filtered};
return {cancel: true};
}
});
}
if (shared.shouldAllow(rules, context, hostname, 'cookie')) {
return {requestHeaders: details.requestHeaders};
} else {
var filtered = details.requestHeaders.filter(h => !isCookie(h));
return {requestHeaders: filtered};
}
}, {urls: ['<all_urls>']}, ['blocking', 'requestHeaders']);
browser.webRequest.onHeadersReceived.addListener(details => {
var context = getHostname(details.url);
return Promise.all([
browser.webRequest.onHeadersReceived.addListener(async details => {
var patterns = await getPatterns();
var context = getHostname(details.url, patterns);
var [rules, recording] = await Promise.all([
getRules(context),
storageGet('recording'),
]).then(([rules, recording]) => {
var csp = (type, value) => {
var name = 'Content-Security-Policy';
if (shared.shouldAllow(rules, context, 'inline', type)) {
if (recording) {
name = 'Content-Security-Policy-Report-Only';
} else {
return;
}
storage.get('recording'),
]);
var csp = (type, value) => {
var name = 'Content-Security-Policy';
if (shared.shouldAllow(rules, context, 'inline', type)) {
if (recording) {
name = 'Content-Security-Policy-Report-Only';
} else {
return;
}
details.responseHeaders.push({
name: name,
value: value,
});
};
}
details.responseHeaders.push({
name: name,
value: value,
});
};
csp('css', "style-src 'self' *");
csp('script', "script-src 'self' *");
csp('media', "img-src 'self' *");
csp('css', "style-src 'self' *");
csp('script', "script-src 'self' *");
csp('media', "img-src 'self' *");
return {responseHeaders: details.responseHeaders};
});
return {responseHeaders: details.responseHeaders};
}, {
urls: ['<all_urls>'],
types: ['main_frame'],

ファイルの表示

@ -2,12 +2,17 @@
const TYPE_MAP = {
'style-src': 'css',
'style-src-elem': 'css',
'style-src-attr': 'css',
'script-src': 'script',
'script-src-elem': 'script',
'script-src-attr': 'script',
'img-src': 'media',
'media-src': 'media',
};
document.addEventListener('securitypolicyviolation', event => {
var type = TYPE_MAP[event.violatedDirective];
var type = TYPE_MAP[event.effectiveDirective];
if (type) {
browser.runtime.sendMessage({
type: 'securitypolicyviolation',

ファイルの表示

@ -1,10 +1,10 @@
:root {
--blue-light: #5e69f0;
--blue-dark: #2425ac;
--orange-light: #3f372f;
--dark-grey: #1b1b1b;
--text-light: #838383;
--text-dark: #fff;
--blue-light: #5e69f0;
--blue-dark: #2425ac;
--orange-light: #3f372f;
--dark-grey: #1b1b1b;
--text-light: #838383;
--text-dark: #fff;
}
.toolbar {
@ -17,13 +17,14 @@
table {
background: var(--orange-light);
color: var(--text-on-light);
border-spacing: 0;
margin-block-end: 0.2em;
}
th, td {
position: relative;
border: 1px solid #1f2042;
min-inline-size: 3.4em;
min-inline-size: 2.4em;
line-height: 1.8;
text-align: center;
font-weight: normal;
@ -52,9 +53,6 @@ table input:focus-visible {
.inherit-allow {
background: var(--blue-light);
}
.inherit-allow ~ span {
color: var(--text-dark);
}
table input:checked {
background: var(--blue-dark);
}
@ -62,8 +60,13 @@ table input ~ span {
pointer-events: none;
position: relative;
z-index: 1;
color: var(--text-light);
}
table input:checked ~ span {
color: var(--text-dark);
color: var(--text-on-dark);
}
@media (prefers-color-scheme: dark) {
:root {
color-scheme: dark;
}
}

ファイルの表示

@ -4,7 +4,7 @@
<meta charset="utf-8">
<link rel="stylesheet" href="popup.css">
</head>
<body text="#d9d9d9" bgcolor="#1f2042">
<body>
<table></table>
<div class="toolbar">
<label>
@ -18,15 +18,14 @@
</div>
<details>
<summary>Help</summary>
<p>Columns represent different types of requests. 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. Grey cells are blocked, blue cells are allowed. Light blue cells inherit rules indirectly because they represent a sub-domain of an allowed domain. Black cells are disabled.</p>
<p>Everything is blocked by default. Click on a cell to allow it, then click reload page to load those assets. Click commit to save the configuration for the current site. Click on the domain or type to allow an entire row or column. Master rows are available for:</p>
<ul>
<li><strong>inline</strong>: Controls <code>&lt;script&gt;</code>-elements that are directly embedded in the HTML code. Categories that cannot be inline are disabled.</li>
<li><strong>first-party</strong>: Sets global defaults for requests to the same domain as the page itself.</li>
<li><strong>sub-domains</strong>: If a domain is allowed, all of its subdomains inhereit that rule.</li>
</ul>
<p>Columns represent different types of requests. 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. Grey cells are blocked, blue cells are allowed. Light blue cells inherit rules indirectly because they represent a sub-domain of an allowed domain. Black cells are disabled.</p>
<p>Everything is blocked by default. Click on a cell to allow it, then click reload page to load those assets. Click commit to save the configuration for the current site. Click on the domain or type to allow an entire row or column. Master rows are available for:</p>
<ul>
<li><strong>inline</strong>: Controls <code>&lt;script&gt;</code>-elements that are directly embedded in the HTML code. Categories that cannot be inline are disabled.</li>
<li><strong>first-party</strong>: Sets global defaults for requests to the same domain as the page itself.</li>
<li><strong>sub-domains</strong>: If a domain is allowed, all of its subdomains inhereit that rule.</li>
</ul>
</details>
<script src="shared.js"></script>
<script src="popup.js"></script>
<script type="module" src="popup.js"></script>
</body>
</html>

ファイルの表示

@ -1,17 +1,19 @@
/* global browser shared */
/* global browser */
import * as shared from './shared.js';
var context;
var requests;
var rules;
var table = document.querySelector('table');
var recording = document.querySelector('[name="recording"]')
var recording = document.querySelector('[name="recording"]');
var commitButton = document.querySelector('[name="commit"]');
var resetButton = document.querySelector('[name="reset"]');
var reloadButton = document.querySelector('[name="reload"]');
var sendMessage = function(type, data) {
return browser.runtime.sendMessage({type: type, data: data});
var sendMessage = async function(type, data) {
return await browser.runtime.sendMessage({type: type, data: data});
};
var getHostnames = function() {
@ -76,19 +78,18 @@ var createCheckbox = function(hostname, type) {
var c = (hostname === 'first-party') ? '*' : context;
input.checked = (rules[c][hostname] || {})[type];
input.onchange = () => {
sendMessage('setRule', {
input.onchange = async () => {
var newRules = await sendMessage('setRule', {
context: context,
hostname: hostname,
type: type,
value: input.checked,
}).then(newRules => {
rules = newRules;
commitButton.disabled = !rules.dirty;
resetButton.disabled = !rules.dirty;
reloadButton.disabled = !rules.dirty;
updateInherit(type);
});
rules = newRules;
commitButton.disabled = !rules.dirty;
resetButton.disabled = !rules.dirty;
reloadButton.disabled = !rules.dirty;
updateInherit(type);
};
return input;
@ -135,54 +136,53 @@ var createRow = function(hostname) {
return tr;
};
var loadContext = function() {
return sendMessage('get').then(data => {
context = data.context;
requests = data.requests;
rules = data.rules;
recording.checked = data.recording;
commitButton.disabled = !rules.dirty;
resetButton.disabled = !rules.dirty;
var loadContext = async function() {
var data = await sendMessage('get');
context = data.context;
requests = data.requests;
rules = data.rules;
recording.checked = data.recording;
commitButton.disabled = !rules.dirty;
resetButton.disabled = !rules.dirty;
table.innerHTML = '';
table.append(createHeader());
table.append(createRow('inline'));
table.append(createRow('first-party'));
table.innerHTML = '';
table.append(createHeader());
table.append(createRow('inline'));
table.append(createRow('first-party'));
for (const hostname of getHostnames()) {
table.append(createRow(hostname));
}
for (const hostname of getHostnames()) {
table.append(createRow(hostname));
}
updateInherit('*');
});
updateInherit('*');
};
browser.webNavigation.onBeforeNavigate.addListener(window.close);
document.querySelector('[name="settings"]').addEventListener('click', event => {
document.querySelector('[name="settings"]').addEventListener('click', () => {
browser.runtime.openOptionsPage();
});
document.addEventListener('DOMContentLoaded', () => {
loadContext();
document.addEventListener('DOMContentLoaded', async () => {
await loadContext();
});
recording.addEventListener('change', event => {
sendMessage('toggleRecording');
recording.addEventListener('change', async () => {
await sendMessage('toggleRecording');
});
reloadButton.addEventListener('click', event => {
browser.tabs.reload({bypassCache: true});
});
commitButton.addEventListener('click', event => {
sendMessage('commit', context).then(() => {
commitButton.disabled = true;
resetButton.disabled = true;
reloadButton.disabled= true;
});
commitButton.addEventListener('click', async () => {
await sendMessage('commit', context);
commitButton.disabled = true;
resetButton.disabled= true;
reloadButton.disabled = true;
});
resetButton.addEventListener('click', event => {
sendMessage('reset', context).then(loadContext);
resetButton.addEventListener('click', async () => {
await sendMessage('reset', context);
await loadContext();
});

ファイルの表示

@ -33,3 +33,9 @@ button {
justify-self: end;
grid-column: 1 / 3;
}
@media (prefers-color-scheme: dark) {
:root {
color-scheme: dark;
}
}

ファイルの表示

@ -5,19 +5,18 @@
<link rel="stylesheet" href="settings.css">
<title>paraMatrix &mdash; edit rules</title>
</head>
<body text="#d9d9d9" bgcolor="#1f2042">
<body>
<form>
<label>
Temporary rules
<textarea class="rules" style="background-color:#868686"></textarea>
<textarea class="rules"></textarea>
</label>
<label>
Commited rules
<textarea class="savedRules" style="background-color:#868686"></textarea>
<textarea class="savedRules"></textarea>
</label>
<button>Save</button>
</form>
<script src="shared.js"></script>
<script src="settings.js"></script>
<script type="module" src="settings.js"></script>
</body>
</html>

ファイルの表示

@ -1,22 +1,24 @@
/* global browser */
var form = document.querySelector('form');
var textarea1 = document.querySelector('textarea.rules');
var textarea2 = document.querySelector('textarea.savedRules');
browser.storage.local.get(['rules', 'savedRules']).then(data => {
var rules = data.rules || {};
var savedRules = data.savedRules || {};
textarea1.value = JSON.stringify(rules, null, 2)
textarea2.value = JSON.stringify(savedRules, null, 2)
var rules = data.rules || {};
var savedRules = data.savedRules || {};
textarea1.value = JSON.stringify(rules, null, 2);
textarea2.value = JSON.stringify(savedRules, null, 2);
});
form.addEventListener('submit', event => {
event.preventDefault();
var rules = JSON.parse(textarea1.value);
var savedRules = JSON.parse(textarea2.value);
browser.storage.local.set({
'rules': rules,
'savedRules': savedRules,
}).then(() => {
location.reload();
});
event.preventDefault();
var rules = JSON.parse(textarea1.value);
var savedRules = JSON.parse(textarea2.value);
browser.storage.local.set({
'rules': rules,
'savedRules': savedRules,
}).then(() => {
location.reload();
});
});

ファイルの表示

@ -1,7 +1,5 @@
var shared = {};
shared.TYPES = ['cookie', 'font', 'css', 'image', 'media', 'script', 'xhr', 'frame', 'other'];
shared.TYPE_MAP = {
export const TYPES = ['cookie', 'font', 'css', 'image', 'media', 'script', 'xhr', 'frame', 'other'];
export const TYPE_MAP = {
'stylesheet': 'css',
'font': 'font',
'image': 'image',
@ -14,7 +12,7 @@ shared.TYPE_MAP = {
'sub_frame': 'frame',
};
shared.shouldAllow = function(rules, context, hostname, type) {
export var shouldAllow = function(rules, context, hostname, type) {
var hostnames = ['*', hostname];
var parts = hostname.split('.');
while (parts.length > 2) {

56
src/storage.js ノーマルファイル
ファイルの表示

@ -0,0 +1,56 @@
/* global browser */
var STORAGE_DEFAULTS = {
'rules': {},
'savedRules': {},
'requests': {},
'recording': true,
};
var STORAGE_AREAS = {
'rules': browser.storage.local,
'savedRules': browser.storage.local,
'requests': browser.storage.session,
'recording': browser.storage.local,
};
var lock = Promise.resolve();
var cache = {};
var _get = async function(key) {
var data = await STORAGE_AREAS[key].get(key);
return data[key] ?? STORAGE_DEFAULTS[key];
};
export var get = function(key) {
if (!cache[key]) {
cache[key] = _get(key);
}
return cache[key];
};
var _change = async function(key, fn) {
var oldValue = await get(key);
var data = {};
data[key] = fn(oldValue);
delete cache[key];
await STORAGE_AREAS[key].set(data);
};
export var change = async function(key, fn) {
lock = lock.then(() => _change(key, fn));
await lock;
};
var invalidateCache = function(changes) {
for (var key in changes) {
delete cache[key];
}
};
browser.storage.local.onChanged.addListener(invalidateCache);
// migrations
browser.runtime.onInstalled.addListener(() => {
// 0.8.0: store requests to session storage
lock = lock.then(() => browser.storage.local.remove('requests'));
});