1
0
mirror of https://github.com/gorhill/uBlock.git synced 2024-11-17 16:02:33 +01:00
uBlock/platform/mv3/extension/js/scripting-manager.js
Raymond Hill 232c44eeb2
[mv3] Add scriptlet support; improve reliability of cosmetic filtering
First iteration of adding scriptlet support. As with cosmetic
filtering, scriptlet niijection occurs only on sites for which
uBO Lite was granted extended permissions.

At the moment, only three scriptlets are supported:
- abort-current-script
- json-prune
- set-constant

More will be added in the future.
2022-09-16 15:56:35 -04:00

255 lines
7.2 KiB
JavaScript

/*******************************************************************************
uBlock Origin - a browser extension to block requests.
Copyright (C) 2022-present Raymond Hill
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see {http://www.gnu.org/licenses/}.
Home: https://github.com/gorhill/uBlock
*/
/* jshint esversion:11 */
'use strict';
/******************************************************************************/
import { browser, dnr } from './ext.js';
import { fetchJSON } from './fetch.js';
/******************************************************************************/
const CSS_TYPE = 0;
const JS_TYPE = 1;
/******************************************************************************/
let cssDetailsPromise;
let scriptletDetailsPromise;
function getCSSDetails() {
if ( cssDetailsPromise !== undefined ) {
return cssDetailsPromise;
}
cssDetailsPromise = fetchJSON('/content-css/css-specific').then(rules => {
return new Map(rules);
});
return cssDetailsPromise;
}
function getScriptletDetails() {
if ( scriptletDetailsPromise !== undefined ) {
return scriptletDetailsPromise;
}
scriptletDetailsPromise = fetchJSON('/content-js/scriptlet-details').then(rules => {
return new Map(rules);
});
return scriptletDetailsPromise;
}
/******************************************************************************/
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 toRegisterable = (fname, entry) => {
const directive = {
id: fname,
allFrames: true,
};
if ( entry.matches ) {
directive.matches = matchesFromHostnames(entry.y);
} else {
directive.matches = [ '*://*/*' ];
}
if ( entry.excludeMatches ) {
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`
];
} else if ( entry.type === JS_TYPE ) {
directive.js = [
`/content-js/${entry.id}/${fname.slice(0,1)}/${fname.slice(1,8)}.js`
];
directive.runAt = 'document_start';
directive.world = 'MAIN';
}
return directive;
};
/******************************************************************************/
const shouldRegister = (origins, matches) => {
for ( const origin of origins ) {
if ( origin === '*' || Array.isArray(matches) === false ) {
return true;
}
let hn = origin;
for (;;) {
if ( matches.includes(hn) ) {
return true;
}
if ( hn === '*' ) { break; }
const pos = hn.indexOf('.');
hn = pos !== -1
? hn.slice(pos+1)
: '*';
}
}
return false;
};
/******************************************************************************/
async function getInjectableCount(hostname) {
const [
rulesetIds,
cssDetails,
scriptletDetails,
] = await Promise.all([
dnr.getEnabledRulesets(),
getCSSDetails(),
getScriptletDetails(),
]);
let total = 0;
for ( const rulesetId of rulesetIds ) {
if ( cssDetails.has(rulesetId) ) {
for ( const entry of cssDetails ) {
if ( shouldRegister([ hostname ], entry[1].y) === true ) {
total += 1;
}
}
}
if ( scriptletDetails.has(rulesetId) ) {
for ( const entry of cssDetails ) {
if ( shouldRegister([ hostname ], entry[1].y) === true ) {
total += 1;
}
}
}
}
return total;
}
/******************************************************************************/
async function registerInjectable() {
const [
origins,
rulesetIds,
registered,
cssDetails,
scriptletDetails,
] = await Promise.all([
browser.permissions.getAll(),
dnr.getEnabledRulesets(),
browser.scripting.getRegisteredContentScripts(),
getCSSDetails(),
getScriptletDetails(),
]).then(results => {
results[0] = new Set(hostnamesFromMatches(results[0].origins));
return results;
});
if ( origins.has('*') && origins.size > 1 ) {
origins.clear();
origins.add('*');
}
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 ( 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);
}
}
}
const before = new Set(registered.map(entry => entry.id));
const toAdd = [];
for ( const [ fname, entry ] of toRegister ) {
if ( before.has(fname) ) { continue; }
toAdd.push(toRegisterable(fname, entry));
}
const toRemove = [];
for ( const fname of before ) {
if ( toRegister.has(fname) ) { continue; }
toRemove.push(fname);
}
const todo = [];
if ( toRemove.length !== 0 ) {
todo.push(browser.scripting.unregisterContentScripts(toRemove));
console.info(`Unregistered ${toRemove.length} content (css/js)`);
}
if ( toAdd.length !== 0 ) {
todo.push(browser.scripting.registerContentScripts(toAdd));
console.info(`Registered ${toAdd.length} content (css/js)`);
}
if ( todo.length === 0 ) { return; }
return Promise.all(todo);
}
/******************************************************************************/
export {
getInjectableCount,
registerInjectable
};