diff --git a/platform/mv3/extension/3p-filters.html b/platform/mv3/extension/3p-filters.html index b1a11d9e9..92dbb3943 100644 --- a/platform/mv3/extension/3p-filters.html +++ b/platform/mv3/extension/3p-filters.html @@ -16,8 +16,13 @@
-

+

+ + _ +

+

+

diff --git a/platform/mv3/extension/_locales/en/messages.json b/platform/mv3/extension/_locales/en/messages.json index 6d48b4be8..5b7b29ecb 100644 --- a/platform/mv3/extension/_locales/en/messages.json +++ b/platform/mv3/extension/_locales/en/messages.json @@ -115,6 +115,14 @@ "message": "Block CSP reports", "description": "background information: https://github.com/gorhill/uBlock/issues/3150" }, + "omnipotenceLabel": { + "message": "Enable extended filtering on all websites", + "description": "Header for a ruleset section in 'Filter lists pane'" + }, + "omnipotenceLegend": { + "message": "uBO Lite can apply extended filtering on a given website only after you explicitly grant the extension permissions to modify data on that website. This setting allows you to grant permissions for extended filtering to all websites at once.", + "description": "Header for a ruleset section in 'Filter lists pane'" + }, "3pGroupDefault": { "message": "Default", "description": "Header for a ruleset section in 'Filter lists pane'" diff --git a/platform/mv3/extension/css/3p-filters.css b/platform/mv3/extension/css/3p-filters.css index 741b92369..93ca6cd21 100644 --- a/platform/mv3/extension/css/3p-filters.css +++ b/platform/mv3/extension/css/3p-filters.css @@ -5,9 +5,14 @@ body { margin-bottom: 6rem; } +legend { + color: var(--ink-3); + font-size: var(--font-size-smaller); + padding: var(--default-gap-xxsmall); + } #actions { background-color: var(--surface-1); - padding: 0.5em 0; + padding: 1px 0; position: sticky; top: 0; z-index: 10; diff --git a/platform/mv3/extension/css/dashboard-common.css b/platform/mv3/extension/css/dashboard-common.css index da2c05226..f7e9c827b 100644 --- a/platform/mv3/extension/css/dashboard-common.css +++ b/platform/mv3/extension/css/dashboard-common.css @@ -1,5 +1,6 @@ body > div.body { margin: 0 1em; + max-width: min(600px, 100vw); } h2, h3 { margin: 1em 0; diff --git a/platform/mv3/extension/css/popup.css b/platform/mv3/extension/css/popup.css index e977144e4..4e0a6d2c8 100644 --- a/platform/mv3/extension/css/popup.css +++ b/platform/mv3/extension/css/popup.css @@ -186,6 +186,9 @@ body.mobile.no-tooltips .toolRibbon .tool { #toggleGreatPowers { position: relative; } +body.hasOmnipotence #toggleGreatPowers { + pointer-events: none; + } #toggleGreatPowers .badge { bottom: 4px; font-size: var(--font-size-xsmall); @@ -202,7 +205,7 @@ body:not(.hasGreatPowers) [data-i18n-title="popupRevokeGreatPowers"], body.hasGreatPowers [data-i18n-title="popupGrantGreatPowers"] { display: none; } -body [data-i18n-title="popupRevokeGreatPowers"] { +body:not(.hasOmnipotence) [data-i18n-title="popupRevokeGreatPowers"] { fill: var(--popup-power-ink); } diff --git a/platform/mv3/extension/js/3p-filters.js b/platform/mv3/extension/js/3p-filters.js index 7783cae8a..aa8858808 100644 --- a/platform/mv3/extension/js/3p-filters.js +++ b/platform/mv3/extension/js/3p-filters.js @@ -23,7 +23,7 @@ /******************************************************************************/ -import { sendMessage } from './ext.js'; +import { browser, sendMessage } from './ext.js'; import { i18n$ } from './i18n.js'; import { dom, qs$, qsa$ } from './dom.js'; import { simpleStorage } from './storage.js'; @@ -42,7 +42,7 @@ const renderNumber = function(value) { /******************************************************************************/ -const renderFilterLists = function(soft) { +function renderFilterLists(soft) { const { enabledRulesets, rulesetDetails } = cachedRulesetData; const listGroupTemplate = qs$('#templates .groupEntry'); const listEntryTemplate = qs$('#templates .listEntry'); @@ -186,11 +186,13 @@ const renderFilterLists = function(soft) { } renderWidgets(); -}; +} /******************************************************************************/ const renderWidgets = function() { + qs$('#omnipotenceWidget input').checked = cachedRulesetData.hasOmnipotence; + dom.cl.toggle( qs$('#buttonApply'), 'disabled', @@ -217,7 +219,30 @@ const renderWidgets = function() { /******************************************************************************/ -const hashFromCurrentFromSettings = function() { +async function onOmnipotenceChanged(ev) { + const input = ev.target; + const newState = input.checked; + + const oldState = await browser.permissions.contains({ + origins: [ '' ] + }); + if ( newState === oldState ) { return; } + if ( newState ) { + browser.permissions.request({ origins: [ '' ] }); + } else { + browser.permissions.remove({ origins: [ '' ] }); + } +} + +dom.on( + qs$('#omnipotenceWidget input'), + 'change', + ev => { onOmnipotenceChanged(ev); } +); + +/******************************************************************************/ + +function hashFromCurrentFromSettings() { const hash = []; const listHash = []; for ( const liEntry of qsa$('#lists .listEntry[data-listkey]') ) { @@ -227,7 +252,7 @@ const hashFromCurrentFromSettings = function() { } hash.push(listHash.sort().join()); return hash.join(); -}; +} self.hasUnsavedData = function() { return hashFromCurrentFromSettings() !== filteringSettingsHash; @@ -251,7 +276,7 @@ dom.on( /******************************************************************************/ -const applyEnabledRulesets = async function() { +async function applyEnabledRulesets() { const enabledRulesets = []; for ( const liEntry of qsa$('#lists .listEntry[data-listkey]') ) { if ( qs$('input[type="checkbox"]:checked', liEntry) === null ) { continue; } @@ -264,13 +289,13 @@ const applyEnabledRulesets = async function() { }); filteringSettingsHash = hashFromCurrentFromSettings(); -}; +} -const buttonApplyHandler = async function() { +async function buttonApplyHandler() { dom.cl.remove(qs$('#buttonApply'), 'enabled'); await applyEnabledRulesets(); renderWidgets(); -}; +} dom.on( qs$('#buttonApply'), @@ -282,13 +307,13 @@ dom.on( // Collapsing of unused lists. -const mustHideUnusedLists = function(which) { +function mustHideUnusedLists(which) { const hideAll = hideUnusedSet.has('*'); if ( which === '*' ) { return hideAll; } return hideUnusedSet.has(which) !== hideAll; -}; +} -const toggleHideUnusedLists = function(which) { +function toggleHideUnusedLists(which) { const doesHideAll = hideUnusedSet.has('*'); let groupSelector; let mustHide; @@ -325,7 +350,7 @@ const toggleHideUnusedLists = function(which) { 'hideUnusedFilterLists', Array.from(hideUnusedSet) ); -}; +} dom.on( qs$('#lists'), diff --git a/platform/mv3/extension/js/background.js b/platform/mv3/extension/js/background.js index 5bfbd17c3..e852b441a 100644 --- a/platform/mv3/extension/js/background.js +++ b/platform/mv3/extension/js/background.js @@ -43,7 +43,7 @@ import { import { getInjectableCount, - registerInjectable, + registerInjectables, } from './scripting-manager.js'; import { @@ -113,26 +113,24 @@ async function saveRulesetConfig() { /******************************************************************************/ -async function hasGreatPowers(origin) { +function hasGreatPowers(origin) { return browser.permissions.contains({ origins: [ `${origin}/*` ], }); } -function grantGreatPowers(hostname) { - return browser.permissions.request({ - origins: [ `*://${hostname}/*` ], +function hasOmnipotence() { + return browser.permissions.contains({ + origins: [ '' ], }); } -function revokeGreatPowers(hostname) { - return browser.permissions.remove({ - origins: [ `*://${hostname}/*` ], - }); +function onPermissionsAdded(permissions) { + registerInjectables(permissions.origins); } -function onPermissionsChanged() { - registerInjectable(); +function onPermissionsRemoved(permissions) { + registerInjectables(permissions.origins); } /******************************************************************************/ @@ -145,7 +143,7 @@ function onMessage(request, sender, callback) { rulesetConfig.enabledRulesets = request.enabledRulesets; return Promise.all([ saveRulesetConfig(), - registerInjectable(), + registerInjectables(), ]); }).then(( ) => { callback(); @@ -157,50 +155,40 @@ function onMessage(request, sender, callback) { Promise.all([ getRulesetDetails(), dnr.getEnabledRulesets(), + hasOmnipotence(), ]).then(results => { - const [ rulesetDetails, enabledRulesets ] = results; + const [ rulesetDetails, enabledRulesets, hasOmnipotence ] = results; callback({ enabledRulesets, rulesetDetails: Array.from(rulesetDetails.values()), + hasOmnipotence, }); }); return true; } - case 'grantGreatPowers': - grantGreatPowers(request.hostname).then(granted => { - console.info(`Granted uBOL great powers on ${request.hostname}: ${granted}`); - callback(granted); - }); - return true; - case 'popupPanelData': { Promise.all([ matchesTrustedSiteDirective(request), + hasOmnipotence(), hasGreatPowers(request.origin), getEnabledRulesetsStats(), getInjectableCount(request.origin), ]).then(results => { callback({ isTrusted: results[0], - hasGreatPowers: results[1], - rulesetDetails: results[2], - injectableCount: results[3], + hasOmnipotence: results[1], + hasGreatPowers: results[2], + rulesetDetails: results[3], + injectableCount: results[4], }); }); return true; } - case 'revokeGreatPowers': - revokeGreatPowers(request.hostname).then(removed => { - console.info(`Revoked great powers from uBOL on ${request.hostname}: ${removed}`); - callback(removed); - }); - return true; - case 'toggleTrustedSiteDirective': { toggleTrustedSiteDirective(request).then(response => { - registerInjectable().then(( ) => { + registerInjectables().then(( ) => { callback(response); }); }); @@ -232,7 +220,7 @@ async function start() { // 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. - registerInjectable(); + registerInjectables(); const enabledRulesets = await dnr.getEnabledRulesets(); console.log(`Enabled rulesets: ${enabledRulesets}`); @@ -249,6 +237,6 @@ async function start() { runtime.onMessage.addListener(onMessage); - browser.permissions.onAdded.addListener(onPermissionsChanged); - browser.permissions.onRemoved.addListener(onPermissionsChanged); + browser.permissions.onAdded.addListener(onPermissionsAdded); + browser.permissions.onRemoved.addListener(onPermissionsRemoved); })(); diff --git a/platform/mv3/extension/js/popup.js b/platform/mv3/extension/js/popup.js index 6b6725ed5..f0c18a8f5 100644 --- a/platform/mv3/extension/js/popup.js +++ b/platform/mv3/extension/js/popup.js @@ -157,9 +157,10 @@ dom.on(qs$('#lessButton'), 'click', ( ) => { /******************************************************************************/ async function grantGreatPowers() { - const granted = await sendMessage({ - what: 'grantGreatPowers', - hostname: tabHostname, + if ( tabHostname === '' ) { return; } + const targetHostname = tabHostname.replace(/^www\./, ''); + const granted = await browser.permissions.request({ + origins: [ `*://*.${targetHostname}/*` ], }); if ( granted !== true ) { return; } dom.cl.add(dom.body, 'hasGreatPowers'); @@ -167,9 +168,10 @@ async function grantGreatPowers() { } async function revokeGreatPowers() { - const removed = await sendMessage({ - what: 'revokeGreatPowers', - hostname: tabHostname, + if ( tabHostname === '' ) { return; } + const targetHostname = tabHostname.replace(/^www\./, ''); + const removed = await browser.permissions.remove({ + origins: [ `*://*.${targetHostname}/*` ], }); if ( removed !== true ) { return; } dom.cl.remove(dom.body, 'hasGreatPowers'); @@ -212,6 +214,12 @@ async function init() { popupPanelData.isTrusted === true ); + dom.cl.toggle( + dom.body, + 'hasOmnipotence', + popupPanelData.hasOmnipotence === true + ); + dom.cl.toggle( dom.body, 'hasGreatPowers', diff --git a/platform/mv3/extension/js/scripting-manager.js b/platform/mv3/extension/js/scripting-manager.js index d003031fe..0a92aa644 100644 --- a/platform/mv3/extension/js/scripting-manager.js +++ b/platform/mv3/extension/js/scripting-manager.js @@ -29,12 +29,7 @@ import { browser, dnr } from './ext.js'; import { fetchJSON } from './fetch.js'; import { getAllTrustedSiteDirectives } from './trusted-sites.js'; -import { - parsedURLromOrigin, - toBroaderHostname, - fidFromFileName, - fnameFromFileId, -} from './utils.js'; +import * as ut from './utils.js'; /******************************************************************************/ @@ -57,28 +52,6 @@ function getScriptingDetails() { /******************************************************************************/ -const matchesFromHostnames = hostnames => { - const out = []; - for ( const hn of hostnames ) { - if ( hn === '*' ) { - out.push('*://*/*'); - } else { - out.push(`*://*.${hn}/*`); - } - } - return out; -}; - -const hostnamesFromMatches = origins => { - const out = []; - for ( const origin of origins ) { - const match = /^\*:\/\/([^\/]+)\/\*/.exec(origin); - if ( match === null ) { continue; } - out.push(match[1]); - } - return out; -}; - const arrayEq = (a, b) => { if ( a === undefined ) { return b === undefined; } if ( b === undefined ) { return false; } @@ -96,20 +69,20 @@ const toRegisterable = (fname, entry) => { id: fname, }; if ( entry.matches ) { - directive.matches = matchesFromHostnames(entry.matches); + directive.matches = ut.matchesFromHostnames(entry.matches); } else { - directive.matches = [ '*://*/*' ]; + directive.matches = [ '' ]; } if ( entry.excludeMatches ) { - directive.excludeMatches = matchesFromHostnames(entry.excludeMatches); + directive.excludeMatches = ut.matchesFromHostnames(entry.excludeMatches); } directive.js = [ `/rulesets/js/${fname.slice(0,2)}/${fname.slice(2)}.js` ]; - if ( (fidFromFileName(fname) & RUN_AT_BIT) !== 0 ) { + if ( (ut.fidFromFileName(fname) & RUN_AT_BIT) !== 0 ) { directive.runAt = 'document_end'; } else { directive.runAt = 'document_start'; } - if ( (fidFromFileName(fname) & MAIN_WORLD_BIT) !== 0 ) { + if ( (ut.fidFromFileName(fname) & MAIN_WORLD_BIT) !== 0 ) { directive.world = 'MAIN'; } return directive; @@ -122,22 +95,31 @@ const MAIN_WORLD_BIT = 0b01; const shouldUpdate = (registered, candidate) => { const matches = candidate.matches && - matchesFromHostnames(candidate.matches); + ut.matchesFromHostnames(candidate.matches); if ( arrayEq(registered.matches, matches) === false ) { return true; } const excludeMatches = candidate.excludeMatches && - matchesFromHostnames(candidate.excludeMatches); + ut.matchesFromHostnames(candidate.excludeMatches); if ( arrayEq(registered.excludeMatches, excludeMatches) === false ) { return true; } return false; }; +const isTrustedHostname = (trustedSites, hn) => { + if ( trustedSites.size === 0 ) { return false; } + while ( hn ) { + if ( trustedSites.has(hn) ) { return true; } + hn = ut.toBroaderHostname(hn); + } + return false; +}; + /******************************************************************************/ async function getInjectableCount(origin) { - const url = parsedURLromOrigin(origin); + const url = ut.parsedURLromOrigin(origin); if ( url === undefined ) { return 0; } const [ @@ -161,7 +143,7 @@ async function getInjectableCount(origin) { } else if ( Array.isArray(fids) ) { total += fids.length; } - hn = toBroaderHostname(hn); + hn = ut.toBroaderHostname(hn); } } @@ -170,49 +152,27 @@ async function getInjectableCount(origin) { /******************************************************************************/ -async function registerInjectable() { - - if ( browser.scripting === undefined ) { return false; } - - const [ - hostnames, +function registerSomeInjectables(args) { + const { + hostnamesSet, trustedSites, rulesetIds, - registered, scriptingDetails, - ] = await Promise.all([ - browser.permissions.getAll(), - getAllTrustedSiteDirectives(), - dnr.getEnabledRulesets(), - browser.scripting.getRegisteredContentScripts(), - getScriptingDetails(), - ]).then(results => { - results[0] = new Set(hostnamesFromMatches(results[0].origins)); - results[1] = new Set(results[1]); - return results; - }); + } = args; - const toRegister = new Map(); - - const isTrustedHostname = hn => { - while ( hn ) { - if ( trustedSites.has(hn) ) { return true; } - hn = toBroaderHostname(hn); - } - return false; - }; + const toRegisterMap = new Map(); 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 = fnameFromFileId(fid); - const existing = toRegister.get(fname); + const fname = ut.fnameFromFileId(fid); + const existing = toRegisterMap.get(fname); if ( existing ) { existing.matches.push(hn); } else { - toRegister.set(fname, { matches: [ hn ] }); + toRegisterMap.set(fname, { matches: [ hn ] }); } } }; @@ -220,47 +180,91 @@ async function registerInjectable() { for ( const rulesetId of rulesetIds ) { const details = scriptingDetails.get(rulesetId); if ( details === undefined ) { continue; } - for ( let hn of hostnames ) { - if ( isTrustedHostname(hn) ) { continue; } + for ( let hn of hostnamesSet ) { + if ( isTrustedHostname(trustedSites, hn) ) { continue; } while ( hn ) { checkMatches(details, hn); - hn = toBroaderHostname(hn); + hn = ut.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 ] }); - } - } - }; + return toRegisterMap; +} + +function registerAllInjectables(args) { + const { + trustedSites, + rulesetIds, + scriptingDetails, + } = args; + + const toRegisterMap = new Map(); 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); + for ( let [ hn, fids ] of details.matches ) { + if ( isTrustedHostname(trustedSites, hn) ) { continue; } + if ( typeof fids === 'number' ) { fids = [ fids ]; } + for ( const fid of fids ) { + const fname = ut.fnameFromFileId(fid); + const existing = toRegisterMap.get(fname); + if ( existing ) { + existing.matches.push(hn); + } else { + toRegisterMap.set(fname, { matches: [ hn ] }); + } } } } + return toRegisterMap; +} + +/******************************************************************************/ + +async function registerInjectables(origins) { + void origins; + + if ( browser.scripting === undefined ) { return false; } + + const [ + hostnamesSet, + trustedSites, + rulesetIds, + scriptingDetails, + registered, + ] = await Promise.all([ + browser.permissions.getAll(), + getAllTrustedSiteDirectives(), + dnr.getEnabledRulesets(), + getScriptingDetails(), + browser.scripting.getRegisteredContentScripts(), + ]).then(results => { + results[0] = new Set(ut.hostnamesFromMatches(results[0].origins)); + results[1] = new Set(results[1]); + return results; + }); + + const toRegisterMap = hostnamesSet.has('*') + ? registerAllInjectables({ + trustedSites, + rulesetIds, + scriptingDetails, + }) + : registerSomeInjectables({ + hostnamesSet, + trustedSites, + rulesetIds, + scriptingDetails, + }); + const before = new Map(registered.map(entry => [ entry.id, entry ])); const toAdd = []; const toUpdate = []; - for ( const [ fname, entry ] of toRegister ) { + for ( const [ fname, entry ] of toRegisterMap ) { if ( before.has(fname) === false ) { toAdd.push(toRegisterable(fname, entry)); continue; @@ -272,7 +276,7 @@ async function registerInjectable() { const toRemove = []; for ( const fname of before.keys() ) { - if ( toRegister.has(fname) ) { continue; } + if ( toRegisterMap.has(fname) ) { continue; } toRemove.push(fname); } @@ -298,5 +302,5 @@ async function registerInjectable() { export { getInjectableCount, - registerInjectable + registerInjectables }; diff --git a/platform/mv3/extension/js/utils.js b/platform/mv3/extension/js/utils.js index 5d7447fac..23ba6a0c5 100644 --- a/platform/mv3/extension/js/utils.js +++ b/platform/mv3/extension/js/utils.js @@ -42,6 +42,34 @@ const toBroaderHostname = hn => { /******************************************************************************/ +const matchesFromHostnames = hostnames => { + const out = []; + for ( const hn of hostnames ) { + if ( hn === '*' ) { + out.push(''); + } else { + out.push(`*://*.${hn}/*`); + } + } + return out; +}; + +const hostnamesFromMatches = origins => { + const out = []; + for ( const origin of origins ) { + if ( origin === '' ) { + out.push('*'); + continue; + } + const match = /^\*:\/\/(?:\*\.)?([^\/]+)\/\*/.exec(origin); + if ( match === null ) { continue; } + out.push(match[1]); + } + return out; +}; + +/******************************************************************************/ + const fnameFromFileId = fid => fid.toString(32).padStart(7, '0'); @@ -53,6 +81,8 @@ const fidFromFileName = fname => export { parsedURLromOrigin, toBroaderHostname, + matchesFromHostnames, + hostnamesFromMatches, fnameFromFileId, fidFromFileName, };