From aaf35d9d7106800ed2dfdded3b40d7afa404e464 Mon Sep 17 00:00:00 2001 From: Raymond Hill Date: Fri, 15 Aug 2025 15:55:33 -0400 Subject: [PATCH] Improve compatibility of `uritransform=` with DNR syntax The `uritransform=` option will now be converted to a proper DNR rule when the following condition are fulfilled: - The value of the `uritransform` option matches `//[replacement]/`, i.e. the pattern to match is empty, and only the replacement part is provided. - The filter pattern is a regex. Is such case, the DNR rule will be a `redirect` making use of the `regexSubstitution` property. In case the above conditions are not fulfilled, the filter will be discarded as incompatible with DNR syntax (as was the case before). This is potentially a breaking change, in cases where a filter assumed that the part to match was the start of the path part of a URL. A reminder that `uritransform` is an option which requires a trusted source, otherwise it is rejected. --- src/js/benchmarks.js | 2 +- src/js/messaging.js | 4 ++- src/js/pagestore.js | 2 +- src/js/static-filtering-parser.js | 27 ++++++++++++-------- src/js/static-net-filtering.js | 41 +++++++++++++++++++++---------- 5 files changed, 50 insertions(+), 26 deletions(-) diff --git a/src/js/benchmarks.js b/src/js/benchmarks.js index 7e24d39bd..b8ee3a886 100644 --- a/src/js/benchmarks.js +++ b/src/js/benchmarks.js @@ -183,7 +183,7 @@ export async function benchmarkStaticNetFiltering(options = {}) { if ( r === 1 ) { blockCount += 1; } else if ( r === 2 ) { allowCount += 1; } if ( r !== 1 ) { - if ( sfne.transformRequest(fctxt) ) { + if ( sfne.transformURL(fctxt) ) { redirectCount += 1; } if ( sfne.hasQuery(fctxt) ) { diff --git a/src/js/messaging.js b/src/js/messaging.js index 10488e98e..7735d77ba 100644 --- a/src/js/messaging.js +++ b/src/js/messaging.js @@ -1886,7 +1886,9 @@ const onMessage = function(request, sender, callback) { return { name: assetKey, text: details.content, - trustedSource: assetKey.startsWith('ublock-'), + trustedSource: assetKey.startsWith('ublock-') || + assetKey === µb.userFiltersPath && + µb.userSettings.userFiltersTrusted, }; }) ); diff --git a/src/js/pagestore.js b/src/js/pagestore.js index db308ddf4..606dfc2bb 100644 --- a/src/js/pagestore.js +++ b/src/js/pagestore.js @@ -956,7 +956,7 @@ const PageStore = class { redirectNonBlockedRequest(fctxt) { const directives = []; - staticNetFilteringEngine.transformRequest(fctxt, directives); + staticNetFilteringEngine.transformURL(fctxt, directives); if ( staticNetFilteringEngine.hasQuery(fctxt) ) { staticNetFilteringEngine.filterQuery(fctxt, directives); } diff --git a/src/js/static-filtering-parser.js b/src/js/static-filtering-parser.js index 07972a9ca..b7739681d 100644 --- a/src/js/static-filtering-parser.js +++ b/src/js/static-filtering-parser.js @@ -841,6 +841,11 @@ export class AstFilterParser { this.netOptionValueParser = new ArglistParser(','); this.scriptletArgListParser = new ArglistParser(','); this.domainRegexValueParser = new ArglistParser('/'); + this.reNetOptionTokens = new RegExp( + `^(${Array.from(netOptionTokenDescriptors.keys()) + .map(s => escapeForRegex(s)) + .join('|')})\\b` + ); } finish() { @@ -1512,12 +1517,7 @@ export class AstFilterParser { for (;;) { const before = s.charAt(j-1); if ( before === '$' ) { return -1; } - const after = s.charAt(j+1); - if ( ')/|'.includes(after) === false ) { - if ( before === '' || '"\'\\`'.includes(before) === false ) { - return j; - } - } + if ( this.reNetOptionTokens.test(s.slice(j+1)) ) { return j; } if ( j === start ) { break; } j = s.lastIndexOf('$', j-1); if ( j === -1 ) { break; } @@ -3080,9 +3080,10 @@ export function parseReplaceByRegexValue(s) { if ( parser.transform ) { pattern = parser.normalizeArg(pattern); } - if ( pattern === '' ) { return; } - pattern = parser.normalizeArg(pattern, '$'); - pattern = parser.normalizeArg(pattern, ','); + if ( pattern !== '' ) { + pattern = parser.normalizeArg(pattern, '$'); + pattern = parser.normalizeArg(pattern, ','); + } parser.nextArg(s, parser.separatorEnd); let replacement = s.slice(parser.argBeg, parser.argEnd); if ( parser.separatorEnd === parser.separatorBeg ) { return; } @@ -3092,6 +3093,9 @@ export function parseReplaceByRegexValue(s) { replacement = parser.normalizeArg(replacement, '$'); replacement = parser.normalizeArg(replacement, ','); const flags = s.slice(parser.separatorEnd); + if ( pattern === '' ) { + return { flags, replacement } + } try { return { re: new RegExp(pattern, flags), replacement }; } catch { @@ -3101,7 +3105,10 @@ export function parseReplaceByRegexValue(s) { export function parseReplaceValue(s) { if ( s.startsWith('/') ) { const r = parseReplaceByRegexValue(s); - if ( r ) { r.type = 'text'; } + if ( r ) { + if ( r.re === undefined ) { return; } + r.type = 'text'; + } return r; } const pos = s.indexOf(':'); diff --git a/src/js/static-net-filtering.js b/src/js/static-net-filtering.js index 898ad80c6..da4d3649d 100644 --- a/src/js/static-net-filtering.js +++ b/src/js/static-net-filtering.js @@ -1231,7 +1231,6 @@ class FilterRegex { ); } if ( refs.$re.test($requestURLRaw) === false ) { return false; } - $patternMatchLeft = $requestURLRaw.search(refs.$re); return true; } @@ -4783,7 +4782,19 @@ StaticNetFilteringEngine.prototype.dnrFromCompiled = function(op, context, ...ar break; } case 'uritransform': { - dnrAddRuleError(rule, `Incompatible with DNR: uritransform=${rule.__modifierValue}`); + const parsed = sfp.parseReplaceByRegexValue(rule.__modifierValue); + if ( parsed.re !== undefined ) { + dnrAddRuleError(rule, `Incompatible with DNR: uritransform=${rule.__modifierValue}`); + break; + } + if ( rule.condition.regexFilter === undefined ) { + dnrAddRuleError(rule, `Incompatible with DNR (need regexFilter): uritransform=${rule.__modifierValue}`); + break; + } + rule.action.type = 'redirect'; + rule.action.redirect = { + regexSubstitution: parsed.replacement.replace(/\$(\d+)/g, '\\$1') + }; break; } case 'urlskip': { @@ -5500,7 +5511,7 @@ function compareRedirectRequests(redirectEngine, a, b) { /******************************************************************************/ -StaticNetFilteringEngine.prototype.transformRequest = function(fctxt, out = []) { +StaticNetFilteringEngine.prototype.transformURL = function(fctxt, out = []) { const directives = this.matchAndFetchModifiers(fctxt, 'uritransform'); if ( directives === undefined ) { return; } const redirectURL = new URL(fctxt.url); @@ -5514,17 +5525,21 @@ StaticNetFilteringEngine.prototype.transformRequest = function(fctxt, out = []) } const cache = directive.cache; if ( cache === undefined ) { continue; } - const before = `${redirectURL.pathname}${redirectURL.search}${redirectURL.hash}`; - if ( cache.re.test(before) !== true ) { continue; } - const after = before.replace(cache.re, cache.replacement); + let { re } = cache; + const before = redirectURL.href; + if ( re === undefined ) { + const logdata = directive.logData(); + if ( logdata === undefined ) { continue; } + try { re = new RegExp(logdata.regex, cache.flags); } + catch { continue; } + } + if ( re.test(before) !== true ) { continue; } + const after = before.replace(re, cache.replacement); + try { void new URL(after); } catch { continue; } if ( after === before ) { continue; } - const hashPos = after.indexOf('#'); - redirectURL.hash = hashPos !== -1 ? after.slice(hashPos) : ''; - const afterMinusHash = hashPos !== -1 ? after.slice(0, hashPos) : after; - const searchPos = afterMinusHash.indexOf('?'); - redirectURL.search = searchPos !== -1 ? afterMinusHash.slice(searchPos) : ''; - redirectURL.pathname = searchPos !== -1 ? after.slice(0, searchPos) : after; + redirectURL.href = after; out.push(directive); + break; } if ( out.length === 0 ) { return; } if ( redirectURL.href !== fctxt.url ) { @@ -5724,7 +5739,7 @@ StaticNetFilteringEngine.prototype.test = function(details) { out.push('not blocked'); } if ( r !== 1 ) { - const entries = this.transformRequest(fctxt); + const entries = this.transformURL(fctxt); if ( entries ) { for ( const entry of entries ) { out.push(`modified: ${entry.logData().raw}`);