summaryrefslogtreecommitdiffstats
path: root/platform/mv3/extension/js/scripting-manager.js
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-17 05:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-17 05:47:55 +0000
commit31d6ff6f931696850c348007241195ab3b2eddc7 (patch)
tree615cb1c57ce9f6611bad93326b9105098f379609 /platform/mv3/extension/js/scripting-manager.js
parentInitial commit. (diff)
downloadublock-origin-31d6ff6f931696850c348007241195ab3b2eddc7.tar.xz
ublock-origin-31d6ff6f931696850c348007241195ab3b2eddc7.zip
Adding upstream version 1.55.0+dfsg.upstream/1.55.0+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'platform/mv3/extension/js/scripting-manager.js')
-rw-r--r--platform/mv3/extension/js/scripting-manager.js563
1 files changed, 563 insertions, 0 deletions
diff --git a/platform/mv3/extension/js/scripting-manager.js b/platform/mv3/extension/js/scripting-manager.js
new file mode 100644
index 0000000..e6eebf2
--- /dev/null
+++ b/platform/mv3/extension/js/scripting-manager.js
@@ -0,0 +1,563 @@
+/*******************************************************************************
+
+ uBlock Origin Lite - a comprehensive, MV3-compliant content blocker
+ 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 } from './ext.js';
+import { fetchJSON } from './fetch.js';
+import { getFilteringModeDetails } from './mode-manager.js';
+import { getEnabledRulesetsDetails } from './ruleset-manager.js';
+
+import * as ut from './utils.js';
+
+/******************************************************************************/
+
+const isGecko = browser.runtime.getURL('').startsWith('moz-extension://');
+
+const resourceDetailPromises = new Map();
+
+function getScriptletDetails() {
+ let promise = resourceDetailPromises.get('scriptlet');
+ if ( promise !== undefined ) { return promise; }
+ promise = fetchJSON('/rulesets/scriptlet-details').then(
+ entries => new Map(entries)
+ );
+ resourceDetailPromises.set('scriptlet', promise);
+ return promise;
+}
+
+function getGenericDetails() {
+ let promise = resourceDetailPromises.get('generic');
+ if ( promise !== undefined ) { return promise; }
+ promise = fetchJSON('/rulesets/generic-details').then(
+ entries => new Map(entries)
+ );
+ resourceDetailPromises.set('generic', promise);
+ return promise;
+}
+
+/******************************************************************************/
+
+// Important: We need to sort the arrays for fast comparison
+const arrayEq = (a = [], b = [], sort = true) => {
+ const alen = a.length;
+ if ( alen !== b.length ) { return false; }
+ if ( sort ) { a.sort(); b.sort(); }
+ for ( let i = 0; i < alen; i++ ) {
+ if ( a[i] !== b[i] ) { return false; }
+ }
+ return true;
+};
+
+/******************************************************************************/
+
+// The extensions API does not always return exactly what we fed it, so we
+// need to normalize some entries to be sure we properly detect changes when
+// comparing registered entries vs. entries to register.
+
+const normalizeRegisteredContentScripts = registered => {
+ for ( const entry of registered ) {
+ const { css = [], js = [] } = entry;
+ for ( let i = 0; i < css.length; i++ ) {
+ const path = css[i];
+ if ( path.startsWith('/') ) { continue; }
+ css[i] = `/${path}`;
+ }
+ for ( let i = 0; i < js.length; i++ ) {
+ const path = js[i];
+ if ( path.startsWith('/') ) { continue; }
+ js[i] = `/${path}`;
+ }
+ }
+ return registered;
+};
+
+/******************************************************************************/
+
+function registerHighGeneric(context, genericDetails) {
+ const { before, filteringModeDetails, rulesetsDetails } = context;
+
+ const excludeHostnames = [];
+ const css = [];
+ for ( const details of rulesetsDetails ) {
+ const hostnames = genericDetails.get(details.id);
+ if ( hostnames !== undefined ) {
+ excludeHostnames.push(...hostnames);
+ }
+ const count = details.css?.generichigh || 0;
+ if ( count === 0 ) { continue; }
+ css.push(`/rulesets/scripting/generichigh/${details.id}.css`);
+ }
+
+ if ( css.length === 0 ) { return; }
+
+ const { none, basic, optimal, complete } = filteringModeDetails;
+ const matches = [];
+ const excludeMatches = [];
+ if ( complete.has('all-urls') ) {
+ excludeMatches.push(...ut.matchesFromHostnames(none));
+ excludeMatches.push(...ut.matchesFromHostnames(basic));
+ excludeMatches.push(...ut.matchesFromHostnames(optimal));
+ excludeMatches.push(...ut.matchesFromHostnames(excludeHostnames));
+ matches.push('<all_urls>');
+ } else {
+ matches.push(
+ ...ut.matchesFromHostnames(
+ ut.subtractHostnameIters(
+ Array.from(complete),
+ excludeHostnames
+ )
+ )
+ );
+ }
+
+ if ( matches.length === 0 ) { return; }
+
+ const registered = before.get('css-generichigh');
+ before.delete('css-generichigh'); // Important!
+
+ // https://github.com/w3c/webextensions/issues/414#issuecomment-1623992885
+ // Once supported, add:
+ // cssOrigin: 'USER',
+ const directive = {
+ id: 'css-generichigh',
+ css,
+ matches,
+ excludeMatches,
+ runAt: 'document_end',
+ };
+
+ // register
+ if ( registered === undefined ) {
+ context.toAdd.push(directive);
+ return;
+ }
+
+ // update
+ if (
+ arrayEq(registered.css, css, false) === false ||
+ arrayEq(registered.matches, matches) === false ||
+ arrayEq(registered.excludeMatches, excludeMatches) === false
+ ) {
+ context.toRemove.push('css-generichigh');
+ context.toAdd.push(directive);
+ }
+}
+
+/******************************************************************************/
+
+function registerGeneric(context, genericDetails) {
+ const { before, filteringModeDetails, rulesetsDetails } = context;
+
+ const excludeHostnames = [];
+ const js = [];
+ for ( const details of rulesetsDetails ) {
+ const hostnames = genericDetails.get(details.id);
+ if ( hostnames !== undefined ) {
+ excludeHostnames.push(...hostnames);
+ }
+ const count = details.css?.generic || 0;
+ if ( count === 0 ) { continue; }
+ js.push(`/rulesets/scripting/generic/${details.id}.js`);
+ }
+
+ if ( js.length === 0 ) { return; }
+
+ js.push('/js/scripting/css-generic.js');
+
+ const { none, basic, optimal, complete } = filteringModeDetails;
+ const matches = [];
+ const excludeMatches = [];
+ if ( complete.has('all-urls') ) {
+ excludeMatches.push(...ut.matchesFromHostnames(none));
+ excludeMatches.push(...ut.matchesFromHostnames(basic));
+ excludeMatches.push(...ut.matchesFromHostnames(optimal));
+ excludeMatches.push(...ut.matchesFromHostnames(excludeHostnames));
+ matches.push('<all_urls>');
+ } else {
+ matches.push(
+ ...ut.matchesFromHostnames(
+ ut.subtractHostnameIters(
+ Array.from(complete),
+ excludeHostnames
+ )
+ )
+ );
+ }
+
+ if ( matches.length === 0 ) { return; }
+
+ const registered = before.get('css-generic');
+ before.delete('css-generic'); // Important!
+
+ const directive = {
+ id: 'css-generic',
+ js,
+ matches,
+ excludeMatches,
+ runAt: 'document_idle',
+ };
+
+ // register
+ if ( registered === undefined ) {
+ context.toAdd.push(directive);
+ return;
+ }
+
+ // update
+ if (
+ arrayEq(registered.js, js, false) === false ||
+ arrayEq(registered.matches, matches) === false ||
+ arrayEq(registered.excludeMatches, excludeMatches) === false
+ ) {
+ context.toRemove.push('css-generic');
+ context.toAdd.push(directive);
+ }
+}
+
+/******************************************************************************/
+
+function registerProcedural(context) {
+ const { before, filteringModeDetails, rulesetsDetails } = context;
+
+ const js = [];
+ for ( const rulesetDetails of rulesetsDetails ) {
+ const count = rulesetDetails.css?.procedural || 0;
+ if ( count === 0 ) { continue; }
+ js.push(`/rulesets/scripting/procedural/${rulesetDetails.id}.js`);
+ }
+ if ( js.length === 0 ) { return; }
+
+ const { none, basic, optimal, complete } = filteringModeDetails;
+ const matches = [
+ ...ut.matchesFromHostnames(optimal),
+ ...ut.matchesFromHostnames(complete),
+ ];
+ if ( matches.length === 0 ) { return; }
+
+ js.push('/js/scripting/css-procedural.js');
+
+ const excludeMatches = [];
+ if ( none.has('all-urls') === false ) {
+ excludeMatches.push(...ut.matchesFromHostnames(none));
+ }
+ if ( basic.has('all-urls') === false ) {
+ excludeMatches.push(...ut.matchesFromHostnames(basic));
+ }
+
+ const registered = before.get('css-procedural');
+ before.delete('css-procedural'); // Important!
+
+ const directive = {
+ id: 'css-procedural',
+ js,
+ allFrames: true,
+ matches,
+ excludeMatches,
+ runAt: 'document_end',
+ };
+
+ // register
+ if ( registered === undefined ) {
+ context.toAdd.push(directive);
+ return;
+ }
+
+ // update
+ if (
+ arrayEq(registered.js, js, false) === false ||
+ arrayEq(registered.matches, matches) === false ||
+ arrayEq(registered.excludeMatches, excludeMatches) === false
+ ) {
+ context.toRemove.push('css-procedural');
+ context.toAdd.push(directive);
+ }
+}
+
+/******************************************************************************/
+
+function registerDeclarative(context) {
+ const { before, filteringModeDetails, rulesetsDetails } = context;
+
+ const js = [];
+ for ( const rulesetDetails of rulesetsDetails ) {
+ const count = rulesetDetails.css?.declarative || 0;
+ if ( count === 0 ) { continue; }
+ js.push(`/rulesets/scripting/declarative/${rulesetDetails.id}.js`);
+ }
+ if ( js.length === 0 ) { return; }
+
+ const { none, basic, optimal, complete } = filteringModeDetails;
+ const matches = [
+ ...ut.matchesFromHostnames(optimal),
+ ...ut.matchesFromHostnames(complete),
+ ];
+ if ( matches.length === 0 ) { return; }
+
+ js.push('/js/scripting/css-declarative.js');
+
+ const excludeMatches = [];
+ if ( none.has('all-urls') === false ) {
+ excludeMatches.push(...ut.matchesFromHostnames(none));
+ }
+ if ( basic.has('all-urls') === false ) {
+ excludeMatches.push(...ut.matchesFromHostnames(basic));
+ }
+
+ const registered = before.get('css-declarative');
+ before.delete('css-declarative'); // Important!
+
+ const directive = {
+ id: 'css-declarative',
+ js,
+ allFrames: true,
+ matches,
+ excludeMatches,
+ runAt: 'document_start',
+ };
+
+ // register
+ if ( registered === undefined ) {
+ context.toAdd.push(directive);
+ return;
+ }
+
+ // update
+ if (
+ arrayEq(registered.js, js, false) === false ||
+ arrayEq(registered.matches, matches) === false ||
+ arrayEq(registered.excludeMatches, excludeMatches) === false
+ ) {
+ context.toRemove.push('css-declarative');
+ context.toAdd.push(directive);
+ }
+}
+
+/******************************************************************************/
+
+function registerSpecific(context) {
+ const { before, filteringModeDetails, rulesetsDetails } = context;
+
+ const js = [];
+ for ( const rulesetDetails of rulesetsDetails ) {
+ const count = rulesetDetails.css?.specific || 0;
+ if ( count === 0 ) { continue; }
+ js.push(`/rulesets/scripting/specific/${rulesetDetails.id}.js`);
+ }
+ if ( js.length === 0 ) { return; }
+
+ const { none, basic, optimal, complete } = filteringModeDetails;
+ const matches = [
+ ...ut.matchesFromHostnames(optimal),
+ ...ut.matchesFromHostnames(complete),
+ ];
+ if ( matches.length === 0 ) { return; }
+
+ js.push('/js/scripting/css-specific.js');
+
+ const excludeMatches = [];
+ if ( none.has('all-urls') === false ) {
+ excludeMatches.push(...ut.matchesFromHostnames(none));
+ }
+ if ( basic.has('all-urls') === false ) {
+ excludeMatches.push(...ut.matchesFromHostnames(basic));
+ }
+
+ const registered = before.get('css-specific');
+ before.delete('css-specific'); // Important!
+
+ const directive = {
+ id: 'css-specific',
+ js,
+ allFrames: true,
+ matches,
+ excludeMatches,
+ runAt: 'document_start',
+ };
+
+ // register
+ if ( registered === undefined ) {
+ context.toAdd.push(directive);
+ return;
+ }
+
+ // update
+ if (
+ arrayEq(registered.js, js, false) === false ||
+ arrayEq(registered.matches, matches) === false ||
+ arrayEq(registered.excludeMatches, excludeMatches) === false
+ ) {
+ context.toRemove.push('css-specific');
+ context.toAdd.push(directive);
+ }
+}
+
+/******************************************************************************/
+
+function registerScriptlet(context, scriptletDetails) {
+ const { before, filteringModeDetails, rulesetsDetails } = context;
+
+ const hasBroadHostPermission =
+ filteringModeDetails.optimal.has('all-urls') ||
+ filteringModeDetails.complete.has('all-urls');
+
+ const permissionRevokedMatches = [
+ ...ut.matchesFromHostnames(filteringModeDetails.none),
+ ...ut.matchesFromHostnames(filteringModeDetails.basic),
+ ];
+ const permissionGrantedHostnames = [
+ ...filteringModeDetails.optimal,
+ ...filteringModeDetails.complete,
+ ];
+
+ for ( const rulesetId of rulesetsDetails.map(v => v.id) ) {
+ const scriptletList = scriptletDetails.get(rulesetId);
+ if ( scriptletList === undefined ) { continue; }
+
+ for ( const [ token, scriptletHostnames ] of scriptletList ) {
+ const id = `${rulesetId}.${token}`;
+ const registered = before.get(id);
+
+ const matches = [];
+ const excludeMatches = [];
+ let targetHostnames = [];
+ if ( hasBroadHostPermission ) {
+ excludeMatches.push(...permissionRevokedMatches);
+ if ( scriptletHostnames.length > 100 ) {
+ targetHostnames = [ '*' ];
+ } else {
+ targetHostnames = scriptletHostnames;
+ }
+ } else if ( permissionGrantedHostnames.length !== 0 ) {
+ if ( scriptletHostnames.includes('*') ) {
+ targetHostnames = permissionGrantedHostnames;
+ } else {
+ targetHostnames = ut.intersectHostnameIters(
+ permissionGrantedHostnames,
+ scriptletHostnames
+ );
+ }
+ }
+ if ( targetHostnames.length === 0 ) { continue; }
+ matches.push(...ut.matchesFromHostnames(targetHostnames));
+
+ before.delete(id); // Important!
+
+ const directive = {
+ id,
+ js: [ `/rulesets/scripting/scriptlet/${id}.js` ],
+ allFrames: true,
+ matches,
+ excludeMatches,
+ runAt: 'document_start',
+ };
+
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1736575
+ // `MAIN` world not yet supported in Firefox
+ if ( isGecko === false ) {
+ directive.world = 'MAIN';
+ }
+
+ // register
+ if ( registered === undefined ) {
+ context.toAdd.push(directive);
+ continue;
+ }
+
+ // update
+ if (
+ arrayEq(registered.matches, matches) === false ||
+ arrayEq(registered.excludeMatches, excludeMatches) === false
+ ) {
+ context.toRemove.push(id);
+ context.toAdd.push(directive);
+ }
+ }
+ }
+}
+
+/******************************************************************************/
+
+async function registerInjectables(origins) {
+ void origins;
+
+ if ( browser.scripting === undefined ) { return false; }
+
+ const [
+ filteringModeDetails,
+ rulesetsDetails,
+ scriptletDetails,
+ genericDetails,
+ registered,
+ ] = await Promise.all([
+ getFilteringModeDetails(),
+ getEnabledRulesetsDetails(),
+ getScriptletDetails(),
+ getGenericDetails(),
+ browser.scripting.getRegisteredContentScripts(),
+ ]);
+ const before = new Map(
+ normalizeRegisteredContentScripts(registered).map(
+ entry => [ entry.id, entry ]
+ )
+ );
+ const toAdd = [], toRemove = [];
+ const context = {
+ filteringModeDetails,
+ rulesetsDetails,
+ before,
+ toAdd,
+ toRemove,
+ };
+
+ registerDeclarative(context);
+ registerProcedural(context);
+ registerScriptlet(context, scriptletDetails);
+ registerSpecific(context);
+ registerGeneric(context, genericDetails);
+ registerHighGeneric(context, genericDetails);
+
+ toRemove.push(...Array.from(before.keys()));
+
+ if ( toRemove.length !== 0 ) {
+ ut.ubolLog(`Unregistered ${toRemove} content (css/js)`);
+ await browser.scripting.unregisterContentScripts({ ids: toRemove })
+ .catch(reason => { console.info(reason); });
+ }
+
+ if ( toAdd.length !== 0 ) {
+ ut.ubolLog(`Registered ${toAdd.map(v => v.id)} content (css/js)`);
+ await browser.scripting.registerContentScripts(toAdd)
+ .catch(reason => { console.info(reason); });
+ }
+
+ return true;
+}
+
+/******************************************************************************/
+
+export {
+ registerInjectables
+};