From eae7cd58fe679d6765d62bb6c01e296d5301433a Mon Sep 17 00:00:00 2001 From: Raymond Hill Date: Sat, 28 Nov 2020 11:26:28 -0500 Subject: [PATCH] Add support for `match-case` option; fine-tune behavior of `redirect=` `match-case` ------------ Related issue: - https://github.com/uBlockOrigin/uAssets/issues/8280#issuecomment-735245452 The new filter option `match-case` can be used only for regex-based filters. Using `match-case` with any other sort of filters will cause uBO to discard the filter. `redirect=` ----------- Related issue: - https://github.com/uBlockOrigin/uBlock-issues/issues/1366 `redirect=` filters with unresolvable resource token at runtime will be discarded. Additionally, the implicit priority is now set to 1 (was 0). The idea is to allow custom `redirect=` filters to be used strictly as fallback `redirect=` filters in case another `redirect=` filter is not picked up. For example, one might create a `redirect=click2load.html:0` filter, to be taken if and only if the blocked resource is not already being redirected by another "official" filter in one of the enabled filter lists. --- src/js/background.js | 4 +- src/js/redirect-engine.js | 10 ++++ src/js/static-filtering-parser.js | 47 ++++++++++------ src/js/static-net-filtering.js | 91 ++++++++++++++++++------------- 4 files changed, 95 insertions(+), 57 deletions(-) diff --git a/src/js/background.js b/src/js/background.js index 7b21c2c70..e913b9ff1 100644 --- a/src/js/background.js +++ b/src/js/background.js @@ -140,8 +140,8 @@ const µBlock = (( ) => { // jshint ignore:line // Read-only systemSettings: { - compiledMagic: 35, // Increase when compiled format changes - selfieMagic: 35, // Increase when selfie format changes + compiledMagic: 36, // Increase when compiled format changes + selfieMagic: 36, // Increase when selfie format changes }, // https://github.com/uBlockOrigin/uBlock-issues/issues/759#issuecomment-546654501 diff --git a/src/js/redirect-engine.js b/src/js/redirect-engine.js index 95db7673d..57a79a173 100644 --- a/src/js/redirect-engine.js +++ b/src/js/redirect-engine.js @@ -304,6 +304,16 @@ RedirectEngine.prototype.tokenToURL = function(fctxt, token) { /******************************************************************************/ +RedirectEngine.prototype.hasToken = function(token) { + const asDataURI = token.charCodeAt(0) === 0x25 /* '%' */; + if ( asDataURI ) { + token = token.slice(1); + } + return this.resources.get(this.aliases.get(token) || token) !== undefined; +}; + +/******************************************************************************/ + RedirectEngine.prototype.toSelfie = async function() { }; diff --git a/src/js/static-filtering-parser.js b/src/js/static-filtering-parser.js index 5b9fdcf21..8504abf57 100644 --- a/src/js/static-filtering-parser.js +++ b/src/js/static-filtering-parser.js @@ -1923,22 +1923,23 @@ const OPTTokenImage = 20; const OPTTokenImportant = 21; const OPTTokenInlineFont = 22; const OPTTokenInlineScript = 23; -const OPTTokenMedia = 24; -const OPTTokenMp4 = 25; -const OPTTokenObject = 26; -const OPTTokenOther = 27; -const OPTTokenPing = 28; -const OPTTokenPopunder = 29; -const OPTTokenPopup = 30; -const OPTTokenRedirect = 31; -const OPTTokenRedirectRule = 32; -const OPTTokenQueryprune = 33; -const OPTTokenScript = 34; -const OPTTokenShide = 35; -const OPTTokenXhr = 36; -const OPTTokenWebrtc = 37; -const OPTTokenWebsocket = 38; -const OPTTokenCount = 39; +const OPTTokenMatchCase = 24; +const OPTTokenMedia = 25; +const OPTTokenMp4 = 26; +const OPTTokenObject = 27; +const OPTTokenOther = 28; +const OPTTokenPing = 29; +const OPTTokenPopunder = 30; +const OPTTokenPopup = 31; +const OPTTokenRedirect = 32; +const OPTTokenRedirectRule = 33; +const OPTTokenQueryprune = 34; +const OPTTokenScript = 35; +const OPTTokenShide = 36; +const OPTTokenXhr = 37; +const OPTTokenWebrtc = 38; +const OPTTokenWebsocket = 39; +const OPTTokenCount = 40; //const OPTPerOptionMask = 0x0000ff00; const OPTCanNegate = 1 << 8; @@ -2021,6 +2022,7 @@ Parser.prototype.OPTTokenImportant = OPTTokenImportant; Parser.prototype.OPTTokenInlineFont = OPTTokenInlineFont; Parser.prototype.OPTTokenInlineScript = OPTTokenInlineScript; Parser.prototype.OPTTokenInvalid = OPTTokenInvalid; +Parser.prototype.OPTTokenMatchCase = OPTTokenMatchCase; Parser.prototype.OPTTokenMedia = OPTTokenMedia; Parser.prototype.OPTTokenMp4 = OPTTokenMp4; Parser.prototype.OPTTokenObject = OPTTokenObject; @@ -2082,6 +2084,7 @@ const netOptionTokenDescriptors = new Map([ [ 'important', OPTTokenImportant | OPTBlockOnly ], [ 'inline-font', OPTTokenInlineFont | OPTNonNetworkType | OPTCanNegate | OPTNonCspableType | OPTNonRedirectableType ], [ 'inline-script', OPTTokenInlineScript | OPTNonNetworkType | OPTCanNegate | OPTNonCspableType | OPTNonRedirectableType ], + [ 'match-case', OPTTokenMatchCase ], [ 'media', OPTTokenMedia | OPTCanNegate | OPTNetworkType | OPTModifiableType | OPTRedirectableType | OPTNonCspableType ], [ 'mp4', OPTTokenMp4 | OPTNetworkType | OPTBlockOnly | OPTModifierType ], [ 'object', OPTTokenObject | OPTCanNegate | OPTNetworkType | OPTModifiableType | OPTRedirectableType | OPTNonCspableType ], @@ -2138,6 +2141,7 @@ Parser.netOptionTokenIds = new Map([ [ 'important', OPTTokenImportant ], [ 'inline-font', OPTTokenInlineFont ], [ 'inline-script', OPTTokenInlineScript ], + [ 'match-case', OPTTokenMatchCase ], [ 'media', OPTTokenMedia ], [ 'mp4', OPTTokenMp4 ], [ 'object', OPTTokenObject ], @@ -2184,6 +2188,7 @@ Parser.netOptionTokenNames = new Map([ [ OPTTokenImportant, 'important' ], [ OPTTokenInlineFont, 'inline-font' ], [ OPTTokenInlineScript, 'inline-script' ], + [ OPTTokenMatchCase, 'match-case' ], [ OPTTokenMedia, 'media' ], [ OPTTokenMp4, 'mp4' ], [ OPTTokenObject, 'object' ], @@ -2462,6 +2467,16 @@ const NetOptionsIterator = class { } } } + // `match-case`: valid only for regex-based filters + { + const i = this.tokenPos[OPTTokenMatchCase]; + if ( i !== -1 && this.parser.patternIsRegex() === false ) { + optSlices[i] = OPTTokenInvalid; + if ( this.interactive ) { + this.parser.errorSlices(optSlices[i+1], optSlices[i+5]); + } + } + } return this; } next() { diff --git a/src/js/static-net-filtering.js b/src/js/static-net-filtering.js index 20895709e..a58e1aa7c 100644 --- a/src/js/static-net-filtering.js +++ b/src/js/static-net-filtering.js @@ -146,6 +146,7 @@ const typeValueToTypeName = [ // valid until the next evaluation. let $requestURL = ''; +let $requestURLRaw = ''; let $requestHostname = ''; let $docHostname = ''; let $docDomain = ''; @@ -867,9 +868,13 @@ const FilterPatternGeneric = class { } static compile(details) { - const anchor = details.anchor; + const out = [ + FilterPatternGeneric.fid, + details.pattern, + details.anchor, + ]; details.anchor = 0; - return [ FilterPatternGeneric.fid, details.pattern, anchor ]; + return out; } static fromCompiled(args) { @@ -1107,20 +1112,22 @@ registerFilterClass(FilterTrailingSeparator); /******************************************************************************/ const FilterRegex = class { - constructor(s) { + constructor(s, matchCase = false) { this.s = s; + if ( matchCase ) { + this.matchCase = true; + } } match() { if ( this.re === null ) { - this.re = FilterRegex.dict.get(this.s); - if ( this.re === undefined ) { - this.re = new RegExp(this.s, 'i'); - FilterRegex.dict.set(this.s, this.re); - } + this.re = new RegExp( + this.s, + this.matchCase ? '' : 'i' + ); } - if ( this.re.test($requestURL) === false ) { return false; } - $patternMatchLeft = $requestURL.search(this.re); + if ( this.re.test($requestURLRaw) === false ) { return false; } + $patternMatchLeft = $requestURLRaw.search(this.re); return true; } @@ -1128,33 +1135,36 @@ const FilterRegex = class { details.pattern.push('/', this.s, '/'); details.regex.push(this.s); details.isRegex = true; + if ( this.matchCase ) { + details.options.push('match-case'); + } } toSelfie() { - return [ this.fid, this.s ]; + return [ this.fid, this.s, this.matchCase ]; } static compile(details) { - return [ FilterRegex.fid, details.pattern ]; + return [ FilterRegex.fid, details.pattern, details.patternMatchCase ]; } static fromCompiled(args) { - return new FilterRegex(args[1]); + return new FilterRegex(args[1], args[2]); } static fromSelfie(args) { - return new FilterRegex(args[1]); + return new FilterRegex(args[1], args[2]); } static keyFromArgs(args) { - return args[1]; + return `${args[1]}\t${args[2]}`; } }; FilterRegex.prototype.re = null; +FilterRegex.prototype.matchCase = false; FilterRegex.isSlow = true; -FilterRegex.dict = new Map(); registerFilterClass(FilterRegex); @@ -2783,6 +2793,7 @@ const FilterParser = class { this.modifyValue = undefined; this.invalid = false; this.pattern = ''; + this.patternMatchCase = false; this.party = AnyParty; this.optionUnitBits = 0; this.domainOpt = ''; @@ -2944,6 +2955,9 @@ const FilterParser = class { } this.optionUnitBits |= this.REDIRECT_BIT; break; + case this.parser.OPTTokenMatchCase: + this.patternMatchCase = true; + break; case this.parser.OPTTokenMp4: id = this.action === AllowAction ? this.parser.OPTTokenRedirectRule @@ -3833,6 +3847,7 @@ FilterContainer.prototype.matchAndFetchModifiers = function( modifierType ) { $requestURL = urlTokenizer.setURL(fctxt.url); + $requestURLRaw = fctxt.url; $docHostname = fctxt.getDocHostname(); $docDomain = fctxt.getDocDomain(); $docEntity.reset(); @@ -4126,6 +4141,7 @@ FilterContainer.prototype.matchStringReverse = function(type, url) { // Prime tokenizer: we get a normalized URL in return. $requestURL = urlTokenizer.setURL(url); + $requestURLRaw = url; this.$filterUnit = 0; // These registers will be used by various filters @@ -4172,6 +4188,7 @@ FilterContainer.prototype.matchString = function(fctxt, modifiers = 0) { // Prime tokenizer: we get a normalized URL in return. $requestURL = urlTokenizer.setURL(fctxt.url); + $requestURLRaw = fctxt.url; this.$filterUnit = 0; // These registers will be used by various filters @@ -4203,6 +4220,7 @@ FilterContainer.prototype.matchHeaders = function(fctxt, headers) { // Prime tokenizer: we get a normalized URL in return. $requestURL = urlTokenizer.setURL(fctxt.url); + $requestURLRaw = fctxt.url; this.$filterUnit = 0; // These registers will be used by various filters @@ -4239,13 +4257,9 @@ FilterContainer.prototype.redirectRequest = function(fctxt) { const directive = directives[0]; if ( (directive.bits & AllowAction) !== 0 ) { return directive; } const modifier = directive.modifier; - if ( modifier.cache === undefined ) { - modifier.cache = this.parseRedirectRequestValue(modifier.value); - } - fctxt.redirectURL = µb.redirectEngine.tokenToURL( - fctxt, - modifier.cache.token - ); + const { token } = this.parseRedirectRequestValue(modifier); + fctxt.redirectURL = µb.redirectEngine.tokenToURL(fctxt, token); + if ( fctxt.redirectURL === undefined ) { return; } return directive; } // Multiple directives mean more work to do. @@ -4258,15 +4272,11 @@ FilterContainer.prototype.redirectRequest = function(fctxt) { winningDirective = directive; break; } - if ( modifier.cache === undefined ) { - modifier.cache = this.parseRedirectRequestValue(modifier.value); - } - if ( - winningDirective === undefined || - modifier.cache.priority > winningPriority - ) { + const { token, priority } = this.parseRedirectRequestValue(modifier); + if ( µb.redirectEngine.hasToken(token) === false ) { continue; } + if ( winningDirective === undefined || priority > winningPriority ) { winningDirective = directive; - winningPriority = modifier.cache.priority; + winningPriority = priority; } } if ( winningDirective === undefined ) { return; } @@ -4279,15 +4289,18 @@ FilterContainer.prototype.redirectRequest = function(fctxt) { return winningDirective; }; -FilterContainer.prototype.parseRedirectRequestValue = function(rawValue) { - let token = rawValue; - let priority = 0; - const match = /:(\d+)$/.exec(rawValue); - if ( match !== null ) { - token = rawValue.slice(0, match.index); - priority = parseInt(match[1], 10); +FilterContainer.prototype.parseRedirectRequestValue = function(modifier) { + if ( modifier.cache === undefined ) { + let token = modifier.value; + let priority = 1; + const match = /:(\d+)$/.exec(token); + if ( match !== null ) { + token = token.slice(0, match.index); + priority = parseInt(match[1], 10); + } + modifier.cache = { token, priority }; } - return { token, priority }; + return modifier.cache; }; /******************************************************************************/