[mv3] General code review

Re-arranged resources in a more tidy way. General code review of
various code paths.
This commit is contained in:
Raymond Hill 2022-10-15 13:05:20 -04:00
parent 30bd6c7bb8
commit 1db3748ab1
No known key found for this signature in database
GPG key ID: 25E1490B761470C2
22 changed files with 1832 additions and 1231 deletions

View file

@ -42,7 +42,6 @@ import {
} from './ruleset-manager.js'; } from './ruleset-manager.js';
import { import {
getInjectableCount,
registerInjectables, registerInjectables,
} from './scripting-manager.js'; } from './scripting-manager.js';
@ -63,6 +62,8 @@ const rulesetConfig = {
firstRun: false, firstRun: false,
}; };
const UBOL_ORIGIN = runtime.getURL('').replace(/\/$/, '');
/******************************************************************************/ /******************************************************************************/
function getCurrentVersion() { function getCurrentVersion() {
@ -165,6 +166,9 @@ function onPermissionsRemoved(permissions) {
/******************************************************************************/ /******************************************************************************/
function onMessage(request, sender, callback) { function onMessage(request, sender, callback) {
if ( sender.origin !== UBOL_ORIGIN ) { return; }
switch ( request.what ) { switch ( request.what ) {
case 'applyRulesets': { case 'applyRulesets': {
@ -214,7 +218,6 @@ function onMessage(request, sender, callback) {
hasOmnipotence(), hasOmnipotence(),
hasGreatPowers(request.origin), hasGreatPowers(request.origin),
getEnabledRulesetsDetails(), getEnabledRulesetsDetails(),
getInjectableCount(request.origin),
]).then(results => { ]).then(results => {
callback({ callback({
level: results[0], level: results[0],
@ -222,7 +225,6 @@ function onMessage(request, sender, callback) {
hasOmnipotence: results[1], hasOmnipotence: results[1],
hasGreatPowers: results[2], hasGreatPowers: results[2],
rulesetDetails: results[3], rulesetDetails: results[3],
injectableCount: results[4],
}); });
}); });
return true; return true;

View file

@ -101,7 +101,7 @@ async function updateRegexRules() {
const toFetch = []; const toFetch = [];
for ( const details of rulesetDetails ) { for ( const details of rulesetDetails ) {
if ( details.rules.regexes === 0 ) { continue; } if ( details.rules.regexes === 0 ) { continue; }
toFetch.push(fetchJSON(`/rulesets/${details.id}.regexes`)); toFetch.push(fetchJSON(`/rulesets/regex/${details.id}.regexes`));
} }
const regexRulesets = await Promise.all(toFetch); const regexRulesets = await Promise.all(toFetch);
@ -196,7 +196,7 @@ async function updateRemoveparamRules() {
const toFetch = []; const toFetch = [];
for ( const details of rulesetDetails ) { for ( const details of rulesetDetails ) {
if ( details.rules.removeparams === 0 ) { continue; } if ( details.rules.removeparams === 0 ) { continue; }
toFetch.push(fetchJSON(`/rulesets/${details.id}.removeparams`)); toFetch.push(fetchJSON(`/rulesets/removeparam/${details.id}.removeparams`));
} }
const removeparamRulesets = await Promise.all(toFetch); const removeparamRulesets = await Promise.all(toFetch);

View file

@ -25,7 +25,7 @@
/******************************************************************************/ /******************************************************************************/
import { browser, dnr } from './ext.js'; import { browser } from './ext.js';
import { fetchJSON } from './fetch.js'; import { fetchJSON } from './fetch.js';
import { getFilteringModeDetails } from './mode-manager.js'; import { getFilteringModeDetails } from './mode-manager.js';
import { getEnabledRulesetsDetails } from './ruleset-manager.js'; import { getEnabledRulesetsDetails } from './ruleset-manager.js';
@ -34,29 +34,69 @@ import * as ut from './utils.js';
/******************************************************************************/ /******************************************************************************/
let scriptingDetailsPromise; const resourceDetailPromises = new Map();
function getScriptingDetails() { function getSpecificDetails() {
if ( scriptingDetailsPromise !== undefined ) { let promise = resourceDetailPromises.get('specific');
return scriptingDetailsPromise; if ( promise !== undefined ) { return promise; }
} promise = fetchJSON('/rulesets/specific-details').then(entries => {
scriptingDetailsPromise = fetchJSON('/rulesets/scripting-details').then(entries => {
const out = new Map(); const out = new Map();
for ( const entry of entries ) { for ( const entry of entries ) {
out.set(entry[0], new Map(entry[1])); out.set(entry[0], new Map(entry[1]));
} }
return out; return out;
}); });
return scriptingDetailsPromise; resourceDetailPromises.set('specific', promise);
return promise;
}
function getDeclarativeDetails() {
let promise = resourceDetailPromises.get('declarative');
if ( promise !== undefined ) { return promise; }
promise = fetchJSON('/rulesets/declarative-details').then(
entries => new Map(entries)
);
resourceDetailPromises.set('declarative', promise);
return promise;
}
function getProceduralDetails() {
let promise = resourceDetailPromises.get('procedural');
if ( promise !== undefined ) { return promise; }
promise = fetchJSON('/rulesets/procedural-details').then(
entries => new Map(entries)
);
resourceDetailPromises.set('procedural', promise);
return promise;
}
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 // Important: We need to sort the arrays for fast comparison
const arrayEq = (a = [], b = []) => { const arrayEq = (a = [], b = [], sort = true) => {
const alen = a.length; const alen = a.length;
if ( alen !== b.length ) { return false; } if ( alen !== b.length ) { return false; }
a.sort(); b.sort(); if ( sort ) { a.sort(); b.sort(); }
for ( let i = 0; i < alen; i++ ) { for ( let i = 0; i < alen; i++ ) {
if ( a[i] !== b[i] ) { return false; } if ( a[i] !== b[i] ) { return false; }
} }
@ -65,81 +105,41 @@ const arrayEq = (a = [], b = []) => {
/******************************************************************************/ /******************************************************************************/
const toRegisterableScript = (context, fname, hostnames) => { // The extensions API does not always return exactly what we fed it, so we
if ( context.before.has(fname) ) { // need to normalize some entries to be sure we properly detect changes when
return toUpdatableScript(context, fname, hostnames); // comparing registered entries vs. entries to register.
}
const matches = hostnames
? ut.matchesFromHostnames(hostnames)
: [ '<all_urls>' ];
const excludeMatches = matches.length === 1 && matches[0] === '<all_urls>'
? ut.matchesFromHostnames(context.filteringModeDetails.none)
: [];
const runAt = (ut.fidFromFileName(fname) & RUN_AT_END_BIT) !== 0
? 'document_end'
: 'document_start';
const directive = {
id: fname,
allFrames: true,
matches,
excludeMatches,
js: [ `/rulesets/js/${fname.slice(0,2)}/${fname.slice(2)}.js` ],
runAt,
};
if ( (ut.fidFromFileName(fname) & MAIN_WORLD_BIT) !== 0 ) {
directive.world = 'MAIN';
}
context.toAdd.push(directive);
};
const toUpdatableScript = (context, fname, hostnames) => { const normalizeRegisteredContentScripts = registered => {
const registered = context.before.get(fname); for ( const entry of registered ) {
context.before.delete(fname); // Important! const { js } = entry;
const directive = { id: fname }; for ( let i = 0; i < js.length; i++ ) {
const matches = hostnames const path = js[i];
? ut.matchesFromHostnames(hostnames) if ( path.startsWith('/') ) { continue; }
: [ '<all_urls>' ]; js[i] = `/${path}`;
if ( arrayEq(registered.matches, matches) === false ) {
directive.matches = matches;
} }
const excludeMatches = matches.length === 1 && matches[0] === '<all_urls>'
? ut.matchesFromHostnames(context.filteringModeDetails.none)
: [];
if ( arrayEq(registered.excludeMatches, excludeMatches) === false ) {
directive.excludeMatches = excludeMatches;
}
if ( directive.matches || directive.excludeMatches ) {
context.toUpdate.push(directive);
} }
return registered;
}; };
const RUN_AT_END_BIT = 0b10;
const MAIN_WORLD_BIT = 0b01;
/******************************************************************************/ /******************************************************************************/
async function registerGeneric(context, args) { function registerGeneric(context, genericDetails) {
const { before } = context; const { before, filteringModeDetails, rulesetsDetails } = context;
const registered = before.get('css-generic');
before.delete('css-generic'); // Important!
const {
filteringModeDetails,
rulesetsDetails,
} = args;
const excludeHostnames = [];
const js = []; const js = [];
for ( const details of rulesetsDetails ) { for ( const details of rulesetsDetails ) {
const hostnames = genericDetails.get(details.id);
if ( hostnames !== undefined ) {
excludeHostnames.push(...hostnames);
}
if ( details.css.generic.count === 0 ) { continue; } if ( details.css.generic.count === 0 ) { continue; }
js.push(`/rulesets/js/${details.id}.generic.js`); js.push(`/rulesets/scripting/generic/${details.id}.generic.js`);
} }
if ( js.length === 0 ) { if ( js.length === 0 ) { return; }
if ( registered !== undefined ) {
context.toRemove.push('css-generic'); js.push('/js/scripting/css-generic.js');
}
return;
}
const matches = []; const matches = [];
const excludeMatches = []; const excludeMatches = [];
@ -147,17 +147,23 @@ async function registerGeneric(context, args) {
excludeMatches.push(...ut.matchesFromHostnames(filteringModeDetails.none)); excludeMatches.push(...ut.matchesFromHostnames(filteringModeDetails.none));
excludeMatches.push(...ut.matchesFromHostnames(filteringModeDetails.network)); excludeMatches.push(...ut.matchesFromHostnames(filteringModeDetails.network));
excludeMatches.push(...ut.matchesFromHostnames(filteringModeDetails.extendedSpecific)); excludeMatches.push(...ut.matchesFromHostnames(filteringModeDetails.extendedSpecific));
excludeMatches.push(...ut.matchesFromHostnames(excludeHostnames));
matches.push('<all_urls>'); matches.push('<all_urls>');
} else { } else {
matches.push(...ut.matchesFromHostnames(filteringModeDetails.extendedGeneric)); matches.push(
...ut.matchesFromHostnames(
ut.subtractHostnameIters(
Array.from(filteringModeDetails.extendedGeneric),
excludeHostnames
)
)
);
} }
if ( matches.length === 0 ) { if ( matches.length === 0 ) { return; }
if ( registered !== undefined ) {
context.toRemove.push('css-generic'); const registered = before.get('css-generic');
} before.delete('css-generic'); // Important!
return;
}
// register // register
if ( registered === undefined ) { if ( registered === undefined ) {
@ -173,7 +179,7 @@ async function registerGeneric(context, args) {
// update // update
const directive = { id: 'css-generic' }; const directive = { id: 'css-generic' };
if ( arrayEq(registered.js, js) === false ) { if ( arrayEq(registered.js, js, false) === false ) {
directive.js = js; directive.js = js;
} }
if ( arrayEq(registered.matches, matches) === false ) { if ( arrayEq(registered.matches, matches) === false ) {
@ -189,62 +195,258 @@ async function registerGeneric(context, args) {
/******************************************************************************/ /******************************************************************************/
async function getInjectableCount(origin) { function registerProcedural(context, proceduralDetails) {
const url = ut.parsedURLromOrigin(origin); const { before, filteringModeDetails, rulesetsDetails } = context;
if ( url === undefined ) { return 0; }
const [ const js = [];
rulesetIds, const hostnameMatches = [];
scriptingDetails, for ( const details of rulesetsDetails ) {
] = await Promise.all([ if ( details.css.procedural === 0 ) { continue; }
dnr.getEnabledRulesets(), js.push(`/rulesets/scripting/procedural/${details.id}.procedural.js`);
getScriptingDetails(), if ( proceduralDetails.has(details.id) ) {
]); hostnameMatches.push(...proceduralDetails.get(details.id));
let total = 0;
for ( const rulesetId of rulesetIds ) {
const hostnamesToFidsMap = scriptingDetails.get(rulesetId);
if ( hostnamesToFidsMap === undefined ) { continue; }
let hn = url.hostname;
while ( hn !== '' ) {
const fids = hostnamesToFidsMap.get(hn);
if ( typeof fids === 'number' ) {
total += 1;
} else if ( Array.isArray(fids) ) {
total += fids.length;
}
hn = ut.toBroaderHostname(hn);
} }
} }
return total; if ( js.length === 0 ) { return; }
js.push('/js/scripting/css-procedural.js');
const {
none,
network,
extendedSpecific,
extendedGeneric,
} = filteringModeDetails;
const matches = [];
const excludeMatches = [];
if ( extendedSpecific.has('all-urls') || extendedGeneric.has('all-urls') ) {
excludeMatches.push(...ut.matchesFromHostnames(none));
excludeMatches.push(...ut.matchesFromHostnames(network));
matches.push(...ut.matchesFromHostnames(hostnameMatches));
} else if ( extendedSpecific.size !== 0 || extendedGeneric.size !== 0 ) {
matches.push(
...ut.matchesFromHostnames(
ut.intersectHostnameIters(
[ ...extendedSpecific, ...extendedGeneric ],
hostnameMatches
)
)
);
}
if ( matches.length === 0 ) { return; }
const registered = before.get('css-procedural');
before.delete('css-procedural'); // Important!
// register
if ( registered === undefined ) {
context.toAdd.push({
id: 'css-procedural',
js,
matches,
excludeMatches,
runAt: 'document_end',
});
return;
}
// update
const directive = { id: 'css-procedural' };
if ( arrayEq(registered.js, js, false) === false ) {
directive.js = js;
}
if ( arrayEq(registered.matches, matches) === false ) {
directive.matches = matches;
}
if ( arrayEq(registered.excludeMatches, excludeMatches) === false ) {
directive.excludeMatches = excludeMatches;
}
if ( directive.js || directive.matches || directive.excludeMatches ) {
context.toUpdate.push(directive);
}
} }
/******************************************************************************/ /******************************************************************************/
function registerSpecific(args) { function registerDeclarative(context, declarativeDetails) {
const { const { before, filteringModeDetails, rulesetsDetails } = context;
filteringModeDetails,
rulesetsDetails,
scriptingDetails,
} = args;
// Combined both specific and generic sets const js = [];
const hostnameMatches = [];
for ( const details of rulesetsDetails ) {
if ( details.css.declarative === 0 ) { continue; }
js.push(`/rulesets/scripting/declarative/${details.id}.declarative.js`);
if ( declarativeDetails.has(details.id) ) {
hostnameMatches.push(...declarativeDetails.get(details.id));
}
}
if ( js.length === 0 ) { return; }
js.push('/js/scripting/css-declarative.js');
const {
none,
network,
extendedSpecific,
extendedGeneric,
} = filteringModeDetails;
const matches = [];
const excludeMatches = [];
if ( extendedSpecific.has('all-urls') || extendedGeneric.has('all-urls') ) {
excludeMatches.push(...ut.matchesFromHostnames(none));
excludeMatches.push(...ut.matchesFromHostnames(network));
matches.push(...ut.matchesFromHostnames(hostnameMatches));
} else if ( extendedSpecific.size !== 0 || extendedGeneric.size !== 0 ) {
matches.push(
...ut.matchesFromHostnames(
ut.intersectHostnameIters(
[ ...extendedSpecific, ...extendedGeneric ],
hostnameMatches
)
)
);
}
if ( matches.length === 0 ) { return; }
const registered = before.get('css-declarative');
before.delete('css-declarative'); // Important!
// register
if ( registered === undefined ) {
context.toAdd.push({
id: 'css-declarative',
js,
matches,
excludeMatches,
runAt: 'document_start',
});
return;
}
// update
const directive = { id: 'css-declarative' };
if ( arrayEq(registered.js, js, false) === false ) {
directive.js = js;
}
if ( arrayEq(registered.matches, matches) === false ) {
directive.matches = matches;
}
if ( arrayEq(registered.excludeMatches, excludeMatches) === false ) {
directive.excludeMatches = excludeMatches;
}
if ( directive.js || directive.matches || directive.excludeMatches ) {
context.toUpdate.push(directive);
}
}
/******************************************************************************/
function registerScriptlet(context, scriptletDetails) {
const { before, filteringModeDetails, rulesetsDetails } = context;
const hasBroadHostPermission =
filteringModeDetails.extendedSpecific.has('all-urls') ||
filteringModeDetails.extendedGeneric.has('all-urls');
const permissionRevokedMatches = [
...ut.matchesFromHostnames(filteringModeDetails.none),
...ut.matchesFromHostnames(filteringModeDetails.network),
];
const permissionGrantedHostnames = [
...filteringModeDetails.extendedSpecific,
...filteringModeDetails.extendedGeneric,
];
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 = [];
if ( hasBroadHostPermission ) {
excludeMatches.push(...permissionRevokedMatches);
matches.push(...ut.matchesFromHostnames(scriptletHostnames));
} else if ( permissionGrantedHostnames.length !== 0 ) {
matches.push(
...ut.matchesFromHostnames(
ut.intersectHostnameIters(
permissionGrantedHostnames,
scriptletHostnames
)
)
);
}
if ( matches.length === 0 ) { continue; }
before.delete(id); // Important!
// register
if ( registered === undefined ) {
context.toAdd.push({
id,
js: [ `/rulesets/scripting/scriptlet/${id}.js` ],
matches,
excludeMatches,
runAt: 'document_start',
world: 'MAIN',
});
continue;
}
// update
const directive = { id };
if ( arrayEq(registered.matches, matches) === false ) {
directive.matches = matches;
}
if ( arrayEq(registered.excludeMatches, excludeMatches) === false ) {
directive.excludeMatches = excludeMatches;
}
if ( directive.matches || directive.excludeMatches ) {
context.toUpdate.push(directive);
}
}
}
}
/******************************************************************************/
function registerSpecific(context, specificDetails) {
const { filteringModeDetails } = context;
let toRegisterMap;
if ( if (
filteringModeDetails.extendedSpecific.has('all-urls') || filteringModeDetails.extendedSpecific.has('all-urls') ||
filteringModeDetails.extendedGeneric.has('all-urls') filteringModeDetails.extendedGeneric.has('all-urls')
) { ) {
return registerAllSpecific(args); toRegisterMap = registerSpecificAll(context, specificDetails);
} else {
toRegisterMap = registerSpecificSome(context, specificDetails);
} }
for ( const [ fname, hostnames ] of toRegisterMap ) {
toRegisterableScript(context, fname, hostnames);
}
}
function registerSpecificSome(context, specificDetails) {
const { filteringModeDetails, rulesetsDetails } = context;
const toRegisterMap = new Map();
const targetHostnames = [ const targetHostnames = [
...filteringModeDetails.extendedSpecific, ...filteringModeDetails.extendedSpecific,
...filteringModeDetails.extendedGeneric, ...filteringModeDetails.extendedGeneric,
]; ];
const toRegisterMap = new Map();
const checkMatches = (hostnamesToFidsMap, hn) => { const checkMatches = (hostnamesToFidsMap, hn) => {
let fids = hostnamesToFidsMap.get(hn); let fids = hostnamesToFidsMap.get(hn);
if ( fids === undefined ) { return; } if ( fids === undefined ) { return; }
@ -266,7 +468,7 @@ function registerSpecific(args) {
}; };
for ( const rulesetDetails of rulesetsDetails ) { for ( const rulesetDetails of rulesetsDetails ) {
const hostnamesToFidsMap = scriptingDetails.get(rulesetDetails.id); const hostnamesToFidsMap = specificDetails.get(rulesetDetails.id);
if ( hostnamesToFidsMap === undefined ) { continue; } if ( hostnamesToFidsMap === undefined ) { continue; }
for ( let hn of targetHostnames ) { for ( let hn of targetHostnames ) {
while ( hn ) { while ( hn ) {
@ -279,21 +481,17 @@ function registerSpecific(args) {
return toRegisterMap; return toRegisterMap;
} }
function registerAllSpecific(args) { function registerSpecificAll(context, specificDetails) {
const { const { filteringModeDetails, rulesetsDetails } = context;
filteringModeDetails,
rulesetsDetails,
scriptingDetails,
} = args;
const toRegisterMap = new Map(); const toRegisterMap = new Map();
const excludeSet = new Set([ const excludeSet = new Set([
...filteringModeDetails.network, ...filteringModeDetails.network,
...filteringModeDetails.none, ...filteringModeDetails.none,
]); ]);
for ( const rulesetDetails of rulesetsDetails ) { for ( const rulesetDetails of rulesetsDetails ) {
const hostnamesToFidsMap = scriptingDetails.get(rulesetDetails.id); const hostnamesToFidsMap = specificDetails.get(rulesetDetails.id);
if ( hostnamesToFidsMap === undefined ) { continue; } if ( hostnamesToFidsMap === undefined ) { continue; }
for ( let [ hn, fids ] of hostnamesToFidsMap ) { for ( let [ hn, fids ] of hostnamesToFidsMap ) {
if ( excludeSet.has(hn) ) { continue; } if ( excludeSet.has(hn) ) { continue; }
@ -319,6 +517,48 @@ function registerAllSpecific(args) {
return toRegisterMap; return toRegisterMap;
} }
const toRegisterableScript = (context, fname, hostnames) => {
if ( context.before.has(fname) ) {
return toUpdatableScript(context, fname, hostnames);
}
const matches = hostnames
? ut.matchesFromHostnames(hostnames)
: [ '<all_urls>' ];
const excludeMatches = matches.length === 1 && matches[0] === '<all_urls>'
? ut.matchesFromHostnames(context.filteringModeDetails.none)
: [];
const directive = {
id: fname,
allFrames: true,
matches,
excludeMatches,
js: [ `/rulesets/scripting/specific/${fname.slice(-1)}/${fname.slice(0,-1)}.js` ],
runAt: 'document_start',
};
context.toAdd.push(directive);
};
const toUpdatableScript = (context, fname, hostnames) => {
const registered = context.before.get(fname);
context.before.delete(fname); // Important!
const directive = { id: fname };
const matches = hostnames
? ut.matchesFromHostnames(hostnames)
: [ '<all_urls>' ];
if ( arrayEq(registered.matches, matches) === false ) {
directive.matches = matches;
}
const excludeMatches = matches.length === 1 && matches[0] === '<all_urls>'
? ut.matchesFromHostnames(context.filteringModeDetails.none)
: [];
if ( arrayEq(registered.excludeMatches, excludeMatches) === false ) {
directive.excludeMatches = excludeMatches;
}
if ( directive.matches || directive.excludeMatches ) {
context.toUpdate.push(directive);
}
};
/******************************************************************************/ /******************************************************************************/
async function registerInjectables(origins) { async function registerInjectables(origins) {
@ -329,37 +569,44 @@ async function registerInjectables(origins) {
const [ const [
filteringModeDetails, filteringModeDetails,
rulesetsDetails, rulesetsDetails,
scriptingDetails, declarativeDetails,
proceduralDetails,
scriptletDetails,
specificDetails,
genericDetails,
registered, registered,
] = await Promise.all([ ] = await Promise.all([
getFilteringModeDetails(), getFilteringModeDetails(),
getEnabledRulesetsDetails(), getEnabledRulesetsDetails(),
getScriptingDetails(), getDeclarativeDetails(),
getProceduralDetails(),
getScriptletDetails(),
getSpecificDetails(),
getGenericDetails(),
browser.scripting.getRegisteredContentScripts(), browser.scripting.getRegisteredContentScripts(),
]); ]);
const before = new Map(
const before = new Map(registered.map(entry => [ entry.id, entry ])); normalizeRegisteredContentScripts(registered).map(
entry => [ entry.id, entry ]
)
);
const toAdd = [], toUpdate = [], toRemove = []; const toAdd = [], toUpdate = [], toRemove = [];
const promises = []; const promises = [];
const context = { const context = {
filteringModeDetails, filteringModeDetails,
rulesetsDetails,
before, before,
toAdd, toAdd,
toUpdate, toUpdate,
toRemove, toRemove,
}; };
await registerGeneric(context, { filteringModeDetails, rulesetsDetails, }); registerDeclarative(context, declarativeDetails);
registerProcedural(context, proceduralDetails);
registerScriptlet(context, scriptletDetails);
registerSpecific(context, specificDetails);
registerGeneric(context, genericDetails);
const toRegisterMap = registerSpecific({
filteringModeDetails,
rulesetsDetails,
scriptingDetails,
});
for ( const [ fname, hostnames ] of toRegisterMap ) {
toRegisterableScript(context, fname, hostnames);
}
toRemove.push(...Array.from(before.keys())); toRemove.push(...Array.from(before.keys()));
if ( toRemove.length !== 0 ) { if ( toRemove.length !== 0 ) {
@ -391,6 +638,5 @@ async function registerInjectables(origins) {
/******************************************************************************/ /******************************************************************************/
export { export {
getInjectableCount,
registerInjectables registerInjectables
}; };

View file

@ -0,0 +1,115 @@
/*******************************************************************************
uBlock Origin - a browser extension to block requests.
Copyright (C) 2014-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';
/******************************************************************************/
// Important!
// Isolate from global scope
(function uBOL_cssDeclarative() {
/******************************************************************************/
const declarativeImports = self.declarativeImports || [];
const lookupSelectors = (hn, out) => {
for ( const { argsList, hostnamesMap } of declarativeImports ) {
let argsIndices = hostnamesMap.get(hn);
if ( argsIndices === undefined ) { continue; }
if ( typeof argsIndices === 'number' ) { argsIndices = [ argsIndices ]; }
for ( const argsIndex of argsIndices ) {
const details = argsList[argsIndex];
if ( details.n && details.n.includes(hn) ) { continue; }
out.push(...details.a.map(json => JSON.parse(json)));
}
}
};
let hn;
try { hn = document.location.hostname; } catch(ex) { }
const selectors = [];
while ( hn ) {
lookupSelectors(hn, selectors);
if ( hn === '*' ) { break; }
const pos = hn.indexOf('.');
if ( pos !== -1 ) {
hn = hn.slice(pos + 1);
} else {
hn = '*';
}
}
declarativeImports.length = 0;
/******************************************************************************/
if ( selectors.length === 0 ) { return; }
const cssRuleFromProcedural = details => {
const { tasks, action } = details;
let mq;
if ( tasks !== undefined ) {
if ( tasks.length > 1 ) { return; }
if ( tasks[0][0] !== 'matches-media' ) { return; }
mq = tasks[0][1];
}
let style;
if ( Array.isArray(action) ) {
if ( action[0] !== 'style' ) { return; }
style = action[1];
}
if ( mq === undefined && style === undefined ) { return; }
if ( mq === undefined ) {
return `${details.selector}\n{${style}}`;
}
if ( style === undefined ) {
return `@media ${mq} {\n${details.selector}\n{display:none!important;}\n}`;
}
return `@media ${mq} {\n${details.selector}\n{${style}}\n}`;
};
const sheetText = [];
for ( const selector of selectors ) {
const ruleText = cssRuleFromProcedural(selector);
if ( ruleText === undefined ) { continue; }
sheetText.push(ruleText);
}
if ( sheetText.length === 0 ) { return; }
try {
const sheet = new CSSStyleSheet();
sheet.replace(`@layer{${sheetText.join('\n')}}`);
document.adoptedStyleSheets = [
...document.adoptedStyleSheets,
sheet
];
} catch(ex) {
}
/******************************************************************************/
})();
/******************************************************************************/

View file

@ -0,0 +1,241 @@
/*******************************************************************************
uBlock Origin - a browser extension to block requests.
Copyright (C) 2014-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';
/******************************************************************************/
// Important!
// Isolate from global scope
(function uBOL_cssGeneric() {
const genericSelectorMap = self.genericSelectorMap || new Map();
if ( genericSelectorMap.size === 0 ) { return; }
self.genericSelectorMap = undefined;
/******************************************************************************/
const maxSurveyTimeSlice = 4;
const maxSurveyNodeSlice = 64;
const styleSheetSelectors = [];
const stopAllRatio = 0.95; // To be investigated
let surveyCount = 0;
let surveyMissCount = 0;
let styleSheetTimer;
let processTimer;
let domChangeTimer;
let lastDomChange = Date.now();
/******************************************************************************/
// https://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/
const hashFromStr = (type, s) => {
const len = s.length;
const step = len + 7 >>> 3;
let hash = type;
for ( let i = 0; i < len; i += step ) {
hash = (hash << 5) - hash + s.charCodeAt(i) | 0;
}
return hash & 0x00FFFFFF;
};
/******************************************************************************/
// Extract all classes/ids: these will be passed to the cosmetic
// filtering engine, and in return we will obtain only the relevant
// CSS selectors.
// https://github.com/gorhill/uBlock/issues/672
// http://www.w3.org/TR/2014/REC-html5-20141028/infrastructure.html#space-separated-tokens
// http://jsperf.com/enumerate-classes/6
const uBOL_idFromNode = (node, out) => {
const raw = node.id;
if ( typeof raw !== 'string' || raw.length === 0 ) { return; }
out.push(hashFromStr(0x23 /* '#' */, raw.trim()));
};
// https://github.com/uBlockOrigin/uBlock-issues/discussions/2076
// Performance: avoid using Element.classList
const uBOL_classesFromNode = (node, out) => {
const s = node.getAttribute('class');
if ( typeof s !== 'string' ) { return; }
const len = s.length;
for ( let beg = 0, end = 0; beg < len; beg += 1 ) {
end = s.indexOf(' ', beg);
if ( end === beg ) { continue; }
if ( end === -1 ) { end = len; }
out.push(hashFromStr(0x2E /* '.' */, s.slice(beg, end)));
beg = end;
}
};
/******************************************************************************/
const pendingNodes = {
addedNodes: [],
nodeSet: new Set(),
add(node) {
this.addedNodes.push(node);
},
next(out) {
for ( const added of this.addedNodes ) {
if ( this.nodeSet.has(added) ) { continue; }
if ( added.nodeType === 1 ) {
this.nodeSet.add(added);
}
if ( added.firstElementChild === null ) { continue; }
for ( const descendant of added.querySelectorAll('[id],[class]') ) {
this.nodeSet.add(descendant);
}
}
this.addedNodes.length = 0;
for ( const node of this.nodeSet ) {
this.nodeSet.delete(node);
out.push(node);
if ( out.length === maxSurveyNodeSlice ) { break; }
}
},
hasNodes() {
return this.addedNodes.length !== 0 || this.nodeSet.size !== 0;
},
};
/******************************************************************************/
const uBOL_processNodes = ( ) => {
const t0 = Date.now();
const hashes = [];
const nodes = [];
const deadline = t0 + maxSurveyTimeSlice;
for (;;) {
pendingNodes.next(nodes);
if ( nodes.length === 0 ) { break; }
for ( const node of nodes ) {
uBOL_idFromNode(node, hashes);
uBOL_classesFromNode(node, hashes);
}
nodes.length = 0;
if ( performance.now() >= deadline ) { break; }
}
for ( const hash of hashes ) {
const selectorList = genericSelectorMap.get(hash);
if ( selectorList === undefined ) { continue; }
styleSheetSelectors.push(selectorList);
genericSelectorMap.delete(hash);
}
surveyCount += 1;
if ( styleSheetSelectors.length === 0 ) {
surveyMissCount += 1;
if (
surveyCount >= 100 &&
(surveyMissCount / surveyCount) >= stopAllRatio
) {
stopAll(`too many misses in surveyor (${surveyMissCount}/${surveyCount})`);
}
return;
}
if ( styleSheetTimer !== undefined ) { return; }
styleSheetTimer = self.requestAnimationFrame(( ) => {
styleSheetTimer = undefined;
uBOL_injectStyleSheet();
});
};
/******************************************************************************/
const uBOL_processChanges = mutations => {
for ( let i = 0; i < mutations.length; i++ ) {
const mutation = mutations[i];
for ( const added of mutation.addedNodes ) {
if ( added.nodeType !== 1 ) { continue; }
pendingNodes.add(added);
}
}
if ( pendingNodes.hasNodes() === false ) { return; }
lastDomChange = Date.now();
if ( processTimer !== undefined ) { return; }
processTimer = self.setTimeout(( ) => {
processTimer = undefined;
uBOL_processNodes();
}, 64);
};
/******************************************************************************/
const uBOL_injectStyleSheet = ( ) => {
try {
const sheet = new CSSStyleSheet();
sheet.replace(`@layer{${styleSheetSelectors.join(',')}{display:none!important;}}`);
document.adoptedStyleSheets = [
...document.adoptedStyleSheets,
sheet
];
} catch(ex) {
}
styleSheetSelectors.length = 0;
};
/******************************************************************************/
pendingNodes.add(document);
uBOL_processNodes();
let domMutationObserver = new MutationObserver(uBOL_processChanges);
domMutationObserver.observe(document, {
childList: true,
subtree: true,
});
const needDomChangeObserver = ( ) => {
domChangeTimer = undefined;
if ( domMutationObserver === undefined ) { return; }
if ( (Date.now() - lastDomChange) > 20000 ) {
return stopAll('no more DOM changes');
}
domChangeTimer = self.setTimeout(needDomChangeObserver, 20000);
};
needDomChangeObserver();
/******************************************************************************/
const stopAll = reason => {
if ( domChangeTimer !== undefined ) {
self.clearTimeout(domChangeTimer);
domChangeTimer = undefined;
}
domMutationObserver.disconnect();
domMutationObserver.takeRecords();
domMutationObserver = undefined;
genericSelectorMap.clear();
console.info(`uBOL: Generic cosmetic filtering stopped because ${reason}`);
};
/******************************************************************************/
})();
/******************************************************************************/

View file

@ -0,0 +1,662 @@
/*******************************************************************************
uBlock Origin - a browser extension to block requests.
Copyright (C) 2014-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';
/******************************************************************************/
// Important!
// Isolate from global scope
(function uBOL_cssProcedural() {
/******************************************************************************/
let proceduralFilterer;
/******************************************************************************/
const addStylesheet = text => {
try {
const sheet = new CSSStyleSheet();
sheet.replace(`@layer{${text}}`);
document.adoptedStyleSheets = [
...document.adoptedStyleSheets,
sheet
];
} catch(ex) {
}
};
const nonVisualElements = {
script: true,
style: true,
};
/******************************************************************************/
// 'P' stands for 'Procedural'
class PSelectorTask {
begin() {
}
end() {
}
}
/******************************************************************************/
class PSelectorVoidTask extends PSelectorTask {
constructor(task) {
super();
console.info(`uBO: :${task[0]}() operator does not exist`);
}
transpose() {
}
}
/******************************************************************************/
class PSelectorHasTextTask extends PSelectorTask {
constructor(task) {
super();
let arg0 = task[1], arg1;
if ( Array.isArray(task[1]) ) {
arg1 = arg0[1]; arg0 = arg0[0];
}
this.needle = new RegExp(arg0, arg1);
}
transpose(node, output) {
if ( this.needle.test(node.textContent) ) {
output.push(node);
}
}
}
/******************************************************************************/
class PSelectorIfTask extends PSelectorTask {
constructor(task) {
super();
this.pselector = new PSelector(task[1]);
}
transpose(node, output) {
if ( this.pselector.test(node) === this.target ) {
output.push(node);
}
}
}
PSelectorIfTask.prototype.target = true;
class PSelectorIfNotTask extends PSelectorIfTask {
}
PSelectorIfNotTask.prototype.target = false;
/******************************************************************************/
class PSelectorMatchesCSSTask extends PSelectorTask {
constructor(task) {
super();
this.name = task[1].name;
this.pseudo = task[1].pseudo ? `::${task[1].pseudo}` : null;
let arg0 = task[1].value, arg1;
if ( Array.isArray(arg0) ) {
arg1 = arg0[1]; arg0 = arg0[0];
}
this.value = new RegExp(arg0, arg1);
}
transpose(node, output) {
const style = window.getComputedStyle(node, this.pseudo);
if ( style !== null && this.value.test(style[this.name]) ) {
output.push(node);
}
}
}
class PSelectorMatchesCSSAfterTask extends PSelectorMatchesCSSTask {
constructor(task) {
super(task);
this.pseudo = '::after';
}
}
class PSelectorMatchesCSSBeforeTask extends PSelectorMatchesCSSTask {
constructor(task) {
super(task);
this.pseudo = '::before';
}
}
/******************************************************************************/
class PSelectorMatchesMediaTask extends PSelectorTask {
constructor(task) {
super();
this.mql = window.matchMedia(task[1]);
if ( this.mql.media === 'not all' ) { return; }
this.mql.addEventListener('change', ( ) => {
if ( proceduralFilterer instanceof Object === false ) { return; }
proceduralFilterer.onDOMChanged([ null ]);
});
}
transpose(node, output) {
if ( this.mql.matches === false ) { return; }
output.push(node);
}
}
/******************************************************************************/
class PSelectorMatchesPathTask extends PSelectorTask {
constructor(task) {
super();
let arg0 = task[1], arg1;
if ( Array.isArray(task[1]) ) {
arg1 = arg0[1]; arg0 = arg0[0];
}
this.needle = new RegExp(arg0, arg1);
}
transpose(node, output) {
if ( this.needle.test(self.location.pathname + self.location.search) ) {
output.push(node);
}
}
}
/******************************************************************************/
class PSelectorMinTextLengthTask extends PSelectorTask {
constructor(task) {
super();
this.min = task[1];
}
transpose(node, output) {
if ( node.textContent.length >= this.min ) {
output.push(node);
}
}
}
/******************************************************************************/
class PSelectorOthersTask extends PSelectorTask {
constructor() {
super();
this.targets = new Set();
}
begin() {
this.targets.clear();
}
end(output) {
const toKeep = new Set(this.targets);
const toDiscard = new Set();
const body = document.body;
let discard = null;
for ( let keep of this.targets ) {
while ( keep !== null && keep !== body ) {
toKeep.add(keep);
toDiscard.delete(keep);
discard = keep.previousElementSibling;
while ( discard !== null ) {
if (
nonVisualElements[discard.localName] !== true &&
toKeep.has(discard) === false
) {
toDiscard.add(discard);
}
discard = discard.previousElementSibling;
}
discard = keep.nextElementSibling;
while ( discard !== null ) {
if (
nonVisualElements[discard.localName] !== true &&
toKeep.has(discard) === false
) {
toDiscard.add(discard);
}
discard = discard.nextElementSibling;
}
keep = keep.parentElement;
}
}
for ( discard of toDiscard ) {
output.push(discard);
}
this.targets.clear();
}
transpose(candidate) {
for ( const target of this.targets ) {
if ( target.contains(candidate) ) { return; }
if ( candidate.contains(target) ) {
this.targets.delete(target);
}
}
this.targets.add(candidate);
}
}
/******************************************************************************/
// https://github.com/AdguardTeam/ExtendedCss/issues/31#issuecomment-302391277
// Prepend `:scope ` if needed.
class PSelectorSpathTask extends PSelectorTask {
constructor(task) {
super();
this.spath = task[1];
this.nth = /^(?:\s*[+~]|:)/.test(this.spath);
if ( this.nth ) { return; }
if ( /^\s*>/.test(this.spath) ) {
this.spath = `:scope ${this.spath.trim()}`;
}
}
transpose(node, output) {
const nodes = this.nth
? PSelectorSpathTask.qsa(node, this.spath)
: node.querySelectorAll(this.spath);
for ( const node of nodes ) {
output.push(node);
}
}
// Helper method for other operators.
static qsa(node, selector) {
const parent = node.parentElement;
if ( parent === null ) { return []; }
let pos = 1;
for (;;) {
node = node.previousElementSibling;
if ( node === null ) { break; }
pos += 1;
}
return parent.querySelectorAll(
`:scope > :nth-child(${pos})${selector}`
);
}
}
/******************************************************************************/
class PSelectorUpwardTask extends PSelectorTask {
constructor(task) {
super();
const arg = task[1];
if ( typeof arg === 'number' ) {
this.i = arg;
} else {
this.s = arg;
}
}
transpose(node, output) {
if ( this.s !== '' ) {
const parent = node.parentElement;
if ( parent === null ) { return; }
node = parent.closest(this.s);
if ( node === null ) { return; }
} else {
let nth = this.i;
for (;;) {
node = node.parentElement;
if ( node === null ) { return; }
nth -= 1;
if ( nth === 0 ) { break; }
}
}
output.push(node);
}
}
PSelectorUpwardTask.prototype.i = 0;
PSelectorUpwardTask.prototype.s = '';
/******************************************************************************/
class PSelectorWatchAttrs extends PSelectorTask {
constructor(task) {
super();
this.observer = null;
this.observed = new WeakSet();
this.observerOptions = {
attributes: true,
subtree: true,
};
const attrs = task[1];
if ( Array.isArray(attrs) && attrs.length !== 0 ) {
this.observerOptions.attributeFilter = task[1];
}
}
// TODO: Is it worth trying to re-apply only the current selector?
handler() {
if ( proceduralFilterer instanceof Object ) {
proceduralFilterer.onDOMChanged([ null ]);
}
}
transpose(node, output) {
output.push(node);
if ( this.observed.has(node) ) { return; }
if ( this.observer === null ) {
this.observer = new MutationObserver(this.handler);
}
this.observer.observe(node, this.observerOptions);
this.observed.add(node);
}
}
/******************************************************************************/
class PSelectorXpathTask extends PSelectorTask {
constructor(task) {
super();
this.xpe = document.createExpression(task[1], null);
this.xpr = null;
}
transpose(node, output) {
this.xpr = this.xpe.evaluate(
node,
XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
this.xpr
);
let j = this.xpr.snapshotLength;
while ( j-- ) {
const node = this.xpr.snapshotItem(j);
if ( node.nodeType === 1 ) {
output.push(node);
}
}
}
}
/******************************************************************************/
class PSelector {
constructor(o) {
this.raw = o.raw;
this.selector = o.selector;
this.tasks = [];
const tasks = [];
if ( Array.isArray(o.tasks) === false ) { return; }
for ( const task of o.tasks ) {
const ctor = this.operatorToTaskMap.get(task[0]) || PSelectorVoidTask;
tasks.push(new ctor(task));
}
this.tasks = tasks;
}
prime(input) {
const root = input || document;
if ( this.selector === '' ) { return [ root ]; }
if ( input !== document && /^ [>+~]/.test(this.selector) ) {
return Array.from(PSelectorSpathTask.qsa(input, this.selector));
}
return Array.from(root.querySelectorAll(this.selector));
}
exec(input) {
let nodes = this.prime(input);
for ( const task of this.tasks ) {
if ( nodes.length === 0 ) { break; }
const transposed = [];
task.begin();
for ( const node of nodes ) {
task.transpose(node, transposed);
}
task.end(transposed);
nodes = transposed;
}
return nodes;
}
test(input) {
const nodes = this.prime(input);
for ( const node of nodes ) {
let output = [ node ];
for ( const task of this.tasks ) {
const transposed = [];
task.begin();
for ( const node of output ) {
task.transpose(node, transposed);
}
task.end(transposed);
output = transposed;
if ( output.length === 0 ) { break; }
}
if ( output.length !== 0 ) { return true; }
}
return false;
}
}
PSelector.prototype.operatorToTaskMap = new Map([
[ 'has', PSelectorIfTask ],
[ 'has-text', PSelectorHasTextTask ],
[ 'if', PSelectorIfTask ],
[ 'if-not', PSelectorIfNotTask ],
[ 'matches-css', PSelectorMatchesCSSTask ],
[ 'matches-css-after', PSelectorMatchesCSSAfterTask ],
[ 'matches-css-before', PSelectorMatchesCSSBeforeTask ],
[ 'matches-media', PSelectorMatchesMediaTask ],
[ 'matches-path', PSelectorMatchesPathTask ],
[ 'min-text-length', PSelectorMinTextLengthTask ],
[ 'not', PSelectorIfNotTask ],
[ 'others', PSelectorOthersTask ],
[ 'spath', PSelectorSpathTask ],
[ 'upward', PSelectorUpwardTask ],
[ 'watch-attr', PSelectorWatchAttrs ],
[ 'xpath', PSelectorXpathTask ],
]);
/******************************************************************************/
class PSelectorRoot extends PSelector {
constructor(o, styleToken) {
super(o);
this.budget = 200; // I arbitrary picked a 1/5 second
this.raw = o.raw;
this.cost = 0;
this.lastAllowanceTime = 0;
this.styleToken = styleToken;
}
prime(input) {
try {
return super.prime(input);
} catch (ex) {
}
return [];
}
}
/******************************************************************************/
class ProceduralFilterer {
constructor(selectors) {
this.selectors = [];
this.masterToken = this.randomToken();
this.styleTokenMap = new Map();
this.styledNodes = new Set();
this.timer = undefined;
this.addSelectors(selectors);
}
addSelectors() {
for ( const selector of selectors ) {
let style, styleToken;
if ( selector.action === undefined ) {
style = 'display:none!important;';
} else if ( selector.action[0] === 'style' ) {
style = selector.action[1];
}
if ( style !== undefined ) {
styleToken = this.styleTokenFromStyle(style);
}
const pselector = new PSelectorRoot(selector, styleToken);
this.selectors.push(pselector);
}
this.onDOMChanged();
}
uBOL_commitNow() {
//console.time('procedural selectors/dom layout changed');
// https://github.com/uBlockOrigin/uBlock-issues/issues/341
// Be ready to unhide nodes which no longer matches any of
// the procedural selectors.
const toUnstyle = this.styledNodes;
this.styledNodes = new Set();
let t0 = Date.now();
for ( const pselector of this.selectors.values() ) {
const allowance = Math.floor((t0 - pselector.lastAllowanceTime) / 2000);
if ( allowance >= 1 ) {
pselector.budget += allowance * 50;
if ( pselector.budget > 200 ) { pselector.budget = 200; }
pselector.lastAllowanceTime = t0;
}
if ( pselector.budget <= 0 ) { continue; }
const nodes = pselector.exec();
const t1 = Date.now();
pselector.budget += t0 - t1;
if ( pselector.budget < -500 ) {
console.info('uBOL: disabling %s', pselector.raw);
pselector.budget = -0x7FFFFFFF;
}
t0 = t1;
if ( nodes.length === 0 ) { continue; }
this.styleNodes(nodes, pselector.styleToken);
}
this.unstyleNodes(toUnstyle);
}
styleTokenFromStyle(style) {
if ( style === undefined ) { return; }
let styleToken = this.styleTokenMap.get(style);
if ( styleToken !== undefined ) { return styleToken; }
styleToken = this.randomToken();
this.styleTokenMap.set(style, styleToken);
addStylesheet(
`[${this.masterToken}][${styleToken}]\n{${style}}\n`,
);
return styleToken;
}
styleNodes(nodes, styleToken) {
if ( styleToken === undefined ) {
for ( const node of nodes ) {
node.textContent = '';
node.remove();
}
return;
}
for ( const node of nodes ) {
node.setAttribute(this.masterToken, '');
node.setAttribute(styleToken, '');
this.styledNodes.add(node);
}
}
unstyleNodes(nodes) {
for ( const node of nodes ) {
if ( this.styledNodes.has(node) ) { continue; }
node.removeAttribute(this.masterToken);
}
}
randomToken() {
const n = Math.random();
return String.fromCharCode(n * 25 + 97) +
Math.floor(
(0.25 + n * 0.75) * Number.MAX_SAFE_INTEGER
).toString(36).slice(-8);
}
onDOMChanged() {
if ( this.timer !== undefined ) { return; }
this.timer = self.requestAnimationFrame(( ) => {
this.timer = undefined;
this.uBOL_commitNow();
});
}
}
/******************************************************************************/
const proceduralImports = self.proceduralImports || [];
const lookupSelectors = (hn, out) => {
for ( const { argsList, hostnamesMap } of proceduralImports ) {
let argsIndices = hostnamesMap.get(hn);
if ( argsIndices === undefined ) { continue; }
if ( typeof argsIndices === 'number' ) { argsIndices = [ argsIndices ]; }
for ( const argsIndex of argsIndices ) {
const details = argsList[argsIndex];
if ( details.n && details.n.includes(hn) ) { continue; }
out.push(...details.a.map(json => JSON.parse(json)));
}
}
};
let hn;
try { hn = document.location.hostname; } catch(ex) { }
const selectors = [];
while ( hn ) {
lookupSelectors(hn, selectors);
if ( hn === '*' ) { break; }
const pos = hn.indexOf('.');
if ( pos !== -1 ) {
hn = hn.slice(pos + 1);
} else {
hn = '*';
}
}
proceduralImports.length = 0;
/******************************************************************************/
if ( selectors.length === 0 ) { return; }
proceduralFilterer = new ProceduralFilterer(selectors);
const observer = new MutationObserver(mutations => {
let domChanged = false;
for ( let i = 0; i < mutations.length && !domChanged; i++ ) {
const mutation = mutations[i];
for ( const added of mutation.addedNodes ) {
if ( added.nodeType !== 1 ) { continue; }
domChanged = true;
break;
}
if ( domChanged === false ) {
for ( const removed of mutation.removedNodes ) {
if ( removed.nodeType !== 1 ) { continue; }
domChanged = true;
break;
}
}
}
if ( domChanged === false ) { return; }
proceduralFilterer.onDOMChanged();
});
observer.observe(document, {
childList: true,
subtree: true,
});
/******************************************************************************/
})();
/******************************************************************************/

View file

@ -42,22 +42,52 @@ const toBroaderHostname = hn => {
/******************************************************************************/ /******************************************************************************/
// Is a descendant hostname of b? // Is hna descendant hostname of hnb?
const isDescendantHostname = (a, b) => { const isDescendantHostname = (hna, hnb) => {
if ( b === 'all-urls' ) { return true; } if ( hnb === 'all-urls' ) { return true; }
if ( a.endsWith(b) === false ) { return false; } if ( hna.endsWith(hnb) === false ) { return false; }
if ( a === b ) { return false; } if ( hna === hnb ) { return false; }
return a.charCodeAt(a.length - b.length - 1) === 0x2E /* '.' */; return hna.charCodeAt(hna.length - hnb.length - 1) === 0x2E /* '.' */;
}; };
const isDescendantHostnameOfIter = (a, iter) => { const isDescendantHostnameOfIter = (hna, iterb) => {
for ( const b of iter ) { const setb = iterb instanceof Set ? iterb : new Set(iterb);
if ( isDescendantHostname(a, b) ) { return true; } if ( setb.has('all-urls') || setb.has('*') ) { return true; }
let hn = hna;
while ( hn ) {
const pos = hn.indexOf('.');
if ( pos === -1 ) { break; }
hn = hn.slice(pos + 1);
if ( setb.has(hn) ) { return true; }
} }
return false; return false;
}; };
const intersectHostnameIters = (itera, iterb) => {
const setb = iterb instanceof Set ? iterb : new Set(iterb);
if ( setb.has('all-urls') || setb.has('*') ) { return Array.from(itera); }
const out = [];
for ( const hna of itera ) {
if ( setb.has(hna) || isDescendantHostnameOfIter(hna, setb) ) {
out.push(hna);
}
}
return out;
};
const subtractHostnameIters = (itera, iterb) => {
const setb = iterb instanceof Set ? iterb : new Set(iterb);
if ( setb.has('all-urls') || setb.has('*') ) { return []; }
const out = [];
for ( const hna of itera ) {
if ( setb.has(hna) ) { continue; }
if ( isDescendantHostnameOfIter(hna, setb) ) { continue; }
out.push(hna);
}
return out;
};
/******************************************************************************/ /******************************************************************************/
const matchesFromHostnames = hostnames => { const matchesFromHostnames = hostnames => {
@ -102,6 +132,8 @@ export {
toBroaderHostname, toBroaderHostname,
isDescendantHostname, isDescendantHostname,
isDescendantHostnameOfIter, isDescendantHostnameOfIter,
intersectHostnameIters,
subtractHostnameIters,
matchesFromHostnames, matchesFromHostnames,
hostnamesFromMatches, hostnamesFromMatches,
fnameFromFileId, fnameFromFileId,

View file

@ -54,7 +54,7 @@ const commandLineArgs = (( ) => {
const outputDir = commandLineArgs.get('output') || '.'; const outputDir = commandLineArgs.get('output') || '.';
const cacheDir = `${outputDir}/../mv3-data`; const cacheDir = `${outputDir}/../mv3-data`;
const rulesetDir = `${outputDir}/rulesets`; const rulesetDir = `${outputDir}/rulesets`;
const scriptletDir = `${rulesetDir}/js`; const scriptletDir = `${rulesetDir}/scripting`;
const env = [ const env = [
'chromium', 'chromium',
'mv3', 'mv3',
@ -148,7 +148,11 @@ const writeOps = [];
const ruleResources = []; const ruleResources = [];
const rulesetDetails = []; const rulesetDetails = [];
const scriptingDetails = new Map(); const declarativeDetails = new Map();
const proceduralDetails = new Map();
const scriptletStats = new Map();
const specificDetails = new Map();
const genericDetails = new Map();
/******************************************************************************/ /******************************************************************************/
@ -284,20 +288,20 @@ async function processNetworkFilters(assetDetails, network) {
log(bad.map(rule => rule._error.map(v => `\t\t${v}`)).join('\n'), true); log(bad.map(rule => rule._error.map(v => `\t\t${v}`)).join('\n'), true);
writeFile( writeFile(
`${rulesetDir}/${assetDetails.id}.json`, `${rulesetDir}/main/${assetDetails.id}.json`,
`${JSON.stringify(plainGood, replacer)}\n` `${JSON.stringify(plainGood, replacer)}\n`
); );
if ( regexes.length !== 0 ) { if ( regexes.length !== 0 ) {
writeFile( writeFile(
`${rulesetDir}/${assetDetails.id}.regexes.json`, `${rulesetDir}/regex/${assetDetails.id}.regexes.json`,
`${JSON.stringify(regexes, replacer)}\n` `${JSON.stringify(regexes, replacer)}\n`
); );
} }
if ( removeparamsGood.length !== 0 ) { if ( removeparamsGood.length !== 0 ) {
writeFile( writeFile(
`${rulesetDir}/${assetDetails.id}.removeparams.json`, `${rulesetDir}/removeparam/${assetDetails.id}.removeparams.json`,
`${JSON.stringify(removeparamsGood, replacer)}\n` `${JSON.stringify(removeparamsGood, replacer)}\n`
); );
} }
@ -366,10 +370,10 @@ const globalPatchedScriptletsSet = new Set();
function addScriptingAPIResources(id, hostnames, fid) { function addScriptingAPIResources(id, hostnames, fid) {
if ( hostnames === undefined ) { return; } if ( hostnames === undefined ) { return; }
for ( const hn of hostnames ) { for ( const hn of hostnames ) {
let hostnamesToFidMap = scriptingDetails.get(id); let hostnamesToFidMap = specificDetails.get(id);
if ( hostnamesToFidMap === undefined ) { if ( hostnamesToFidMap === undefined ) {
hostnamesToFidMap = new Map(); hostnamesToFidMap = new Map();
scriptingDetails.set(id, hostnamesToFidMap); specificDetails.set(id, hostnamesToFidMap);
} }
let fids = hostnamesToFidMap.get(hn); let fids = hostnamesToFidMap.get(hn);
if ( fids === undefined ) { if ( fids === undefined ) {
@ -383,11 +387,9 @@ function addScriptingAPIResources(id, hostnames, fid) {
} }
} }
const toIsolatedStartFileId = s => (uidint32(s) & ~0b11) | 0b00; const toCSSSpecific = s => (uidint32(s) & ~0b11) | 0b00;
const toMainStartFileId = s => (uidint32(s) & ~0b11) | 0b01;
const toIsolatedEndFileId = s => (uidint32(s) & ~0b11) | 0b10;
const pathFromFileName = fname => `${scriptletDir}/${fname.slice(0,2)}/${fname.slice(2)}.js`; const pathFromFileName = fname => `${fname.slice(-1)}/${fname.slice(0,-1)}.js`;
/******************************************************************************/ /******************************************************************************/
@ -411,18 +413,17 @@ async function processGenericCosmeticFilters(assetDetails, bucketsMap, exclusion
'$rulesetId$', '$rulesetId$',
assetDetails.id assetDetails.id
).replace( ).replace(
/\bself\.\$excludeHostnameSet\$/m, /\bself\.\$genericSelectorMap\$/m,
`${JSON.stringify(exclusions, scriptletJsonReplacer)}`
).replace(
/\bself\.\$genericSelectorLists\$/m,
`${JSON.stringify(selectorLists, scriptletJsonReplacer)}` `${JSON.stringify(selectorLists, scriptletJsonReplacer)}`
); );
writeFile( writeFile(
`${scriptletDir}/${assetDetails.id}.generic.js`, `${scriptletDir}/generic/${assetDetails.id}.generic.js`,
patchedScriptlet patchedScriptlet
); );
genericDetails.set(assetDetails.id, exclusions.sort());
log(`CSS-generic: ${count} plain CSS selectors`); log(`CSS-generic: ${count} plain CSS selectors`);
return out; return out;
@ -434,10 +435,11 @@ const MAX_COSMETIC_FILTERS_PER_FILE = 256;
// This merges selectors which are used by the same hostnames // This merges selectors which are used by the same hostnames
function groupCosmeticByHostnames(mapin) { function groupSelectorsByHostnames(mapin) {
if ( mapin === undefined ) { return []; } if ( mapin === undefined ) { return []; }
const merged = new Map(); const merged = new Map();
for ( const [ selector, details ] of mapin ) { for ( const [ selector, details ] of mapin ) {
if ( details.rejected ) { continue; }
const json = JSON.stringify(details); const json = JSON.stringify(details);
let entries = merged.get(json); let entries = merged.get(json);
if ( entries === undefined ) { if ( entries === undefined ) {
@ -460,7 +462,7 @@ function groupCosmeticByHostnames(mapin) {
// Also, we sort the hostnames to increase likelihood that selector with // Also, we sort the hostnames to increase likelihood that selector with
// same hostnames will end up in same generated scriptlet. // same hostnames will end up in same generated scriptlet.
function groupCosmeticBySelectors(arrayin) { function groupHostnamesBySelectors(arrayin) {
const contentMap = new Map(); const contentMap = new Map();
for ( const entry of arrayin ) { for ( const entry of arrayin ) {
const id = uidint32(JSON.stringify(entry.selectors)); const id = uidint32(JSON.stringify(entry.selectors));
@ -527,11 +529,32 @@ const scriptletJsonReplacer = (k, v) => {
/******************************************************************************/ /******************************************************************************/
function argsMap2List(argsMap, hostnamesMap) {
const argsList = [];
const indexMap = new Map();
for ( const [ id, details ] of argsMap ) {
indexMap.set(id, argsList.length);
argsList.push(details);
}
for ( const [ hn, ids ] of hostnamesMap ) {
if ( typeof ids === 'number' ) {
hostnamesMap.set(hn, indexMap.get(ids));
continue;
}
for ( let i = 0; i < ids.length; i++ ) {
ids[i] = indexMap.get(ids[i]);
}
}
return argsList;
}
/******************************************************************************/
async function processCosmeticFilters(assetDetails, mapin) { async function processCosmeticFilters(assetDetails, mapin) {
if ( mapin === undefined ) { return 0; } if ( mapin === undefined ) { return 0; }
const contentArray = groupCosmeticBySelectors( const contentArray = groupHostnamesBySelectors(
groupCosmeticByHostnames(mapin) groupSelectorsByHostnames(mapin)
); );
// We do not want more than n CSS files per subscription, so we will // We do not want more than n CSS files per subscription, so we will
@ -559,22 +582,23 @@ async function processCosmeticFilters(assetDetails, mapin) {
if ( details.y === undefined ) { continue; } if ( details.y === undefined ) { continue; }
scriptletHostnameToIdMap(details.y, id, hostnamesMap); scriptletHostnameToIdMap(details.y, id, hostnamesMap);
} }
const argsList = argsMap2List(argsMap, hostnamesMap);
const patchedScriptlet = originalScriptletMap.get('css-specific') const patchedScriptlet = originalScriptletMap.get('css-specific')
.replace( .replace(
'$rulesetId$', '$rulesetId$',
assetDetails.id assetDetails.id
).replace( ).replace(
/\bself\.\$argsMap\$/m, /\bself\.\$argsList\$/m,
`${JSON.stringify(argsMap, scriptletJsonReplacer)}` `${JSON.stringify(argsList, scriptletJsonReplacer)}`
).replace( ).replace(
/\bself\.\$hostnamesMap\$/m, /\bself\.\$hostnamesMap\$/m,
`${JSON.stringify(hostnamesMap, scriptletJsonReplacer)}` `${JSON.stringify(hostnamesMap, scriptletJsonReplacer)}`
); );
const fid = toIsolatedStartFileId(patchedScriptlet); const fid = toCSSSpecific(patchedScriptlet);
if ( globalPatchedScriptletsSet.has(fid) === false ) { if ( globalPatchedScriptletsSet.has(fid) === false ) {
globalPatchedScriptletsSet.add(fid); globalPatchedScriptletsSet.add(fid);
const fname = fnameFromFileId(fid); const fname = fnameFromFileId(fid);
writeFile(pathFromFileName(fname), patchedScriptlet); writeFile(`${scriptletDir}/specific/${pathFromFileName(fname)}`, patchedScriptlet);
generatedFiles.push(fname); generatedFiles.push(fname);
} }
for ( const entry of slice ) { for ( const entry of slice ) {
@ -593,65 +617,137 @@ async function processCosmeticFilters(assetDetails, mapin) {
/******************************************************************************/ /******************************************************************************/
async function processProceduralCosmeticFilters(assetDetails, mapin) { async function processDeclarativeCosmeticFilters(assetDetails, mapin) {
if ( mapin === undefined ) { return 0; } if ( mapin === undefined ) { return 0; }
if ( mapin.size === 0 ) { return 0; }
const contentArray = groupCosmeticBySelectors( // Distinguish declarative-compiled-as-procedural from actual procedural.
groupCosmeticByHostnames(mapin) const declaratives = new Map();
mapin.forEach((details, jsonSelector) => {
const selector = JSON.parse(jsonSelector);
if ( selector.cssable !== true ) { return; }
declaratives.set(jsonSelector, details);
});
if ( declaratives.size === 0 ) { return 0; }
const contentArray = groupHostnamesBySelectors(
groupSelectorsByHostnames(declaratives)
); );
// We do not want more than n CSS files per subscription, so we will const argsMap = contentArray.map(entry => [
// group multiple unrelated selectors in the same file, and distinct
// css declarations will be injected programmatically according to the
// hostname of the current document.
//
// The cosmetic filters will be injected programmatically as content
// script and the decisions to activate the cosmetic filters will be
// done at injection time according to the document's hostname.
const originalScriptletMap = await loadAllSourceScriptlets();
const generatedFiles = [];
for ( let i = 0; i < contentArray.length; i += MAX_COSMETIC_FILTERS_PER_FILE ) {
const slice = contentArray.slice(i, i + MAX_COSMETIC_FILTERS_PER_FILE);
const argsMap = slice.map(entry => [
entry[0], entry[0],
{ {
a: entry[1].a ? entry[1].a.map(v => JSON.parse(v)) : undefined, a: entry[1].a,
n: entry[1].n n: entry[1].n,
} }
]); ]);
const hostnamesMap = new Map(); const hostnamesMap = new Map();
for ( const [ id, details ] of slice ) { for ( const [ id, details ] of contentArray ) {
if ( details.y === undefined ) { continue; } if ( details.y === undefined ) { continue; }
scriptletHostnameToIdMap(details.y, id, hostnamesMap); scriptletHostnameToIdMap(details.y, id, hostnamesMap);
} }
const patchedScriptlet = originalScriptletMap.get('css-specific-procedural')
const argsList = argsMap2List(argsMap, hostnamesMap);
const originalScriptletMap = await loadAllSourceScriptlets();
const patchedScriptlet = originalScriptletMap.get('css-declarative')
.replace( .replace(
'$rulesetId$', '$rulesetId$',
assetDetails.id assetDetails.id
).replace( ).replace(
/\bself\.\$argsMap\$/m, /\bself\.\$argsList\$/m,
`${JSON.stringify(argsMap, scriptletJsonReplacer)}` `${JSON.stringify(argsList, scriptletJsonReplacer)}`
).replace( ).replace(
/\bself\.\$hostnamesMap\$/m, /\bself\.\$hostnamesMap\$/m,
`${JSON.stringify(hostnamesMap, scriptletJsonReplacer)}` `${JSON.stringify(hostnamesMap, scriptletJsonReplacer)}`
); );
const fid = toIsolatedEndFileId(patchedScriptlet); writeFile(`${scriptletDir}/declarative/${assetDetails.id}.declarative.js`, patchedScriptlet);
if ( globalPatchedScriptletsSet.has(fid) === false ) {
globalPatchedScriptletsSet.add(fid); {
const fname = fnameFromFileId(fid); const hostnames = new Set();
writeFile(pathFromFileName(fname), patchedScriptlet); for ( const entry of contentArray ) {
generatedFiles.push(fname); if ( Array.isArray(entry[1].y) === false ) { continue; }
for ( const hn of entry[1].y ) {
hostnames.add(hn);
} }
for ( const entry of slice ) {
addScriptingAPIResources(assetDetails.id, entry[1].y, fid);
} }
if ( hostnames.has('*') ) {
hostnames.clear();
hostnames.add('*');
}
declarativeDetails.set(assetDetails.id, Array.from(hostnames).sort());
} }
if ( generatedFiles.length !== 0 ) { if ( contentArray.length !== 0 ) {
log(`Declarative-related distinct filters: ${contentArray.length} distinct combined selectors`);
}
return contentArray.length;
}
/******************************************************************************/
async function processProceduralCosmeticFilters(assetDetails, mapin) {
if ( mapin === undefined ) { return 0; }
if ( mapin.size === 0 ) { return 0; }
// Distinguish declarative-compiled-as-procedural from actual procedural.
const procedurals = new Map();
mapin.forEach((details, jsonSelector) => {
const selector = JSON.parse(jsonSelector);
if ( selector.cssable ) { return; }
procedurals.set(jsonSelector, details);
});
if ( procedurals.size === 0 ) { return 0; }
const contentArray = groupHostnamesBySelectors(
groupSelectorsByHostnames(procedurals)
);
const argsMap = contentArray.map(entry => [
entry[0],
{
a: entry[1].a,
n: entry[1].n,
}
]);
const hostnamesMap = new Map();
for ( const [ id, details ] of contentArray ) {
if ( details.y === undefined ) { continue; }
scriptletHostnameToIdMap(details.y, id, hostnamesMap);
}
const argsList = argsMap2List(argsMap, hostnamesMap);
const originalScriptletMap = await loadAllSourceScriptlets();
const patchedScriptlet = originalScriptletMap.get('css-procedural')
.replace(
'$rulesetId$',
assetDetails.id
).replace(
/\bself\.\$argsList\$/m,
`${JSON.stringify(argsList, scriptletJsonReplacer)}`
).replace(
/\bself\.\$hostnamesMap\$/m,
`${JSON.stringify(hostnamesMap, scriptletJsonReplacer)}`
);
writeFile(`${scriptletDir}/procedural/${assetDetails.id}.procedural.js`, patchedScriptlet);
{
const hostnames = new Set();
for ( const entry of contentArray ) {
if ( Array.isArray(entry[1].y) === false ) { continue; }
for ( const hn of entry[1].y ) {
hostnames.add(hn);
}
}
if ( hostnames.has('*') ) {
hostnames.clear();
hostnames.add('*');
}
proceduralDetails.set(assetDetails.id, Array.from(hostnames).sort());
}
if ( contentArray.length !== 0 ) {
log(`Procedural-related distinct filters: ${contentArray.length} distinct combined selectors`); log(`Procedural-related distinct filters: ${contentArray.length} distinct combined selectors`);
log(`Procedural-related injectable files: ${generatedFiles.length}`);
log(`\t${generatedFiles.join(', ')}`);
} }
return contentArray.length; return contentArray.length;
@ -753,28 +849,34 @@ async function processScriptletFilters(assetDetails, mapin) {
for ( const [ argsHash, details ] of argsDetails ) { for ( const [ argsHash, details ] of argsDetails ) {
scriptletHostnameToIdMap(details.y, uidint32(argsHash), hostnamesMap); scriptletHostnameToIdMap(details.y, uidint32(argsHash), hostnamesMap);
} }
const argsList = argsMap2List(argsMap, hostnamesMap);
const patchedScriptlet = originalScriptletMap.get(token) const patchedScriptlet = originalScriptletMap.get(token)
.replace( .replace(
'$rulesetId$', '$rulesetId$',
assetDetails.id assetDetails.id
).replace( ).replace(
/\bself\.\$argsMap\$/m, /\bself\.\$argsList\$/m,
`${JSON.stringify(argsMap, scriptletJsonReplacer)}` `${JSON.stringify(argsList, scriptletJsonReplacer)}`
).replace( ).replace(
/\bself\.\$hostnamesMap\$/m, /\bself\.\$hostnamesMap\$/m,
`${JSON.stringify(hostnamesMap, scriptletJsonReplacer)}` `${JSON.stringify(hostnamesMap, scriptletJsonReplacer)}`
); );
// ends-with 1 = scriptlet resource const fname = `${assetDetails.id}.${token}.js`;
const fid = toMainStartFileId(patchedScriptlet); const fpath = `${scriptletDir}/scriptlet/${fname}`;
if ( globalPatchedScriptletsSet.has(fid) === false ) { writeFile(fpath, patchedScriptlet);
globalPatchedScriptletsSet.add(fid);
const fname = fnameFromFileId(fid);
writeFile(pathFromFileName(fname), patchedScriptlet);
generatedFiles.push(fname); generatedFiles.push(fname);
const hostnameMatches = new Set(hostnamesMap.keys());
if ( hostnameMatches.has('*') ) {
hostnameMatches.clear();
hostnameMatches.add('*');
} }
for ( const details of argsDetails.values() ) { let rulesetScriptlets = scriptletStats.get(assetDetails.id);
addScriptingAPIResources(assetDetails.id, details.y, fid); if ( rulesetScriptlets === undefined ) {
scriptletStats.set(assetDetails.id, rulesetScriptlets = []);
} }
rulesetScriptlets.push([ token, Array.from(hostnameMatches).sort() ]);
} }
if ( generatedFiles.length !== 0 ) { if ( generatedFiles.length !== 0 ) {
@ -790,15 +892,18 @@ async function processScriptletFilters(assetDetails, mapin) {
/******************************************************************************/ /******************************************************************************/
const rulesetFromURLS = async function(assetDetails) { async function rulesetFromURLs(assetDetails) {
log('============================'); log('============================');
log(`Listset for '${assetDetails.id}':`); log(`Listset for '${assetDetails.id}':`);
if ( assetDetails.text === undefined ) {
const text = await fetchAsset(assetDetails); const text = await fetchAsset(assetDetails);
if ( text === '' ) { return; } if ( text === '' ) { return; }
assetDetails.text = text;
}
const results = await dnrRulesetFromRawLists( const results = await dnrRulesetFromRawLists(
[ { name: assetDetails.id, text } ], [ { name: assetDetails.id, text: assetDetails.text } ],
{ env } { env }
); );
@ -826,6 +931,11 @@ const rulesetFromURLS = async function(assetDetails) {
proceduralCosmetic.set(JSON.stringify(parsed), details); proceduralCosmetic.set(JSON.stringify(parsed), details);
} }
} }
if ( rejectedCosmetic.length !== 0 ) {
log(`Rejected cosmetic filters: ${rejectedCosmetic.length}`);
log(rejectedCosmetic.map(line => `\t${line}`).join('\n'), true);
}
const genericCosmeticStats = await processGenericCosmeticFilters( const genericCosmeticStats = await processGenericCosmeticFilters(
assetDetails, assetDetails,
results.genericCosmetic, results.genericCosmetic,
@ -835,15 +945,14 @@ const rulesetFromURLS = async function(assetDetails) {
assetDetails, assetDetails,
declarativeCosmetic declarativeCosmetic
); );
const declarativeStats = await processDeclarativeCosmeticFilters(
assetDetails,
proceduralCosmetic
);
const proceduralStats = await processProceduralCosmeticFilters( const proceduralStats = await processProceduralCosmeticFilters(
assetDetails, assetDetails,
proceduralCosmetic proceduralCosmetic
); );
if ( rejectedCosmetic.length !== 0 ) {
log(`Rejected cosmetic filters: ${rejectedCosmetic.length}`);
log(rejectedCosmetic.map(line => `\t${line}`).join('\n'));
}
const scriptletStats = await processScriptletFilters( const scriptletStats = await processScriptletFilters(
assetDetails, assetDetails,
results.scriptlet results.scriptlet
@ -871,6 +980,7 @@ const rulesetFromURLS = async function(assetDetails) {
css: { css: {
generic: genericCosmeticStats, generic: genericCosmeticStats,
specific: specificCosmeticStats, specific: specificCosmeticStats,
declarative: declarativeStats,
procedural: proceduralStats, procedural: proceduralStats,
}, },
scriptlets: { scriptlets: {
@ -881,9 +991,9 @@ const rulesetFromURLS = async function(assetDetails) {
ruleResources.push({ ruleResources.push({
id: assetDetails.id, id: assetDetails.id,
enabled: assetDetails.enabled, enabled: assetDetails.enabled,
path: `/rulesets/${assetDetails.id}.json` path: `/rulesets/main/${assetDetails.id}.json`
}); });
}; }
/******************************************************************************/ /******************************************************************************/
@ -925,12 +1035,12 @@ async function main() {
'https://ublockorigin.pages.dev/filters/resource-abuse.txt', 'https://ublockorigin.pages.dev/filters/resource-abuse.txt',
'https://ublockorigin.pages.dev/filters/unbreak.txt', 'https://ublockorigin.pages.dev/filters/unbreak.txt',
'https://ublockorigin.pages.dev/filters/quick-fixes.txt', 'https://ublockorigin.pages.dev/filters/quick-fixes.txt',
'https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/ubol-filters.txt', 'https://ublockorigin.pages.dev/filters/ubol-filters.txt',
'https://secure.fanboy.co.nz/easylist.txt', 'https://secure.fanboy.co.nz/easylist.txt',
'https://secure.fanboy.co.nz/easyprivacy.txt', 'https://secure.fanboy.co.nz/easyprivacy.txt',
'https://pgl.yoyo.org/adservers/serverlist.php?hostformat=hosts&showintro=1&mimetype=plaintext', 'https://pgl.yoyo.org/adservers/serverlist.php?hostformat=hosts&showintro=1&mimetype=plaintext',
]; ];
await rulesetFromURLS({ await rulesetFromURLs({
id: 'default', id: 'default',
name: 'Ads, trackers, miners, and more' , name: 'Ads, trackers, miners, and more' ,
enabled: true, enabled: true,
@ -967,7 +1077,7 @@ async function main() {
} }
const id = ids[0]; const id = ids[0];
const asset = assets[id]; const asset = assets[id];
await rulesetFromURLS({ await rulesetFromURLs({
id: id.toLowerCase(), id: id.toLowerCase(),
lang: asset.lang, lang: asset.lang,
name: asset.title, name: asset.title,
@ -986,7 +1096,7 @@ async function main() {
const contentURL = Array.isArray(asset.contentURL) const contentURL = Array.isArray(asset.contentURL)
? asset.contentURL[0] ? asset.contentURL[0]
: asset.contentURL; : asset.contentURL;
await rulesetFromURLS({ await rulesetFromURLs({
id: id.toLowerCase(), id: id.toLowerCase(),
name: asset.title, name: asset.title,
enabled: false, enabled: false,
@ -996,14 +1106,14 @@ async function main() {
} }
// Handpicked rulesets from abroad // Handpicked rulesets from abroad
await rulesetFromURLS({ await rulesetFromURLs({
id: 'cname-trackers', id: 'cname-trackers',
name: 'AdGuard CNAME-cloaked trackers', name: 'AdGuard CNAME-cloaked trackers',
enabled: true, enabled: true,
urls: [ 'https://raw.githubusercontent.com/AdguardTeam/cname-trackers/master/combined_disguised_trackers.txt' ], urls: [ 'https://raw.githubusercontent.com/AdguardTeam/cname-trackers/master/combined_disguised_trackers.txt' ],
homeURL: 'https://github.com/AdguardTeam/cname-trackers#cname-cloaked-trackers', homeURL: 'https://github.com/AdguardTeam/cname-trackers#cname-cloaked-trackers',
}); });
await rulesetFromURLS({ await rulesetFromURLs({
id: 'stevenblack-hosts', id: 'stevenblack-hosts',
name: 'Steven Black\'s hosts file', name: 'Steven Black\'s hosts file',
enabled: false, enabled: false,
@ -1018,15 +1128,35 @@ async function main() {
// We sort the hostnames for convenience/performance in the extension's // We sort the hostnames for convenience/performance in the extension's
// script manager -- the scripting API does a sort() internally. // script manager -- the scripting API does a sort() internally.
for ( const [ rulesetId, hostnamesToFidsMap ] of scriptingDetails ) { for ( const [ rulesetId, hostnamesToFidsMap ] of specificDetails ) {
scriptingDetails.set( specificDetails.set(
rulesetId, rulesetId,
Array.from(hostnamesToFidsMap).sort() Array.from(hostnamesToFidsMap).sort()
); );
} }
writeFile( writeFile(
`${rulesetDir}/scripting-details.json`, `${rulesetDir}/specific-details.json`,
`${JSON.stringify(scriptingDetails, jsonSetMapReplacer)}\n` `${JSON.stringify(specificDetails, jsonSetMapReplacer)}\n`
);
writeFile(
`${rulesetDir}/declarative-details.json`,
`${JSON.stringify(declarativeDetails, jsonSetMapReplacer, 1)}\n`
);
writeFile(
`${rulesetDir}/procedural-details.json`,
`${JSON.stringify(proceduralDetails, jsonSetMapReplacer, 1)}\n`
);
writeFile(
`${rulesetDir}/scriptlet-details.json`,
`${JSON.stringify(scriptletStats, jsonSetMapReplacer, 1)}\n`
);
writeFile(
`${rulesetDir}/generic-details.json`,
`${JSON.stringify(genericDetails, jsonSetMapReplacer, 1)}\n`
); );
await Promise.all(writeOps); await Promise.all(writeOps);

View file

@ -43,7 +43,7 @@
// $rulesetId$ // $rulesetId$
const argsMap = new Map(self.$argsMap$); const argsList = self.$argsList$;
const hostnamesMap = new Map(self.$hostnamesMap$); const hostnamesMap = new Map(self.$hostnamesMap$);
@ -157,10 +157,10 @@ let hn;
try { hn = document.location.hostname; } catch(ex) { } try { hn = document.location.hostname; } catch(ex) { }
while ( hn ) { while ( hn ) {
if ( hostnamesMap.has(hn) ) { if ( hostnamesMap.has(hn) ) {
let argsHashes = hostnamesMap.get(hn); let argsIndices = hostnamesMap.get(hn);
if ( typeof argsHashes === 'number' ) { argsHashes = [ argsHashes ]; } if ( typeof argsIndices === 'number' ) { argsIndices = [ argsIndices ]; }
for ( const argsHash of argsHashes ) { for ( const argsIndex of argsIndices ) {
const details = argsMap.get(argsHash); const details = argsList[argsIndex];
if ( details.n && details.n.includes(hn) ) { continue; } if ( details.n && details.n.includes(hn) ) { continue; }
try { scriptlet(...details.a); } catch(ex) {} try { scriptlet(...details.a); } catch(ex) {}
} }
@ -174,9 +174,7 @@ while ( hn ) {
} }
} }
/******************************************************************************/ argsList.length = 0;
argsMap.clear();
hostnamesMap.clear(); hostnamesMap.clear();
/******************************************************************************/ /******************************************************************************/

View file

@ -41,7 +41,7 @@
// $rulesetId$ // $rulesetId$
const argsMap = new Map(self.$argsMap$); const argsList = self.$argsList$;
const hostnamesMap = new Map(self.$hostnamesMap$); const hostnamesMap = new Map(self.$hostnamesMap$);
@ -115,10 +115,10 @@ let hn;
try { hn = document.location.hostname; } catch(ex) { } try { hn = document.location.hostname; } catch(ex) { }
while ( hn ) { while ( hn ) {
if ( hostnamesMap.has(hn) ) { if ( hostnamesMap.has(hn) ) {
let argsHashes = hostnamesMap.get(hn); let argsIndices = hostnamesMap.get(hn);
if ( typeof argsHashes === 'number' ) { argsHashes = [ argsHashes ]; } if ( typeof argsIndices === 'number' ) { argsIndices = [ argsIndices ]; }
for ( const argsHash of argsHashes ) { for ( const argsIndex of argsIndices ) {
const details = argsMap.get(argsHash); const details = argsList[argsIndex];
if ( details.n && details.n.includes(hn) ) { continue; } if ( details.n && details.n.includes(hn) ) { continue; }
try { scriptlet(...details.a); } catch(ex) {} try { scriptlet(...details.a); } catch(ex) {}
} }
@ -132,9 +132,7 @@ while ( hn ) {
} }
} }
/******************************************************************************/ argsList.length = 0;
argsMap.clear();
hostnamesMap.clear(); hostnamesMap.clear();
/******************************************************************************/ /******************************************************************************/

View file

@ -41,7 +41,7 @@
// $rulesetId$ // $rulesetId$
const argsMap = new Map(self.$argsMap$); const argsList = self.$argsList$;
const hostnamesMap = new Map(self.$hostnamesMap$); const hostnamesMap = new Map(self.$hostnamesMap$);
@ -89,10 +89,10 @@ let hn;
try { hn = document.location.hostname; } catch(ex) { } try { hn = document.location.hostname; } catch(ex) { }
while ( hn ) { while ( hn ) {
if ( hostnamesMap.has(hn) ) { if ( hostnamesMap.has(hn) ) {
let argsHashes = hostnamesMap.get(hn); let argsIndices = hostnamesMap.get(hn);
if ( typeof argsHashes === 'number' ) { argsHashes = [ argsHashes ]; } if ( typeof argsIndices === 'number' ) { argsIndices = [ argsIndices ]; }
for ( const argsHash of argsHashes ) { for ( const argsIndex of argsIndices ) {
const details = argsMap.get(argsHash); const details = argsList[argsIndex];
if ( details.n && details.n.includes(hn) ) { continue; } if ( details.n && details.n.includes(hn) ) { continue; }
try { scriptlet(...details.a); } catch(ex) {} try { scriptlet(...details.a); } catch(ex) {}
} }
@ -106,9 +106,7 @@ while ( hn ) {
} }
} }
/******************************************************************************/ argsList.length = 0;
argsMap.clear();
hostnamesMap.clear(); hostnamesMap.clear();
/******************************************************************************/ /******************************************************************************/

View file

@ -0,0 +1,51 @@
/*******************************************************************************
uBlock Origin - a browser extension to block requests.
Copyright (C) 2014-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';
/******************************************************************************/
/// name css-declarative
/******************************************************************************/
// Important!
// Isolate from global scope
(function uBOL_cssDeclarativeImport() {
/******************************************************************************/
// $rulesetId$
const argsList = self.$argsList$;
const hostnamesMap = new Map(self.$hostnamesMap$);
self.declarativeImports = self.declarativeImports || [];
self.declarativeImports.push({ argsList, hostnamesMap });
/******************************************************************************/
})();
/******************************************************************************/

View file

@ -31,258 +31,32 @@
// Important! // Important!
// Isolate from global scope // Isolate from global scope
(function uBOL_cssGeneric() { (function uBOL_cssGenericImport() {
/******************************************************************************/ /******************************************************************************/
// $rulesetId$ // $rulesetId$
{ const toImport = self.$genericSelectorMap$;
const excludeHostnameSet = new Set(self.$excludeHostnameSet$);
let hn; const genericSelectorMap = self.genericSelectorMap || new Map();
try { hn = document.location.hostname; } catch(ex) { }
while ( hn ) {
if ( excludeHostnameSet.has(hn) ) { return; }
const pos = hn.indexOf('.');
if ( pos === -1 ) { break; }
hn = hn.slice(pos+1);
}
excludeHostnameSet.clear();
}
const genericSelectorLists = new Map(self.$genericSelectorLists$); if ( genericSelectorMap.size === 0 ) {
self.genericSelectorMap = new Map(toImport);
/******************************************************************************/
const queriedHashes = new Set();
const maxSurveyTimeSlice = 4;
const styleSheetSelectors = [];
const stopAllRatio = 0.95; // To be investigated
let surveyCount = 0;
let surveyMissCount = 0;
let styleSheetTimer;
let processTimer;
let domChangeTimer;
let lastDomChange = Date.now();
/******************************************************************************/
// https://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/
const hashFromStr = (type, s) => {
const len = s.length;
const step = len + 7 >>> 3;
let hash = type;
for ( let i = 0; i < len; i += step ) {
hash = (hash << 5) - hash + s.charCodeAt(i) | 0;
}
return hash & 0x00FFFFFF;
};
/******************************************************************************/
// Extract all classes/ids: these will be passed to the cosmetic
// filtering engine, and in return we will obtain only the relevant
// CSS selectors.
// https://github.com/gorhill/uBlock/issues/672
// http://www.w3.org/TR/2014/REC-html5-20141028/infrastructure.html#space-separated-tokens
// http://jsperf.com/enumerate-classes/6
const uBOL_idFromNode = (node, out) => {
const raw = node.id;
if ( typeof raw !== 'string' || raw.length === 0 ) { return; }
const s = raw.trim();
const hash = hashFromStr(0x23 /* '#' */, s);
if ( queriedHashes.has(hash) ) { return; }
out.push(hash);
queriedHashes.add(hash);
};
// https://github.com/uBlockOrigin/uBlock-issues/discussions/2076
// Performance: avoid using Element.classList
const uBOL_classesFromNode = (node, out) => {
const s = node.getAttribute('class');
if ( typeof s !== 'string' ) { return; }
const len = s.length;
for ( let beg = 0, end = 0, token = ''; beg < len; beg += 1 ) {
end = s.indexOf(' ', beg);
if ( end === beg ) { continue; }
if ( end === -1 ) { end = len; }
token = s.slice(beg, end);
beg = end;
const hash = hashFromStr(0x2E /* '.' */, token);
if ( queriedHashes.has(hash) ) { continue; }
out.push(hash);
queriedHashes.add(hash);
}
};
/******************************************************************************/
const pendingNodes = {
nodeLists: [],
buffer: [
null, null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null,
],
j: 0,
add(nodes) {
if ( nodes.length === 0 ) { return; }
this.nodeLists.push(nodes);
},
next() {
if ( this.nodeLists.length === 0 ) { return 0; }
const maxSurveyBuffer = this.buffer.length;
const nodeLists = this.nodeLists;
let ib = 0;
do {
const nodeList = nodeLists[0];
let j = this.j;
let n = j + maxSurveyBuffer - ib;
if ( n > nodeList.length ) {
n = nodeList.length;
}
for ( let i = j; i < n; i++ ) {
this.buffer[ib++] = nodeList[j++];
}
if ( j !== nodeList.length ) {
this.j = j;
break;
}
this.j = 0;
this.nodeLists.shift();
} while ( ib < maxSurveyBuffer && nodeLists.length !== 0 );
return ib;
},
hasNodes() {
return this.nodeLists.length !== 0;
},
};
/******************************************************************************/
const uBOL_processNodes = ( ) => {
const t0 = Date.now();
const hashes = [];
const nodes = pendingNodes.buffer;
const deadline = t0 + maxSurveyTimeSlice;
let processed = 0;
for (;;) {
const n = pendingNodes.next();
if ( n === 0 ) { break; }
for ( let i = 0; i < n; i++ ) {
const node = nodes[i];
nodes[i] = null;
uBOL_idFromNode(node, hashes);
uBOL_classesFromNode(node, hashes);
}
processed += n;
if ( performance.now() >= deadline ) { break; }
}
for ( const hash of hashes ) {
const selectorList = genericSelectorLists.get(hash);
if ( selectorList === undefined ) { continue; }
styleSheetSelectors.push(selectorList);
genericSelectorLists.delete(hash);
}
surveyCount += 1;
if ( styleSheetSelectors.length === 0 ) {
surveyMissCount += 1;
if (
surveyCount >= 100 &&
(surveyMissCount / surveyCount) >= stopAllRatio
) {
stopAll('too many misses in surveyor');
}
return; return;
} }
if ( styleSheetTimer !== undefined ) { return; }
styleSheetTimer = self.requestAnimationFrame(( ) => {
styleSheetTimer = undefined;
uBOL_injectStyleSheet();
});
};
/******************************************************************************/ for ( const toImportEntry of toImport ) {
const existing = genericSelectorMap.get(toImportEntry[0]);
const uBOL_processChanges = mutations => { genericSelectorMap.set(
for ( let i = 0; i < mutations.length; i++ ) { toImportEntry[0],
const mutation = mutations[i]; existing === undefined
for ( const added of mutation.addedNodes ) { ? toImportEntry[1]
if ( added.nodeType !== 1 ) { continue; } : `${existing},${toImportEntry[1]}`
pendingNodes.add([ added ]); );
if ( added.firstElementChild === null ) { continue; }
pendingNodes.add(added.querySelectorAll('[id],[class]'));
} }
}
if ( pendingNodes.hasNodes() === false ) { return; }
lastDomChange = Date.now();
if ( processTimer !== undefined ) { return; }
processTimer = self.setTimeout(( ) => {
processTimer = undefined;
uBOL_processNodes();
}, 64);
};
/******************************************************************************/ self.genericSelectorMap = genericSelectorMap;
const uBOL_injectStyleSheet = ( ) => {
try {
const sheet = new CSSStyleSheet();
sheet.replace(`@layer{${styleSheetSelectors.join(',')}{display:none!important;}}`);
document.adoptedStyleSheets = [
...document.adoptedStyleSheets,
sheet
];
} catch(ex) {
}
styleSheetSelectors.length = 0;
};
/******************************************************************************/
pendingNodes.add(document.querySelectorAll('[id],[class]'));
uBOL_processNodes();
let domMutationObserver = new MutationObserver(uBOL_processChanges);
domMutationObserver.observe(document, {
childList: true,
subtree: true,
});
const needDomChangeObserver = ( ) => {
domChangeTimer = undefined;
if ( domMutationObserver === undefined ) { return; }
if ( (Date.now() - lastDomChange) > 20000 ) {
return stopAll('no more DOM changes');
}
domChangeTimer = self.setTimeout(needDomChangeObserver, 20000);
};
needDomChangeObserver();
/******************************************************************************/
const stopAll = reason => {
if ( domChangeTimer !== undefined ) {
self.clearTimeout(domChangeTimer);
domChangeTimer = undefined;
}
domMutationObserver.disconnect();
domMutationObserver.takeRecords();
domMutationObserver = undefined;
genericSelectorLists.clear();
queriedHashes.clear();
console.info(`uBOL: Generic cosmetic filtering stopped because ${reason}`);
};
/******************************************************************************/ /******************************************************************************/

View file

@ -25,659 +25,24 @@
/******************************************************************************/ /******************************************************************************/
/// name css-specific-procedural /// name css-procedural
/******************************************************************************/ /******************************************************************************/
// Important! // Important!
// Isolate from global scope // Isolate from global scope
(function uBOL_cssSpecificProcedural() { (function uBOL_cssProceduralImport() {
/******************************************************************************/ /******************************************************************************/
// $rulesetId$ // $rulesetId$
const argsMap = new Map(self.$argsMap$); const argsList = self.$argsList$;
const hostnamesMap = new Map(self.$hostnamesMap$); const hostnamesMap = new Map(self.$hostnamesMap$);
/******************************************************************************/ self.proceduralImports = self.proceduralImports || [];
self.proceduralImports.push({ argsList, hostnamesMap });
let proceduralFilterer;
/******************************************************************************/
const addStylesheet = text => {
try {
const sheet = new CSSStyleSheet();
sheet.replace(`@layer{${text}}`);
document.adoptedStyleSheets = [
...document.adoptedStyleSheets,
sheet
];
} catch(ex) {
}
};
const nonVisualElements = {
script: true,
style: true,
};
// 'P' stands for 'Procedural'
class PSelectorTask {
begin() {
}
end() {
}
}
class PSelectorVoidTask extends PSelectorTask {
constructor(task) {
super();
console.info(`uBO: :${task[0]}() operator does not exist`);
}
transpose() {
}
}
class PSelectorHasTextTask extends PSelectorTask {
constructor(task) {
super();
let arg0 = task[1], arg1;
if ( Array.isArray(task[1]) ) {
arg1 = arg0[1]; arg0 = arg0[0];
}
this.needle = new RegExp(arg0, arg1);
}
transpose(node, output) {
if ( this.needle.test(node.textContent) ) {
output.push(node);
}
}
}
class PSelectorIfTask extends PSelectorTask {
constructor(task) {
super();
this.pselector = new PSelector(task[1]);
}
transpose(node, output) {
if ( this.pselector.test(node) === this.target ) {
output.push(node);
}
}
}
PSelectorIfTask.prototype.target = true;
class PSelectorIfNotTask extends PSelectorIfTask {
}
PSelectorIfNotTask.prototype.target = false;
class PSelectorMatchesCSSTask extends PSelectorTask {
constructor(task) {
super();
this.name = task[1].name;
this.pseudo = task[1].pseudo ? `::${task[1].pseudo}` : null;
let arg0 = task[1].value, arg1;
if ( Array.isArray(arg0) ) {
arg1 = arg0[1]; arg0 = arg0[0];
}
this.value = new RegExp(arg0, arg1);
}
transpose(node, output) {
const style = window.getComputedStyle(node, this.pseudo);
if ( style !== null && this.value.test(style[this.name]) ) {
output.push(node);
}
}
}
class PSelectorMatchesCSSAfterTask extends PSelectorMatchesCSSTask {
constructor(task) {
super(task);
this.pseudo = '::after';
}
}
class PSelectorMatchesCSSBeforeTask extends PSelectorMatchesCSSTask {
constructor(task) {
super(task);
this.pseudo = '::before';
}
}
class PSelectorMatchesMediaTask extends PSelectorTask {
constructor(task) {
super();
this.mql = window.matchMedia(task[1]);
if ( this.mql.media === 'not all' ) { return; }
this.mql.addEventListener('change', ( ) => {
if ( proceduralFilterer instanceof Object === false ) { return; }
proceduralFilterer.onDOMChanged([ null ]);
});
}
transpose(node, output) {
if ( this.mql.matches === false ) { return; }
output.push(node);
}
}
class PSelectorMatchesPathTask extends PSelectorTask {
constructor(task) {
super();
let arg0 = task[1], arg1;
if ( Array.isArray(task[1]) ) {
arg1 = arg0[1]; arg0 = arg0[0];
}
this.needle = new RegExp(arg0, arg1);
}
transpose(node, output) {
if ( this.needle.test(self.location.pathname + self.location.search) ) {
output.push(node);
}
}
}
class PSelectorMinTextLengthTask extends PSelectorTask {
constructor(task) {
super();
this.min = task[1];
}
transpose(node, output) {
if ( node.textContent.length >= this.min ) {
output.push(node);
}
}
}
class PSelectorOthersTask extends PSelectorTask {
constructor() {
super();
this.targets = new Set();
}
begin() {
this.targets.clear();
}
end(output) {
const toKeep = new Set(this.targets);
const toDiscard = new Set();
const body = document.body;
let discard = null;
for ( let keep of this.targets ) {
while ( keep !== null && keep !== body ) {
toKeep.add(keep);
toDiscard.delete(keep);
discard = keep.previousElementSibling;
while ( discard !== null ) {
if (
nonVisualElements[discard.localName] !== true &&
toKeep.has(discard) === false
) {
toDiscard.add(discard);
}
discard = discard.previousElementSibling;
}
discard = keep.nextElementSibling;
while ( discard !== null ) {
if (
nonVisualElements[discard.localName] !== true &&
toKeep.has(discard) === false
) {
toDiscard.add(discard);
}
discard = discard.nextElementSibling;
}
keep = keep.parentElement;
}
}
for ( discard of toDiscard ) {
output.push(discard);
}
this.targets.clear();
}
transpose(candidate) {
for ( const target of this.targets ) {
if ( target.contains(candidate) ) { return; }
if ( candidate.contains(target) ) {
this.targets.delete(target);
}
}
this.targets.add(candidate);
}
}
// https://github.com/AdguardTeam/ExtendedCss/issues/31#issuecomment-302391277
// Prepend `:scope ` if needed.
class PSelectorSpathTask extends PSelectorTask {
constructor(task) {
super();
this.spath = task[1];
this.nth = /^(?:\s*[+~]|:)/.test(this.spath);
if ( this.nth ) { return; }
if ( /^\s*>/.test(this.spath) ) {
this.spath = `:scope ${this.spath.trim()}`;
}
}
transpose(node, output) {
const nodes = this.nth
? PSelectorSpathTask.qsa(node, this.spath)
: node.querySelectorAll(this.spath);
for ( const node of nodes ) {
output.push(node);
}
}
// Helper method for other operators.
static qsa(node, selector) {
const parent = node.parentElement;
if ( parent === null ) { return []; }
let pos = 1;
for (;;) {
node = node.previousElementSibling;
if ( node === null ) { break; }
pos += 1;
}
return parent.querySelectorAll(
`:scope > :nth-child(${pos})${selector}`
);
}
}
class PSelectorUpwardTask extends PSelectorTask {
constructor(task) {
super();
const arg = task[1];
if ( typeof arg === 'number' ) {
this.i = arg;
} else {
this.s = arg;
}
}
transpose(node, output) {
if ( this.s !== '' ) {
const parent = node.parentElement;
if ( parent === null ) { return; }
node = parent.closest(this.s);
if ( node === null ) { return; }
} else {
let nth = this.i;
for (;;) {
node = node.parentElement;
if ( node === null ) { return; }
nth -= 1;
if ( nth === 0 ) { break; }
}
}
output.push(node);
}
}
PSelectorUpwardTask.prototype.i = 0;
PSelectorUpwardTask.prototype.s = '';
class PSelectorWatchAttrs extends PSelectorTask {
constructor(task) {
super();
this.observer = null;
this.observed = new WeakSet();
this.observerOptions = {
attributes: true,
subtree: true,
};
const attrs = task[1];
if ( Array.isArray(attrs) && attrs.length !== 0 ) {
this.observerOptions.attributeFilter = task[1];
}
}
// TODO: Is it worth trying to re-apply only the current selector?
handler() {
if ( proceduralFilterer instanceof Object ) {
proceduralFilterer.onDOMChanged([ null ]);
}
}
transpose(node, output) {
output.push(node);
if ( this.observed.has(node) ) { return; }
if ( this.observer === null ) {
this.observer = new MutationObserver(this.handler);
}
this.observer.observe(node, this.observerOptions);
this.observed.add(node);
}
}
class PSelectorXpathTask extends PSelectorTask {
constructor(task) {
super();
this.xpe = document.createExpression(task[1], null);
this.xpr = null;
}
transpose(node, output) {
this.xpr = this.xpe.evaluate(
node,
XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
this.xpr
);
let j = this.xpr.snapshotLength;
while ( j-- ) {
const node = this.xpr.snapshotItem(j);
if ( node.nodeType === 1 ) {
output.push(node);
}
}
}
}
class PSelector {
constructor(o) {
if ( PSelector.prototype.operatorToTaskMap === undefined ) {
PSelector.prototype.operatorToTaskMap = new Map([
[ 'has', PSelectorIfTask ],
[ 'has-text', PSelectorHasTextTask ],
[ 'if', PSelectorIfTask ],
[ 'if-not', PSelectorIfNotTask ],
[ 'matches-css', PSelectorMatchesCSSTask ],
[ 'matches-css-after', PSelectorMatchesCSSAfterTask ],
[ 'matches-css-before', PSelectorMatchesCSSBeforeTask ],
[ 'matches-media', PSelectorMatchesMediaTask ],
[ 'matches-path', PSelectorMatchesPathTask ],
[ 'min-text-length', PSelectorMinTextLengthTask ],
[ 'not', PSelectorIfNotTask ],
[ 'others', PSelectorOthersTask ],
[ 'spath', PSelectorSpathTask ],
[ 'upward', PSelectorUpwardTask ],
[ 'watch-attr', PSelectorWatchAttrs ],
[ 'xpath', PSelectorXpathTask ],
]);
}
this.raw = o.raw;
this.selector = o.selector;
this.tasks = [];
const tasks = [];
if ( Array.isArray(o.tasks) === false ) { return; }
for ( const task of o.tasks ) {
const ctor = this.operatorToTaskMap.get(task[0]) || PSelectorVoidTask;
tasks.push(new ctor(task));
}
// Initialize only after all tasks have been successfully instantiated
this.tasks = tasks;
}
prime(input) {
const root = input || document;
if ( this.selector === '' ) { return [ root ]; }
if ( input !== document && /^ [>+~]/.test(this.selector) ) {
return Array.from(PSelectorSpathTask.qsa(input, this.selector));
}
return Array.from(root.querySelectorAll(this.selector));
}
exec(input) {
let nodes = this.prime(input);
for ( const task of this.tasks ) {
if ( nodes.length === 0 ) { break; }
const transposed = [];
task.begin();
for ( const node of nodes ) {
task.transpose(node, transposed);
}
task.end(transposed);
nodes = transposed;
}
return nodes;
}
test(input) {
const nodes = this.prime(input);
for ( const node of nodes ) {
let output = [ node ];
for ( const task of this.tasks ) {
const transposed = [];
task.begin();
for ( const node of output ) {
task.transpose(node, transposed);
}
task.end(transposed);
output = transposed;
if ( output.length === 0 ) { break; }
}
if ( output.length !== 0 ) { return true; }
}
return false;
}
}
PSelector.prototype.operatorToTaskMap = undefined;
class PSelectorRoot extends PSelector {
constructor(o, styleToken) {
super(o);
this.budget = 200; // I arbitrary picked a 1/5 second
this.raw = o.raw;
this.cost = 0;
this.lastAllowanceTime = 0;
this.styleToken = styleToken;
}
prime(input) {
try {
return super.prime(input);
} catch (ex) {
}
return [];
}
}
/******************************************************************************/
class ProceduralFilterer {
constructor(selectors) {
this.selectors = [];
this.masterToken = this.randomToken();
this.styleTokenMap = new Map();
this.styledNodes = new Set();
this.timer = undefined;
this.addSelectors(selectors);
}
addSelectors() {
for ( const selector of selectors ) {
let style, styleToken;
if ( selector.action === undefined ) {
style = 'display:none!important;';
} else if ( selector.action[0] === 'style' ) {
style = selector.action[1];
}
if ( style !== undefined ) {
styleToken = this.styleTokenFromStyle(style);
}
const pselector = new PSelectorRoot(selector, styleToken);
this.selectors.push(pselector);
}
this.onDOMChanged();
}
uBOL_commitNow() {
//console.time('procedural selectors/dom layout changed');
// https://github.com/uBlockOrigin/uBlock-issues/issues/341
// Be ready to unhide nodes which no longer matches any of
// the procedural selectors.
const toUnstyle = this.styledNodes;
this.styledNodes = new Set();
let t0 = Date.now();
for ( const pselector of this.selectors.values() ) {
const allowance = Math.floor((t0 - pselector.lastAllowanceTime) / 2000);
if ( allowance >= 1 ) {
pselector.budget += allowance * 50;
if ( pselector.budget > 200 ) { pselector.budget = 200; }
pselector.lastAllowanceTime = t0;
}
if ( pselector.budget <= 0 ) { continue; }
const nodes = pselector.exec();
const t1 = Date.now();
pselector.budget += t0 - t1;
if ( pselector.budget < -500 ) {
console.info('uBOL: disabling %s', pselector.raw);
pselector.budget = -0x7FFFFFFF;
}
t0 = t1;
if ( nodes.length === 0 ) { continue; }
this.styleNodes(nodes, pselector.styleToken);
}
this.unstyleNodes(toUnstyle);
}
styleTokenFromStyle(style) {
if ( style === undefined ) { return; }
let styleToken = this.styleTokenMap.get(style);
if ( styleToken !== undefined ) { return styleToken; }
styleToken = this.randomToken();
this.styleTokenMap.set(style, styleToken);
addStylesheet(
`[${this.masterToken}][${styleToken}]\n{${style}}\n`,
);
return styleToken;
}
styleNodes(nodes, styleToken) {
if ( styleToken === undefined ) {
for ( const node of nodes ) {
node.textContent = '';
node.remove();
}
return;
}
for ( const node of nodes ) {
node.setAttribute(this.masterToken, '');
node.setAttribute(styleToken, '');
this.styledNodes.add(node);
}
}
unstyleNodes(nodes) {
for ( const node of nodes ) {
if ( this.styledNodes.has(node) ) { continue; }
node.removeAttribute(this.masterToken);
}
}
randomToken() {
const n = Math.random();
return String.fromCharCode(n * 25 + 97) +
Math.floor(
(0.25 + n * 0.75) * Number.MAX_SAFE_INTEGER
).toString(36).slice(-8);
}
onDOMChanged() {
if ( this.timer !== undefined ) { return; }
this.timer = self.requestAnimationFrame(( ) => {
this.timer = undefined;
this.uBOL_commitNow();
});
}
}
/******************************************************************************/
let hn;
try { hn = document.location.hostname; } catch(ex) { }
const selectors = [];
while ( hn ) {
if ( hostnamesMap.has(hn) ) {
let argsHashes = hostnamesMap.get(hn);
if ( typeof argsHashes === 'number' ) { argsHashes = [ argsHashes ]; }
for ( const argsHash of argsHashes ) {
const details = argsMap.get(argsHash);
if ( details.n && details.n.includes(hn) ) { continue; }
selectors.push(...details.a);
}
}
if ( hn === '*' ) { break; }
const pos = hn.indexOf('.');
if ( pos !== -1 ) {
hn = hn.slice(pos + 1);
} else {
hn = '*';
}
}
const proceduralSelectors = [];
const styleSelectors = [];
for ( const selector of selectors ) {
if ( selector.cssable ) {
styleSelectors.push(selector);
} else {
proceduralSelectors.push(selector);
}
}
/******************************************************************************/
// Declarative selectors
if ( styleSelectors.length !== 0 ) {
const cssRuleFromProcedural = details => {
const { tasks, action } = details;
let mq;
if ( tasks !== undefined ) {
if ( tasks.length > 1 ) { return; }
if ( tasks[0][0] !== 'matches-media' ) { return; }
mq = tasks[0][1];
}
let style;
if ( Array.isArray(action) ) {
if ( action[0] !== 'style' ) { return; }
style = action[1];
}
if ( mq === undefined && style === undefined ) { return; }
if ( mq === undefined ) {
return `${details.selector}\n{${style}}`;
}
if ( style === undefined ) {
return `@media ${mq} {\n${details.selector}\n{display:none!important;}\n}`;
}
return `@media ${mq} {\n${details.selector}\n{${style}}\n}`;
};
const sheetText = [];
for ( const selector of styleSelectors ) {
const ruleText = cssRuleFromProcedural(selector);
if ( ruleText === undefined ) { continue; }
sheetText.push(ruleText);
}
if ( sheetText.length !== 0 ) {
addStylesheet(sheetText.join('\n'));
}
}
/******************************************************************************/
if ( proceduralSelectors.length !== 0 ) {
proceduralFilterer = new ProceduralFilterer(proceduralSelectors);
const observer = new MutationObserver(mutations => {
let domChanged = false;
for ( let i = 0; i < mutations.length && !domChanged; i++ ) {
const mutation = mutations[i];
for ( const added of mutation.addedNodes ) {
if ( added.nodeType !== 1 ) { continue; }
domChanged = true;
}
for ( const removed of mutation.removedNodes ) {
if ( removed.nodeType !== 1 ) { continue; }
domChanged = true;
}
}
if ( domChanged === false ) { return; }
proceduralFilterer.onDOMChanged();
});
observer.observe(document, {
childList: true,
subtree: true,
});
}
/******************************************************************************/
argsMap.clear();
hostnamesMap.clear();
/******************************************************************************/ /******************************************************************************/

View file

@ -37,7 +37,7 @@
// $rulesetId$ // $rulesetId$
const argsMap = new Map(self.$argsMap$); const argsList = self.$argsList$;
const hostnamesMap = new Map(self.$hostnamesMap$); const hostnamesMap = new Map(self.$hostnamesMap$);
@ -48,10 +48,10 @@ try { hn = document.location.hostname; } catch(ex) { }
const styles = []; const styles = [];
while ( hn ) { while ( hn ) {
if ( hostnamesMap.has(hn) ) { if ( hostnamesMap.has(hn) ) {
let argsHashes = hostnamesMap.get(hn); let argsIndices = hostnamesMap.get(hn);
if ( typeof argsHashes === 'number' ) { argsHashes = [ argsHashes ]; } if ( typeof argsIndices === 'number' ) { argsIndices = [ argsIndices ]; }
for ( const argsHash of argsHashes ) { for ( const argsIndex of argsIndices ) {
const details = argsMap.get(argsHash); const details = argsList[argsIndex];
if ( details.n && details.n.includes(hn) ) { continue; } if ( details.n && details.n.includes(hn) ) { continue; }
styles.push(details.a); styles.push(details.a);
} }
@ -65,6 +65,9 @@ while ( hn ) {
} }
} }
argsList.length = 0;
hostnamesMap.clear();
if ( styles.length === 0 ) { return; } if ( styles.length === 0 ) { return; }
try { try {
@ -79,11 +82,6 @@ try {
/******************************************************************************/ /******************************************************************************/
argsMap.clear();
hostnamesMap.clear();
/******************************************************************************/
})(); })();
/******************************************************************************/ /******************************************************************************/

View file

@ -40,7 +40,7 @@
// $rulesetId$ // $rulesetId$
const argsMap = new Map(self.$argsMap$); const argsList = self.$argsList$;
const hostnamesMap = new Map(self.$hostnamesMap$); const hostnamesMap = new Map(self.$hostnamesMap$);
@ -133,10 +133,10 @@ let hn;
try { hn = document.location.hostname; } catch(ex) { } try { hn = document.location.hostname; } catch(ex) { }
while ( hn ) { while ( hn ) {
if ( hostnamesMap.has(hn) ) { if ( hostnamesMap.has(hn) ) {
let argsHashes = hostnamesMap.get(hn); let argsIndices = hostnamesMap.get(hn);
if ( typeof argsHashes === 'number' ) { argsHashes = [ argsHashes ]; } if ( typeof argsIndices === 'number' ) { argsIndices = [ argsIndices ]; }
for ( const argsHash of argsHashes ) { for ( const argsIndex of argsIndices ) {
const details = argsMap.get(argsHash); const details = argsList[argsIndex];
if ( details.n && details.n.includes(hn) ) { continue; } if ( details.n && details.n.includes(hn) ) { continue; }
try { scriptlet(...details.a); } catch(ex) {} try { scriptlet(...details.a); } catch(ex) {}
} }
@ -150,9 +150,7 @@ while ( hn ) {
} }
} }
/******************************************************************************/ argsList.length = 0;
argsMap.clear();
hostnamesMap.clear(); hostnamesMap.clear();
/******************************************************************************/ /******************************************************************************/

View file

@ -28,7 +28,7 @@
/******************************************************************************/ /******************************************************************************/
/// name no-addEventListener-if /// name no-addeventlistener-if
/// alias noaelif /// alias noaelif
/// alias aeld /// alias aeld
@ -42,7 +42,7 @@
// $rulesetId$ // $rulesetId$
const argsMap = new Map(self.$argsMap$); const argsList = self.$argsList$;
const hostnamesMap = new Map(self.$hostnamesMap$); const hostnamesMap = new Map(self.$hostnamesMap$);
@ -89,10 +89,10 @@ let hn;
try { hn = document.location.hostname; } catch(ex) { } try { hn = document.location.hostname; } catch(ex) { }
while ( hn ) { while ( hn ) {
if ( hostnamesMap.has(hn) ) { if ( hostnamesMap.has(hn) ) {
let argsHashes = hostnamesMap.get(hn); let argsIndices = hostnamesMap.get(hn);
if ( typeof argsHashes === 'number' ) { argsHashes = [ argsHashes ]; } if ( typeof argsIndices === 'number' ) { argsIndices = [ argsIndices ]; }
for ( const argsHash of argsHashes ) { for ( const argsIndex of argsIndices ) {
const details = argsMap.get(argsHash); const details = argsList[argsIndex];
if ( details.n && details.n.includes(hn) ) { continue; } if ( details.n && details.n.includes(hn) ) { continue; }
try { scriptlet(...details.a); } catch(ex) {} try { scriptlet(...details.a); } catch(ex) {}
} }
@ -106,9 +106,7 @@ while ( hn ) {
} }
} }
/******************************************************************************/ argsList.length = 0;
argsMap.clear();
hostnamesMap.clear(); hostnamesMap.clear();
/******************************************************************************/ /******************************************************************************/

View file

@ -28,7 +28,8 @@
/******************************************************************************/ /******************************************************************************/
/// name no-setInterval-if /// name no-setinterval-if
/// alias no-setInterval-if
/// alias nosiif /// alias nosiif
/******************************************************************************/ /******************************************************************************/
@ -41,7 +42,7 @@
// $rulesetId$ // $rulesetId$
const argsMap = new Map(self.$argsMap$); const argsList = self.$argsList$;
const hostnamesMap = new Map(self.$hostnamesMap$); const hostnamesMap = new Map(self.$hostnamesMap$);
@ -92,10 +93,10 @@ let hn;
try { hn = document.location.hostname; } catch(ex) { } try { hn = document.location.hostname; } catch(ex) { }
while ( hn ) { while ( hn ) {
if ( hostnamesMap.has(hn) ) { if ( hostnamesMap.has(hn) ) {
let argsHashes = hostnamesMap.get(hn); let argsIndices = hostnamesMap.get(hn);
if ( typeof argsHashes === 'number' ) { argsHashes = [ argsHashes ]; } if ( typeof argsIndices === 'number' ) { argsIndices = [ argsIndices ]; }
for ( const argsHash of argsHashes ) { for ( const argsIndex of argsIndices ) {
const details = argsMap.get(argsHash); const details = argsList[argsIndex];
if ( details.n && details.n.includes(hn) ) { continue; } if ( details.n && details.n.includes(hn) ) { continue; }
try { scriptlet(...details.a); } catch(ex) {} try { scriptlet(...details.a); } catch(ex) {}
} }
@ -109,9 +110,7 @@ while ( hn ) {
} }
} }
/******************************************************************************/ argsList.length = 0;
argsMap.clear();
hostnamesMap.clear(); hostnamesMap.clear();
/******************************************************************************/ /******************************************************************************/

View file

@ -28,7 +28,8 @@
/******************************************************************************/ /******************************************************************************/
/// name no-setTimeout-if /// name no-settimeout-if
/// alias no-setTimeout-if
/// alias nostif /// alias nostif
/******************************************************************************/ /******************************************************************************/
@ -41,7 +42,7 @@
// $rulesetId$ // $rulesetId$
const argsMap = new Map(self.$argsMap$); const argsList = self.$argsList$;
const hostnamesMap = new Map(self.$hostnamesMap$); const hostnamesMap = new Map(self.$hostnamesMap$);
@ -92,10 +93,10 @@ let hn;
try { hn = document.location.hostname; } catch(ex) { } try { hn = document.location.hostname; } catch(ex) { }
while ( hn ) { while ( hn ) {
if ( hostnamesMap.has(hn) ) { if ( hostnamesMap.has(hn) ) {
let argsHashes = hostnamesMap.get(hn); let argsIndices = hostnamesMap.get(hn);
if ( typeof argsHashes === 'number' ) { argsHashes = [ argsHashes ]; } if ( typeof argsIndices === 'number' ) { argsIndices = [ argsIndices ]; }
for ( const argsHash of argsHashes ) { for ( const argsIndex of argsIndices ) {
const details = argsMap.get(argsHash); const details = argsList[argsIndex];
if ( details.n && details.n.includes(hn) ) { continue; } if ( details.n && details.n.includes(hn) ) { continue; }
try { scriptlet(...details.a); } catch(ex) {} try { scriptlet(...details.a); } catch(ex) {}
} }
@ -109,9 +110,7 @@ while ( hn ) {
} }
} }
/******************************************************************************/ argsList.length = 0;
argsMap.clear();
hostnamesMap.clear(); hostnamesMap.clear();
/******************************************************************************/ /******************************************************************************/

View file

@ -29,6 +29,7 @@
/******************************************************************************/ /******************************************************************************/
/// name no-windowOpen-if /// name no-windowOpen-if
/// alias no-windowopen-if
/// alias nowoif /// alias nowoif
/******************************************************************************/ /******************************************************************************/
@ -41,7 +42,7 @@
// $rulesetId$ // $rulesetId$
const argsMap = new Map(self.$argsMap$); const argsList = self.$argsList$;
const hostnamesMap = new Map(self.$hostnamesMap$); const hostnamesMap = new Map(self.$hostnamesMap$);
@ -129,10 +130,10 @@ let hn;
try { hn = document.location.hostname; } catch(ex) { } try { hn = document.location.hostname; } catch(ex) { }
while ( hn ) { while ( hn ) {
if ( hostnamesMap.has(hn) ) { if ( hostnamesMap.has(hn) ) {
let argsHashes = hostnamesMap.get(hn); let argsIndices = hostnamesMap.get(hn);
if ( typeof argsHashes === 'number' ) { argsHashes = [ argsHashes ]; } if ( typeof argsIndices === 'number' ) { argsIndices = [ argsIndices ]; }
for ( const argsHash of argsHashes ) { for ( const argsIndex of argsIndices ) {
const details = argsMap.get(argsHash); const details = argsList[argsIndex];
if ( details.n && details.n.includes(hn) ) { continue; } if ( details.n && details.n.includes(hn) ) { continue; }
try { scriptlet(...details.a); } catch(ex) {} try { scriptlet(...details.a); } catch(ex) {}
} }
@ -146,9 +147,7 @@ while ( hn ) {
} }
} }
/******************************************************************************/ argsList.length = 0;
argsMap.clear();
hostnamesMap.clear(); hostnamesMap.clear();
/******************************************************************************/ /******************************************************************************/

View file

@ -41,7 +41,7 @@
// $rulesetId$ // $rulesetId$
const argsMap = new Map(self.$argsMap$); const argsList = self.$argsList$;
const hostnamesMap = new Map(self.$hostnamesMap$); const hostnamesMap = new Map(self.$hostnamesMap$);
@ -175,10 +175,10 @@ let hn;
try { hn = document.location.hostname; } catch(ex) { } try { hn = document.location.hostname; } catch(ex) { }
while ( hn ) { while ( hn ) {
if ( hostnamesMap.has(hn) ) { if ( hostnamesMap.has(hn) ) {
let argsHashes = hostnamesMap.get(hn); let argsIndices = hostnamesMap.get(hn);
if ( typeof argsHashes === 'number' ) { argsHashes = [ argsHashes ]; } if ( typeof argsIndices === 'number' ) { argsIndices = [ argsIndices ]; }
for ( const argsHash of argsHashes ) { for ( const argsIndex of argsIndices ) {
const details = argsMap.get(argsHash); const details = argsList[argsIndex];
if ( details.n && details.n.includes(hn) ) { continue; } if ( details.n && details.n.includes(hn) ) { continue; }
try { scriptlet(...details.a); } catch(ex) {} try { scriptlet(...details.a); } catch(ex) {}
} }
@ -192,9 +192,7 @@ while ( hn ) {
} }
} }
/******************************************************************************/ argsList.length = 0;
argsMap.clear();
hostnamesMap.clear(); hostnamesMap.clear();
/******************************************************************************/ /******************************************************************************/

View file

@ -36,7 +36,7 @@ cp LICENSE.txt $DES/
echo "*** uBOLite.mv3: Copying mv3-specific files" echo "*** uBOLite.mv3: Copying mv3-specific files"
cp platform/mv3/extension/*.html $DES/ cp platform/mv3/extension/*.html $DES/
cp platform/mv3/extension/css/* $DES/css/ cp platform/mv3/extension/css/* $DES/css/
cp platform/mv3/extension/js/* $DES/js/ cp -R platform/mv3/extension/js/* $DES/js/
cp platform/mv3/extension/img/* $DES/img/ cp platform/mv3/extension/img/* $DES/img/
cp -R platform/mv3/extension/_locales $DES/ cp -R platform/mv3/extension/_locales $DES/