diff --git a/platform/mv3/extension/js/background.js b/platform/mv3/extension/js/background.js index 9f6e7c72d..857b5080e 100644 --- a/platform/mv3/extension/js/background.js +++ b/platform/mv3/extension/js/background.js @@ -25,19 +25,34 @@ /******************************************************************************/ -import { browser, dnr, i18n, runtime } from './ext.js'; -import { fetchJSON } from './fetch.js'; -import { getInjectableCount, registerInjectable } from './scripting-manager.js'; -import { parsedURLromOrigin } from './utils.js'; +import { + browser, + dnr, + runtime, +} from './ext.js'; + +import { + CURRENT_CONFIG_BASE_RULE_ID, + getRulesetDetails, + getDynamicRules, + defaultRulesetsFromLanguage, + enableRulesets, + getEnabledRulesetsStats, + updateRegexRules, +} from './ruleset-manager.js'; + +import { + getInjectableCount, + registerInjectable, +} from './scripting-manager.js'; + +import { + matchesTrustedSiteDirective, + toggleTrustedSiteDirective, +} from './trusted-sites.js'; /******************************************************************************/ -const RULE_REALM_SIZE = 1000000; -const REGEXES_REALM_START = 1000000; -const REGEXES_REALM_END = REGEXES_REALM_START + RULE_REALM_SIZE; -const TRUSTED_DIRECTIVE_BASE_RULE_ID = 8000000; -const CURRENT_CONFIG_BASE_RULE_ID = 9000000; - const rulesetConfig = { version: '', enabledRulesets: [], @@ -45,42 +60,6 @@ const rulesetConfig = { /******************************************************************************/ -let rulesetDetailsPromise; - -function getRulesetDetails() { - if ( rulesetDetailsPromise !== undefined ) { - return rulesetDetailsPromise; - } - rulesetDetailsPromise = fetchJSON('/rulesets/ruleset-details').then(entries => { - const map = new Map( - entries.map(entry => [ entry.id, entry ]) - ); - return map; - }); - return rulesetDetailsPromise; -} - -/******************************************************************************/ - -let dynamicRuleMapPromise; - -function getDynamicRules() { - if ( dynamicRuleMapPromise !== undefined ) { - return dynamicRuleMapPromise; - } - dynamicRuleMapPromise = dnr.getDynamicRules().then(rules => { - const map = new Map( - rules.map(rule => [ rule.id, rule ]) - ); - console.log(`Dynamic rule count: ${map.size}`); - console.log(`Available dynamic rule count: ${dnr.MAX_NUMBER_OF_DYNAMIC_AND_SESSION_RULES - map.size}`); - return map; - }); - return dynamicRuleMapPromise; -} - -/******************************************************************************/ - function getCurrentVersion() { return runtime.getManifest().version; } @@ -134,276 +113,6 @@ async function saveRulesetConfig() { /******************************************************************************/ -async function updateRegexRules() { - const [ - rulesetDetails, - dynamicRules - ] = await Promise.all([ - getRulesetDetails(), - dnr.getDynamicRules(), - ]); - - // Avoid testing already tested regexes - const validRegexSet = new Set( - dynamicRules.filter(rule => - rule.condition?.regexFilter && true || false - ).map(rule => - rule.condition.regexFilter - ) - ); - const allRules = []; - const toCheck = []; - - // Fetch regexes for all enabled rulesets - const toFetch = []; - for ( const details of rulesetDetails.values() ) { - if ( details.enabled !== true ) { continue; } - if ( details.rules.regexes === 0 ) { continue; } - toFetch.push(fetchJSON(`/rulesets/${details.id}.regexes`)); - } - const regexRulesets = await Promise.all(toFetch); - - // Validate fetched regexes - let regexRuleId = REGEXES_REALM_START; - for ( const rules of regexRulesets ) { - if ( Array.isArray(rules) === false ) { continue; } - for ( const rule of rules ) { - rule.id = regexRuleId++; - const { - regexFilter: regex, - isUrlFilterCaseSensitive: isCaseSensitive - } = rule.condition; - allRules.push(rule); - toCheck.push( - validRegexSet.has(regex) - ? { isSupported: true } - : dnr.isRegexSupported({ regex, isCaseSensitive }) - ); - } - } - - // Collate results - const results = await Promise.all(toCheck); - const newRules = []; - for ( let i = 0; i < allRules.length; i++ ) { - const rule = allRules[i]; - const result = results[i]; - if ( result instanceof Object && result.isSupported ) { - newRules.push(rule); - } else { - console.info(`${result.reason}: ${rule.condition.regexFilter}`); - } - } - console.info( - `Rejected regex filters: ${allRules.length-newRules.length} out of ${allRules.length}` - ); - - // Add validated regex rules to dynamic ruleset without affecting rules - // outside regex rule realm. - const dynamicRuleMap = await getDynamicRules(); - const newRuleMap = new Map(newRules.map(rule => [ rule.id, rule ])); - const addRules = []; - const removeRuleIds = []; - for ( const oldRule of dynamicRuleMap.values() ) { - if ( oldRule.id < REGEXES_REALM_START ) { continue; } - if ( oldRule.id >= REGEXES_REALM_END ) { continue; } - const newRule = newRuleMap.get(oldRule.id); - if ( newRule === undefined ) { - removeRuleIds.push(oldRule.id); - dynamicRuleMap.delete(oldRule.id); - } else if ( JSON.stringify(oldRule) !== JSON.stringify(newRule) ) { - removeRuleIds.push(oldRule.id); - addRules.push(newRule); - dynamicRuleMap.set(oldRule.id, newRule); - } - } - for ( const newRule of newRuleMap.values() ) { - if ( dynamicRuleMap.has(newRule.id) ) { continue; } - addRules.push(newRule); - dynamicRuleMap.set(newRule.id, newRule); - } - if ( addRules.length !== 0 || removeRuleIds.length !== 0 ) { - return dnr.updateDynamicRules({ addRules, removeRuleIds }); - } -} - -/******************************************************************************/ - -async function matchesTrustedSiteDirective(details) { - const url = parsedURLromOrigin(details.origin); - if ( url === undefined ) { return false; } - - const dynamicRuleMap = await getDynamicRules(); - let rule = dynamicRuleMap.get(TRUSTED_DIRECTIVE_BASE_RULE_ID); - if ( rule === undefined ) { return false; } - const domainSet = new Set(rule.condition.requestDomains); - let hostname = url.hostname; - for (;;) { - if ( domainSet.has(hostname) ) { return true; } - const pos = hostname.indexOf('.'); - if ( pos === -1 ) { break; } - hostname = hostname.slice(pos+1); - } - - return false; -} - -async function addTrustedSiteDirective(details) { - const url = parsedURLromOrigin(details.origin); - if ( url === undefined ) { return false; } - - const dynamicRuleMap = await getDynamicRules(); - let rule = dynamicRuleMap.get(TRUSTED_DIRECTIVE_BASE_RULE_ID); - if ( rule !== undefined ) { - rule.condition.initiatorDomains = undefined; - if ( Array.isArray(rule.condition.requestDomains) === false ) { - rule.condition.requestDomains = []; - } - } - - if ( rule === undefined ) { - rule = { - id: TRUSTED_DIRECTIVE_BASE_RULE_ID, - action: { - type: 'allowAllRequests', - }, - condition: { - requestDomains: [ url.hostname ], - resourceTypes: [ 'main_frame' ], - }, - priority: TRUSTED_DIRECTIVE_BASE_RULE_ID, - }; - dynamicRuleMap.set(TRUSTED_DIRECTIVE_BASE_RULE_ID, rule); - } else if ( rule.condition.requestDomains.includes(url.hostname) === false ) { - rule.condition.requestDomains.push(url.hostname); - } - - await dnr.updateDynamicRules({ - addRules: [ rule ], - removeRuleIds: [ TRUSTED_DIRECTIVE_BASE_RULE_ID ], - }); - - return true; -} - -async function removeTrustedSiteDirective(details) { - const url = parsedURLromOrigin(details.origin); - if ( url === undefined ) { return false; } - - const dynamicRuleMap = await getDynamicRules(); - let rule = dynamicRuleMap.get(TRUSTED_DIRECTIVE_BASE_RULE_ID); - if ( rule === undefined ) { return false; } - rule.condition.initiatorDomains = undefined; - if ( Array.isArray(rule.condition.requestDomains) === false ) { - rule.condition.requestDomains = []; - } - - const domainSet = new Set(rule.condition.requestDomains); - const beforeCount = domainSet.size; - let hostname = url.hostname; - for (;;) { - domainSet.delete(hostname); - const pos = hostname.indexOf('.'); - if ( pos === -1 ) { break; } - hostname = hostname.slice(pos+1); - } - - if ( domainSet.size === beforeCount ) { return false; } - - if ( domainSet.size === 0 ) { - dynamicRuleMap.delete(TRUSTED_DIRECTIVE_BASE_RULE_ID); - await dnr.updateDynamicRules({ - removeRuleIds: [ TRUSTED_DIRECTIVE_BASE_RULE_ID ] - }); - return false; - } - - rule.condition.requestDomains = Array.from(domainSet); - - await dnr.updateDynamicRules({ - addRules: [ rule ], - removeRuleIds: [ TRUSTED_DIRECTIVE_BASE_RULE_ID ], - }); - - return false; -} - -async function toggleTrustedSiteDirective(details) { - return details.state - ? removeTrustedSiteDirective(details) - : addTrustedSiteDirective(details); -} - -/******************************************************************************/ - -async function enableRulesets(ids) { - const afterIds = new Set(ids); - const beforeIds = new Set(await dnr.getEnabledRulesets()); - const enableRulesetIds = []; - const disableRulesetIds = []; - for ( const id of afterIds ) { - if ( beforeIds.has(id) ) { continue; } - enableRulesetIds.push(id); - } - for ( const id of beforeIds ) { - if ( afterIds.has(id) ) { continue; } - disableRulesetIds.push(id); - } - if ( enableRulesetIds.length !== 0 || disableRulesetIds.length !== 0 ) { - return dnr.updateEnabledRulesets({ enableRulesetIds,disableRulesetIds }); - } -} - -async function getEnabledRulesetsStats() { - const [ - rulesetDetails, - ids, - ] = await Promise.all([ - getRulesetDetails(), - dnr.getEnabledRulesets(), - ]); - const out = []; - for ( const id of ids ) { - const ruleset = rulesetDetails.get(id); - if ( ruleset === undefined ) { continue; } - out.push(ruleset); - } - return out; -} - -async function defaultRulesetsFromLanguage() { - const out = [ 'default' ]; - - const dropCountry = lang => { - const pos = lang.indexOf('-'); - if ( pos === -1 ) { return lang; } - return lang.slice(0, pos); - }; - - const langSet = new Set(); - - await i18n.getAcceptLanguages().then(langs => { - for ( const lang of langs.map(dropCountry) ) { - langSet.add(lang); - } - }); - langSet.add(dropCountry(i18n.getUILanguage())); - - const reTargetLang = new RegExp( - `\\b(${Array.from(langSet).join('|')})\\b` - ); - - const rulesetDetails = await getRulesetDetails(); - for ( const [ id, details ] of rulesetDetails ) { - if ( typeof details.lang !== 'string' ) { continue; } - if ( reTargetLang.test(details.lang) === false ) { continue; } - out.push(id); - } - return out; -} - -/******************************************************************************/ - async function hasGreatPowers(origin) { return browser.permissions.contains({ origins: [ `${origin}/*` ] @@ -491,7 +200,9 @@ function onMessage(request, sender, callback) { case 'toggleTrustedSiteDirective': { toggleTrustedSiteDirective(request).then(response => { - callback(response); + registerInjectable().then(( ) => { + callback(response); + }); }); return true; } diff --git a/platform/mv3/extension/js/ruleset-manager.js b/platform/mv3/extension/js/ruleset-manager.js new file mode 100644 index 000000000..faf9bd521 --- /dev/null +++ b/platform/mv3/extension/js/ruleset-manager.js @@ -0,0 +1,262 @@ +/******************************************************************************* + + uBlock Origin - a browser extension to block requests. + 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 { dnr, i18n } from './ext.js'; +import { fetchJSON } from './fetch.js'; + +/******************************************************************************/ + +const RULE_REALM_SIZE = 1000000; +const REGEXES_REALM_START = 1000000; +const REGEXES_REALM_END = REGEXES_REALM_START + RULE_REALM_SIZE; +const TRUSTED_DIRECTIVE_BASE_RULE_ID = 8000000; +const CURRENT_CONFIG_BASE_RULE_ID = 9000000; + +/******************************************************************************/ + +let rulesetDetailsPromise; + +function getRulesetDetails() { + if ( rulesetDetailsPromise !== undefined ) { + return rulesetDetailsPromise; + } + rulesetDetailsPromise = fetchJSON('/rulesets/ruleset-details').then(entries => { + const map = new Map( + entries.map(entry => [ entry.id, entry ]) + ); + return map; + }); + return rulesetDetailsPromise; +} + +/******************************************************************************/ + +let dynamicRuleMapPromise; + +function getDynamicRules() { + if ( dynamicRuleMapPromise !== undefined ) { + return dynamicRuleMapPromise; + } + dynamicRuleMapPromise = dnr.getDynamicRules().then(rules => { + const map = new Map( + rules.map(rule => [ rule.id, rule ]) + ); + console.log(`Dynamic rule count: ${map.size}`); + console.log(`Available dynamic rule count: ${dnr.MAX_NUMBER_OF_DYNAMIC_AND_SESSION_RULES - map.size}`); + return map; + }); + return dynamicRuleMapPromise; +} + +/******************************************************************************/ + +async function updateRegexRules() { + const [ + rulesetDetails, + dynamicRules + ] = await Promise.all([ + getRulesetDetails(), + dnr.getDynamicRules(), + ]); + + // Avoid testing already tested regexes + const validRegexSet = new Set( + dynamicRules.filter(rule => + rule.condition?.regexFilter && true || false + ).map(rule => + rule.condition.regexFilter + ) + ); + const allRules = []; + const toCheck = []; + + // Fetch regexes for all enabled rulesets + const toFetch = []; + for ( const details of rulesetDetails.values() ) { + if ( details.enabled !== true ) { continue; } + if ( details.rules.regexes === 0 ) { continue; } + toFetch.push(fetchJSON(`/rulesets/${details.id}.regexes`)); + } + const regexRulesets = await Promise.all(toFetch); + + // Validate fetched regexes + let regexRuleId = REGEXES_REALM_START; + for ( const rules of regexRulesets ) { + if ( Array.isArray(rules) === false ) { continue; } + for ( const rule of rules ) { + rule.id = regexRuleId++; + const { + regexFilter: regex, + isUrlFilterCaseSensitive: isCaseSensitive + } = rule.condition; + allRules.push(rule); + toCheck.push( + validRegexSet.has(regex) + ? { isSupported: true } + : dnr.isRegexSupported({ regex, isCaseSensitive }) + ); + } + } + + // Collate results + const results = await Promise.all(toCheck); + const newRules = []; + for ( let i = 0; i < allRules.length; i++ ) { + const rule = allRules[i]; + const result = results[i]; + if ( result instanceof Object && result.isSupported ) { + newRules.push(rule); + } else { + console.info(`${result.reason}: ${rule.condition.regexFilter}`); + } + } + console.info( + `Rejected regex filters: ${allRules.length-newRules.length} out of ${allRules.length}` + ); + + // Add validated regex rules to dynamic ruleset without affecting rules + // outside regex rule realm. + const dynamicRuleMap = await getDynamicRules(); + const newRuleMap = new Map(newRules.map(rule => [ rule.id, rule ])); + const addRules = []; + const removeRuleIds = []; + for ( const oldRule of dynamicRuleMap.values() ) { + if ( oldRule.id < REGEXES_REALM_START ) { continue; } + if ( oldRule.id >= REGEXES_REALM_END ) { continue; } + const newRule = newRuleMap.get(oldRule.id); + if ( newRule === undefined ) { + removeRuleIds.push(oldRule.id); + dynamicRuleMap.delete(oldRule.id); + } else if ( JSON.stringify(oldRule) !== JSON.stringify(newRule) ) { + removeRuleIds.push(oldRule.id); + addRules.push(newRule); + dynamicRuleMap.set(oldRule.id, newRule); + } + } + for ( const newRule of newRuleMap.values() ) { + if ( dynamicRuleMap.has(newRule.id) ) { continue; } + addRules.push(newRule); + dynamicRuleMap.set(newRule.id, newRule); + } + if ( addRules.length !== 0 || removeRuleIds.length !== 0 ) { + return dnr.updateDynamicRules({ addRules, removeRuleIds }); + } +} + +/******************************************************************************/ + +async function defaultRulesetsFromLanguage() { + const out = [ 'default' ]; + + const dropCountry = lang => { + const pos = lang.indexOf('-'); + if ( pos === -1 ) { return lang; } + return lang.slice(0, pos); + }; + + const langSet = new Set(); + + await i18n.getAcceptLanguages().then(langs => { + for ( const lang of langs.map(dropCountry) ) { + langSet.add(lang); + } + }); + langSet.add(dropCountry(i18n.getUILanguage())); + + const reTargetLang = new RegExp( + `\\b(${Array.from(langSet).join('|')})\\b` + ); + + const rulesetDetails = await getRulesetDetails(); + for ( const [ id, details ] of rulesetDetails ) { + if ( typeof details.lang !== 'string' ) { continue; } + if ( reTargetLang.test(details.lang) === false ) { continue; } + out.push(id); + } + return out; +} + +/******************************************************************************/ + +async function enableRulesets(ids) { + const afterIds = new Set(ids); + const beforeIds = new Set(await dnr.getEnabledRulesets()); + const enableRulesetIds = []; + const disableRulesetIds = []; + for ( const id of afterIds ) { + if ( beforeIds.has(id) ) { continue; } + enableRulesetIds.push(id); + } + for ( const id of beforeIds ) { + if ( afterIds.has(id) ) { continue; } + disableRulesetIds.push(id); + } + + if ( enableRulesetIds.length !== 0 ) { + console.info(`Enable rulesets: ${enableRulesetIds}`); + } + if ( disableRulesetIds.length !== 0 ) { + console.info(`Disable ruleset: ${disableRulesetIds}`); + } + if ( enableRulesetIds.length !== 0 || disableRulesetIds.length !== 0 ) { + return dnr.updateEnabledRulesets({ enableRulesetIds, disableRulesetIds }); + } +} + +/******************************************************************************/ + +async function getEnabledRulesetsStats() { + const [ + rulesetDetails, + ids, + ] = await Promise.all([ + getRulesetDetails(), + dnr.getEnabledRulesets(), + ]); + const out = []; + for ( const id of ids ) { + const ruleset = rulesetDetails.get(id); + if ( ruleset === undefined ) { continue; } + out.push(ruleset); + } + return out; +} + +/******************************************************************************/ + +export { + REGEXES_REALM_START, + REGEXES_REALM_END, + TRUSTED_DIRECTIVE_BASE_RULE_ID, + CURRENT_CONFIG_BASE_RULE_ID, + getRulesetDetails, + getDynamicRules, + enableRulesets, + defaultRulesetsFromLanguage, + getEnabledRulesetsStats, + updateRegexRules, +}; diff --git a/platform/mv3/extension/js/scripting-manager.js b/platform/mv3/extension/js/scripting-manager.js index acfdc6663..ce0a24051 100644 --- a/platform/mv3/extension/js/scripting-manager.js +++ b/platform/mv3/extension/js/scripting-manager.js @@ -27,7 +27,14 @@ import { browser, dnr } from './ext.js'; import { fetchJSON } from './fetch.js'; -import { parsedURLromOrigin } from './utils.js'; +import { matchesTrustedSiteDirective } from './trusted-sites.js'; + +import { + parsedURLromOrigin, + toBroaderHostname, + fidFromFileName, + fnameFromFileId, +} from './utils.js'; /******************************************************************************/ @@ -72,12 +79,6 @@ const hostnamesFromMatches = origins => { return out; }; -const toBroaderHostname = hn => { - if ( hn === '*' ) { return ''; } - const pos = hn.indexOf('.'); - return pos !== -1 ? hn.slice(pos+1) : '*'; -}; - const arrayEq = (a, b) => { if ( a === undefined ) { return b === undefined; } if ( b === undefined ) { return false; } @@ -103,9 +104,9 @@ const toRegisterable = (fname, entry) => { if ( entry.excludeMatches ) { directive.excludeMatches = matchesFromHostnames(entry.excludeMatches); } - directive.js = [ `/rulesets/js/${fname.slice(0,1)}/${fname.slice(1)}.js` ]; + directive.js = [ `/rulesets/js/${fname.slice(0,2)}/${fname.slice(2)}.js` ]; directive.runAt = 'document_start'; - if ( (parseInt(fname,16) & MAIN_WORLD_BIT) !== 0 ) { + if ( (fidFromFileName(fname) & MAIN_WORLD_BIT) !== 0 ) { directive.world = 'MAIN'; } return directive; @@ -150,8 +151,12 @@ async function getInjectableCount(origin) { const details = scriptingDetails.get(rulesetId); let hn = url.hostname; while ( hn !== '' ) { - const fnames = details.matches.get(hn); - total += fnames && fnames.length || 0; + const fids = details.matches?.get(hn); + if ( typeof fids === 'number' ) { + total += 1; + } else if ( Array.isArray(fids) ) { + total += fids.length; + } hn = toBroaderHostname(hn); } } @@ -161,8 +166,6 @@ async function getInjectableCount(origin) { /******************************************************************************/ -// TODO: Mind trusted-site directives. - async function registerInjectable() { const [ @@ -176,27 +179,37 @@ async function registerInjectable() { browser.scripting.getRegisteredContentScripts(), getScriptingDetails(), ]).then(results => { - results[0] = new Set(hostnamesFromMatches(results[0].origins)); + results[0] = new Map( + hostnamesFromMatches(results[0].origins).map(hn => [ hn, false ]) + ); return results; }); if ( hostnames.has('*') && hostnames.size > 1 ) { hostnames.clear(); - hostnames.add('*'); + hostnames.set('*', false); } + await Promise.all( + Array.from(hostnames.keys()).map( + hn => matchesTrustedSiteDirective({ hostname: hn }) + .then(trusted => hostnames.set(hn, trusted)) + ) + ); + const toRegister = new Map(); - const checkRealm = (details, prop, hn) => { - const fids = details[prop]?.get(hn); + const checkMatches = (details, hn) => { + let fids = details.matches?.get(hn); if ( fids === undefined ) { return; } + if ( typeof fids === 'number' ) { fids = [ fids ]; } for ( const fid of fids ) { - const fname = fid.toString(16).padStart(8,'0'); + const fname = fnameFromFileId(fid); const existing = toRegister.get(fname); if ( existing ) { - existing[prop].push(hn); + existing.matches.push(hn); } else { - toRegister.set(fname, { [prop]: [ hn ] }); + toRegister.set(fname, { matches: [ hn ] }); } } }; @@ -204,10 +217,37 @@ async function registerInjectable() { for ( const rulesetId of rulesetIds ) { const details = scriptingDetails.get(rulesetId); if ( details === undefined ) { continue; } - for ( let hn of hostnames ) { + for ( let [ hn, trusted ] of hostnames ) { + if ( trusted ) { continue; } while ( hn !== '' ) { - checkRealm(details, 'matches', hn); - checkRealm(details, 'excludeMatches', hn); + checkMatches(details, hn); + hn = toBroaderHostname(hn); + } + } + } + + const checkExcludeMatches = (details, hn) => { + let fids = details.excludeMatches?.get(hn); + if ( fids === undefined ) { return; } + if ( typeof fids === 'number' ) { fids = [ fids ]; } + for ( const fid of fids ) { + const fname = fnameFromFileId(fid); + const existing = toRegister.get(fname); + if ( existing === undefined ) { continue; } + if ( existing.excludeMatches ) { + existing.excludeMatches.push(hn); + } else { + toRegister.set(fname, { excludeMatches: [ hn ] }); + } + } + }; + + for ( const rulesetId of rulesetIds ) { + const details = scriptingDetails.get(rulesetId); + if ( details === undefined ) { continue; } + for ( let hn of hostnames.keys() ) { + while ( hn !== '' ) { + checkExcludeMatches(details, hn); hn = toBroaderHostname(hn); } } diff --git a/platform/mv3/extension/js/trusted-sites.js b/platform/mv3/extension/js/trusted-sites.js new file mode 100644 index 000000000..bb56c0b81 --- /dev/null +++ b/platform/mv3/extension/js/trusted-sites.js @@ -0,0 +1,160 @@ +/******************************************************************************* + + uBlock Origin - a browser extension to block requests. + 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 { dnr } from './ext.js'; + +import { + parsedURLromOrigin, + toBroaderHostname, +} from './utils.js'; + +import { + TRUSTED_DIRECTIVE_BASE_RULE_ID, + getDynamicRules +} from './ruleset-manager.js'; + +/******************************************************************************/ + +async function matchesTrustedSiteDirective(details) { + const hostname = + details.hostname || + parsedURLromOrigin(details.origin)?.hostname || + undefined; + if ( hostname === undefined ) { return false; } + + const dynamicRuleMap = await getDynamicRules(); + let rule = dynamicRuleMap.get(TRUSTED_DIRECTIVE_BASE_RULE_ID); + if ( rule === undefined ) { return false; } + + const domainSet = new Set(rule.condition.requestDomains); + let hn = hostname; + while ( hn ) { + if ( domainSet.has(hn) ) { return true; } + hn = toBroaderHostname(hn); + } + + return false; +} + +/******************************************************************************/ + +async function addTrustedSiteDirective(details) { + const url = parsedURLromOrigin(details.origin); + if ( url === undefined ) { return false; } + + const dynamicRuleMap = await getDynamicRules(); + let rule = dynamicRuleMap.get(TRUSTED_DIRECTIVE_BASE_RULE_ID); + if ( rule !== undefined ) { + rule.condition.initiatorDomains = undefined; + if ( Array.isArray(rule.condition.requestDomains) === false ) { + rule.condition.requestDomains = []; + } + } + + if ( rule === undefined ) { + rule = { + id: TRUSTED_DIRECTIVE_BASE_RULE_ID, + action: { + type: 'allowAllRequests', + }, + condition: { + requestDomains: [ url.hostname ], + resourceTypes: [ 'main_frame' ], + }, + priority: TRUSTED_DIRECTIVE_BASE_RULE_ID, + }; + dynamicRuleMap.set(TRUSTED_DIRECTIVE_BASE_RULE_ID, rule); + } else if ( rule.condition.requestDomains.includes(url.hostname) === false ) { + rule.condition.requestDomains.push(url.hostname); + } + + await dnr.updateDynamicRules({ + addRules: [ rule ], + removeRuleIds: [ TRUSTED_DIRECTIVE_BASE_RULE_ID ], + }); + + return true; +} + +/******************************************************************************/ + +async function removeTrustedSiteDirective(details) { + const url = parsedURLromOrigin(details.origin); + if ( url === undefined ) { return false; } + + const dynamicRuleMap = await getDynamicRules(); + let rule = dynamicRuleMap.get(TRUSTED_DIRECTIVE_BASE_RULE_ID); + if ( rule === undefined ) { return false; } + rule.condition.initiatorDomains = undefined; + if ( Array.isArray(rule.condition.requestDomains) === false ) { + rule.condition.requestDomains = []; + } + + const domainSet = new Set(rule.condition.requestDomains); + const beforeCount = domainSet.size; + let hostname = url.hostname; + for (;;) { + domainSet.delete(hostname); + const pos = hostname.indexOf('.'); + if ( pos === -1 ) { break; } + hostname = hostname.slice(pos+1); + } + + if ( domainSet.size === beforeCount ) { return false; } + + if ( domainSet.size === 0 ) { + dynamicRuleMap.delete(TRUSTED_DIRECTIVE_BASE_RULE_ID); + await dnr.updateDynamicRules({ + removeRuleIds: [ TRUSTED_DIRECTIVE_BASE_RULE_ID ] + }); + return false; + } + + rule.condition.requestDomains = Array.from(domainSet); + + await dnr.updateDynamicRules({ + addRules: [ rule ], + removeRuleIds: [ TRUSTED_DIRECTIVE_BASE_RULE_ID ], + }); + + return false; +} + +/******************************************************************************/ + +async function toggleTrustedSiteDirective(details) { + return details.state + ? removeTrustedSiteDirective(details) + : addTrustedSiteDirective(details); +} + +/******************************************************************************/ + +export { + matchesTrustedSiteDirective, + toggleTrustedSiteDirective, +}; diff --git a/platform/mv3/extension/js/utils.js b/platform/mv3/extension/js/utils.js index b9d28869f..5d7447fac 100644 --- a/platform/mv3/extension/js/utils.js +++ b/platform/mv3/extension/js/utils.js @@ -34,4 +34,25 @@ function parsedURLromOrigin(origin) { /******************************************************************************/ -export { parsedURLromOrigin }; +const toBroaderHostname = hn => { + if ( hn === '*' ) { return ''; } + const pos = hn.indexOf('.'); + return pos !== -1 ? hn.slice(pos+1) : '*'; +}; + +/******************************************************************************/ + +const fnameFromFileId = fid => + fid.toString(32).padStart(7, '0'); + +const fidFromFileName = fname => + parseInt(fname, 32); + +/******************************************************************************/ + +export { + parsedURLromOrigin, + toBroaderHostname, + fnameFromFileId, + fidFromFileName, +}; diff --git a/platform/mv3/make-rulesets.js b/platform/mv3/make-rulesets.js index 76683334c..9e5b9e9d4 100644 --- a/platform/mv3/make-rulesets.js +++ b/platform/mv3/make-rulesets.js @@ -28,9 +28,9 @@ import https from 'https'; import path from 'path'; import process from 'process'; import { createHash } from 'crypto'; - import { dnrRulesetFromRawLists } from './js/static-dnr-filtering.js'; import { StaticFilteringParser } from './js/static-filtering-parser.js'; +import { fnameFromFileId } from './js/utils.js'; /******************************************************************************/ @@ -349,25 +349,27 @@ function addScriptingAPIResources(id, entry, prop, fid) { for ( const hn of entry[prop] ) { let details = scriptingDetails.get(id); if ( details === undefined ) { - details = { - matches: new Map(), - excludeMatches: new Map(), - }; + details = {}; scriptingDetails.set(id, details); } + if ( details[prop] === undefined ) { + details[prop] = new Map(); + } let fids = details[prop].get(hn); if ( fids === undefined ) { - fids = new Set(); + details[prop].set(hn, fid); + } else if ( fids instanceof Set ) { + fids.add(fid); + } else if ( fid !== fids ) { + fids = new Set([ fids, fid ]); details[prop].set(hn, fids); } - fids.add(fid); } } const toCSSFileId = s => uidint32(s) & ~0b1; const toJSFileId = s => uidint32(s) | 0b1; -const fileNameFromId = id => id.toString(16).padStart(8,'0'); - +const pathFromFileName = fname => `${scriptletDir}/${fname.slice(0,2)}/${fname.slice(2)}.js`; /******************************************************************************/ @@ -487,8 +489,8 @@ async function processCosmeticFilters(assetDetails, mapin) { const fid = toCSSFileId(patchedScriptlet); if ( globalPatchedScriptletsSet.has(fid) === false ) { globalPatchedScriptletsSet.add(fid); - const fname = fileNameFromId(fid); - writeFile(`${scriptletDir}/${fname.slice(0,1)}/${fname.slice(1)}.js`, patchedScriptlet, {}); + const fname = fnameFromFileId(fid); + writeFile(pathFromFileName(fname), patchedScriptlet, {}); generatedFiles.push(fname); } for ( const entry of slice ) { @@ -649,8 +651,8 @@ async function processScriptletFilters(assetDetails, mapin) { const fid = toJSFileId(patchedScriptlet); if ( globalPatchedScriptletsSet.has(fid) === false ) { globalPatchedScriptletsSet.add(fid); - const fname = fileNameFromId(fid); - writeFile(`${scriptletDir}/${fname.slice(0,1)}/${fname.slice(1)}.js`, patchedScriptlet, {}); + const fname = fnameFromFileId(fid); + writeFile(pathFromFileName(fname), patchedScriptlet, {}); generatedFiles.push(fname); } for ( const details of argsDetails.values() ) { diff --git a/tools/make-mv3.sh b/tools/make-mv3.sh index 9ef0ac08c..f12d579d7 100755 --- a/tools/make-mv3.sh +++ b/tools/make-mv3.sh @@ -49,6 +49,7 @@ if [ "$1" != "quick" ]; then ./tools/make-nodejs.sh $TMPDIR cp platform/mv3/package.json $TMPDIR/ cp platform/mv3/*.js $TMPDIR/ + cp platform/mv3/extension/js/utils.js $TMPDIR/js/ cp assets/assets.json $TMPDIR/ cp -R platform/mv3/scriptlets $TMPDIR/ cd $TMPDIR