Add ability to inject scriptlets according to origin of ancestor contexts

New syntax for scriptlet-based filtering: it is now possible to
inject a scriptlet in a context according to whether an ancestor
origin matches a specific hostname. Example:

  example.com>>##+js(set, iAmEmbeddedInExampleDotCom, true)

The new `>>` specifier means that the scriptlet will not be injected
in `example.com`, but will be injected in all direct and indirect
embedded contexts in pages loaded from `example.com`

The new ancestor domain specifier also works for entity directives:

  example.*##+js(set, iAmEmbeddedInExampleDotEffectiveTLD, true)

This is an experimental feature, to be further evaluated and
discussed by filter list maintainers. This is not a complete
implementation, by choice. Do not open issues regarding this
new syntax, the current shortcomings are known.

The new syntax is being discussed internally at:
https://github.com/uBlockOrigin/uBlock-discussions/discussions/244
This commit is contained in:
Raymond Hill 2025-03-06 08:57:27 -05:00
parent e636c32f2a
commit a483f7955f
No known key found for this signature in database
GPG key ID: 25E1490B761470C2
10 changed files with 103 additions and 44 deletions

View file

@ -321,7 +321,6 @@ export async function benchmarkCosmeticFiltering() {
frameId: undefined,
hostname: '',
domain: '',
entity: '',
};
const options = {
noSpecificCosmeticFiltering: false,

View file

@ -23,6 +23,7 @@
import { MRUCache } from './mrucache.js';
import { StaticExtFilteringHostnameDB } from './static-ext-filtering-db.js';
import { entityFromHostname } from './uri-utils.js';
import logger from './logger.js';
import µb from './background.js';
@ -818,9 +819,10 @@ CosmeticFilteringEngine.prototype.retrieveSpecificSelectors = function(
3
);
// Retrieve filters with a entity-based hostname value
if ( request.entity !== '' ) {
const entity = entityFromHostname(hostname, request.domain);
if ( entity !== '' ) {
this.specificFilters.retrieve(
`${hostname.slice(0, -request.domain.length)}${request.entity}`,
entity,
options.noSpecificCosmeticFiltering ? discardSets : retrieveSets,
1
);

View file

@ -25,7 +25,6 @@ import * as sfp from './static-filtering-parser.js';
import {
domainFromHostname,
domainFromURI,
entityFromDomain,
hostnameFromURI,
isNetworkURI,
} from './uri-utils.js';
@ -684,7 +683,7 @@ const retrieveContentScriptParameters = async function(sender, request) {
request.frameId = frameId;
request.hostname = hostnameFromURI(request.url);
request.domain = domainFromHostname(request.hostname);
request.entity = entityFromDomain(request.domain);
request.ancestors = pageStore.getFrameAncestorDetails(frameId);
const scf = response.specificCosmeticFilters =
cosmeticFilteringEngine.retrieveSpecificSelectors(request, response);

View file

@ -535,6 +535,21 @@ const PageStore = class {
return sender.frameURL;
}
getFrameAncestorDetails(frameId) {
if ( frameId === 0 ) { return []; }
const out = [];
for (;;) {
const frameStore = this.getFrameStore(frameId);
if ( frameStore === null ) { break; }
const { domain, hostname } = frameStore;
if ( hostname !== undefined ) {
out.push({ domain, hostname });
}
frameId = frameStore.parentId;
}
return out.slice(1);
}
// There is no event to tell us a specific subframe has been removed from
// the main document. The code below will remove subframes which are no
// longer present in the root document. Removing obsolete subframes is

View file

@ -20,6 +20,7 @@
*/
import { StaticExtFilteringHostnameDB } from './static-ext-filtering-db.js';
import { entityFromHostname } from './uri-utils.js';
import { redirectEngine as reng } from './redirect-engine.js';
/******************************************************************************/
@ -208,13 +209,23 @@ export class ScriptletFilteringEngine {
$scriptlets.clear();
$exceptions.clear();
const { hostname } = request;
const { ancestors = [], domain, hostname } = request;
this.scriptletDB.retrieve(hostname, [ $scriptlets, $exceptions ]);
const entity = request.entity !== ''
? `${hostname.slice(0, -request.domain.length)}${request.entity}`
: '*';
this.scriptletDB.retrieve(entity, [ $scriptlets, $exceptions ], 1);
const entity = entityFromHostname(hostname, domain);
if ( entity !== '' ) {
this.scriptletDB.retrieve(entity, [ $scriptlets, $exceptions ], 1);
} else {
this.scriptletDB.retrieve('*', [ $scriptlets, $exceptions ], 1);
}
for ( const ancestor of ancestors ) {
const { domain, hostname } = ancestor;
this.scriptletDB.retrieve(`${hostname}>>`, [ $scriptlets, $exceptions ], 1);
const entity = entityFromHostname(hostname, domain);
if ( entity !== '' ) {
this.scriptletDB.retrieve(`${entity}>>`, [ $scriptlets, $exceptions ], 1);
}
}
if ( $scriptlets.size === 0 ) { return; }
// Wholly disable scriptlet injection?

View file

@ -23,7 +23,6 @@
import {
domainFromHostname,
entityFromDomain,
hostnameFromURI,
} from './uri-utils.js';
@ -335,7 +334,7 @@ export class ScriptletFilteringEngineEx extends ScriptletFilteringEngine {
url: details.url,
hostname,
domain,
entity: entityFromDomain(domain),
ancestors: details.ancestors,
});
if ( scriptletDetails === undefined ) {
contentScriptRegisterer.unregister(hostname);

View file

@ -335,6 +335,27 @@ export const nodeNameFromNodeType = new Map([
/******************************************************************************/
// Local constants
const DOMAIN_CAN_USE_WILDCARD = 0b000001;
const DOMAIN_CAN_USE_ENTITY = 0b000010;
const DOMAIN_CAN_USE_SINGLE_WILDCARD = 0b000100;
const DOMAIN_CAN_BE_NEGATED = 0b001000;
const DOMAIN_CAN_BE_REGEX = 0b010000;
const DOMAIN_CAN_BE_ANCESTOR = 0b100000;
const DOMAIN_FROM_FROMTO_LIST = DOMAIN_CAN_USE_ENTITY |
DOMAIN_CAN_BE_NEGATED |
DOMAIN_CAN_BE_REGEX;
const DOMAIN_FROM_DENYALLOW_LIST = 0;
const DOMAIN_FROM_EXT_LIST = DOMAIN_CAN_USE_ENTITY |
DOMAIN_CAN_USE_SINGLE_WILDCARD |
DOMAIN_CAN_BE_NEGATED |
DOMAIN_CAN_BE_REGEX |
DOMAIN_CAN_BE_ANCESTOR;
/******************************************************************************/
// Precomputed AST layouts for most common filters.
const astTemplates = {
@ -1839,7 +1860,7 @@ export class AstFilterParser {
const hn = match[0].replace(this.reHostnameLabel, s => {
if ( this.reHasUnicodeChar.test(s) === false ) { return s; }
if ( s.charCodeAt(0) === 0x2D /* - */ ) { s = '*' + s; }
return this.normalizeHostnameValue(s, 0b0001) || s;
return this.normalizeHostnameValue(s, DOMAIN_CAN_USE_WILDCARD) || s;
});
normal = hn + normal.slice(match.index + match[0].length);
}
@ -2018,11 +2039,11 @@ export class AstFilterParser {
}
switch ( nodeOptionType ) {
case NODE_TYPE_NET_OPTION_NAME_DENYALLOW:
this.linkDown(next, this.parseDomainList(next, '|'), 0b00000);
this.linkDown(next, this.parseDomainList(next, '|'), DOMAIN_FROM_DENYALLOW_LIST);
break;
case NODE_TYPE_NET_OPTION_NAME_FROM:
case NODE_TYPE_NET_OPTION_NAME_TO:
this.linkDown(next, this.parseDomainList(next, '|', 0b11010));
this.linkDown(next, this.parseDomainList(next, '|', DOMAIN_FROM_FROMTO_LIST));
break;
default:
break;
@ -2054,7 +2075,7 @@ export class AstFilterParser {
return this.getNodeTransform(valueNode);
}
parseDomainList(parent, separator, mode = 0b00000) {
parseDomainList(parent, separator, mode = 0) {
const parentBeg = this.nodes[parent+NODE_BEG_INDEX];
const parentEnd = this.nodes[parent+NODE_END_INDEX];
const containerNode = this.allocTypedNode(
@ -2128,7 +2149,7 @@ export class AstFilterParser {
if ( not ) {
this.addNodeFlags(parent, NODE_FLAG_IS_NEGATED);
head = this.allocTypedNode(NODE_TYPE_OPTION_VALUE_NOT, beg, beg + 1);
if ( (parseDetails.mode & 0b1000) === 0 ) {
if ( (parseDetails.mode & DOMAIN_CAN_BE_NEGATED) === 0 ) {
this.addNodeFlags(parent, NODE_FLAG_ERROR);
}
beg += 1;
@ -2173,23 +2194,29 @@ export class AstFilterParser {
parseDetails.len = end - parentBeg;
}
// mode bits:
// 0b00001: can use wildcard at any position
// 0b00010: can use entity-based hostnames
// 0b00100: can use single wildcard
// 0b01000: can be negated
// 0b10000: can be a regex
normalizeDomainValue(node, type, modeBits) {
const s = this.getNodeString(node);
if ( type === 0 ) {
return this.normalizeHostnameValue(s, modeBits);
const raw = this.getNodeString(node);
const isAncestor = raw.endsWith('>>');
if ( isAncestor ) {
if ( (modeBits & DOMAIN_CAN_BE_ANCESTOR) === 0 ) { return ''; }
}
if ( (modeBits & 0b10000) === 0 ) { return ''; }
const regex = type === 1 ? s : `/${s.slice(10, -2)}/`;
const source = this.normalizeRegexPattern(regex);
if ( source === '' ) { return ''; }
if ( type === 1 && source === regex ) { return; }
return `/${source}/`;
const before = isAncestor ? raw.slice(0, -2) : raw;
let after;
if ( type === 0 ) {
after = this.normalizeHostnameValue(before, modeBits) ?? before;
if ( after === '' ) { return ''; }
} else {
if ( (modeBits & DOMAIN_CAN_BE_REGEX) === 0 ) { return ''; }
const regex = type === 1 ? before : `/${before.slice(10, -2)}/`;
const source = this.normalizeRegexPattern(regex);
if ( source === '' ) { return ''; }
after = type === 2 || source !== regex ? `/${source}/` : before;
}
if ( isAncestor ) {
after = `${after}>>`;
}
if ( after === raw ) { return; }
return after;
}
parseExt(parent, anchorBeg, anchorLen) {
@ -2207,7 +2234,8 @@ export class AstFilterParser {
);
this.addFlags(AST_FLAG_HAS_OPTIONS);
this.addNodeToRegister(NODE_TYPE_EXT_OPTIONS, next);
this.linkDown(next, this.parseDomainList(next, ',', 0b11110));
const down = this.parseDomainList(next, ',', DOMAIN_FROM_EXT_LIST);
this.linkDown(next, down);
prev = this.linkRight(prev, next);
}
next = this.allocTypedNode(
@ -2800,17 +2828,11 @@ export class AstFilterParser {
// Ultimately, let the browser API do the hostname normalization, after
// making some other trivial checks.
//
// mode bits:
// 0b00001: can use wildcard at any position
// 0b00010: can use entity-based hostnames
// 0b00100: can use single wildcard
// 0b01000: can be negated
//
// returns:
// undefined: no normalization needed, use original hostname
// empty string: hostname is invalid
// non-empty string: normalized hostname
normalizeHostnameValue(s, modeBits = 0b00000) {
normalizeHostnameValue(s, modeBits = 0) {
if ( this.reHostnameAscii.test(s) ) { return; }
if ( this.reBadHostnameChars.test(s) ) { return ''; }
let hn = s;
@ -2818,13 +2840,13 @@ export class AstFilterParser {
if ( hasWildcard ) {
if ( modeBits === 0 ) { return ''; }
if ( hn.length === 1 ) {
if ( (modeBits & 0b0100) === 0 ) { return ''; }
if ( (modeBits & DOMAIN_CAN_USE_SINGLE_WILDCARD) === 0 ) { return ''; }
return;
}
if ( (modeBits & 0b0010) !== 0 ) {
if ( (modeBits & DOMAIN_CAN_USE_ENTITY) !== 0 ) {
if ( this.rePlainEntity.test(hn) ) { return; }
if ( this.reIsEntity.test(hn) === false ) { return ''; }
} else if ( (modeBits & 0b0001) === 0 ) {
} else if ( (modeBits & DOMAIN_CAN_USE_WILDCARD) === 0 ) {
return '';
}
hn = hn.replace(/\*/g, '__asterisk__');
@ -2841,7 +2863,7 @@ export class AstFilterParser {
hn = this.punycoder.hostname.replace(/__asterisk__/g, '*');
}
if (
(modeBits & 0b0001) === 0 && (
(modeBits & DOMAIN_CAN_USE_WILDCARD) === 0 && (
hn.charCodeAt(0) === 0x2E /* . */ ||
exCharCodeAt(hn, -1) === 0x2E /* . */
)

View file

@ -928,6 +928,7 @@ vAPI.Tabs = class extends vAPI.Tabs {
if ( pageStore === null ) { return; }
pageStore.setFrameURL(details);
if ( pageStore.getNetFilteringSwitch() ) {
details.ancestors = pageStore.getFrameAncestorDetails(frameId);
scriptletFilteringEngine.injectNow(details);
}
}

View file

@ -1256,6 +1256,7 @@ const webRequest = {
const pageStore = µb.pageStoreFromTabId(details.tabId);
if ( pageStore === null ) { return; }
if ( pageStore.getNetFilteringSwitch() === false ) { return; }
details.ancestors = pageStore.getFrameAncestorDetails(details.frameId);
scriptletFilteringEngine.injectNow(details);
},
{

View file

@ -70,6 +70,15 @@ function entityFromDomain(domain) {
return pos !== -1 ? domain.slice(0, pos) + '.*' : '';
}
function entityFromHostname(hostname, domain) {
if ( domain === undefined ) {
domain = domainFromHostname(hostname);
}
const entity = entityFromDomain(domain);
if ( entity === '' ) { return ''; }
return `${hostname.slice(0, -domain.length)}${entity}`
}
function hostnameFromURI(uri) {
let match = reHostnameFromCommonURL.exec(uri);
if ( match !== null ) { return match[0].slice(8, -1); }
@ -164,6 +173,7 @@ export {
domainFromHostname,
domainFromURI,
entityFromDomain,
entityFromHostname,
hostnameFromNetworkURL,
hostnameFromURI,
isNetworkURI,