1
0
mirror of https://github.com/gorhill/uBlock.git synced 2024-07-05 11:37:01 +02:00

[mv3] Add badge reflecting number of injectable content on current site

Additonally, general code review.
This commit is contained in:
Raymond Hill 2022-09-17 08:26:41 -04:00
parent c0bce368a7
commit e1b54514cc
No known key found for this signature in database
GPG Key ID: 25E1490B761470C2
10 changed files with 234 additions and 123 deletions

View File

@ -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"] {

View File

@ -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}`);

View File

@ -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 || [] ) {

View File

@ -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);
}
}
}

View File

@ -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 };

View File

@ -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}`);

View File

@ -137,9 +137,7 @@ try {
return oe.apply(this, arguments);
}
}.bind();
})(
self.$args$
);
})(...self.$args$);
/******************************************************************************/

View File

@ -113,9 +113,7 @@ try {
return Reflect.apply(...arguments).then(o => pruner(o));
},
});
})(
self.$args$
);
})(...self.$args$);
/******************************************************************************/

View File

@ -155,9 +155,7 @@ try {
});
};
trapChain(window, chain);
})(
self.$args$
);
})(...self.$args$);
/******************************************************************************/

View File

@ -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