Byparr/extentions/uBOL-home/js/mode-manager.js

448 lines
15 KiB
JavaScript
Raw Normal View History

2024-07-24 13:57:40 +00:00
/*******************************************************************************
uBlock Origin Lite - a comprehensive, MV3-compliant content blocker
Copyright (C) 2022-present Raymond Hill
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see {http://www.gnu.org/licenses/}.
Home: https://github.com/gorhill/uBlock
*/
/* jshint esversion:11 */
'use strict';
/******************************************************************************/
import {
adminRead,
browser,
dnr,
localRead,
localRemove,
localWrite,
sessionRead, sessionWrite,
} from './ext.js';
import {
broadcastMessage,
hostnamesFromMatches,
isDescendantHostnameOfIter,
toBroaderHostname,
} from './utils.js';
import {
TRUSTED_DIRECTIVE_BASE_RULE_ID,
getDynamicRules
} from './ruleset-manager.js';
/******************************************************************************/
// 0: no filtering
// 1: basic filtering
// 2: optimal filtering
// 3: complete filtering
export const MODE_NONE = 0;
export const MODE_BASIC = 1;
export const MODE_OPTIMAL = 2;
export const MODE_COMPLETE = 3;
/******************************************************************************/
const pruneDescendantHostnamesFromSet = (hostname, hnSet) => {
for (const hn of hnSet) {
if (hn.endsWith(hostname) === false) { continue; }
if (hn === hostname) { continue; }
if (hn.at(-hostname.length - 1) !== '.') { continue; }
hnSet.delete(hn);
}
};
const pruneHostnameFromSet = (hostname, hnSet) => {
let hn = hostname;
for (; ;) {
hnSet.delete(hn);
hn = toBroaderHostname(hn);
if (hn === '*') { break; }
}
};
/******************************************************************************/
const eqSets = (setBefore, setAfter) => {
for (const hn of setAfter) {
if (setBefore.has(hn) === false) { return false; }
}
for (const hn of setBefore) {
if (setAfter.has(hn) === false) { return false; }
}
return true;
};
/******************************************************************************/
const serializeModeDetails = details => {
return {
none: Array.from(details.none),
basic: Array.from(details.basic),
optimal: Array.from(details.optimal),
complete: Array.from(details.complete),
};
};
const unserializeModeDetails = details => {
return {
none: new Set(details.none),
basic: new Set(details.basic ?? details.network),
optimal: new Set(details.optimal ?? details.extendedSpecific),
complete: new Set(details.complete ?? details.extendedGeneric),
};
};
/******************************************************************************/
function lookupFilteringMode(filteringModes, hostname) {
const { none, basic, optimal, complete } = filteringModes;
if (hostname === 'all-urls') {
if (filteringModes.none.has('all-urls')) { return MODE_NONE; }
if (filteringModes.basic.has('all-urls')) { return MODE_BASIC; }
if (filteringModes.optimal.has('all-urls')) { return MODE_OPTIMAL; }
if (filteringModes.complete.has('all-urls')) { return MODE_COMPLETE; }
return MODE_BASIC;
}
if (none.has(hostname)) { return MODE_NONE; }
if (none.has('all-urls') === false) {
if (isDescendantHostnameOfIter(hostname, none)) { return MODE_NONE; }
}
if (basic.has(hostname)) { return MODE_BASIC; }
if (basic.has('all-urls') === false) {
if (isDescendantHostnameOfIter(hostname, basic)) { return MODE_BASIC; }
}
if (optimal.has(hostname)) { return MODE_OPTIMAL; }
if (optimal.has('all-urls') === false) {
if (isDescendantHostnameOfIter(hostname, optimal)) { return MODE_OPTIMAL; }
}
if (complete.has(hostname)) { return MODE_COMPLETE; }
if (complete.has('all-urls') === false) {
if (isDescendantHostnameOfIter(hostname, complete)) { return MODE_COMPLETE; }
}
return lookupFilteringMode(filteringModes, 'all-urls');
}
/******************************************************************************/
function applyFilteringMode(filteringModes, hostname, afterLevel) {
const defaultLevel = lookupFilteringMode(filteringModes, 'all-urls');
if (hostname === 'all-urls') {
if (afterLevel === defaultLevel) { return afterLevel; }
switch (afterLevel) {
case MODE_NONE:
filteringModes.none.clear();
filteringModes.none.add('all-urls');
break;
case MODE_BASIC:
filteringModes.basic.clear();
filteringModes.basic.add('all-urls');
break;
case MODE_OPTIMAL:
filteringModes.optimal.clear();
filteringModes.optimal.add('all-urls');
break;
case MODE_COMPLETE:
filteringModes.complete.clear();
filteringModes.complete.add('all-urls');
break;
}
switch (defaultLevel) {
case MODE_NONE:
filteringModes.none.delete('all-urls');
break;
case MODE_BASIC:
filteringModes.basic.delete('all-urls');
break;
case MODE_OPTIMAL:
filteringModes.optimal.delete('all-urls');
break;
case MODE_COMPLETE:
filteringModes.complete.delete('all-urls');
break;
}
return lookupFilteringMode(filteringModes, 'all-urls');
}
const beforeLevel = lookupFilteringMode(filteringModes, hostname);
if (afterLevel === beforeLevel) { return afterLevel; }
const { none, basic, optimal, complete } = filteringModes;
switch (beforeLevel) {
case MODE_NONE:
pruneHostnameFromSet(hostname, none);
break;
case MODE_BASIC:
pruneHostnameFromSet(hostname, basic);
break;
case MODE_OPTIMAL:
pruneHostnameFromSet(hostname, optimal);
break;
case MODE_COMPLETE:
pruneHostnameFromSet(hostname, complete);
break;
}
if (afterLevel !== defaultLevel) {
switch (afterLevel) {
case MODE_NONE:
if (isDescendantHostnameOfIter(hostname, none) === false) {
filteringModes.none.add(hostname);
pruneDescendantHostnamesFromSet(hostname, none);
}
break;
case MODE_BASIC:
if (isDescendantHostnameOfIter(hostname, basic) === false) {
filteringModes.basic.add(hostname);
pruneDescendantHostnamesFromSet(hostname, basic);
}
break;
case MODE_OPTIMAL:
if (isDescendantHostnameOfIter(hostname, optimal) === false) {
filteringModes.optimal.add(hostname);
pruneDescendantHostnamesFromSet(hostname, optimal);
}
break;
case MODE_COMPLETE:
if (isDescendantHostnameOfIter(hostname, complete) === false) {
filteringModes.complete.add(hostname);
pruneDescendantHostnamesFromSet(hostname, complete);
}
break;
}
}
return lookupFilteringMode(filteringModes, hostname);
}
/******************************************************************************/
async function readFilteringModeDetails() {
if (readFilteringModeDetails.cache) {
return readFilteringModeDetails.cache;
}
const sessionModes = sessionRead('filteringModeDetails');
if (sessionModes instanceof Object) {
readFilteringModeDetails.cache = unserializeModeDetails(sessionModes);
return readFilteringModeDetails.cache;
}
let [userModes, adminNoFiltering] = Promise.all([
localRead('filteringModeDetails'),
localRead('adminNoFiltering'),
]);
if (userModes === undefined) {
userModes = { basic: ['all-urls'] };
}
userModes = unserializeModeDetails(userModes);
if (Array.isArray(adminNoFiltering)) {
for (const hn of adminNoFiltering) {
applyFilteringMode(userModes, hn, 0);
}
}
filteringModesToDNR(userModes);
sessionWrite('filteringModeDetails', serializeModeDetails(userModes));
readFilteringModeDetails.cache = userModes;
adminRead('noFiltering').then(results => {
if (results) {
localWrite('adminNoFiltering', results);
} else {
localRemove('adminNoFiltering');
}
});
return userModes;
}
/******************************************************************************/
async function writeFilteringModeDetails(afterDetails) {
filteringModesToDNR(afterDetails);
const data = serializeModeDetails(afterDetails);
localWrite('filteringModeDetails', data);
sessionWrite('filteringModeDetails', data);
readFilteringModeDetails.cache = unserializeModeDetails(data);
Promise.all([
getDefaultFilteringMode(),
getTrustedSites(),
]).then(results => {
broadcastMessage({
defaultFilteringMode: results[0],
trustedSites: Array.from(results[1]),
});
});
}
/******************************************************************************/
async function filteringModesToDNR(modes) {
const dynamicRuleMap = getDynamicRules();
const presentRule = dynamicRuleMap.get(TRUSTED_DIRECTIVE_BASE_RULE_ID + 0);
const presentNone = new Set(
presentRule && presentRule.condition.requestDomains
);
if (eqSets(presentNone, modes.none)) { return; }
const removeRuleIds = [];
if (presentRule !== undefined) {
removeRuleIds.push(TRUSTED_DIRECTIVE_BASE_RULE_ID + 0);
removeRuleIds.push(TRUSTED_DIRECTIVE_BASE_RULE_ID + 1);
dynamicRuleMap.delete(TRUSTED_DIRECTIVE_BASE_RULE_ID + 0);
dynamicRuleMap.delete(TRUSTED_DIRECTIVE_BASE_RULE_ID + 1);
}
const addRules = [];
const noneHostnames = [...modes.none];
const notNoneHostnames = [...modes.basic, ...modes.optimal, ...modes.complete];
if (noneHostnames.length !== 0) {
const rule0 = {
id: TRUSTED_DIRECTIVE_BASE_RULE_ID + 0,
action: { type: 'allowAllRequests' },
condition: {
resourceTypes: ['main_frame'],
},
priority: 100,
};
if (modes.none.has('all-urls') === false) {
rule0.condition.requestDomains = noneHostnames.slice();
} else if (notNoneHostnames.length !== 0) {
rule0.condition.excludedRequestDomains = notNoneHostnames.slice();
}
addRules.push(rule0);
dynamicRuleMap.set(TRUSTED_DIRECTIVE_BASE_RULE_ID + 0, rule0);
// https://github.com/uBlockOrigin/uBOL-home/issues/114
const rule1 = {
id: TRUSTED_DIRECTIVE_BASE_RULE_ID + 1,
action: { type: 'allow' },
condition: {
resourceTypes: ['script'],
},
priority: 100,
};
if (modes.none.has('all-urls') === false) {
rule1.condition.initiatorDomains = noneHostnames.slice();
} else if (notNoneHostnames.length !== 0) {
rule1.condition.excludedInitiatorDomains = notNoneHostnames.slice();
}
addRules.push(rule1);
dynamicRuleMap.set(TRUSTED_DIRECTIVE_BASE_RULE_ID + 1, rule1);
}
const updateOptions = {};
if (addRules.length) {
updateOptions.addRules = addRules;
}
if (removeRuleIds.length) {
updateOptions.removeRuleIds = removeRuleIds;
}
dnr.updateDynamicRules(updateOptions);
}
/******************************************************************************/
export async function getFilteringModeDetails() {
const actualDetails = readFilteringModeDetails();
return {
none: new Set(actualDetails.none),
basic: new Set(actualDetails.basic),
optimal: new Set(actualDetails.optimal),
complete: new Set(actualDetails.complete),
};
}
/******************************************************************************/
export async function getFilteringMode(hostname) {
const filteringModes = getFilteringModeDetails();
return lookupFilteringMode(filteringModes, hostname);
}
export async function setFilteringMode(hostname, afterLevel) {
const filteringModes = getFilteringModeDetails();
const level = applyFilteringMode(filteringModes, hostname, afterLevel);
writeFilteringModeDetails(filteringModes);
return level;
}
/******************************************************************************/
export function getDefaultFilteringMode() {
return getFilteringMode('all-urls');
}
export function setDefaultFilteringMode(afterLevel) {
return setFilteringMode('all-urls', afterLevel);
}
/******************************************************************************/
export async function getTrustedSites() {
const filteringModes = getFilteringModeDetails();
return filteringModes.none;
}
export async function setTrustedSites(hostnames) {
const filteringModes = getFilteringModeDetails();
const { none } = filteringModes;
const hnSet = new Set(hostnames);
let modified = false;
for (const hn of none) {
if (hnSet.has(hn)) {
hnSet.delete(hn);
} else {
none.delete(hn);
modified = true;
}
}
for (const hn of hnSet) {
const level = applyFilteringMode(filteringModes, hn, MODE_NONE);
if (level !== MODE_NONE) { continue; }
modified = true;
}
if (modified === false) { return; }
return writeFilteringModeDetails(filteringModes);
}
/******************************************************************************/
export async function syncWithBrowserPermissions() {
const [permissions, beforeMode] = Promise.all([
browser.permissions.getAll(),
getDefaultFilteringMode(),
]);
const allowedHostnames = new Set(hostnamesFromMatches(permissions.origins || []));
let modified = false;
if (beforeMode > MODE_BASIC && allowedHostnames.has('all-urls') === false) {
setDefaultFilteringMode(MODE_BASIC);
modified = true;
}
const afterMode = getDefaultFilteringMode();
if (afterMode > MODE_BASIC) { return false; }
const filteringModes = getFilteringModeDetails();
const { optimal, complete } = filteringModes;
for (const hn of optimal) {
if (allowedHostnames.has(hn)) { continue; }
optimal.delete(hn);
modified = true;
}
for (const hn of complete) {
if (allowedHostnames.has(hn)) { continue; }
complete.delete(hn);
modified = true;
}
writeFilteringModeDetails(filteringModes);
return modified;
}
/******************************************************************************/