mirror of
https://github.com/gorhill/uBlock.git
synced 2026-03-11 09:04:36 +00:00
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:
parent
e636c32f2a
commit
a483f7955f
10 changed files with 103 additions and 44 deletions
|
|
@ -321,7 +321,6 @@ export async function benchmarkCosmeticFiltering() {
|
|||
frameId: undefined,
|
||||
hostname: '',
|
||||
domain: '',
|
||||
entity: '',
|
||||
};
|
||||
const options = {
|
||||
noSpecificCosmeticFiltering: false,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 /* . */
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue