From e1b54514cce0658a1ae27038f547e8adce5ab304 Mon Sep 17 00:00:00 2001 From: Raymond Hill Date: Sat, 17 Sep 2022 08:26:41 -0400 Subject: [PATCH] [mv3] Add badge reflecting number of injectable content on current site Additonally, general code review. --- platform/mv3/extension/css/popup.css | 5 +- platform/mv3/extension/js/background.js | 35 +++- platform/mv3/extension/js/popup.js | 4 + .../mv3/extension/js/scripting-manager.js | 75 ++++--- platform/mv3/extension/js/utils.js | 37 ++++ platform/mv3/make-rulesets.js | 188 ++++++++++-------- .../mv3/scriptlets/abort-current-script.js | 4 +- platform/mv3/scriptlets/json-prune.js | 4 +- platform/mv3/scriptlets/set-constant.js | 4 +- src/js/static-net-filtering.js | 1 + 10 files changed, 234 insertions(+), 123 deletions(-) create mode 100644 platform/mv3/extension/js/utils.js diff --git a/platform/mv3/extension/css/popup.css b/platform/mv3/extension/css/popup.css index 0bc4b1933..e09228c17 100644 --- a/platform/mv3/extension/css/popup.css +++ b/platform/mv3/extension/css/popup.css @@ -185,11 +185,12 @@ body.mobile.no-tooltips .toolRibbon .tool { position: relative; } #toggleGreatPowers .badge { + bottom: 4px; font-size: var(--font-size-xsmall); line-height: 1; - right: 4px; + pointer-events: none; position: absolute; - bottom: 2px; + right: 4px; } body:not(.hasGreatPowers) [data-i18n-title="popupGrantGreatPowers"], body.hasGreatPowers [data-i18n-title="popupRevokeGreatPowers"] { diff --git a/platform/mv3/extension/js/background.js b/platform/mv3/extension/js/background.js index 07c289812..a8a66c9d2 100644 --- a/platform/mv3/extension/js/background.js +++ b/platform/mv3/extension/js/background.js @@ -27,7 +27,8 @@ import { browser, dnr, i18n, runtime } from './ext.js'; import { fetchJSON } from './fetch.js'; -import { registerInjectable } from './scripting-manager.js'; +import { getInjectableCount, registerInjectable } from './scripting-manager.js'; +import { parsedURLromOrigin } from './utils.js'; /******************************************************************************/ @@ -229,7 +230,9 @@ async function updateRegexRules() { /******************************************************************************/ async function matchesTrustedSiteDirective(details) { - const url = new URL(details.origin); + 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; } @@ -241,11 +244,14 @@ async function matchesTrustedSiteDirective(details) { if ( pos === -1 ) { break; } hostname = hostname.slice(pos+1); } + return false; } async function addTrustedSiteDirective(details) { - const url = new URL(details.origin); + 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 ) { @@ -254,6 +260,7 @@ async function addTrustedSiteDirective(details) { rule.condition.requestDomains = []; } } + if ( rule === undefined ) { rule = { id: TRUSTED_DIRECTIVE_BASE_RULE_ID, @@ -270,15 +277,19 @@ async function addTrustedSiteDirective(details) { } 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 = new URL(details.origin); + 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; } @@ -286,6 +297,7 @@ async function removeTrustedSiteDirective(details) { 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; @@ -295,7 +307,9 @@ async function removeTrustedSiteDirective(details) { 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({ @@ -303,11 +317,14 @@ async function removeTrustedSiteDirective(details) { }); return false; } + rule.condition.requestDomains = Array.from(domainSet); + await dnr.updateDynamicRules({ addRules: [ rule ], removeRuleIds: [ TRUSTED_DIRECTIVE_BASE_RULE_ID ], }); + return false; } @@ -450,11 +467,13 @@ function onMessage(request, sender, callback) { matchesTrustedSiteDirective(request), hasGreatPowers(request.origin), getEnabledRulesetsStats(), + getInjectableCount(request.origin), ]).then(results => { callback({ isTrusted: results[0], hasGreatPowers: results[1], rulesetDetails: results[2], + injectableCount: results[3], }); }); return true; @@ -493,13 +512,15 @@ async function start() { // We need to update the regex rules only when ruleset version changes. const currentVersion = getCurrentVersion(); if ( currentVersion !== rulesetConfig.version ) { - await updateRegexRules(); console.log(`Version change: ${rulesetConfig.version} => ${currentVersion}`); + await Promise.all([ + updateRegexRules(), + registerInjectable(), + ]); rulesetConfig.version = currentVersion; + saveRulesetConfig(); } - saveRulesetConfig(); - const enabledRulesets = await dnr.getEnabledRulesets(); console.log(`Enabled rulesets: ${enabledRulesets}`); diff --git a/platform/mv3/extension/js/popup.js b/platform/mv3/extension/js/popup.js index b991a6ded..6b6725ed5 100644 --- a/platform/mv3/extension/js/popup.js +++ b/platform/mv3/extension/js/popup.js @@ -219,6 +219,10 @@ async function init() { ); dom.text(qs$('#hostname'), tabHostname); + dom.text( + qs$('#toggleGreatPowers .badge'), + popupPanelData.injectableCount || '' + ); const parent = qs$('#rulesetStats'); for ( const details of popupPanelData.rulesetDetails || [] ) { diff --git a/platform/mv3/extension/js/scripting-manager.js b/platform/mv3/extension/js/scripting-manager.js index ea1433238..6a07db9eb 100644 --- a/platform/mv3/extension/js/scripting-manager.js +++ b/platform/mv3/extension/js/scripting-manager.js @@ -27,6 +27,7 @@ import { browser, dnr } from './ext.js'; import { fetchJSON } from './fetch.js'; +import { parsedURLromOrigin } from './utils.js'; /******************************************************************************/ @@ -89,21 +90,21 @@ const toRegisterable = (fname, entry) => { id: fname, allFrames: true, }; - if ( entry.matches ) { + if ( entry.y ) { directive.matches = matchesFromHostnames(entry.y); } else { directive.matches = [ '*://*/*' ]; } - if ( entry.excludeMatches ) { + if ( entry.n ) { directive.excludeMatches = matchesFromHostnames(entry.n); } if ( entry.type === CSS_TYPE ) { directive.css = [ - `/content-css/${entry.id}/${fname.slice(0,1)}/${fname.slice(1,8)}.css` + `/content-css/${fname.slice(0,1)}/${fname.slice(1,2)}/${fname.slice(2,8)}.css` ]; } else if ( entry.type === JS_TYPE ) { directive.js = [ - `/content-js/${entry.id}/${fname.slice(0,1)}/${fname.slice(1,8)}.js` + `/content-js/${fname.slice(0,1)}/${fname.slice(1,8)}.js` ]; directive.runAt = 'document_start'; directive.world = 'MAIN'; @@ -115,15 +116,12 @@ const toRegisterable = (fname, entry) => { /******************************************************************************/ const shouldRegister = (origins, matches) => { + if ( Array.isArray(matches) === false ) { return true; } for ( const origin of origins ) { - if ( origin === '*' || Array.isArray(matches) === false ) { - return true; - } + if ( origin === '*' ) { return true; } let hn = origin; for (;;) { - if ( matches.includes(hn) ) { - return true; - } + if ( matches.includes(hn) ) { return true; } if ( hn === '*' ) { break; } const pos = hn.indexOf('.'); hn = pos !== -1 @@ -136,7 +134,9 @@ const shouldRegister = (origins, matches) => { /******************************************************************************/ -async function getInjectableCount(hostname) { +async function getInjectableCount(origin) { + const url = parsedURLromOrigin(origin); + if ( url === undefined ) { return 0; } const [ rulesetIds, @@ -151,23 +151,22 @@ async function getInjectableCount(hostname) { let total = 0; for ( const rulesetId of rulesetIds ) { - if ( cssDetails.has(rulesetId) ) { - for ( const entry of cssDetails ) { - if ( shouldRegister([ hostname ], entry[1].y) === true ) { + const entries = cssDetails.get(rulesetId); + for ( const entry of entries ) { + if ( shouldRegister([ url.hostname ], entry[1].y) ) { total += 1; } } } - if ( scriptletDetails.has(rulesetId) ) { - for ( const entry of cssDetails ) { - if ( shouldRegister([ hostname ], entry[1].y) === true ) { + const entries = cssDetails.get(rulesetId); + for ( const entry of entries ) { + if ( shouldRegister([ url.hostname ], entry[1].y) ) { total += 1; } } } - } return total; @@ -199,23 +198,47 @@ async function registerInjectable() { origins.add('*'); } + const mergeEntries = (a, b) => { + if ( b.y !== undefined ) { + if ( a.y === undefined ) { + a.y = new Set(b.y); + } else { + b.y.forEach(v => a.y.add(v)); + } + } + if ( b.n !== undefined ) { + if ( a.n === undefined ) { + a.n = new Set(b.n); + } else { + b.n.forEach(v => a.n.add(v)); + } + } + return a; + }; + const toRegister = new Map(); for ( const rulesetId of rulesetIds ) { if ( cssDetails.has(rulesetId) ) { for ( const [ fname, entry ] of cssDetails.get(rulesetId) ) { - entry.id = rulesetId; - entry.type = CSS_TYPE; - if ( shouldRegister(origins, entry.y) !== true ) { continue; } - toRegister.set(fname, entry); + if ( shouldRegister(origins, entry.y) === false ) { continue; } + let existing = toRegister.get(fname); + if ( existing === undefined ) { + existing = { type: CSS_TYPE }; + toRegister.set(fname, existing); + } + mergeEntries(existing, entry); } } if ( scriptletDetails.has(rulesetId) ) { for ( const [ fname, entry ] of scriptletDetails.get(rulesetId) ) { - entry.id = rulesetId; - entry.type = JS_TYPE; - if ( shouldRegister(origins, entry.y) !== true ) { continue; } - toRegister.set(fname, entry); + if ( shouldRegister(origins, entry.y) === false ) { continue; } + let existing = toRegister.get(fname); + if ( existing === undefined ) { + existing = { type: JS_TYPE }; + toRegister.set(fname, existing); + } + mergeEntries(existing, entry); } } } diff --git a/platform/mv3/extension/js/utils.js b/platform/mv3/extension/js/utils.js new file mode 100644 index 000000000..b9d28869f --- /dev/null +++ b/platform/mv3/extension/js/utils.js @@ -0,0 +1,37 @@ +/******************************************************************************* + + 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'; + +/******************************************************************************/ + +function parsedURLromOrigin(origin) { + try { + return new URL(origin); + } catch(ex) { + } +} + +/******************************************************************************/ + +export { parsedURLromOrigin }; diff --git a/platform/mv3/make-rulesets.js b/platform/mv3/make-rulesets.js index 14d6f8b09..b095bd5ed 100644 --- a/platform/mv3/make-rulesets.js +++ b/platform/mv3/make-rulesets.js @@ -305,6 +305,8 @@ function optimizeExtendedFilters(filters) { /******************************************************************************/ +const globalCSSFileSet = new Set(); + const style = [ ' display:none!important;', ' position:absolute!important;', @@ -320,15 +322,34 @@ function processCosmeticFilters(assetDetails, mapin) { for ( const entry of optimized ) { const selectors = entry.payload.join(',\n'); const fname = createHash('sha256').update(selectors).digest('hex').slice(0,8); - const fpath = `${assetDetails.id}/${fname.slice(0,1)}/${fname.slice(1,8)}`; - writeFile( - `${cssDir}/${fpath}.css`, - `${selectors} {\n${style}\n}\n` - ); - cssEntries.set(fname, { - y: entry.matches, - n: entry.excludeMatches, - }); + if ( globalCSSFileSet.has(fname) === false ) { + globalCSSFileSet.add(fname); + const fpath = `${fname.slice(0,1)}/${fname.slice(1,2)}/${fname.slice(2,8)}`; + writeFile( + `${cssDir}/${fpath}.css`, + `${selectors} {\n${style}\n}\n` + ); + } + const existing = cssEntries.get(fname); + if ( existing === undefined ) { + cssEntries.set(fname, { + y: entry.matches, + n: entry.excludeMatches, + }); + continue; + } + if ( entry.matches ) { + for ( const hn of entry.matches ) { + if ( existing.y.includes(hn) ) { continue; } + existing.y.push(hn); + } + } + if ( entry.excludeMatches ) { + for ( const hn of entry.excludeMatches ) { + if ( existing.n.includes(hn) ) { continue; } + existing.n.push(hn); + } + } } log(`CSS entries: ${cssEntries.size}`); @@ -342,11 +363,58 @@ function processCosmeticFilters(assetDetails, mapin) { /******************************************************************************/ +// Load all available scriptlets into a key-val map, where the key is the +// scriptlet token, and val is the whole content of the file. + +const scriptletDealiasingMap = new Map(); +let scriptletsMapPromise; + +function loadAllScriptlets() { + if ( scriptletsMapPromise !== undefined ) { + return scriptletsMapPromise; + } + + scriptletsMapPromise = fs.readdir('./scriptlets').then(files => { + const reScriptletNameOrAlias = /^\/\/\/\s+(?:name|alias)\s+(\S+)/gm; + const readPromises = []; + for ( const file of files ) { + readPromises.push( + fs.readFile(`./scriptlets/${file}`, { encoding: 'utf8' }) + ); + } + return Promise.all(readPromises).then(results => { + const originalScriptletMap = new Map(); + for ( const text of results ) { + const aliasSet = new Set(); + for (;;) { + const match = reScriptletNameOrAlias.exec(text); + if ( match === null ) { break; } + aliasSet.add(match[1]); + } + if ( aliasSet.size === 0 ) { continue; } + const aliases = Array.from(aliasSet); + originalScriptletMap.set(aliases[0], text); + for ( let i = 0; i < aliases.length; i++ ) { + scriptletDealiasingMap.set(aliases[i], aliases[0]); + } + } + return originalScriptletMap; + }); + }); + + return scriptletsMapPromise; +} + +/******************************************************************************/ + +const globalPatchedScriptletsSet = new Set(); + async function processScriptletFilters(assetDetails, mapin) { if ( mapin === undefined ) { return 0; } - const originalScriptletMap = new Map(); - const dealiasingMap = new Map(); + // Load all available scriptlets into a key-val map, where the key is the + // scriptlet token, and val is the whole content of the file. + const originalScriptletMap = await loadAllScriptlets(); const parseArguments = (raw) => { const out = []; @@ -376,7 +444,7 @@ async function processScriptletFilters(assetDetails, mapin) { let pos = filter.indexOf(','); if ( pos === -1 ) { pos = end; } const parts = filter.trim().split(',').map(s => s.trim()); - const token = dealiasingMap.get(parts[0]) || ''; + const token = scriptletDealiasingMap.get(parts[0]) || ''; if ( token !== '' && originalScriptletMap.has(token) ) { return { token, @@ -387,83 +455,45 @@ async function processScriptletFilters(assetDetails, mapin) { const patchScriptlet = (filter) => { return originalScriptletMap.get(filter.token).replace( - /^self\.\$args\$$/m, - `...${JSON.stringify(filter.args, null, 4)}` + /^(\}\)\(\.\.\.)self\.\$args\$(\);)$/m, + `$1${JSON.stringify(filter.args, null, 4)}$2` ); }; - // Load all available scriptlets into a key-val map, where the key is the - // scriptlet token, and val is the whole content of the file. - const files = await fs.readdir('./scriptlets'); - const reScriptletNameOrAlias = /^\/\/\/\s+(?:name|alias)\s+(\S+)/gm; - for ( const file of files ) { - const text = await fs.readFile( - `./scriptlets/${file}`, - { encoding: 'utf8' } - ); - const aliasSet = new Set(); - for (;;) { - const match = reScriptletNameOrAlias.exec(text); - if ( match === null ) { break; } - aliasSet.add(match[1]); - } - if ( aliasSet.size === 0 ) { continue; } - const aliases = Array.from(aliasSet); - originalScriptletMap.set(aliases[0], text); - for ( let i = 0; i < aliases.length; i++ ) { - dealiasingMap.set(aliases[i], aliases[0]); - } - } + // Generate distinct scriptlet files according to patched scriptlets + const scriptletEntries = new Map(); - // Merge entries after dealiasing and expanding arguments - const normalizedMap = new Map(); - for ( const [ rawFilter, toAdd ] of mapin ) { + for ( const [ rawFilter, entry ] of mapin ) { const normalized = parseFilter(rawFilter); if ( normalized === undefined ) { continue; } - const key = JSON.stringify(normalized); - const toMerge = normalizedMap.get(key); - if ( toMerge === undefined ) { - normalizedMap.set(key, toAdd); + const json = JSON.stringify(normalized); + const fname = createHash('sha256').update(json).digest('hex').slice(0,8); + if ( globalPatchedScriptletsSet.has(fname) === false ) { + globalPatchedScriptletsSet.add(fname); + const scriptlet = patchScriptlet(normalized); + const fpath = `${fname.slice(0,1)}/${fname.slice(1,8)}`; + writeFile(`${scriptletDir}/${fpath}.js`, scriptlet); + } + const existing = scriptletEntries.get(fname); + if ( existing === undefined ) { + scriptletEntries.set(fname, { + y: entry.matches, + n: entry.excludeMatches, + }); continue; } - const matches = new Set(toMerge.matches || []); - const excludeMatches = new Set(toMerge.excludeMatches || []); - if ( toAdd.matches && toAdd.matches.size !== 0 ) { - toAdd.matches.forEach(hn => { - matches.add(hn); - }); + if ( entry.matches ) { + for ( const hn of entry.matches ) { + if ( existing.y.includes(hn) ) { continue; } + existing.y.push(hn); + } } - if ( toAdd.excludeMatches && toAdd.excludeMatches.size !== 0 ) { - toAdd.excludeMatches.forEach(hn => { - excludeMatches.add(hn); - }); + if ( entry.excludeMatches ) { + for ( const hn of entry.excludeMatches ) { + if ( existing.n.includes(hn) ) { continue; } + existing.n.push(hn); + } } - if ( matches.size !== 0 ) { - toMerge.matches = matches.has('*') - ? [ '*' ] - : Array.from(matches); - } - if ( excludeMatches.size !== 0 ) { - toMerge.excludeMatches = excludeMatches.has('*') - ? [ '*' ] - : Array.from(excludeMatches); - } - } - - // Combine injected resources for same matches/excludeMatches instances - //const optimized = optimizeExtendedFilters(normalizedMap); - - // Generate distinct scriptlets according to patched scriptlets - const scriptletEntries = new Map(); - for ( const [ json, entry ] of normalizedMap ) { - const fname = createHash('sha256').update(json).digest('hex').slice(0,8); - const scriptlet = patchScriptlet(JSON.parse(json)); - const fpath = `${assetDetails.id}/${fname.slice(0,1)}/${fname.slice(1,8)}`; - writeFile(`${scriptletDir}/${fpath}.js`, scriptlet); - scriptletEntries.set(fname, { - y: entry.matches, - n: entry.excludeMatches, - }); } log(`Scriptlet entries: ${scriptletEntries.size}`); diff --git a/platform/mv3/scriptlets/abort-current-script.js b/platform/mv3/scriptlets/abort-current-script.js index 066bd3f38..f26b17c1f 100644 --- a/platform/mv3/scriptlets/abort-current-script.js +++ b/platform/mv3/scriptlets/abort-current-script.js @@ -137,9 +137,7 @@ try { return oe.apply(this, arguments); } }.bind(); -})( -self.$args$ -); +})(...self.$args$); /******************************************************************************/ diff --git a/platform/mv3/scriptlets/json-prune.js b/platform/mv3/scriptlets/json-prune.js index 6e6f205d8..a6013a936 100644 --- a/platform/mv3/scriptlets/json-prune.js +++ b/platform/mv3/scriptlets/json-prune.js @@ -113,9 +113,7 @@ try { return Reflect.apply(...arguments).then(o => pruner(o)); }, }); -})( -self.$args$ -); +})(...self.$args$); /******************************************************************************/ diff --git a/platform/mv3/scriptlets/set-constant.js b/platform/mv3/scriptlets/set-constant.js index 27eb66b67..3a6a6d83f 100644 --- a/platform/mv3/scriptlets/set-constant.js +++ b/platform/mv3/scriptlets/set-constant.js @@ -155,9 +155,7 @@ try { }); }; trapChain(window, chain); -})( -self.$args$ -); +})(...self.$args$); /******************************************************************************/ diff --git a/src/js/static-net-filtering.js b/src/js/static-net-filtering.js index 4a1ce8006..02757d7a8 100644 --- a/src/js/static-net-filtering.js +++ b/src/js/static-net-filtering.js @@ -4176,6 +4176,7 @@ FilterContainer.prototype.dnrFromCompiled = function(op, context, ...args) { }; mergeRules(rulesetMap, 'resourceTypes'); mergeRules(rulesetMap, 'initiatorDomains'); + mergeRules(rulesetMap, 'requestDomains'); mergeRules(rulesetMap, 'removeParams'); // Patch case-sensitiveness