/******************************************************************************* 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; } /******************************************************************************/