From d19e62a595d6bd0f1e347bc8d0cc5bacd0d6a620 Mon Sep 17 00:00:00 2001 From: Raymond Hill Date: Tue, 13 Jun 2023 11:40:41 -0400 Subject: [PATCH] [mv3] Add support for admin configurations See `managed_storage.json` for available settings. Currently only `noFiltering` setting is availale. `noFiltering` is an array of strings, each being a domain for which no filtering should occur. Related discussion: - https://github.com/uBlockOrigin/uBOL-issues/discussions/35 --- platform/mv3/chromium/manifest.json | 3 + platform/mv3/extension/js/background.js | 91 ++-- platform/mv3/extension/js/ext.js | 49 +- platform/mv3/extension/js/mode-manager.js | 508 +++++++++--------- platform/mv3/extension/js/ruleset-manager.js | 4 - .../mv3/extension/js/scripting-manager.js | 50 +- platform/mv3/extension/js/settings.js | 20 +- platform/mv3/extension/managed_storage.json | 11 + tools/make-mv3.sh | 1 + 9 files changed, 362 insertions(+), 375 deletions(-) create mode 100644 platform/mv3/extension/managed_storage.json diff --git a/platform/mv3/chromium/manifest.json b/platform/mv3/chromium/manifest.json index 81c55d87e..036dc2d24 100644 --- a/platform/mv3/chromium/manifest.json +++ b/platform/mv3/chromium/manifest.json @@ -38,6 +38,9 @@ "storage" ], "short_name": "uBO Lite", + "storage": { + "managed_schema": "managed_storage.json" + }, "version": "1.0", "web_accessible_resources": [] } diff --git a/platform/mv3/extension/js/background.js b/platform/mv3/extension/js/background.js index 5e43347d7..1667ab175 100644 --- a/platform/mv3/extension/js/background.js +++ b/platform/mv3/extension/js/background.js @@ -34,9 +34,7 @@ import { } from './ext.js'; import { - CURRENT_CONFIG_BASE_RULE_ID, getRulesetDetails, - getDynamicRules, defaultRulesetsFromLanguage, enableRulesets, getEnabledRulesetsDetails, @@ -70,6 +68,7 @@ const rulesetConfig = { const UBOL_ORIGIN = runtime.getURL('').replace(/\/$/, ''); let firstRun = false; +let wakeupRun = false; /******************************************************************************/ @@ -83,53 +82,23 @@ async function loadRulesetConfig() { rulesetConfig.version = data.version; rulesetConfig.enabledRulesets = data.enabledRulesets; rulesetConfig.autoReload = data.autoReload; - return false; + wakeupRun = true; + return; } data = await localRead('rulesetConfig'); if ( data ) { rulesetConfig.version = data.version; rulesetConfig.enabledRulesets = data.enabledRulesets; rulesetConfig.autoReload = data.autoReload; - return false; - } - data = await loadRulesetConfig.convertLegacyStorage(); - if ( data ) { - rulesetConfig.version = data.version; - rulesetConfig.enabledRulesets = data.enabledRulesets; - rulesetConfig.autoReload = data.autoReload; - return false; + sessionWrite('rulesetConfig', rulesetConfig); + return; } rulesetConfig.enabledRulesets = await defaultRulesetsFromLanguage(); sessionWrite('rulesetConfig', rulesetConfig); localWrite('rulesetConfig', rulesetConfig); - return true; + firstRun = true; } -// TODO: To remove after next stable release is widespread (2023-06-04) -loadRulesetConfig.convertLegacyStorage = async function() { - const dynamicRuleMap = await getDynamicRules(); - const configRule = dynamicRuleMap.get(CURRENT_CONFIG_BASE_RULE_ID); - if ( configRule === undefined ) { return; } - let rawConfig; - try { - rawConfig = JSON.parse(self.atob(configRule.condition.urlFilter)); - } catch(ex) { - return; - } - if ( rawConfig === undefined ) { return; } - const config = { - version: rawConfig[0], - enabledRulesets: rawConfig[1], - autoReload: rawConfig[2], - }; - localWrite('rulesetConfig', config); - sessionWrite('rulesetConfig', config); - dnr.updateDynamicRules({ - removeRuleIds: [ CURRENT_CONFIG_BASE_RULE_ID ], - }); - return config; -}; - async function saveRulesetConfig() { sessionWrite('rulesetConfig', rulesetConfig); return localWrite('rulesetConfig', rulesetConfig); @@ -153,12 +122,13 @@ function hasOmnipotence() { async function onPermissionsRemoved() { const beforeMode = await getDefaultFilteringMode(); const modified = await syncWithBrowserPermissions(); - if ( modified === false ) { return; } + if ( modified === false ) { return false; } const afterMode = await getDefaultFilteringMode(); if ( beforeMode > 1 && afterMode <= 1 ) { updateDynamicRules(); } registerInjectables(); + return true; } /******************************************************************************/ @@ -180,6 +150,7 @@ function onMessage(request, sender, callback) { }).catch(reason => { console.log(reason); }); + callback(); return; } @@ -273,8 +244,7 @@ function onMessage(request, sender, callback) { } case 'setDefaultFilteringMode': { - getDefaultFilteringMode( - ).then(beforeLevel => + getDefaultFilteringMode().then(beforeLevel => setDefaultFilteringMode(request.level).then(afterLevel => ({ beforeLevel, afterLevel }) ) @@ -298,37 +268,44 @@ function onMessage(request, sender, callback) { /******************************************************************************/ async function start() { - firstRun = await loadRulesetConfig(); - await enableRulesets(rulesetConfig.enabledRulesets); + await loadRulesetConfig(); + + if ( wakeupRun === false ) { + await enableRulesets(rulesetConfig.enabledRulesets); + } // We need to update the regex rules only when ruleset version changes. - const currentVersion = getCurrentVersion(); - if ( currentVersion !== rulesetConfig.version ) { - ubolLog(`Version change: ${rulesetConfig.version} => ${currentVersion}`); - updateDynamicRules().then(( ) => { - rulesetConfig.version = currentVersion; - saveRulesetConfig(); - }); + if ( wakeupRun === false ) { + const currentVersion = getCurrentVersion(); + if ( currentVersion !== rulesetConfig.version ) { + ubolLog(`Version change: ${rulesetConfig.version} => ${currentVersion}`); + updateDynamicRules().then(( ) => { + rulesetConfig.version = currentVersion; + saveRulesetConfig(); + }); + } } // Permissions may have been removed while the extension was disabled - await onPermissionsRemoved(); + const permissionsChanged = await onPermissionsRemoved(); // Unsure whether the browser remembers correctly registered css/scripts // after we quit the browser. For now uBOL will check unconditionally at // launch time whether content css/scripts are properly registered. - registerInjectables(); + if ( wakeupRun === false || permissionsChanged ) { + registerInjectables(); - const enabledRulesets = await dnr.getEnabledRulesets(); - ubolLog(`Enabled rulesets: ${enabledRulesets}`); + const enabledRulesets = await dnr.getEnabledRulesets(); + ubolLog(`Enabled rulesets: ${enabledRulesets}`); - dnr.getAvailableStaticRuleCount().then(count => { - ubolLog(`Available static rule count: ${count}`); - }); + dnr.getAvailableStaticRuleCount().then(count => { + ubolLog(`Available static rule count: ${count}`); + }); + } // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/declarativeNetRequest // Firefox API does not support `dnr.setExtensionActionOptions` - if ( dnr.setExtensionActionOptions ) { + if ( wakeupRun === false && dnr.setExtensionActionOptions ) { dnr.setExtensionActionOptions({ displayActionCountAsBadgeText: true }); } diff --git a/platform/mv3/extension/js/ext.js b/platform/mv3/extension/js/ext.js index a7edca1b3..0a8a2ddca 100644 --- a/platform/mv3/extension/js/ext.js +++ b/platform/mv3/extension/js/ext.js @@ -25,22 +25,22 @@ /******************************************************************************/ -const browser = +export const browser = self.browser instanceof Object && self.browser instanceof Element === false ? self.browser : self.chrome; -const dnr = browser.declarativeNetRequest; -const i18n = browser.i18n; -const runtime = browser.runtime; +export const dnr = browser.declarativeNetRequest; +export const i18n = browser.i18n; +export const runtime = browser.runtime; /******************************************************************************/ // The extension's service worker can be evicted at any time, so when we // send a message, we try a few more times when the message fails to be sent. -function sendMessage(msg) { +export function sendMessage(msg) { return new Promise((resolve, reject) => { let i = 5; const send = ( ) => { @@ -61,37 +61,43 @@ function sendMessage(msg) { /******************************************************************************/ -async function localRead(key) { +export async function localRead(key) { if ( browser.storage instanceof Object === false ) { return; } if ( browser.storage.local instanceof Object === false ) { return; } try { const bin = await browser.storage.local.get(key); if ( bin instanceof Object === false ) { return; } - return bin[key]; + return bin[key] ?? undefined; } catch(ex) { } } -async function localWrite(key, value) { +export async function localWrite(key, value) { if ( browser.storage instanceof Object === false ) { return; } if ( browser.storage.local instanceof Object === false ) { return; } return browser.storage.local.set({ [key]: value }); } +export async function localRemove(key) { + if ( browser.storage instanceof Object === false ) { return; } + if ( browser.storage.local instanceof Object === false ) { return; } + return browser.storage.local.remove(key); +} + /******************************************************************************/ -async function sessionRead(key) { +export async function sessionRead(key) { if ( browser.storage instanceof Object === false ) { return; } if ( browser.storage.session instanceof Object === false ) { return; } try { const bin = await browser.storage.session.get(key); if ( bin instanceof Object === false ) { return; } - return bin[key]; + return bin[key] ?? undefined; } catch(ex) { } } -async function sessionWrite(key, value) { +export async function sessionWrite(key, value) { if ( browser.storage instanceof Object === false ) { return; } if ( browser.storage.session instanceof Object === false ) { return; } return browser.storage.session.set({ [key]: value }); @@ -99,12 +105,15 @@ async function sessionWrite(key, value) { /******************************************************************************/ -export { - browser, - dnr, - i18n, - runtime, - sendMessage, - localRead, localWrite, - sessionRead, sessionWrite, -}; +export async function adminRead(key) { + if ( browser.storage instanceof Object === false ) { return; } + if ( browser.storage.local instanceof Object === false ) { return; } + try { + const bin = await browser.storage.managed.get(key); + if ( bin instanceof Object === false ) { return; } + return bin[key] ?? undefined; + } catch(ex) { + } +} + +/******************************************************************************/ diff --git a/platform/mv3/extension/js/mode-manager.js b/platform/mv3/extension/js/mode-manager.js index f181ea781..b69bfd55e 100644 --- a/platform/mv3/extension/js/mode-manager.js +++ b/platform/mv3/extension/js/mode-manager.js @@ -28,8 +28,9 @@ import { browser, dnr, - localRead, localWrite, + localRead, localWrite, localRemove, sessionRead, sessionWrite, + adminRead, } from './ext.js'; import { @@ -40,12 +41,23 @@ import { import { TRUSTED_DIRECTIVE_BASE_RULE_ID, - BLOCKING_MODES_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; } @@ -55,8 +67,6 @@ const pruneDescendantHostnamesFromSet = (hostname, hnSet) => { } }; -/******************************************************************************/ - const pruneHostnameFromSet = (hostname, hnSet) => { let hn = hostname; for (;;) { @@ -80,317 +90,297 @@ const eqSets = (setBefore, setAfter) => { /******************************************************************************/ -// 0: no blocking -// 1: network -// 2: specific content -// 3: generic content +const serializeModeDetails = details => { + return { + none: Array.from(details.none), + basic: Array.from(details.basic), + optimal: Array.from(details.optimal), + complete: Array.from(details.complete), + }; +}; -async function getActualFilteringModeDetails() { - if ( getActualFilteringModeDetails.cache ) { - return getActualFilteringModeDetails.cache; - } - let details = await sessionRead('filteringModeDetails'); - if ( details === undefined ) { - details = await localRead('filteringModeDetails'); - if ( details === undefined ) { - details = await getActualFilteringModeDetails.convertLegacyStorage(); - if ( details === undefined ) { - details = { - network: [ 'all-urls' ], - }; - } - } - if ( details ) { - sessionWrite('filteringModeDetails', details); - } - } - const out = { +const unserializeModeDetails = details => { + return { none: new Set(details.none), - network: new Set(details.network), - extendedSpecific: new Set(details.extendedSpecific), - extendedGeneric: new Set(details.extendedGeneric), + basic: new Set(details.basic ?? details.network), + optimal: new Set(details.optimal ?? details.extendedSpecific), + complete: new Set(details.complete ?? details.extendedGeneric), }; - getActualFilteringModeDetails.cache = out; - return out; -} - -// TODO: To remove after next stable release is widespread (2023-06-04) -getActualFilteringModeDetails.convertLegacyStorage = async function() { - const dynamicRuleMap = await getDynamicRules(); - const trustedSiteDirectives = (( ) => { - const rule = dynamicRuleMap.get(TRUSTED_DIRECTIVE_BASE_RULE_ID); - return rule ? rule.condition.requestDomains : []; - })(); - const rule = dynamicRuleMap.get(BLOCKING_MODES_RULE_ID); - if ( rule === undefined ) { return; } - dnr.updateDynamicRules({ - removeRuleIds: [ - BLOCKING_MODES_RULE_ID, - ], - }); - const details = { - none: trustedSiteDirectives || [], - network: rule.condition.excludedInitiatorDomains || [], - extendedSpecific: rule.condition.excludedRequestDomains || [], - extendedGeneric: rule.condition.initiatorDomains || [], - }; - sessionWrite('filteringModeDetails', details); - localWrite('filteringModeDetails', details); - return details; }; /******************************************************************************/ -async function getFilteringModeDetails() { - const actualDetails = await getActualFilteringModeDetails(); - return { - none: new Set(actualDetails.none), - network: new Set(actualDetails.network), - extendedSpecific: new Set(actualDetails.extendedSpecific), - extendedGeneric: new Set(actualDetails.extendedGeneric), - }; -} - -/******************************************************************************/ - -async function setFilteringModeDetails(afterDetails) { - const actualDetails = await getActualFilteringModeDetails(); - if ( eqSets(actualDetails.none, afterDetails.none) === false ) { - const dynamicRuleMap = await getDynamicRules(); - const removeRuleIds = []; - if ( dynamicRuleMap.has(TRUSTED_DIRECTIVE_BASE_RULE_ID) ) { - removeRuleIds.push(TRUSTED_DIRECTIVE_BASE_RULE_ID); - dynamicRuleMap.delete(TRUSTED_DIRECTIVE_BASE_RULE_ID); - } - const addRules = []; - if ( afterDetails.none.size !== 0 ) { - const rule = { - id: TRUSTED_DIRECTIVE_BASE_RULE_ID, - action: { type: 'allowAllRequests' }, - condition: { - resourceTypes: [ 'main_frame' ], - }, - priority: 100, - }; - if ( - afterDetails.none.size !== 1 || - afterDetails.none.has('all-urls') === false - ) { - rule.condition.requestDomains = Array.from(afterDetails.none); - } - addRules.push(rule); - dynamicRuleMap.set(TRUSTED_DIRECTIVE_BASE_RULE_ID, rule); - } - if ( addRules.length !== 0 || removeRuleIds.length !== 0 ) { - const updateOptions = {}; - if ( addRules.length ) { - updateOptions.addRules = addRules; - } - if ( removeRuleIds.length ) { - updateOptions.removeRuleIds = removeRuleIds; - } - await dnr.updateDynamicRules(updateOptions); - } - } - const data = { - none: Array.from(afterDetails.none), - network: Array.from(afterDetails.network), - extendedSpecific: Array.from(afterDetails.extendedSpecific), - extendedGeneric: Array.from(afterDetails.extendedGeneric), - }; - sessionWrite('filteringModeDetails', data); - localWrite('filteringModeDetails', data); - getActualFilteringModeDetails.cache = undefined; -} - -/******************************************************************************/ - -async function getFilteringMode(hostname) { - const filteringModes = await getFilteringModeDetails(); - const { - none, - network, - extendedSpecific, - extendedGeneric, - } = filteringModes; - if ( none.has(hostname) ) { return 0; } - if ( none.has('all-urls') === false ) { - if ( isDescendantHostnameOfIter(hostname, none) ) { return 0; } - } - if ( network.has(hostname) ) { return 1; } - if ( network.has('all-urls') === false ) { - if ( isDescendantHostnameOfIter(hostname, network) ) { return 1; } - } - if ( extendedSpecific.has(hostname) ) { return 2; } - if ( extendedSpecific.has('all-urls') === false ) { - if ( isDescendantHostnameOfIter(hostname, extendedSpecific) ) { return 2; } - } - if ( extendedGeneric.has(hostname) ) { return 3; } - if ( extendedGeneric.has('all-urls') === false ) { - if ( isDescendantHostnameOfIter(hostname, extendedGeneric) ) { return 3; } - } - return getDefaultFilteringMode(); -} - -/******************************************************************************/ - -async function setFilteringMode(hostname, afterLevel) { +function lookupFilteringMode(filteringModes, hostname) { + const { none, basic, optimal, complete } = filteringModes; if ( hostname === 'all-urls' ) { - return setDefaultFilteringMode(afterLevel); + 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; } - const [ - beforeLevel, - defaultLevel, - filteringModes - ] = await Promise.all([ - getFilteringMode(hostname), - getDefaultFilteringMode(), - getFilteringModeDetails(), - ]); + 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, - network, - extendedSpecific, - extendedGeneric, - } = filteringModes; + const { none, basic, optimal, complete } = filteringModes; switch ( beforeLevel ) { - case 0: + case MODE_NONE: pruneHostnameFromSet(hostname, none); break; - case 1: - pruneHostnameFromSet(hostname, network); + case MODE_BASIC: + pruneHostnameFromSet(hostname, basic); break; - case 2: - pruneHostnameFromSet(hostname, extendedSpecific); + case MODE_OPTIMAL: + pruneHostnameFromSet(hostname, optimal); break; - case 3: - pruneHostnameFromSet(hostname, extendedGeneric); + case MODE_COMPLETE: + pruneHostnameFromSet(hostname, complete); break; } if ( afterLevel !== defaultLevel ) { switch ( afterLevel ) { - case 0: + case MODE_NONE: if ( isDescendantHostnameOfIter(hostname, none) === false ) { filteringModes.none.add(hostname); pruneDescendantHostnamesFromSet(hostname, none); } break; - case 1: - if ( isDescendantHostnameOfIter(hostname, network) === false ) { - filteringModes.network.add(hostname); - pruneDescendantHostnamesFromSet(hostname, network); + case MODE_BASIC: + if ( isDescendantHostnameOfIter(hostname, basic) === false ) { + filteringModes.basic.add(hostname); + pruneDescendantHostnamesFromSet(hostname, basic); } break; - case 2: - if ( isDescendantHostnameOfIter(hostname, extendedSpecific) === false ) { - filteringModes.extendedSpecific.add(hostname); - pruneDescendantHostnamesFromSet(hostname, extendedSpecific); + case MODE_OPTIMAL: + if ( isDescendantHostnameOfIter(hostname, optimal) === false ) { + filteringModes.optimal.add(hostname); + pruneDescendantHostnamesFromSet(hostname, optimal); } break; - case 3: - if ( isDescendantHostnameOfIter(hostname, extendedGeneric) === false ) { - filteringModes.extendedGeneric.add(hostname); - pruneDescendantHostnamesFromSet(hostname, extendedGeneric); + case MODE_COMPLETE: + if ( isDescendantHostnameOfIter(hostname, complete) === false ) { + filteringModes.complete.add(hostname); + pruneDescendantHostnamesFromSet(hostname, complete); } break; } } - await setFilteringModeDetails(filteringModes); - return getFilteringMode(hostname); + return lookupFilteringMode(filteringModes, hostname); } /******************************************************************************/ -async function getDefaultFilteringMode() { - const filteringModes = await getFilteringModeDetails(); - if ( filteringModes.none.has('all-urls') ) { return 0; } - if ( filteringModes.network.has('all-urls') ) { return 1; } - if ( filteringModes.extendedSpecific.has('all-urls') ) { return 2; } - if ( filteringModes.extendedGeneric.has('all-urls') ) { return 3; } - return 1; -} - -/******************************************************************************/ - -async function setDefaultFilteringMode(afterLevel) { - const [ beforeLevel, filteringModes ] = await Promise.all([ - getDefaultFilteringMode(), - getFilteringModeDetails(), +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 ( afterLevel === beforeLevel ) { return afterLevel; } - switch ( afterLevel ) { - case 0: - filteringModes.none.clear(); - filteringModes.none.add('all-urls'); - break; - case 1: - filteringModes.network.clear(); - filteringModes.network.add('all-urls'); - break; - case 2: - filteringModes.extendedSpecific.clear(); - filteringModes.extendedSpecific.add('all-urls'); - break; - case 3: - filteringModes.extendedGeneric.clear(); - filteringModes.extendedGeneric.add('all-urls'); - break; + if ( userModes === undefined ) { + userModes = { basic: [ 'all-urls' ] }; } - switch ( beforeLevel ) { - case 0: - filteringModes.none.delete('all-urls'); - break; - case 1: - filteringModes.network.delete('all-urls'); - break; - case 2: - filteringModes.extendedSpecific.delete('all-urls'); - break; - case 3: - filteringModes.extendedGeneric.delete('all-urls'); - break; + userModes = unserializeModeDetails(userModes); + if ( Array.isArray(adminNoFiltering) ) { + for ( const hn of adminNoFiltering ) { + applyFilteringMode(userModes, hn, 0); + } } - await setFilteringModeDetails(filteringModes); - return getDefaultFilteringMode(); + 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 syncWithBrowserPermissions() { - const permissions = await browser.permissions.getAll(); +async function writeFilteringModeDetails(afterDetails) { + await filteringModesToDNR(afterDetails); + const data = serializeModeDetails(afterDetails); + localWrite('filteringModeDetails', data); + sessionWrite('filteringModeDetails', data); + readFilteringModeDetails.cache = unserializeModeDetails(data); +} + +/******************************************************************************/ + +async function filteringModesToDNR(modes) { + const dynamicRuleMap = await getDynamicRules(); + const presentRule = dynamicRuleMap.get(TRUSTED_DIRECTIVE_BASE_RULE_ID); + 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); + dynamicRuleMap.delete(TRUSTED_DIRECTIVE_BASE_RULE_ID); + } + const addRules = []; + if ( modes.none.size !== 0 ) { + const rule = { + id: TRUSTED_DIRECTIVE_BASE_RULE_ID, + action: { type: 'allowAllRequests' }, + condition: { + resourceTypes: [ 'main_frame' ], + }, + priority: 100, + }; + if ( + modes.none.size !== 1 || + modes.none.has('all-urls') === false + ) { + rule.condition.requestDomains = Array.from(modes.none); + } + addRules.push(rule); + dynamicRuleMap.set(TRUSTED_DIRECTIVE_BASE_RULE_ID, rule); + } + const updateOptions = {}; + if ( addRules.length ) { + updateOptions.addRules = addRules; + } + if ( removeRuleIds.length ) { + updateOptions.removeRuleIds = removeRuleIds; + } + 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 syncWithBrowserPermissions() { + const [ permissions, beforeMode ] = await Promise.all([ + browser.permissions.getAll(), + getDefaultFilteringMode(), + ]); const allowedHostnames = new Set(hostnamesFromMatches(permissions.origins || [])); - const beforeMode = await getDefaultFilteringMode(); let modified = false; - if ( beforeMode > 1 && allowedHostnames.has('all-urls') === false ) { - await setDefaultFilteringMode(1); + if ( beforeMode > MODE_BASIC && allowedHostnames.has('all-urls') === false ) { + await setDefaultFilteringMode(MODE_BASIC); modified = true; } const afterMode = await getDefaultFilteringMode(); - if ( afterMode > 1 ) { return false; } + if ( afterMode > MODE_BASIC ) { return false; } const filteringModes = await getFilteringModeDetails(); - const { extendedSpecific, extendedGeneric } = filteringModes; - for ( const hn of extendedSpecific ) { + const { optimal, complete } = filteringModes; + for ( const hn of optimal ) { if ( allowedHostnames.has(hn) ) { continue; } - extendedSpecific.delete(hn); + optimal.delete(hn); modified = true; } - for ( const hn of extendedGeneric ) { + for ( const hn of complete ) { if ( allowedHostnames.has(hn) ) { continue; } - extendedGeneric.delete(hn); + complete.delete(hn); modified = true; } - await setFilteringModeDetails(filteringModes); + await writeFilteringModeDetails(filteringModes); return modified; } /******************************************************************************/ - -export { - getFilteringMode, - setFilteringMode, - getDefaultFilteringMode, - setDefaultFilteringMode, - getFilteringModeDetails, - syncWithBrowserPermissions, -}; diff --git a/platform/mv3/extension/js/ruleset-manager.js b/platform/mv3/extension/js/ruleset-manager.js index a98310dbb..fd25320b8 100644 --- a/platform/mv3/extension/js/ruleset-manager.js +++ b/platform/mv3/extension/js/ruleset-manager.js @@ -41,8 +41,6 @@ const REDIRECT_REALM_END = REDIRECT_REALM_START + RULE_REALM_SIZE; const CSP_REALM_START = REDIRECT_REALM_END; const CSP_REALM_END = CSP_REALM_START + RULE_REALM_SIZE; const TRUSTED_DIRECTIVE_BASE_RULE_ID = 8000000; -const BLOCKING_MODES_RULE_ID = TRUSTED_DIRECTIVE_BASE_RULE_ID + 1; -const CURRENT_CONFIG_BASE_RULE_ID = 9000000; /******************************************************************************/ @@ -509,8 +507,6 @@ async function getEnabledRulesetsDetails() { /******************************************************************************/ export { - BLOCKING_MODES_RULE_ID, - CURRENT_CONFIG_BASE_RULE_ID, TRUSTED_DIRECTIVE_BASE_RULE_ID, getRulesetDetails, getDynamicRules, diff --git a/platform/mv3/extension/js/scripting-manager.js b/platform/mv3/extension/js/scripting-manager.js index 2447acafa..1d9d42876 100644 --- a/platform/mv3/extension/js/scripting-manager.js +++ b/platform/mv3/extension/js/scripting-manager.js @@ -110,20 +110,20 @@ function registerGeneric(context, genericDetails) { js.push('/js/scripting/css-generic.js'); - const { none, network, extendedSpecific, extendedGeneric } = filteringModeDetails; + const { none, basic, optimal, complete } = filteringModeDetails; const matches = []; const excludeMatches = []; - if ( extendedGeneric.has('all-urls') ) { + if ( complete.has('all-urls') ) { excludeMatches.push(...ut.matchesFromHostnames(none)); - excludeMatches.push(...ut.matchesFromHostnames(network)); - excludeMatches.push(...ut.matchesFromHostnames(extendedSpecific)); + excludeMatches.push(...ut.matchesFromHostnames(basic)); + excludeMatches.push(...ut.matchesFromHostnames(optimal)); excludeMatches.push(...ut.matchesFromHostnames(excludeHostnames)); matches.push(''); } else { matches.push( ...ut.matchesFromHostnames( ut.subtractHostnameIters( - Array.from(extendedGeneric), + Array.from(complete), excludeHostnames ) ) @@ -173,10 +173,10 @@ function registerProcedural(context) { } if ( js.length === 0 ) { return; } - const { none, network, extendedSpecific, extendedGeneric } = filteringModeDetails; + const { none, basic, optimal, complete } = filteringModeDetails; const matches = [ - ...ut.matchesFromHostnames(extendedSpecific), - ...ut.matchesFromHostnames(extendedGeneric), + ...ut.matchesFromHostnames(optimal), + ...ut.matchesFromHostnames(complete), ]; if ( matches.length === 0 ) { return; } @@ -186,8 +186,8 @@ function registerProcedural(context) { if ( none.has('all-urls') === false ) { excludeMatches.push(...ut.matchesFromHostnames(none)); } - if ( network.has('all-urls') === false ) { - excludeMatches.push(...ut.matchesFromHostnames(network)); + if ( basic.has('all-urls') === false ) { + excludeMatches.push(...ut.matchesFromHostnames(basic)); } const registered = before.get('css-procedural'); @@ -232,10 +232,10 @@ function registerDeclarative(context) { } if ( js.length === 0 ) { return; } - const { none, network, extendedSpecific, extendedGeneric } = filteringModeDetails; + const { none, basic, optimal, complete } = filteringModeDetails; const matches = [ - ...ut.matchesFromHostnames(extendedSpecific), - ...ut.matchesFromHostnames(extendedGeneric), + ...ut.matchesFromHostnames(optimal), + ...ut.matchesFromHostnames(complete), ]; if ( matches.length === 0 ) { return; } @@ -245,8 +245,8 @@ function registerDeclarative(context) { if ( none.has('all-urls') === false ) { excludeMatches.push(...ut.matchesFromHostnames(none)); } - if ( network.has('all-urls') === false ) { - excludeMatches.push(...ut.matchesFromHostnames(network)); + if ( basic.has('all-urls') === false ) { + excludeMatches.push(...ut.matchesFromHostnames(basic)); } const registered = before.get('css-declarative'); @@ -291,10 +291,10 @@ function registerSpecific(context) { } if ( js.length === 0 ) { return; } - const { none, network, extendedSpecific, extendedGeneric } = filteringModeDetails; + const { none, basic, optimal, complete } = filteringModeDetails; const matches = [ - ...ut.matchesFromHostnames(extendedSpecific), - ...ut.matchesFromHostnames(extendedGeneric), + ...ut.matchesFromHostnames(optimal), + ...ut.matchesFromHostnames(complete), ]; if ( matches.length === 0 ) { return; } @@ -304,8 +304,8 @@ function registerSpecific(context) { if ( none.has('all-urls') === false ) { excludeMatches.push(...ut.matchesFromHostnames(none)); } - if ( network.has('all-urls') === false ) { - excludeMatches.push(...ut.matchesFromHostnames(network)); + if ( basic.has('all-urls') === false ) { + excludeMatches.push(...ut.matchesFromHostnames(basic)); } const registered = before.get('css-specific'); @@ -347,16 +347,16 @@ function registerScriptlet(context, scriptletDetails) { const { before, filteringModeDetails, rulesetsDetails } = context; const hasBroadHostPermission = - filteringModeDetails.extendedSpecific.has('all-urls') || - filteringModeDetails.extendedGeneric.has('all-urls'); + filteringModeDetails.optimal.has('all-urls') || + filteringModeDetails.complete.has('all-urls'); const permissionRevokedMatches = [ ...ut.matchesFromHostnames(filteringModeDetails.none), - ...ut.matchesFromHostnames(filteringModeDetails.network), + ...ut.matchesFromHostnames(filteringModeDetails.basic), ]; const permissionGrantedHostnames = [ - ...filteringModeDetails.extendedSpecific, - ...filteringModeDetails.extendedGeneric, + ...filteringModeDetails.optimal, + ...filteringModeDetails.complete, ]; for ( const rulesetId of rulesetsDetails.map(v => v.id) ) { diff --git a/platform/mv3/extension/js/settings.js b/platform/mv3/extension/js/settings.js index f21f892f5..f21d773a0 100644 --- a/platform/mv3/extension/js/settings.js +++ b/platform/mv3/extension/js/settings.js @@ -241,32 +241,32 @@ const renderWidgets = function() { async function onFilteringModeChange(ev) { const input = ev.target; const newLevel = parseInt(input.value, 10); - let granted = false; switch ( newLevel ) { case 1: { // Revoke broad permissions - granted = await browser.permissions.remove({ + await browser.permissions.remove({ origins: [ '' ] }); + cachedRulesetData.defaultFilteringMode = 1; break; } case 2: case 3: { // Request broad permissions - granted = await browser.permissions.request({ + const granted = await browser.permissions.request({ origins: [ '' ] }); + if ( granted ) { + const actualLevel = await sendMessage({ + what: 'setDefaultFilteringMode', + level: newLevel, + }); + cachedRulesetData.defaultFilteringMode = actualLevel; + } break; } default: break; } - if ( granted ) { - const actualLevel = await sendMessage({ - what: 'setDefaultFilteringMode', - level: newLevel, - }); - cachedRulesetData.defaultFilteringMode = actualLevel; - } renderFilterLists(true); renderWidgets(); } diff --git a/platform/mv3/extension/managed_storage.json b/platform/mv3/extension/managed_storage.json new file mode 100644 index 000000000..6faefaa8f --- /dev/null +++ b/platform/mv3/extension/managed_storage.json @@ -0,0 +1,11 @@ +{ + "$schema": "http://json-schema.org/draft-03/schema#", + "type": "object", + "properties": { + "noFiltering": { + "title": "List of domains for which no filtering should occur", + "type": "array", + "items": { "type": "string" } + } + } +} diff --git a/tools/make-mv3.sh b/tools/make-mv3.sh index e5a22ff0f..85776418e 100755 --- a/tools/make-mv3.sh +++ b/tools/make-mv3.sh @@ -66,6 +66,7 @@ if [ "$PLATFORM" = "firefox" ]; then cp platform/mv3/firefox/background.html $DES/ fi cp platform/mv3/extension/*.html $DES/ +cp platform/mv3/extension/*.json $DES/ cp platform/mv3/extension/css/* $DES/css/ cp -R platform/mv3/extension/js/* $DES/js/ cp platform/mv3/extension/img/* $DES/img/