mirror of https://github.com/gorhill/uBlock
471 lines
16 KiB
JavaScript
471 lines
16 KiB
JavaScript
/*******************************************************************************
|
|
|
|
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
|
|
*/
|
|
|
|
import {
|
|
TRUSTED_DIRECTIVE_BASE_RULE_ID,
|
|
getDynamicRules,
|
|
} from './ruleset-manager.js';
|
|
|
|
import {
|
|
adminRead,
|
|
browser,
|
|
dnr,
|
|
localRead, localRemove, localWrite,
|
|
sessionRead, sessionWrite,
|
|
} from './ext.js';
|
|
|
|
import {
|
|
broadcastMessage,
|
|
hostnamesFromMatches,
|
|
isDescendantHostnameOfIter,
|
|
toBroaderHostname,
|
|
} from './utils.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) => {
|
|
if ( setBefore.size !== setAfter.size ) { return false; }
|
|
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 = await sessionRead('filteringModeDetails');
|
|
if ( sessionModes instanceof Object ) {
|
|
readFilteringModeDetails.cache = unserializeModeDetails(sessionModes);
|
|
return readFilteringModeDetails.cache;
|
|
}
|
|
let [ userModes, adminNoFiltering ] = await 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) {
|
|
await 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 = await getDynamicRules();
|
|
const trustedRule = dynamicRuleMap.get(TRUSTED_DIRECTIVE_BASE_RULE_ID+0);
|
|
const beforeRequestDomainSet = new Set(trustedRule?.condition.requestDomains);
|
|
const beforeExcludedRrequestDomainSet = new Set(trustedRule?.condition.excludedRequestDomains);
|
|
if ( trustedRule !== undefined && beforeRequestDomainSet.size === 0 ) {
|
|
beforeRequestDomainSet.add('all-urls');
|
|
} else {
|
|
beforeExcludedRrequestDomainSet.add('all-urls');
|
|
}
|
|
|
|
const noneHostnames = new Set([ ...modes.none ]);
|
|
const notNoneHostnames = new Set([ ...modes.basic, ...modes.optimal, ...modes.complete ]);
|
|
let afterRequestDomainSet = new Set();
|
|
let afterExcludedRequestDomainSet = new Set();
|
|
if ( noneHostnames.has('all-urls') ) {
|
|
afterRequestDomainSet = new Set([ 'all-urls' ]);
|
|
afterExcludedRequestDomainSet = notNoneHostnames;
|
|
} else {
|
|
afterRequestDomainSet = noneHostnames;
|
|
afterExcludedRequestDomainSet = new Set([ 'all-urls' ]);
|
|
}
|
|
|
|
if ( eqSets(beforeRequestDomainSet, afterRequestDomainSet) ) {
|
|
if ( eqSets(beforeExcludedRrequestDomainSet, afterExcludedRequestDomainSet) ) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
const removeRuleIds = [
|
|
TRUSTED_DIRECTIVE_BASE_RULE_ID+0,
|
|
TRUSTED_DIRECTIVE_BASE_RULE_ID+1,
|
|
];
|
|
dynamicRuleMap.delete(TRUSTED_DIRECTIVE_BASE_RULE_ID+0);
|
|
dynamicRuleMap.delete(TRUSTED_DIRECTIVE_BASE_RULE_ID+1);
|
|
|
|
const allowEverywhere = afterRequestDomainSet.delete('all-urls');
|
|
afterExcludedRequestDomainSet.delete('all-urls');
|
|
|
|
const addRules = [];
|
|
if (
|
|
allowEverywhere ||
|
|
afterRequestDomainSet.size !== 0 ||
|
|
afterExcludedRequestDomainSet.size !== 0
|
|
) {
|
|
const rule0 = {
|
|
id: TRUSTED_DIRECTIVE_BASE_RULE_ID+0,
|
|
action: { type: 'allowAllRequests' },
|
|
condition: {
|
|
resourceTypes: [ 'main_frame' ],
|
|
},
|
|
priority: 100,
|
|
};
|
|
if ( afterRequestDomainSet.size !== 0 ) {
|
|
rule0.condition.requestDomains = Array.from(afterRequestDomainSet);
|
|
} else if ( afterExcludedRequestDomainSet.size !== 0 ) {
|
|
rule0.condition.excludedRequestDomains = Array.from(afterExcludedRequestDomainSet);
|
|
}
|
|
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 ( rule0.condition.requestDomains ) {
|
|
rule1.condition.initiatorDomains = rule0.condition.requestDomains.slice();
|
|
} else if ( rule0.condition.excludedRequestDomains ) {
|
|
rule1.condition.excludedInitiatorDomains = rule0.condition.excludedRequestDomains.slice();
|
|
}
|
|
addRules.push(rule1);
|
|
dynamicRuleMap.set(TRUSTED_DIRECTIVE_BASE_RULE_ID+1, rule1);
|
|
}
|
|
|
|
const updateOptions = { removeRuleIds };
|
|
if ( addRules.length ) {
|
|
updateOptions.addRules = addRules;
|
|
}
|
|
await dnr.updateDynamicRules(updateOptions);
|
|
}
|
|
|
|
/******************************************************************************/
|
|
|
|
export async function getFilteringModeDetails() {
|
|
const actualDetails = await 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 = await getFilteringModeDetails();
|
|
return lookupFilteringMode(filteringModes, hostname);
|
|
}
|
|
|
|
export async function setFilteringMode(hostname, afterLevel) {
|
|
const filteringModes = await getFilteringModeDetails();
|
|
const level = applyFilteringMode(filteringModes, hostname, afterLevel);
|
|
await 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 = await getFilteringModeDetails();
|
|
return filteringModes.none;
|
|
}
|
|
|
|
export async function setTrustedSites(hostnames) {
|
|
const filteringModes = await getFilteringModeDetails();
|
|
const { none } = filteringModes;
|
|
const hnSet = new Set(hostnames);
|
|
let modified = false;
|
|
// Set default mode to Basic when removing No-filtering as default mode
|
|
if ( none.has('all-urls') && hnSet.has('all-urls') === false ) {
|
|
applyFilteringMode(filteringModes, 'all-urls', MODE_BASIC);
|
|
modified = true;
|
|
}
|
|
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 ] = await 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 ) {
|
|
await setDefaultFilteringMode(MODE_BASIC);
|
|
modified = true;
|
|
}
|
|
const afterMode = await getDefaultFilteringMode();
|
|
if ( afterMode > MODE_BASIC ) { return false; }
|
|
const filteringModes = await 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;
|
|
}
|
|
await writeFilteringModeDetails(filteringModes);
|
|
return modified;
|
|
}
|
|
|
|
/******************************************************************************/
|