From 61d6cd803c997a8c2020cff8b00ff48babf74364 Mon Sep 17 00:00:00 2001 From: Raymond Hill Date: Sun, 1 Dec 2024 19:43:34 -0500 Subject: [PATCH] draft --- platform/mv3/extension/js/background.js | 23 +- platform/mv3/extension/js/ext.js | 6 + platform/mv3/extension/js/mode-manager.js | 109 +--- platform/mv3/extension/js/popup.js | 20 +- platform/mv3/extension/js/ruleset-manager.js | 503 ++++++++++--------- platform/mv3/extension/js/strict-block.js | 75 +-- 6 files changed, 315 insertions(+), 421 deletions(-) diff --git a/platform/mv3/extension/js/background.js b/platform/mv3/extension/js/background.js index 2e3a41124..5461d671a 100644 --- a/platform/mv3/extension/js/background.js +++ b/platform/mv3/extension/js/background.js @@ -37,6 +37,7 @@ import { dnr, localRead, localRemove, localWrite, runtime, + sessionRemove, windows, } from './ext.js'; @@ -47,6 +48,7 @@ import { import { enableRulesets, + excludeFromStrictBlock, getEnabledRulesetsDetails, getRulesetDetails, updateDynamicRules, @@ -335,6 +337,13 @@ function onMessage(request, sender, callback) { }); return true; + case 'excludeFromStrictBlock': { + excludeFromStrictBlock(request.hostname, request.permanent).then(( ) => { + callback(); + }); + return true; + } + case 'getMatchedRules': getMatchedRules(request.tabId).then(entries => { callback(entries); @@ -361,18 +370,22 @@ async function start() { await loadRulesetConfig(); if ( process.wakeupRun === false ) { - await enableRulesets(rulesetConfig.enabledRulesets); + sessionRemove('excludedStrictBlockHostnames'); } + const rulesetsUpdated = process.wakeupRun === false && + await enableRulesets(rulesetConfig.enabledRulesets); + // We need to update the regex rules only when ruleset version changes. if ( process.wakeupRun === false ) { const currentVersion = getCurrentVersion(); if ( currentVersion !== rulesetConfig.version ) { ubolLog(`Version change: ${rulesetConfig.version} => ${currentVersion}`); - updateDynamicRules().then(( ) => { - rulesetConfig.version = currentVersion; - saveRulesetConfig(); - }); + rulesetConfig.version = currentVersion; + saveRulesetConfig(); + if ( rulesetsUpdated === false ) { + updateDynamicRules(); + } } } diff --git a/platform/mv3/extension/js/ext.js b/platform/mv3/extension/js/ext.js index 94d5118a2..0a5a98f4f 100644 --- a/platform/mv3/extension/js/ext.js +++ b/platform/mv3/extension/js/ext.js @@ -98,6 +98,12 @@ export async function sessionWrite(key, value) { return browser.storage.session.set({ [key]: value }); } +export async function sessionRemove(key) { + if ( browser.storage instanceof Object === false ) { return; } + if ( browser.storage.session instanceof Object === false ) { return; } + return browser.storage.session.remove(key); +} + /******************************************************************************/ export async function adminRead(key) { diff --git a/platform/mv3/extension/js/mode-manager.js b/platform/mv3/extension/js/mode-manager.js index 335c8da16..d6b89da77 100644 --- a/platform/mv3/extension/js/mode-manager.js +++ b/platform/mv3/extension/js/mode-manager.js @@ -19,11 +19,6 @@ Home: https://github.com/gorhill/uBlock */ -import { - TRUSTED_DIRECTIVE_BASE_RULE_ID, - getDynamicRules, -} from './ruleset-manager.js'; - import { broadcastMessage, hostnamesFromMatches, @@ -33,12 +28,12 @@ import { import { browser, - dnr, localRead, localWrite, sessionRead, sessionWrite, } from './ext.js'; import { adminReadEx } from './admin.js'; +import { filteringModesToDNR } from './ruleset-manager.js'; /******************************************************************************/ @@ -74,19 +69,6 @@ const pruneHostnameFromSet = (hostname, hnSet) => { /******************************************************************************/ -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), @@ -284,93 +266,6 @@ async function writeFilteringModeDetails(afterDetails) { /******************************************************************************/ -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 { @@ -401,7 +296,7 @@ export function getDefaultFilteringMode() { return getFilteringMode('all-urls'); } -export function setDefaultFilteringMode(afterLevel) { +export async function setDefaultFilteringMode(afterLevel) { return setFilteringMode('all-urls', afterLevel); } diff --git a/platform/mv3/extension/js/popup.js b/platform/mv3/extension/js/popup.js index b12a3fbcc..7de94450b 100644 --- a/platform/mv3/extension/js/popup.js +++ b/platform/mv3/extension/js/popup.js @@ -34,7 +34,7 @@ import punycode from './punycode.js'; const popupPanelData = {}; const currentTab = {}; -let tabHostname = ''; +const tabURL = new URL(runtime.getURL('/')); /******************************************************************************/ @@ -68,8 +68,8 @@ function setFilteringMode(level, commit = false) { } async function commitFilteringMode() { - if ( tabHostname === '' ) { return; } - const targetHostname = normalizedHostname(tabHostname); + if ( tabURL.hostname === '' ) { return; } + const targetHostname = normalizedHostname(tabURL.hostname); const modeSlider = qs$('.filteringModeSlider'); const afterLevel = parseInt(modeSlider.dataset.level, 10); const beforeLevel = parseInt(modeSlider.dataset.levelBefore, 10); @@ -100,7 +100,9 @@ async function commitFilteringMode() { } if ( actualLevel !== beforeLevel && popupPanelData.autoReload ) { self.setTimeout(( ) => { - browser.tabs.reload(currentTab.id); + browser.tabs.update(currentTab.id, { + url: tabURL.href, + }); }, 437); } } @@ -317,8 +319,12 @@ async function init() { let url; try { + const strictBlockURL = runtime.getURL('/strict-block.html'); url = new URL(currentTab.url); - tabHostname = url.hostname || ''; + if ( url.href.startsWith(strictBlockURL) ) { + url = new URL(url.hash.slice(1)); + } + tabURL.href = url.href || ''; } catch(ex) { } @@ -326,7 +332,7 @@ async function init() { const response = await sendMessage({ what: 'popupPanelData', origin: url.origin, - hostname: normalizedHostname(tabHostname), + hostname: normalizedHostname(tabURL.hostname), }); if ( response instanceof Object ) { Object.assign(popupPanelData, response); @@ -337,7 +343,7 @@ async function init() { setFilteringMode(popupPanelData.level); - dom.text('#hostname', punycode.toUnicode(tabHostname)); + dom.text('#hostname', punycode.toUnicode(tabURL.hostname)); dom.cl.toggle('#showMatchedRules', 'enabled', popupPanelData.isSideloaded === true && diff --git a/platform/mv3/extension/js/ruleset-manager.js b/platform/mv3/extension/js/ruleset-manager.js index b824dc025..28062e5b7 100644 --- a/platform/mv3/extension/js/ruleset-manager.js +++ b/platform/mv3/extension/js/ruleset-manager.js @@ -26,24 +26,34 @@ import { runtime, } from './ext.js'; +import { + localRead, localRemove, localWrite, + sessionRead, sessionRemove, sessionWrite, +} from './ext.js'; + import { fetchJSON } from './fetch.js'; import { getAdminRulesets } from './admin.js'; import { ubolLog } from './debug.js'; /******************************************************************************/ -const RULE_REALM_SIZE = 1000000; -const REGEXES_REALM_START = 1000000; -const REGEXES_REALM_END = REGEXES_REALM_START + RULE_REALM_SIZE; -const REMOVEPARAMS_REALM_START = REGEXES_REALM_END; -const REMOVEPARAMS_REALM_END = REMOVEPARAMS_REALM_START + RULE_REALM_SIZE; -const REDIRECT_REALM_START = REMOVEPARAMS_REALM_END; -const REDIRECT_REALM_END = REDIRECT_REALM_START + RULE_REALM_SIZE; -const MODIFYHEADERS_REALM_START = REDIRECT_REALM_END; -const MODIFYHEADERS_REALM_END = MODIFYHEADERS_REALM_START + RULE_REALM_SIZE; -const STRICTBLOCK_REALM_START = MODIFYHEADERS_REALM_END; const TRUSTED_DIRECTIVE_BASE_RULE_ID = 8000000; +let dynamicRulesetId = 1; + +/******************************************************************************/ + +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; +}; + /******************************************************************************/ function getRulesetDetails() { @@ -61,29 +71,17 @@ function getRulesetDetails() { /******************************************************************************/ -function getDynamicRules() { +function getDynamicRules(...args) { if ( getDynamicRules.promise !== undefined ) { return getDynamicRules.promise; } - getDynamicRules.promise = dnr.getDynamicRules().then(rules => { - const rulesMap = new Map(rules.map(rule => [ rule.id, rule ])); - ubolLog(`Dynamic rule count: ${rulesMap.size}`); - ubolLog(`Available dynamic rule count: ${dnr.MAX_NUMBER_OF_DYNAMIC_AND_SESSION_RULES - rulesMap.size}`); - return rulesMap; + getDynamicRules.promise = dnr.getDynamicRules(...args).then(rules => { + getDynamicRules.promise = undefined; + return rules; }); return getDynamicRules.promise; } -function getDynamicRuleIds() { - if ( getDynamicRuleIds.promise !== undefined ) { - return getDynamicRuleIds.promise; - } - getDynamicRuleIds.promise = dnr.getDynamicRules().then(rules => { - return rules.map(rule => rule.id); - }); - return getDynamicRuleIds.promise; -} - /******************************************************************************/ async function pruneInvalidRegexRules(realm, rulesIn) { @@ -129,7 +127,7 @@ pruneInvalidRegexRules.validated = new Map(); /******************************************************************************/ -async function updateRegexRules() { +async function updateRegexRules(toAdd) { const rulesetDetails = await getEnabledRulesetsDetails(); // Fetch regexes for all enabled rulesets @@ -142,69 +140,31 @@ async function updateRegexRules() { // Collate all regexes rules const allRules = []; - let regexRuleId = REGEXES_REALM_START; for ( const rules of regexRulesets ) { if ( Array.isArray(rules) === false ) { continue; } for ( const rule of rules ) { - rule.id = regexRuleId++; + rule.id = dynamicRulesetId++; allRules.push(rule); } } + if ( allRules.length === 0 ) { return; } - const validatedRules = await pruneInvalidRegexRules('regexes', allRules); + const validRules = await pruneInvalidRegexRules('regexes', allRules); + if ( validRules.length === 0 ) { return; } - // Add validated regex rules to dynamic ruleset without affecting rules - // outside regex rules realm. - const dynamicRuleMap = await getDynamicRules(); - const newRuleMap = new Map(validatedRules.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; } - - if ( removeRuleIds.length !== 0 ) { - ubolLog(`Remove ${removeRuleIds.length} DNR regex rules`); - } - if ( addRules.length !== 0 ) { - ubolLog(`Add ${addRules.length} DNR regex rules`); - } - - return dnr.updateDynamicRules({ addRules, removeRuleIds }).catch(reason => { - console.error(`updateRegexRules() / ${reason}`); - }); + ubolLog(`Add ${validRules.length} DNR regex rules`); + toAdd.push(...validRules); } /******************************************************************************/ -async function updateRemoveparamRules() { +async function updateRemoveparamRules(toAdd) { const [ hasOmnipotence, rulesetDetails, - dynamicRuleMap, ] = await Promise.all([ browser.permissions.contains({ origins: [ '' ] }), getEnabledRulesetsDetails(), - getDynamicRules(), ]); // Fetch removeparam rules for all enabled rulesets @@ -218,69 +178,32 @@ async function updateRemoveparamRules() { // Removeparam rules can only be enforced with omnipotence const allRules = []; if ( hasOmnipotence ) { - let removeparamRuleId = REMOVEPARAMS_REALM_START; for ( const rules of removeparamRulesets ) { if ( Array.isArray(rules) === false ) { continue; } for ( const rule of rules ) { - rule.id = removeparamRuleId++; + rule.id = dynamicRulesetId++; allRules.push(rule); } } } + if ( allRules.length === 0 ) { return; } - const validatedRules = await pruneInvalidRegexRules('removeparam', allRules); + const validRules = await pruneInvalidRegexRules('removeparam', allRules); + if ( validRules.length === 0 ) { return; } - // Add removeparam rules to dynamic ruleset without affecting rules - // outside removeparam rules realm. - const newRuleMap = new Map(validatedRules.map(rule => [ rule.id, rule ])); - const addRules = []; - const removeRuleIds = []; - - for ( const oldRule of dynamicRuleMap.values() ) { - if ( oldRule.id < REMOVEPARAMS_REALM_START ) { continue; } - if ( oldRule.id >= REMOVEPARAMS_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; } - - if ( removeRuleIds.length !== 0 ) { - ubolLog(`Remove ${removeRuleIds.length} DNR removeparam rules`); - } - if ( addRules.length !== 0 ) { - ubolLog(`Add ${addRules.length} DNR removeparam rules`); - } - - return dnr.updateDynamicRules({ addRules, removeRuleIds }).catch(reason => { - console.error(`updateRemoveparamRules() / ${reason}`); - }); + ubolLog(`Add ${validRules.length} DNR removeparam rules`); + toAdd.push(...validRules); } /******************************************************************************/ -async function updateRedirectRules() { +async function updateRedirectRules(toAdd) { const [ hasOmnipotence, rulesetDetails, - dynamicRuleMap, ] = await Promise.all([ browser.permissions.contains({ origins: [ '' ] }), getEnabledRulesetsDetails(), - getDynamicRules(), ]); // Fetch redirect rules for all enabled rulesets @@ -294,69 +217,32 @@ async function updateRedirectRules() { // Redirect rules can only be enforced with omnipotence const allRules = []; if ( hasOmnipotence ) { - let redirectRuleId = REDIRECT_REALM_START; for ( const rules of redirectRulesets ) { if ( Array.isArray(rules) === false ) { continue; } for ( const rule of rules ) { - rule.id = redirectRuleId++; + rule.id = dynamicRulesetId++; allRules.push(rule); } } } + if ( allRules.length === 0 ) { return; } - const validatedRules = await pruneInvalidRegexRules('redirect', allRules); + const validRules = await pruneInvalidRegexRules('redirect', allRules); + if ( validRules.length === 0 ) { return; } - // Add redirect rules to dynamic ruleset without affecting rules - // outside redirect rules realm. - const newRuleMap = new Map(validatedRules.map(rule => [ rule.id, rule ])); - const addRules = []; - const removeRuleIds = []; - - for ( const oldRule of dynamicRuleMap.values() ) { - if ( oldRule.id < REDIRECT_REALM_START ) { continue; } - if ( oldRule.id >= REDIRECT_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; } - - if ( removeRuleIds.length !== 0 ) { - ubolLog(`Remove ${removeRuleIds.length} DNR redirect rules`); - } - if ( addRules.length !== 0 ) { - ubolLog(`Add ${addRules.length} DNR redirect rules`); - } - - return dnr.updateDynamicRules({ addRules, removeRuleIds }).catch(reason => { - console.error(`updateRedirectRules() / ${reason}`); - }); + ubolLog(`Add ${validRules.length} DNR redirect rules`); + toAdd.push(...validRules); } /******************************************************************************/ -async function updateModifyHeadersRules() { +async function updateModifyHeadersRules(toAdd) { const [ hasOmnipotence, rulesetDetails, - dynamicRuleMap, ] = await Promise.all([ browser.permissions.contains({ origins: [ '' ] }), getEnabledRulesetsDetails(), - getDynamicRules(), ]); // Fetch modifyHeaders rules for all enabled rulesets @@ -370,69 +256,36 @@ async function updateModifyHeadersRules() { // Redirect rules can only be enforced with omnipotence const allRules = []; if ( hasOmnipotence ) { - let ruleId = MODIFYHEADERS_REALM_START; for ( const rules of rulesets ) { if ( Array.isArray(rules) === false ) { continue; } for ( const rule of rules ) { - rule.id = ruleId++; + rule.id = dynamicRulesetId++; allRules.push(rule); } } } + if ( allRules.length === 0 ) { return; } - const validatedRules = await pruneInvalidRegexRules('modify-headers', allRules); + const validRules = await pruneInvalidRegexRules('modify-headers', allRules); + if ( validRules.length === 0 ) { return; } - // Add modifyHeaders rules to dynamic ruleset without affecting rules - // outside modifyHeaders realm. - const newRuleMap = new Map(validatedRules.map(rule => [ rule.id, rule ])); - const addRules = []; - const removeRuleIds = []; - - for ( const oldRule of dynamicRuleMap.values() ) { - if ( oldRule.id < MODIFYHEADERS_REALM_START ) { continue; } - if ( oldRule.id >= MODIFYHEADERS_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; } - - if ( removeRuleIds.length !== 0 ) { - ubolLog(`Remove ${removeRuleIds.length} DNR modifyHeaders rules`); - } - if ( addRules.length !== 0 ) { - ubolLog(`Add ${addRules.length} DNR modifyHeaders rules`); - } - - return dnr.updateDynamicRules({ addRules, removeRuleIds }).catch(reason => { - console.error(`updateModifyHeadersRules() / ${reason}`); - }); + ubolLog(`Add ${validRules.length} DNR modify-headers rules`); + toAdd.push(...validRules); } /******************************************************************************/ -async function updateStrictBlockRules() { +async function updateStrictBlockRules(toAdd) { const [ hasOmnipotence, rulesetDetails, - dynamicRuleMap, + permanentlyExcluded = [], + temporarilyExcluded = [], ] = await Promise.all([ browser.permissions.contains({ origins: [ '' ] }), getEnabledRulesetsDetails(), - getDynamicRules(), + localRead('excludedStrictBlockHostnames'), + sessionRead('excludedStrictBlockHostnames'), ]); // Fetch strick-block hostnames @@ -443,6 +296,8 @@ async function updateStrictBlockRules() { } const strictBlockRulesets = await Promise.all(toFetch); + const allExcluded = permanentlyExcluded.concat(temporarilyExcluded); + // Strict-block rules can only be enforced with omnipotence let toStrictBlock = new Set(); if ( hasOmnipotence ) { @@ -450,63 +305,207 @@ async function updateStrictBlockRules() { if ( Array.isArray(hostnames) === false ) { continue; } toStrictBlock = toStrictBlock.union(new Set(hostnames)); } + } else if ( allExcluded.length !== 0 ) { + localRemove('excludedStrictBlockHostnames'); + sessionRemove('excludedStrictBlockHostnames'); + allExcluded.length = 0; } + for ( const hn of allExcluded ) { + toStrictBlock.delete(hn); + } + if ( toStrictBlock.size === 0 ) { return; } + const ubolOrigin = runtime.getURL('').replace(/\/$/, ''); + const rule = { + id: dynamicRulesetId++, + action: { + type: 'redirect', + redirect: { + regexSubstitution: `${ubolOrigin}/strict-block.html#\\0`, + }, + }, + condition: { + regexFilter: '^https:?//.+', + requestDomains: Array.from(toStrictBlock), + resourceTypes: [ 'main_frame' ], + }, + priority: 29, + }; + if ( allExcluded.length !== 0 ) { + rule.condition.excludedRequestDomains = allExcluded; + } + toAdd.push(rule); + + ubolLog(`Add 1 DNR strict-block rules with ${toStrictBlock.size} domains`); +} + +/******************************************************************************/ + +async function updateDynamicRules() { + dynamicRulesetId = 1; const addRules = []; - const removeRuleIds = []; - - if ( dynamicRuleMap.has(STRICTBLOCK_REALM_START) ) { - removeRuleIds.push(STRICTBLOCK_REALM_START); - dynamicRuleMap.delete(STRICTBLOCK_REALM_START); - } - - if ( toStrictBlock.size !== 0 ) { - const ubolOrigin = runtime.getURL('').replace(/\/$/, ''); - addRules.push({ - id: STRICTBLOCK_REALM_START, - action: { - type: 'redirect', - redirect: { - regexSubstitution: `${ubolOrigin}/strict-block.html#\\0`, - }, - }, - condition: { - regexFilter: '^.+$', - requestDomains: Array.from(toStrictBlock), - resourceTypes: [ 'main_frame' ], - }, - priority: 19, - }); - dynamicRuleMap.set(STRICTBLOCK_REALM_START, addRules[0]); - } - + const [ removeRuleIds ] = await Promise.all([ + getDynamicRules().then(rules => + rules.map(rule => rule.id) + .filter(id => id < TRUSTED_DIRECTIVE_BASE_RULE_ID) + ), + updateRegexRules(addRules), + updateRemoveparamRules(addRules), + updateRedirectRules(addRules), + updateModifyHeadersRules(addRules), + updateStrictBlockRules(addRules), + ]); if ( addRules.length === 0 && removeRuleIds.length === 0 ) { return; } - if ( removeRuleIds.length !== 0 ) { - ubolLog(`Remove ${removeRuleIds.length} DNR strict-block rules`); + ubolLog(`Remove ${removeRuleIds.length} dynamic DNR rules`); } if ( addRules.length !== 0 ) { - ubolLog(`Add ${addRules.length} DNR strict-block rules`); + ubolLog(`Add ${addRules.length} dynamic DNR rules`); } - return dnr.updateDynamicRules({ addRules, removeRuleIds }).catch(reason => { - console.error(`updateStrictBlockRules() / ${reason}`); + console.error(`updateDynamicRules() / ${reason}`); }); } /******************************************************************************/ -// TODO: group all omnipotence-related rules into one realm. +async function filteringModesToDNR(modes) { + const trustedRules = await getDynamicRules({ + ruleIds: [ TRUSTED_DIRECTIVE_BASE_RULE_ID+0 ], + }); + const trustedRule = trustedRules.length !== 0 && trustedRules[0] || undefined; + 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'); + } -async function updateDynamicRules() { - ubolLog('Called updateDynamicRules()'); - return Promise.all([ - updateRegexRules(), - updateRemoveparamRules(), - updateRedirectRules(), - updateModifyHeadersRules(), - updateStrictBlockRules(), + 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 = []; + if ( trustedRule ) { + removeRuleIds.push( + TRUSTED_DIRECTIVE_BASE_RULE_ID+0, + 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); + unexcludeFromStrictBlock(afterRequestDomainSet); + // 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); + } + + if ( addRules.length === 0 && removeRuleIds.length === 0 ) { return; } + + return dnr.updateDynamicRules({ addRules, removeRuleIds }); +} + +/******************************************************************************/ + +async function excludeFromStrictBlock(hostname, permanent) { + if ( typeof hostname !== 'string' || hostname === '' ) { return; } + const readFn = permanent ? localRead : sessionRead; + const hostnames = new Set(await readFn('excludedStrictBlockHostnames')); + hostnames.add(hostname); + const writeFn = permanent ? localWrite : sessionWrite; + await writeFn('excludedStrictBlockHostnames', Array.from(hostnames)); + return updateDynamicRules(); +} + +async function unexcludeFromStrictBlock(hostnames) { + const [ + permanentlyExcluded, + temporarilyExcluded, + ] = await Promise.all([ + localRead('excludedStrictBlockHostnames').then(r => r = new Set(r)), + sessionRead('excludedStrictBlockHostnames').then(r => r = new Set(r)), ]); + const permanentCountBefore = permanentlyExcluded.size; + const temporaryCountBefore = temporarilyExcluded.size; + for ( const hn of hostnames ) { + permanentlyExcluded.delete(hn); + temporarilyExcluded.delete(hn); + } + const promises = []; + if ( permanentlyExcluded.size !== permanentCountBefore ) { + if ( permanentlyExcluded.size === 0 ) { + promises.push( + localRemove('excludedStrictBlockHostnames') + ); + } else { + promises.push( + localWrite('excludedStrictBlockHostnames', Array.from(permanentlyExcluded)) + ); + } + } + if ( temporarilyExcluded.size !== temporaryCountBefore ) { + if ( temporarilyExcluded.size === 0 ) { + promises.push( + sessionRemove('excludedStrictBlockHostnames') + ); + } else { + promises.push( + sessionWrite('excludedStrictBlockHostnames', Array.from(temporarilyExcluded)) + ); + } + } + if ( promises.length === 0 ) { return; } + await Promise.all(promises); + return updateDynamicRules(); } /******************************************************************************/ @@ -583,7 +582,7 @@ async function enableRulesets(ids) { } if ( enableRulesetSet.size === 0 && disableRulesetSet.size === 0 ) { - return; + return false; } const enableRulesetIds = Array.from(enableRulesetSet); @@ -597,7 +596,9 @@ async function enableRulesets(ids) { } await dnr.updateEnabledRulesets({ enableRulesetIds, disableRulesetIds }); - return updateDynamicRules(); + await updateDynamicRules(); + + return true; } /******************************************************************************/ @@ -622,11 +623,11 @@ async function getEnabledRulesetsDetails() { /******************************************************************************/ export { - TRUSTED_DIRECTIVE_BASE_RULE_ID, - getRulesetDetails, - getDynamicRules, - enableRulesets, defaultRulesetsFromLanguage, + enableRulesets, + excludeFromStrictBlock, + filteringModesToDNR, + getRulesetDetails, getEnabledRulesetsDetails, updateDynamicRules, }; diff --git a/platform/mv3/extension/js/strict-block.js b/platform/mv3/extension/js/strict-block.js index c9c686d9d..8311be51f 100644 --- a/platform/mv3/extension/js/strict-block.js +++ b/platform/mv3/extension/js/strict-block.js @@ -38,7 +38,7 @@ setURL(self.location.hash.slice(1)); /******************************************************************************/ -const urlToFragment = raw => { +function urlToFragment(raw) { try { const fragment = new DocumentFragment(); const url = new URL(raw); @@ -51,12 +51,23 @@ const urlToFragment = raw => { } catch(_) { } return raw; -}; +} + +/******************************************************************************/ + +async function proceed() { + await sendMessage({ + what: 'excludeFromStrictBlock', + hostname: toURL.hostname, + permanent: qs$('#disableWarning').checked, + }); + window.location.replace(toURL.href); +} /******************************************************************************/ dom.clear('#theURL > p > span:first-of-type'); -qs$('#theURL > p > span:first-of-type').append(urlToFragment(toURL)); +qs$('#theURL > p > span:first-of-type').append(urlToFragment(toURL.href)); /******************************************************************************/ @@ -64,8 +75,6 @@ qs$('#theURL > p > span:first-of-type').append(urlToFragment(toURL)); // Parse URL to extract as much useful information as possible. This is // useful to assist the user in deciding whether to navigate to the web page. (( ) => { - if ( typeof URL !== 'function' ) { return; } - const reURL = /^https?:\/\//; const liFromParam = function(name, value) { @@ -124,27 +133,26 @@ qs$('#theURL > p > span:first-of-type').append(urlToFragment(toURL)); return true; }; - if ( renderParams(qs$('#parsed'), toURL) === false ) { return; } + if ( renderParams(qs$('#parsed'), toURL.href) === false ) { return; } dom.cl.remove('#toggleParse', 'hidden'); dom.on('#toggleParse', 'click', ( ) => { dom.cl.toggle('#theURL', 'collapsed'); - vAPI.localStorage.setItem( - 'document-blocked-expand-url', - (dom.cl.has('#theURL', 'collapsed') === false).toString() - ); + //vAPI.localStorage.setItem( + // 'document-blocked-expand-url', + // (dom.cl.has('#theURL', 'collapsed') === false).toString() + //); }); - vAPI.localStorage.getItemAsync('document-blocked-expand-url').then(value => { - dom.cl.toggle('#theURL', 'collapsed', value !== 'true' && value !== true); - }); + //vAPI.localStorage.getItemAsync('document-blocked-expand-url').then(value => { + // dom.cl.toggle('#theURL', 'collapsed', value !== 'true' && value !== true); + //}); })(); /******************************************************************************/ -// https://www.reddit.com/r/uBlockOrigin/comments/breeux/close_this_window_doesnt_work_on_firefox/ - +// https://www.reddit.com/r/uBlockOrigin/comments/breeux/ if ( window.history.length > 1 ) { dom.on('#back', 'click', ( ) => { window.history.back(); @@ -159,37 +167,6 @@ if ( window.history.length > 1 ) { qs$('#back').style.display = 'none'; } -/******************************************************************************/ - -const getTargetHostname = function() { - const url = new URL(toURL); - return url.hostname; -}; - -const proceedToURL = function() { - window.location.replace(toURL); -}; - -const proceedTemporary = async function() { - await sendMessage({ - what: 'temporarilyWhitelistDocument', - hostname: getTargetHostname(), - }); - proceedToURL(); -}; - -const proceedPermanent = async function() { - await sendMessage({ - what: 'toggleHostnameSwitch', - name: 'no-strict-blocking', - hostname: getTargetHostname(), - deep: true, - state: true, - persist: true, - }); - proceedToURL(); -}; - dom.on('#disableWarning', 'change', ev => { const checked = ev.target.checked; dom.cl.toggle('[data-i18n="docblockedBack"]', 'disabled', checked); @@ -197,11 +174,7 @@ dom.on('#disableWarning', 'change', ev => { }); dom.on('#proceed', 'click', ( ) => { - if ( qs$('#disableWarning').checked ) { - proceedPermanent(); - } else { - proceedTemporary(); - } + proceed(); }); /******************************************************************************/