1
0
mirror of https://github.com/gorhill/uBlock.git synced 2024-11-20 01:12:38 +01:00

[mv3] Add ability to handle entity-based CSS and scriptlet injection filters

This commit adds the ability to inject entity-based plain CSS
filters and also a set of the most commonly used entity-based
scriptlet injection filters.

Since the scripting API is not compatible with entity patterns,
the entity-related content scripts are injected in all documents
and the entity-matching is done by the content script themselves.

Given this, entity-based content scripts are enabled only when
working in the Complete filtering mode, there won't be any
entity-based filters injected in lower modes.

Also, since there is no way to reasonably have access to the
Public Suffix List in the content scripts, the entity-matching
algorithm is an approximation, though I expect false positives
to be rare (time will tell). In the event of such false
positive, simply falling back to Optimal mode will fix the
issue.

The following issues have been fixed at the same time:

Fixed the no-filtering mode related rules having lower priority
then redirect rules, i.e. redirect rules would still be applied
despite disabling all filtering on a site.

Fixed improper detection of changes to the generic-related CSS
content script, potentially causing undue delays when for example
trying to access the popup panel while working in Complete mode.
The scripting MV3 can be quite slow when registering/updating
large content scripts, so uBOL does its best to call the API only
if really needed, but there had been a regression in the recent
builds preventing uBO from properly detecting unchanged content
script parameters.
This commit is contained in:
Raymond Hill 2022-10-20 15:48:06 -04:00
parent e84b374279
commit 433adac6ff
No known key found for this signature in database
GPG Key ID: 25E1490B761470C2
15 changed files with 1549 additions and 64 deletions

View File

@ -134,6 +134,7 @@ async function setFilteringModeDetails(afterDetails) {
requestDomains: [], requestDomains: [],
resourceTypes: [ 'main_frame' ], resourceTypes: [ 'main_frame' ],
}, },
priority: 100,
}; };
if ( actualDetails.none.size ) { if ( actualDetails.none.size ) {
rule.condition.requestDomains = Array.from(actualDetails.none); rule.condition.requestDomains = Array.from(actualDetails.none);

View File

@ -295,12 +295,17 @@ async function init() {
if ( popupPanelData.hasOmnipotence ) { if ( popupPanelData.hasOmnipotence ) {
ruleCount += rules.removeparam + rules.redirect; ruleCount += rules.removeparam + rules.redirect;
} }
let specificCount = 0;
if ( css.specific instanceof Object ) {
specificCount += css.specific.domainBased;
specificCount += css.specific.entityBased;
}
dom.text( dom.text(
qs$('p', div), qs$('p', div),
i18n$('perRulesetStats') i18n$('perRulesetStats')
.replace('{{ruleCount}}', ruleCount.toLocaleString()) .replace('{{ruleCount}}', ruleCount.toLocaleString())
.replace('{{filterCount}}', filters.accepted.toLocaleString()) .replace('{{filterCount}}', filters.accepted.toLocaleString())
.replace('{{cssSpecificCount}}', css.specific.toLocaleString()) .replace('{{cssSpecificCount}}', specificCount.toLocaleString())
); );
parent.append(div); parent.append(div);
} }

View File

@ -133,8 +133,9 @@ function registerGeneric(context, genericDetails) {
if ( hostnames !== undefined ) { if ( hostnames !== undefined ) {
excludeHostnames.push(...hostnames); excludeHostnames.push(...hostnames);
} }
if ( details.css.generic instanceof Object === false ) { continue; }
if ( details.css.generic.count === 0 ) { continue; } if ( details.css.generic.count === 0 ) { continue; }
js.push(`/rulesets/scripting/generic/${details.id}.generic.js`); js.push(`/rulesets/scripting/generic/${details.id}.js`);
} }
if ( js.length === 0 ) { return; } if ( js.length === 0 ) { return; }
@ -202,7 +203,7 @@ function registerProcedural(context, proceduralDetails) {
const hostnameMatches = []; const hostnameMatches = [];
for ( const details of rulesetsDetails ) { for ( const details of rulesetsDetails ) {
if ( details.css.procedural === 0 ) { continue; } if ( details.css.procedural === 0 ) { continue; }
js.push(`/rulesets/scripting/procedural/${details.id}.procedural.js`); js.push(`/rulesets/scripting/procedural/${details.id}.js`);
if ( proceduralDetails.has(details.id) ) { if ( proceduralDetails.has(details.id) ) {
hostnameMatches.push(...proceduralDetails.get(details.id)); hostnameMatches.push(...proceduralDetails.get(details.id));
} }
@ -278,7 +279,7 @@ function registerDeclarative(context, declarativeDetails) {
const hostnameMatches = []; const hostnameMatches = [];
for ( const details of rulesetsDetails ) { for ( const details of rulesetsDetails ) {
if ( details.css.declarative === 0 ) { continue; } if ( details.css.declarative === 0 ) { continue; }
js.push(`/rulesets/scripting/declarative/${details.id}.declarative.js`); js.push(`/rulesets/scripting/declarative/${details.id}.js`);
if ( declarativeDetails.has(details.id) ) { if ( declarativeDetails.has(details.id) ) {
hostnameMatches.push(...declarativeDetails.get(details.id)); hostnameMatches.push(...declarativeDetails.get(details.id));
} }
@ -420,6 +421,71 @@ function registerScriptlet(context, scriptletDetails) {
/******************************************************************************/ /******************************************************************************/
function registerScriptletEntity(context) {
const { before, filteringModeDetails, rulesetsDetails } = context;
const js = [];
for ( const details of rulesetsDetails ) {
const { scriptlets } = details;
if ( scriptlets instanceof Object === false ) { continue; }
if ( Array.isArray(scriptlets.entityBasedTokens) === false ) { continue; }
if ( scriptlets.entityBasedTokens.length === 0 ) { continue; }
for ( const token of scriptlets.entityBasedTokens ) {
js.push(`/rulesets/scripting/scriptlet-entity/${details.id}.${token}.js`);
}
}
if ( js.length === 0 ) { return; }
const matches = [];
const excludeMatches = [];
if ( filteringModeDetails.extendedGeneric.has('all-urls') ) {
excludeMatches.push(...ut.matchesFromHostnames(filteringModeDetails.none));
excludeMatches.push(...ut.matchesFromHostnames(filteringModeDetails.network));
excludeMatches.push(...ut.matchesFromHostnames(filteringModeDetails.extendedSpecific));
matches.push('<all_urls>');
} else {
matches.push(
...ut.matchesFromHostnames(filteringModeDetails.extendedGeneric)
);
}
if ( matches.length === 0 ) { return; }
const registered = before.get('scriptlet.entity');
before.delete('scriptlet.entity'); // Important!
// register
if ( registered === undefined ) {
context.toAdd.push({
id: 'scriptlet.entity',
js,
matches,
excludeMatches,
runAt: 'document_start',
world: 'MAIN',
});
return;
}
// update
const directive = { id: 'scriptlet.entity' };
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(context, specificDetails) { function registerSpecific(context, specificDetails) {
const { filteringModeDetails } = context; const { filteringModeDetails } = context;
@ -561,6 +627,68 @@ const toUpdatableScript = (context, fname, hostnames) => {
/******************************************************************************/ /******************************************************************************/
function registerSpecificEntity(context) {
const { before, filteringModeDetails, rulesetsDetails } = context;
const js = [];
for ( const details of rulesetsDetails ) {
if ( details.css.specific instanceof Object === false ) { continue; }
if ( details.css.specific.entityBased === 0 ) { continue; }
js.push(`/rulesets/scripting/specific-entity/${details.id}.js`);
}
if ( js.length === 0 ) { return; }
const matches = [];
const excludeMatches = [];
if ( filteringModeDetails.extendedGeneric.has('all-urls') ) {
excludeMatches.push(...ut.matchesFromHostnames(filteringModeDetails.none));
excludeMatches.push(...ut.matchesFromHostnames(filteringModeDetails.network));
excludeMatches.push(...ut.matchesFromHostnames(filteringModeDetails.extendedSpecific));
matches.push('<all_urls>');
} else {
matches.push(
...ut.matchesFromHostnames(filteringModeDetails.extendedGeneric)
);
}
if ( matches.length === 0 ) { return; }
js.push('/js/scripting/css-specific.entity.js');
const registered = before.get('css-specific.entity');
before.delete('css-specific.entity'); // Important!
// register
if ( registered === undefined ) {
context.toAdd.push({
id: 'css-specific.entity',
js,
matches,
excludeMatches,
runAt: 'document_start',
});
return;
}
// update
const directive = { id: 'css-specific.entity' };
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);
}
}
/******************************************************************************/
async function registerInjectables(origins) { async function registerInjectables(origins) {
void origins; void origins;
@ -604,7 +732,9 @@ async function registerInjectables(origins) {
registerDeclarative(context, declarativeDetails); registerDeclarative(context, declarativeDetails);
registerProcedural(context, proceduralDetails); registerProcedural(context, proceduralDetails);
registerScriptlet(context, scriptletDetails); registerScriptlet(context, scriptletDetails);
registerScriptletEntity(context);
registerSpecific(context, specificDetails); registerSpecific(context, specificDetails);
registerSpecificEntity(context);
registerGeneric(context, genericDetails); registerGeneric(context, genericDetails);
toRemove.push(...Array.from(before.keys())); toRemove.push(...Array.from(before.keys()));

View File

@ -0,0 +1,86 @@
/*******************************************************************************
uBlock Origin - a browser extension to block requests.
Copyright (C) 2019-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_cssSpecificEntity() {
/******************************************************************************/
// $rulesetId$
const specificEntityImports = self.specificEntityImports || [];
/******************************************************************************/
const lookupSelectors = (hn, entity, out) => {
for ( const { argsList, entitiesMap } of specificEntityImports ) {
let argsIndices = entitiesMap.get(entity);
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);
}
}
};
let hn = '';
try { hn = document.location.hostname; } catch(ex) { }
const selectors = [];
const hnparts = hn.split('.');
const hnpartslen = hnparts.length - 1;
for ( let i = 0; i < hnpartslen; i++ ) {
for ( let j = hnpartslen; j > i; j-- ) {
lookupSelectors(
hnparts.slice(i).join('.'),
hnparts.slice(i,j).join('.'),
selectors
);
}
}
self.specificEntityImports = undefined;
if ( selectors.length === 0 ) { return; }
try {
const sheet = new CSSStyleSheet();
sheet.replace(`@layer{${selectors.join(',')}{display:none!important;}}`);
document.adoptedStyleSheets = [
...document.adoptedStyleSheets,
sheet
];
} catch(ex) {
}
/******************************************************************************/
})();
/******************************************************************************/

View File

@ -441,7 +441,7 @@ async function processGenericCosmeticFilters(assetDetails, bucketsMap, exclusion
); );
writeFile( writeFile(
`${scriptletDir}/generic/${assetDetails.id}.generic.js`, `${scriptletDir}/generic/${assetDetails.id}.js`,
patchedScriptlet patchedScriptlet
); );
@ -573,11 +573,50 @@ function argsMap2List(argsMap, hostnamesMap) {
/******************************************************************************/ /******************************************************************************/
async function processCosmeticFilters(assetDetails, mapin) { function splitDomainAndEntity(mapin) {
if ( mapin === undefined ) { return 0; } const domainBased = new Map();
const entityBased = new Map();
for ( const [ selector, domainDetails ] of mapin ) {
domainBased.set(selector, domainDetails);
if ( domainDetails.rejected ) { continue; }
if ( Array.isArray(domainDetails.matches) === false ) { continue; }
const domainMatches = [];
const entityMatches = [];
for ( const hn of domainDetails.matches ) {
if ( hn.endsWith('.*') ) {
entityMatches.push(hn.slice(0, -2));
} else {
domainMatches.push(hn);
}
}
if ( entityMatches.length === 0 ) { continue; }
if ( domainMatches.length !== 0 ) {
domainDetails.matches = domainMatches;
} else {
domainBased.delete(selector);
}
const entityDetails = {
matches: entityMatches,
};
if ( Array.isArray(domainDetails.excludeMatches) ) {
entityDetails.excludeMatches = domainDetails.excludeMatches.slice();
}
entityBased.set(selector, entityDetails);
}
return { domainBased, entityBased };
}
const contentArray = groupHostnamesBySelectors( /******************************************************************************/
groupSelectorsByHostnames(mapin)
async function processCosmeticFilters(assetDetails, mapin) {
if ( mapin === undefined ) { return; }
const { domainBased, entityBased } = splitDomainAndEntity(mapin);
const entityBasedEntries = groupHostnamesBySelectors(
groupSelectorsByHostnames(entityBased)
);
const domainBasedEntries = groupHostnamesBySelectors(
groupSelectorsByHostnames(domainBased)
); );
// 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
@ -591,8 +630,8 @@ async function processCosmeticFilters(assetDetails, mapin) {
const originalScriptletMap = await loadAllSourceScriptlets(); const originalScriptletMap = await loadAllSourceScriptlets();
const generatedFiles = []; const generatedFiles = [];
for ( let i = 0; i < contentArray.length; i += MAX_COSMETIC_FILTERS_PER_FILE ) { for ( let i = 0; i < domainBasedEntries.length; i += MAX_COSMETIC_FILTERS_PER_FILE ) {
const slice = contentArray.slice(i, i + MAX_COSMETIC_FILTERS_PER_FILE); const slice = domainBasedEntries.slice(i, i + MAX_COSMETIC_FILTERS_PER_FILE);
const argsMap = slice.map(entry => [ const argsMap = slice.map(entry => [
entry[0], entry[0],
{ {
@ -629,13 +668,51 @@ async function processCosmeticFilters(assetDetails, mapin) {
} }
} }
// For entity-based entries, we generate a single scriptlet which will be
// injected only in Complete mode.
if ( entityBasedEntries.length !== 0 ) {
const argsMap = entityBasedEntries.map(entry => [
entry[0],
{
a: entry[1].a ? entry[1].a.join(',') : undefined,
n: entry[1].n,
}
]);
const entitiesMap = new Map();
for ( const [ id, details ] of entityBasedEntries ) {
if ( details.y === undefined ) { continue; }
scriptletHostnameToIdMap(details.y, id, entitiesMap);
}
const argsList = argsMap2List(argsMap, entitiesMap);
const patchedScriptlet = originalScriptletMap.get('css-specific.entity')
.replace(
'$rulesetId$',
assetDetails.id
).replace(
/\bself\.\$argsList\$/m,
`${JSON.stringify(argsList, scriptletJsonReplacer)}`
).replace(
/\bself\.\$entitiesMap\$/m,
`${JSON.stringify(entitiesMap, scriptletJsonReplacer)}`
);
const fname = `${assetDetails.id}`;
writeFile(`${scriptletDir}/specific-entity/${fname}.js`, patchedScriptlet);
generatedFiles.push(fname);
}
if ( generatedFiles.length !== 0 ) { if ( generatedFiles.length !== 0 ) {
log(`CSS-specific distinct filters: ${contentArray.length} distinct combined selectors`); log(`CSS-specific domain-based: ${domainBased.size} distinct filters`);
log(`\tCombined into ${domainBasedEntries.length} distinct entries`);
log(`CSS-specific entity-based: ${entityBased.size} distinct filters`);
log(`\tCombined into ${entityBasedEntries.length} distinct entries`);
log(`CSS-specific injectable files: ${generatedFiles.length}`); log(`CSS-specific injectable files: ${generatedFiles.length}`);
log(`\t${generatedFiles.join(', ')}`); log(`\t${generatedFiles.join(', ')}`);
} }
return contentArray.length; return {
domainBased: domainBasedEntries.length,
entityBased: entityBasedEntries.length,
};
} }
/******************************************************************************/ /******************************************************************************/
@ -683,7 +760,7 @@ async function processDeclarativeCosmeticFilters(assetDetails, mapin) {
/\bself\.\$hostnamesMap\$/m, /\bself\.\$hostnamesMap\$/m,
`${JSON.stringify(hostnamesMap, scriptletJsonReplacer)}` `${JSON.stringify(hostnamesMap, scriptletJsonReplacer)}`
); );
writeFile(`${scriptletDir}/declarative/${assetDetails.id}.declarative.js`, patchedScriptlet); writeFile(`${scriptletDir}/declarative/${assetDetails.id}.js`, patchedScriptlet);
{ {
const hostnames = new Set(); const hostnames = new Set();
@ -752,7 +829,7 @@ async function processProceduralCosmeticFilters(assetDetails, mapin) {
/\bself\.\$hostnamesMap\$/m, /\bself\.\$hostnamesMap\$/m,
`${JSON.stringify(hostnamesMap, scriptletJsonReplacer)}` `${JSON.stringify(hostnamesMap, scriptletJsonReplacer)}`
); );
writeFile(`${scriptletDir}/procedural/${assetDetails.id}.procedural.js`, patchedScriptlet); writeFile(`${scriptletDir}/procedural/${assetDetails.id}.js`, patchedScriptlet);
{ {
const hostnames = new Set(); const hostnames = new Set();
@ -779,13 +856,29 @@ async function processProceduralCosmeticFilters(assetDetails, mapin) {
/******************************************************************************/ /******************************************************************************/
async function processScriptletFilters(assetDetails, mapin) { async function processScriptletFilters(assetDetails, mapin) {
if ( mapin === undefined ) { return 0; } if ( mapin === undefined ) { return; }
const { domainBased, entityBased } = splitDomainAndEntity(mapin);
// Load all available scriptlets into a key-val map, where the key is the // Load all available scriptlets into a key-val map, where the key is the
// scriptlet token, and val is the whole content of the file. // scriptlet token, and val is the whole content of the file.
const originalScriptletMap = await loadAllSourceScriptlets(); const originalScriptletMap = await loadAllSourceScriptlets();
const parseArguments = (raw) => { let domainBasedTokens;
if ( domainBased.size !== 0 ) {
domainBasedTokens = await processDomainScriptletFilters(assetDetails, domainBased, originalScriptletMap);
}
let entityBasedTokens;
if ( entityBased.size !== 0 ) {
entityBasedTokens = await processEntityScriptletFilters(assetDetails, entityBased, originalScriptletMap);
}
return { domainBasedTokens, entityBasedTokens };
}
/******************************************************************************/
const parseScriptletArguments = raw => {
const out = []; const out = [];
let s = raw; let s = raw;
let len = s.length; let len = s.length;
@ -805,23 +898,26 @@ async function processScriptletFilters(assetDetails, mapin) {
i++; i++;
} }
return out; return out;
}; };
const parseFilter = (raw) => { const parseScriptletFilter = (raw, scriptletMap, tokenSuffix = '') => {
const filter = raw.slice(4, -1); const filter = raw.slice(4, -1);
const end = filter.length; const end = filter.length;
let pos = filter.indexOf(','); let pos = filter.indexOf(',');
if ( pos === -1 ) { pos = end; } if ( pos === -1 ) { pos = end; }
const parts = filter.trim().split(',').map(s => s.trim()); const parts = filter.trim().split(',').map(s => s.trim());
const token = scriptletDealiasingMap.get(parts[0]) || ''; const token = scriptletDealiasingMap.get(parts[0]) || '';
if ( token !== '' && originalScriptletMap.has(token) ) { if ( token === '' ) { return; }
if ( scriptletMap.has(`${token}${tokenSuffix}`) === false ) { return; }
return { return {
token, token,
args: parseArguments(parts.slice(1).join(',').trim()), args: parseScriptletArguments(parts.slice(1).join(',').trim()),
};
}
}; };
};
/******************************************************************************/
async function processDomainScriptletFilters(assetDetails, domainBased, originalScriptletMap) {
// For each instance of distinct scriptlet, we will collect distinct // For each instance of distinct scriptlet, we will collect distinct
// instances of arguments, and for each distinct set of argument, we // instances of arguments, and for each distinct set of argument, we
// will collect the set of hostnames for which the scriptlet/args is meant // will collect the set of hostnames for which the scriptlet/args is meant
@ -831,8 +927,13 @@ async function processScriptletFilters(assetDetails, mapin) {
// should have no more generated content script per subscription than the // should have no more generated content script per subscription than the
// number of distinct source scriptlets. // number of distinct source scriptlets.
const scriptletDetails = new Map(); const scriptletDetails = new Map();
for ( const [ rawFilter, entry ] of mapin ) { const rejectedFilters = [];
const normalized = parseFilter(rawFilter); for ( const [ rawFilter, entry ] of domainBased ) {
if ( entry.rejected ) {
rejectedFilters.push(rawFilter);
continue;
}
const normalized = parseScriptletFilter(rawFilter, originalScriptletMap);
if ( normalized === undefined ) { continue; } if ( normalized === undefined ) { continue; }
let argsDetails = scriptletDetails.get(normalized.token); let argsDetails = scriptletDetails.get(normalized.token);
if ( argsDetails === undefined ) { if ( argsDetails === undefined ) {
@ -861,7 +962,11 @@ async function processScriptletFilters(assetDetails, mapin) {
} }
} }
log(`Rejected scriptlet filters: ${rejectedFilters.length}`);
log(rejectedFilters.map(line => `\t${line}`).join('\n'), true);
const generatedFiles = []; const generatedFiles = [];
const tokens = [];
for ( const [ token, argsDetails ] of scriptletDetails ) { for ( const [ token, argsDetails ] of scriptletDetails ) {
const argsMap = Array.from(argsDetails).map(entry => [ const argsMap = Array.from(argsDetails).map(entry => [
@ -889,6 +994,7 @@ async function processScriptletFilters(assetDetails, mapin) {
const fpath = `${scriptletDir}/scriptlet/${fname}`; const fpath = `${scriptletDir}/scriptlet/${fname}`;
writeFile(fpath, patchedScriptlet); writeFile(fpath, patchedScriptlet);
generatedFiles.push(fname); generatedFiles.push(fname);
tokens.push(token);
const hostnameMatches = new Set(hostnamesMap.keys()); const hostnameMatches = new Set(hostnamesMap.keys());
if ( hostnameMatches.has('*') ) { if ( hostnameMatches.has('*') ) {
@ -910,7 +1016,97 @@ async function processScriptletFilters(assetDetails, mapin) {
log(`\t${generatedFiles.join(', ')}`); log(`\t${generatedFiles.join(', ')}`);
} }
return generatedFiles.length; return tokens;
}
/******************************************************************************/
async function processEntityScriptletFilters(assetDetails, entityBased, originalScriptletMap) {
// For each instance of distinct scriptlet, we will collect distinct
// instances of arguments, and for each distinct set of argument, we
// will collect the set of hostnames for which the scriptlet/args is meant
// to execute. This will allow us a single content script file and the
// scriptlets execution will depend on hostname testing against the
// URL of the document at scriptlet execution time. In the end, we
// should have no more generated content script per subscription than the
// number of distinct source scriptlets.
const scriptletMap = new Map();
const rejectedFilters = [];
for ( const [ rawFilter, entry ] of entityBased ) {
if ( entry.rejected ) {
rejectedFilters.push(rawFilter);
continue;
}
const normalized = parseScriptletFilter(rawFilter, originalScriptletMap, '.entity');
if ( normalized === undefined ) { continue; }
let argsDetails = scriptletMap.get(normalized.token);
if ( argsDetails === undefined ) {
argsDetails = new Map();
scriptletMap.set(normalized.token, argsDetails);
}
const argsHash = JSON.stringify(normalized.args);
let scriptletDetails = argsDetails.get(argsHash);
if ( scriptletDetails === undefined ) {
scriptletDetails = {
a: normalized.args,
y: new Set(),
n: new Set(),
};
argsDetails.set(argsHash, scriptletDetails);
}
if ( entry.matches ) {
for ( const entity of entry.matches ) {
scriptletDetails.y.add(entity);
}
}
if ( entry.excludeMatches ) {
for ( const hn of entry.excludeMatches ) {
scriptletDetails.n.add(hn);
}
}
}
log(`Rejected scriptlet filters: ${rejectedFilters.length}`);
log(rejectedFilters.map(line => `\t${line}`).join('\n'), true);
const generatedFiles = [];
const tokens = [];
for ( const [ token, argsDetails ] of scriptletMap ) {
const argsMap = Array.from(argsDetails).map(entry => [
uidint32(entry[0]),
{ a: entry[1].a, n: entry[1].n }
]);
const entitiesMap = new Map();
for ( const [ argsHash, details ] of argsDetails ) {
scriptletHostnameToIdMap(details.y, uidint32(argsHash), entitiesMap);
}
const argsList = argsMap2List(argsMap, entitiesMap);
const patchedScriptlet = originalScriptletMap.get(`${token}.entity`)
.replace(
'$rulesetId$',
assetDetails.id
).replace(
/\bself\.\$argsList\$/m,
`${JSON.stringify(argsList, scriptletJsonReplacer)}`
).replace(
/\bself\.\$entitiesMap\$/m,
`${JSON.stringify(entitiesMap, scriptletJsonReplacer)}`
);
const fname = `${assetDetails.id}.${token}.js`;
const fpath = `${scriptletDir}/scriptlet-entity/${fname}`;
writeFile(fpath, patchedScriptlet);
generatedFiles.push(fname);
tokens.push(token);
}
if ( generatedFiles.length !== 0 ) {
log(`Scriptlet-related entity-based injectable files: ${generatedFiles.length}`);
log(`\t${generatedFiles.join(', ')}`);
}
return tokens;
} }
/******************************************************************************/ /******************************************************************************/
@ -925,7 +1121,6 @@ async function rulesetFromURLs(assetDetails) {
assetDetails.text = text; assetDetails.text = text;
} }
const extensionPaths = []; const extensionPaths = [];
for ( const [ fname, details ] of redirectResourcesMap ) { for ( const [ fname, details ] of redirectResourcesMap ) {
const path = `/web_accessible_resources/${fname}`; const path = `/web_accessible_resources/${fname}`;
@ -966,6 +1161,13 @@ async function rulesetFromURLs(assetDetails) {
continue; continue;
} }
const parsed = JSON.parse(selector); const parsed = JSON.parse(selector);
const matches =
details.matches.filter(hn => hn.endsWith('.*') === false);
if ( matches.length === 0 ) {
rejectedCosmetic.push(`Entity-based filter not supported: ${parsed.raw}`);
continue;
}
details.matches = matches;
parsed.raw = undefined; parsed.raw = undefined;
proceduralCosmetic.set(JSON.stringify(parsed), details); proceduralCosmetic.set(JSON.stringify(parsed), details);
} }
@ -1023,9 +1225,7 @@ async function rulesetFromURLs(assetDetails) {
declarative: declarativeStats, declarative: declarativeStats,
procedural: proceduralStats, procedural: proceduralStats,
}, },
scriptlets: { scriptlets: scriptletStats,
total: scriptletStats,
},
}); });
ruleResources.push({ ruleResources.push({

View File

@ -0,0 +1,181 @@
/*******************************************************************************
uBlock Origin - a browser extension to block requests.
Copyright (C) 2019-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
The scriptlets below are meant to be injected only into a
web page context.
*/
/* jshint esversion:11 */
'use strict';
/******************************************************************************/
/// name abort-current-script.entity
/// alias acs.entity
/// alias abort-current-inline-script.entity
/// alias acis.entity
/******************************************************************************/
// Important!
// Isolate from global scope
(function uBOL_abortCurrentScriptEntity() {
/******************************************************************************/
// $rulesetId$
const argsList = self.$argsList$;
const entitiesMap = new Map(self.$entitiesMap$);
/******************************************************************************/
// Issues to mind before changing anything:
// https://github.com/uBlockOrigin/uBlock-issues/issues/2154
const scriptlet = (
target = '',
needle = '',
context = ''
) => {
if ( target === '' ) { return; }
const reRegexEscape = /[.*+?^${}()|[\]\\]/g;
const reNeedle = (( ) => {
if ( needle === '' ) { return /^/; }
if ( /^\/.+\/$/.test(needle) ) {
return new RegExp(needle.slice(1,-1));
}
return new RegExp(needle.replace(reRegexEscape, '\\$&'));
})();
const reContext = (( ) => {
if ( context === '' ) { return; }
if ( /^\/.+\/$/.test(context) ) {
return new RegExp(context.slice(1,-1));
}
return new RegExp(context.replace(reRegexEscape, '\\$&'));
})();
const chain = target.split('.');
let owner = window;
let prop;
for (;;) {
prop = chain.shift();
if ( chain.length === 0 ) { break; }
owner = owner[prop];
if ( owner instanceof Object === false ) { return; }
}
let value;
let desc = Object.getOwnPropertyDescriptor(owner, prop);
if (
desc instanceof Object === false ||
desc.get instanceof Function === false
) {
value = owner[prop];
desc = undefined;
}
const magic = String.fromCharCode(Date.now() % 26 + 97) +
Math.floor(Math.random() * 982451653 + 982451653).toString(36);
const scriptTexts = new WeakMap();
const getScriptText = elem => {
let text = elem.textContent;
if ( text.trim() !== '' ) { return text; }
if ( scriptTexts.has(elem) ) { return scriptTexts.get(elem); }
const [ , mime, content ] =
/^data:([^,]*),(.+)$/.exec(elem.src.trim()) ||
[ '', '', '' ];
try {
switch ( true ) {
case mime.endsWith(';base64'):
text = self.atob(content);
break;
default:
text = self.decodeURIComponent(content);
break;
}
} catch(ex) {
}
scriptTexts.set(elem, text);
return text;
};
const validate = ( ) => {
const e = document.currentScript;
if ( e instanceof HTMLScriptElement === false ) { return; }
if ( reContext !== undefined && reContext.test(e.src) === false ) {
return;
}
if ( reNeedle.test(getScriptText(e)) === false ) { return; }
throw new ReferenceError(magic);
};
Object.defineProperty(owner, prop, {
get: function() {
validate();
return desc instanceof Object
? desc.get.call(owner)
: value;
},
set: function(a) {
validate();
if ( desc instanceof Object ) {
desc.set.call(owner, a);
} else {
value = a;
}
}
});
const oe = window.onerror;
window.onerror = function(msg) {
if ( typeof msg === 'string' && msg.includes(magic) ) {
return true;
}
if ( oe instanceof Function ) {
return oe.apply(this, arguments);
}
}.bind();
};
/******************************************************************************/
const hnparts = [];
try { hnparts.push(...document.location.hostname.split('.')); } catch(ex) { }
const hnpartslen = hnparts.length - 1;
for ( let i = 0; i < hnpartslen; i++ ) {
for ( let j = hnpartslen; j > i; j-- ) {
const hn = hnparts.slice(i).join('.');
const en = hnparts.slice(i,j).join('.');
let argsIndices = entitiesMap.get(en);
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; }
try { scriptlet(...details.a); } catch(ex) {}
}
}
}
argsList.length = 0;
entitiesMap.clear();
/******************************************************************************/
})();
/******************************************************************************/

View File

@ -0,0 +1,139 @@
/*******************************************************************************
uBlock Origin - a browser extension to block requests.
Copyright (C) 2019-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
The scriptlets below are meant to be injected only into a
web page context.
*/
/* jshint esversion:11 */
'use strict';
/******************************************************************************/
/// name abort-on-property-read.entity
/// alias aopr.entity
/******************************************************************************/
// Important!
// Isolate from global scope
(function uBOL_abortOnPropertyReadEntity() {
/******************************************************************************/
// $rulesetId$
const argsList = self.$argsList$;
const entitiesMap = new Map(self.$entitiesMap$);
/******************************************************************************/
const ObjGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
const ObjDefineProperty = Object.defineProperty;
const magic =
String.fromCharCode(Date.now() % 26 + 97) +
Math.floor(Math.random() * 982451653 + 982451653).toString(36);
const abort = function() {
throw new ReferenceError(magic);
};
const makeProxy = function(owner, chain) {
const pos = chain.indexOf('.');
if ( pos === -1 ) {
const desc = ObjGetOwnPropertyDescriptor(owner, chain);
if ( !desc || desc.get !== abort ) {
ObjDefineProperty(owner, chain, {
get: abort,
set: function(){}
});
}
return;
}
const prop = chain.slice(0, pos);
let v = owner[prop];
chain = chain.slice(pos + 1);
if ( v ) {
makeProxy(v, chain);
return;
}
const desc = ObjGetOwnPropertyDescriptor(owner, prop);
if ( desc && desc.set !== undefined ) { return; }
ObjDefineProperty(owner, prop, {
get: function() { return v; },
set: function(a) {
v = a;
if ( a instanceof Object ) {
makeProxy(a, chain);
}
}
});
};
const scriptlet = (
chain = ''
) => {
const owner = window;
makeProxy(owner, chain);
const oe = window.onerror;
window.onerror = function(msg, src, line, col, error) {
if ( typeof msg === 'string' && msg.includes(magic) ) {
return true;
}
if ( oe instanceof Function ) {
return oe(msg, src, line, col, error);
}
}.bind();
};
/******************************************************************************/
const hnparts = [];
try { hnparts.push(...document.location.hostname.split('.')); } catch(ex) { }
const hnpartslen = hnparts.length - 1;
for ( let i = 0; i < hnpartslen; i++ ) {
for ( let j = hnpartslen; j > i; j-- ) {
const hn = hnparts.slice(i).join('.');
const en = hnparts.slice(i,j).join('.');
let argsIndices = entitiesMap.get(en);
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; }
try { scriptlet(...details.a); } catch(ex) {}
}
}
}
argsList.length = 0;
entitiesMap.clear();
/******************************************************************************/
})();
/******************************************************************************/

View File

@ -0,0 +1,113 @@
/*******************************************************************************
uBlock Origin - a browser extension to block requests.
Copyright (C) 2019-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
The scriptlets below are meant to be injected only into a
web page context.
*/
/* jshint esversion:11 */
'use strict';
/******************************************************************************/
/// name abort-on-property-write.entity
/// alias aopw.entity
/******************************************************************************/
// Important!
// Isolate from global scope
(function uBOL_abortOnPropertyWriteEntity() {
/******************************************************************************/
// $rulesetId$
const argsList = self.$argsList$;
const entitiesMap = new Map(self.$entitiesMap$);
/******************************************************************************/
const magic =
String.fromCharCode(Date.now() % 26 + 97) +
Math.floor(Math.random() * 982451653 + 982451653).toString(36);
const abort = function() {
throw new ReferenceError(magic);
};
const scriptlet = (
prop = ''
) => {
let owner = window;
for (;;) {
const pos = prop.indexOf('.');
if ( pos === -1 ) { break; }
owner = owner[prop.slice(0, pos)];
if ( owner instanceof Object === false ) { return; }
prop = prop.slice(pos + 1);
}
delete owner[prop];
Object.defineProperty(owner, prop, {
set: function() {
abort();
}
});
const oe = window.onerror;
window.onerror = function(msg, src, line, col, error) {
if ( typeof msg === 'string' && msg.includes(magic) ) {
return true;
}
if ( oe instanceof Function ) {
return oe(msg, src, line, col, error);
}
}.bind();
};
/******************************************************************************/
const hnparts = [];
try { hnparts.push(...document.location.hostname.split('.')); } catch(ex) { }
const hnpartslen = hnparts.length - 1;
for ( let i = 0; i < hnpartslen; i++ ) {
for ( let j = hnpartslen; j > i; j-- ) {
const hn = hnparts.slice(i).join('.');
const en = hnparts.slice(i,j).join('.');
let argsIndices = entitiesMap.get(en);
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; }
try { scriptlet(...details.a); } catch(ex) {}
}
}
}
argsList.length = 0;
entitiesMap.clear();
/******************************************************************************/
})();
/******************************************************************************/

View File

@ -0,0 +1,51 @@
/*******************************************************************************
uBlock Origin - a browser extension to block requests.
Copyright (C) 2019-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-specific.entity
/******************************************************************************/
// Important!
// Isolate from global scope
(function uBOL_cssSpecificEntityImport() {
/******************************************************************************/
// $rulesetId$
const argsList = self.$argsList$;
const entitiesMap = new Map(self.$entitiesMap$);
self.specificEntityImports = self.specificEntityImports || [];
self.specificEntityImports.push({ argsList, entitiesMap });
/******************************************************************************/
})();
/******************************************************************************/

View File

@ -0,0 +1,113 @@
/*******************************************************************************
uBlock Origin - a browser extension to block requests.
Copyright (C) 2019-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
The scriptlets below are meant to be injected only into a
web page context.
*/
/* jshint esversion:11 */
'use strict';
/******************************************************************************/
/// name no-addeventlistener-if.entity
/// alias noaelif.entity
/// alias aeld.entity
/******************************************************************************/
// Important!
// Isolate from global scope
(function uBOL_noAddEventListenerIfEntity() {
/******************************************************************************/
// $rulesetId$
const argsList = self.$argsList$;
const entitiesMap = new Map(self.$entitiesMap$);
/******************************************************************************/
const regexpFromArg = arg => {
if ( arg === '' ) { return /^/; }
if ( /^\/.+\/$/.test(arg) ) { return new RegExp(arg.slice(1,-1)); }
return new RegExp(arg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
};
/******************************************************************************/
const scriptlet = (
needle1 = '',
needle2 = ''
) => {
const reNeedle1 = regexpFromArg(needle1);
const reNeedle2 = regexpFromArg(needle2);
self.EventTarget.prototype.addEventListener = new Proxy(
self.EventTarget.prototype.addEventListener,
{
apply: function(target, thisArg, args) {
let type, handler;
try {
type = String(args[0]);
handler = String(args[1]);
} catch(ex) {
}
if (
reNeedle1.test(type) === false ||
reNeedle2.test(handler) === false
) {
return target.apply(thisArg, args);
}
}
}
);
};
/******************************************************************************/
const hnparts = [];
try { hnparts.push(...document.location.hostname.split('.')); } catch(ex) { }
const hnpartslen = hnparts.length - 1;
for ( let i = 0; i < hnpartslen; i++ ) {
for ( let j = hnpartslen; j > i; j-- ) {
const hn = hnparts.slice(i).join('.');
const en = hnparts.slice(i,j).join('.');
let argsIndices = entitiesMap.get(en);
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; }
try { scriptlet(...details.a); } catch(ex) {}
}
}
}
argsList.length = 0;
entitiesMap.clear();
/******************************************************************************/
})();
/******************************************************************************/

View File

@ -0,0 +1,117 @@
/*******************************************************************************
uBlock Origin - a browser extension to block requests.
Copyright (C) 2019-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
The scriptlets below are meant to be injected only into a
web page context.
*/
/* jshint esversion:11 */
'use strict';
/******************************************************************************/
/// name no-settimeout-if.entity
/// alias no-setTimeout-if.entity
/// alias nostif.entity
/******************************************************************************/
// Important!
// Isolate from global scope
(function uBOL_noSetTimeoutIfEntity() {
/******************************************************************************/
// $rulesetId$
const argsList = self.$argsList$;
const entitiesMap = new Map(self.$entitiesMap$);
/******************************************************************************/
const scriptlet = (
needle = '',
delay = ''
) => {
const needleNot = needle.charAt(0) === '!';
if ( needleNot ) { needle = needle.slice(1); }
if ( delay === '' ) { delay = undefined; }
let delayNot = false;
if ( delay !== undefined ) {
delayNot = delay.charAt(0) === '!';
if ( delayNot ) { delay = delay.slice(1); }
delay = parseInt(delay, 10);
}
if ( needle.startsWith('/') && needle.endsWith('/') ) {
needle = needle.slice(1,-1);
} else if ( needle !== '' ) {
needle = needle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
const reNeedle = new RegExp(needle);
const regexpTest = RegExp.prototype.test;
self.setTimeout = new Proxy(self.setTimeout, {
apply: function(target, thisArg, args) {
const a = String(args[0]);
const b = args[1];
let defuse;
if ( needle !== '' ) {
defuse = regexpTest.call(reNeedle, a) !== needleNot;
}
if ( defuse !== false && delay !== undefined ) {
defuse = (b === delay || isNaN(b) && isNaN(delay) ) !== delayNot;
}
if ( defuse ) {
args[0] = function(){};
}
return target.apply(thisArg, args);
}
});
};
/******************************************************************************/
const hnparts = [];
try { hnparts.push(...document.location.hostname.split('.')); } catch(ex) { }
const hnpartslen = hnparts.length - 1;
for ( let i = 0; i < hnpartslen; i++ ) {
for ( let j = hnpartslen; j > i; j-- ) {
const hn = hnparts.slice(i).join('.');
const en = hnparts.slice(i,j).join('.');
let argsIndices = entitiesMap.get(en);
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; }
try { scriptlet(...details.a); } catch(ex) {}
}
}
}
argsList.length = 0;
entitiesMap.clear();
/******************************************************************************/
})();
/******************************************************************************/

View File

@ -0,0 +1,154 @@
/*******************************************************************************
uBlock Origin - a browser extension to block requests.
Copyright (C) 2019-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
The scriptlets below are meant to be injected only into a
web page context.
*/
/* jshint esversion:11 */
'use strict';
/******************************************************************************/
/// name no-windowopen-if.entity
/// alias no-windowOpen-if.entity
/// alias nowoif.entity
/******************************************************************************/
// Important!
// Isolate from global scope
(function uBOL_noWindowOpenIfEntity() {
/******************************************************************************/
// $rulesetId$
const argsList = self.$argsList$;
const entitiesMap = new Map(self.$entitiesMap$);
/******************************************************************************/
const scriptlet = (
needle = '',
delay = '',
options = ''
) => {
const newSyntax = /^[01]?$/.test(needle) === false;
let pattern = '';
let targetResult = true;
let autoRemoveAfter = -1;
if ( newSyntax ) {
pattern = needle;
if ( pattern.startsWith('!') ) {
targetResult = false;
pattern = pattern.slice(1);
}
autoRemoveAfter = parseInt(delay);
if ( isNaN(autoRemoveAfter) ) {
autoRemoveAfter = -1;
}
} else {
pattern = delay;
if ( needle === '0' ) {
targetResult = false;
}
}
if ( pattern === '' ) {
pattern = '.?';
} else if ( /^\/.+\/$/.test(pattern) ) {
pattern = pattern.slice(1,-1);
} else {
pattern = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
const rePattern = new RegExp(pattern);
const createDecoy = function(tag, urlProp, url) {
const decoy = document.createElement(tag);
decoy[urlProp] = url;
decoy.style.setProperty('height','1px', 'important');
decoy.style.setProperty('position','fixed', 'important');
decoy.style.setProperty('top','-1px', 'important');
decoy.style.setProperty('width','1px', 'important');
document.body.appendChild(decoy);
setTimeout(( ) => decoy.remove(), autoRemoveAfter * 1000);
return decoy;
};
window.open = new Proxy(window.open, {
apply: function(target, thisArg, args) {
const url = args[0];
if ( rePattern.test(url) !== targetResult ) {
return target.apply(thisArg, args);
}
if ( autoRemoveAfter < 0 ) { return null; }
const decoy = /\bobj\b/.test(options)
? createDecoy('object', 'data', url)
: createDecoy('iframe', 'src', url);
let popup = decoy.contentWindow;
if ( typeof popup === 'object' && popup !== null ) {
Object.defineProperty(popup, 'closed', { value: false });
} else {
const noopFunc = (function(){}).bind(self);
popup = new Proxy(self, {
get: function(target, prop) {
if ( prop === 'closed' ) { return false; }
const r = Reflect.get(...arguments);
if ( typeof r === 'function' ) { return noopFunc; }
return target[prop];
},
set: function() {
return Reflect.set(...arguments);
},
});
}
return popup;
}
});
};
/******************************************************************************/
const hnparts = [];
try { hnparts.push(...document.location.hostname.split('.')); } catch(ex) { }
const hnpartslen = hnparts.length - 1;
for ( let i = 0; i < hnpartslen; i++ ) {
for ( let j = hnpartslen; j > i; j-- ) {
const hn = hnparts.slice(i).join('.');
const en = hnparts.slice(i,j).join('.');
let argsIndices = entitiesMap.get(en);
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; }
try { scriptlet(...details.a); } catch(ex) {}
}
}
}
argsList.length = 0;
entitiesMap.clear();
/******************************************************************************/
})();
/******************************************************************************/

View File

@ -0,0 +1,199 @@
/*******************************************************************************
uBlock Origin - a browser extension to block requests.
Copyright (C) 2019-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
The scriptlets below are meant to be injected only into a
web page context.
*/
/* jshint esversion:11 */
'use strict';
/******************************************************************************/
/// name set-constant.entity
/// alias set.entity
/******************************************************************************/
// Important!
// Isolate from global scope
(function uBOL_setConstantEntity() {
/******************************************************************************/
// $rulesetId$
const argsList = self.$argsList$;
const entitiesMap = new Map(self.$entitiesMap$);
/******************************************************************************/
const scriptlet = (
chain = '',
cValue = ''
) => {
if ( chain === '' ) { return; }
if ( cValue === 'undefined' ) {
cValue = undefined;
} else if ( cValue === 'false' ) {
cValue = false;
} else if ( cValue === 'true' ) {
cValue = true;
} else if ( cValue === 'null' ) {
cValue = null;
} else if ( cValue === "''" ) {
cValue = '';
} else if ( cValue === '[]' ) {
cValue = [];
} else if ( cValue === '{}' ) {
cValue = {};
} else if ( cValue === 'noopFunc' ) {
cValue = function(){};
} else if ( cValue === 'trueFunc' ) {
cValue = function(){ return true; };
} else if ( cValue === 'falseFunc' ) {
cValue = function(){ return false; };
} else if ( /^\d+$/.test(cValue) ) {
cValue = parseFloat(cValue);
if ( isNaN(cValue) ) { return; }
if ( Math.abs(cValue) > 0x7FFF ) { return; }
} else {
return;
}
let aborted = false;
const mustAbort = function(v) {
if ( aborted ) { return true; }
aborted =
(v !== undefined && v !== null) &&
(cValue !== undefined && cValue !== null) &&
(typeof v !== typeof cValue);
return aborted;
};
// https://github.com/uBlockOrigin/uBlock-issues/issues/156
// Support multiple trappers for the same property.
const trapProp = function(owner, prop, configurable, handler) {
if ( handler.init(owner[prop]) === false ) { return; }
const odesc = Object.getOwnPropertyDescriptor(owner, prop);
let prevGetter, prevSetter;
if ( odesc instanceof Object ) {
owner[prop] = cValue;
if ( odesc.get instanceof Function ) {
prevGetter = odesc.get;
}
if ( odesc.set instanceof Function ) {
prevSetter = odesc.set;
}
}
try {
Object.defineProperty(owner, prop, {
configurable,
get() {
if ( prevGetter !== undefined ) {
prevGetter();
}
return handler.getter(); // cValue
},
set(a) {
if ( prevSetter !== undefined ) {
prevSetter(a);
}
handler.setter(a);
}
});
} catch(ex) {
}
};
const trapChain = function(owner, chain) {
const pos = chain.indexOf('.');
if ( pos === -1 ) {
trapProp(owner, chain, false, {
v: undefined,
init: function(v) {
if ( mustAbort(v) ) { return false; }
this.v = v;
return true;
},
getter: function() {
return cValue;
},
setter: function(a) {
if ( mustAbort(a) === false ) { return; }
cValue = a;
}
});
return;
}
const prop = chain.slice(0, pos);
const v = owner[prop];
chain = chain.slice(pos + 1);
if ( v instanceof Object || typeof v === 'object' && v !== null ) {
trapChain(v, chain);
return;
}
trapProp(owner, prop, true, {
v: undefined,
init: function(v) {
this.v = v;
return true;
},
getter: function() {
return this.v;
},
setter: function(a) {
this.v = a;
if ( a instanceof Object ) {
trapChain(a, chain);
}
}
});
};
trapChain(window, chain);
};
/******************************************************************************/
const hnparts = [];
try { hnparts.push(...document.location.hostname.split('.')); } catch(ex) { }
const hnpartslen = hnparts.length - 1;
for ( let i = 0; i < hnpartslen; i++ ) {
for ( let j = hnpartslen; j > i; j-- ) {
const hn = hnparts.slice(i).join('.');
const en = hnparts.slice(i,j).join('.');
let argsIndices = entitiesMap.get(en);
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; }
try { scriptlet(...details.a); } catch(ex) {}
}
}
}
argsList.length = 0;
entitiesMap.clear();
/******************************************************************************/
})();
/******************************************************************************/

View File

@ -100,12 +100,10 @@ function addExtendedToDNR(context, parser) {
const { raw, exception } = parser.result; const { raw, exception } = parser.result;
for ( const { hn, not, bad } of parser.extOptions() ) { for ( const { hn, not, bad } of parser.extOptions() ) {
if ( bad ) { continue; } if ( bad ) { continue; }
if ( hn.endsWith('.*') ) { continue; }
if ( exception ) { continue; } if ( exception ) { continue; }
let details = context.scriptletFilters.get(raw); let details = context.scriptletFilters.get(raw);
if ( details === undefined ) { if ( details === undefined ) {
details = {}; context.scriptletFilters.set(raw, details = {});
context.scriptletFilters.set(raw, details);
} }
if ( not ) { if ( not ) {
if ( details.excludeMatches === undefined ) { if ( details.excludeMatches === undefined ) {
@ -174,8 +172,6 @@ function addExtendedToDNR(context, parser) {
let rejected; let rejected;
if ( compiled === undefined ) { if ( compiled === undefined ) {
rejected = `Invalid filter: ${hn}##${raw}`; rejected = `Invalid filter: ${hn}##${raw}`;
} else if ( hn.endsWith('.*') ) {
rejected = `Entity not supported: ${hn}##${raw}`;
} }
if ( rejected ) { if ( rejected ) {
compiled = rejected; compiled = rejected;

View File

@ -4298,7 +4298,7 @@ FilterContainer.prototype.dnrFromCompiled = function(op, context, ...args) {
filterCount: context.filterCount, filterCount: context.filterCount,
acceptedFilterCount: context.acceptedFilterCount, acceptedFilterCount: context.acceptedFilterCount,
rejectedFilterCount: context.rejectedFilterCount, rejectedFilterCount: context.rejectedFilterCount,
generichideExclusions, generichideExclusions: Array.from(new Set(generichideExclusions)),
}; };
}; };