-
Notifications
You must be signed in to change notification settings - Fork 107
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: $removeparam
#4528
base: master
Are you sure you want to change the base?
feat: $removeparam
#4528
Changes from 22 commits
94f5868
29e9c39
3ec1181
fd43ad1
9497047
56f90cc
47b1302
7ea4488
29e9022
3cb0861
9c1e12b
c8da3bb
533110e
1c07025
90d051b
ec917aa
c1db908
d48fe63
88f35a1
160febd
4040a89
90ce5c2
da507f4
3c6236b
09ea7e7
76e6a51
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -776,10 +776,14 @@ export default class FilterEngine extends EventEmitter<EngineEventHandlers> { | |
} else if (filter.isGenericHide() || filter.isSpecificHide()) { | ||
hideExceptions.push(filter); | ||
} else if (filter.isException()) { | ||
exceptions.push(filter); | ||
if (filter.isRemoveParam()) { | ||
redirects.push(filter); | ||
} else { | ||
exceptions.push(filter); | ||
} | ||
} else if (filter.isImportant()) { | ||
importants.push(filter); | ||
} else if (filter.isRedirect()) { | ||
} else if (filter.isRedirectable()) { | ||
redirects.push(filter); | ||
} else { | ||
filters.push(filter); | ||
|
@@ -1443,29 +1447,97 @@ export default class FilterEngine extends EventEmitter<EngineEventHandlers> { | |
if (request.isSupported) { | ||
// Check the filters in the following order: | ||
// 1. $important (not subject to exceptions) | ||
// 2. redirection ($redirect=resource) | ||
// 2. redirection ($removeparam and $redirect=resource) | ||
// 3. normal filters | ||
// 4. exceptions | ||
result.filter = this.importants.match(request, this.isFilterExcluded.bind(this)); | ||
|
||
let redirectNone: NetworkFilter | undefined; | ||
let redirectRule: NetworkFilter | undefined; | ||
|
||
// 1st priority: @@||<entity>$removeparam | ||
// 2nd priority: ||<entity>$removeparam | ||
// 3rd priority: @@||<entity>$removeparam=x | ||
// 4th priority: ||<entity>$removeparam=x | ||
let removeparamExceptionFilter: NetworkFilter | undefined; | ||
let redirectUrl: URL | undefined; | ||
|
||
// If `result.filter` is `undefined`, it means there was no $important | ||
// filter found so far. We look for a $redirect filter. There is some | ||
// extra logic to handle special cases like redirect-rule and | ||
// redirect=none. | ||
// | ||
// * If redirect=none is found, then cancel all redirects. | ||
// * Else if redirect-rule is found, only redirect if request would be blocked. | ||
// * Else if redirect is found, redirect. | ||
// filter found so far. We look for $removeparam and $redirect filter. | ||
if (result.filter === undefined) { | ||
const redirects = this.redirects | ||
.matchAll(request, this.isFilterExcluded.bind(this)) | ||
// highest priorty wins | ||
.sort((a, b) => b.getRedirectPriority() - a.getRedirectPriority()); | ||
const redirectableFilters = this.redirects.matchAll( | ||
request, | ||
this.isFilterExcluded.bind(this), | ||
); | ||
const redirectFilters: NetworkFilter[] = []; | ||
const removeparamFilters: Map<string, NetworkFilter> = new Map(); | ||
const removeparamExceptionFilters: Map<string, NetworkFilter> = new Map(); | ||
for (const filter of redirectableFilters) { | ||
if (filter.isRemoveParam()) { | ||
if (filter.isException()) { | ||
removeparamExceptionFilters.set(filter.removeparam!, filter); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it might be possible to inform the type checker from There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it's happening because I defined it as |
||
} else { | ||
removeparamFilters.set(filter.removeparam!, filter); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it safe to use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. those conditions (1p, 3p, and request types) should be handled in the network filter matching process. |
||
} | ||
} else { | ||
redirectFilters.push(filter); | ||
} | ||
} | ||
|
||
// We don't need to match `removeparam` at all if: | ||
// * `removeparam` filters not matched | ||
// * URL doesn't have search parameter | ||
if (removeparamFilters.size !== 0) { | ||
redirectUrl = new URL(request.url); | ||
if (redirectUrl.searchParams.size !== 0) { | ||
for (const [key, filter] of removeparamFilters) { | ||
// Remove all params in case of option value is empty | ||
// We will not match individual exceptions since it has a higher priority than them | ||
if (key === '') { | ||
result.filter = filter; | ||
removeparamExceptionFilter = removeparamExceptionFilters.get(''); | ||
|
||
// In case of non-existence of global exception, we will remove params | ||
if (removeparamExceptionFilter === undefined) { | ||
redirectUrl.search = ''; | ||
} | ||
|
||
break; | ||
} | ||
|
||
if (redirectUrl.searchParams.has(key)) { | ||
result.filter = filter; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That seems invalid in case there is more than one |
||
removeparamExceptionFilter = | ||
removeparamExceptionFilters.get('') ?? removeparamExceptionFilters.get(key); | ||
|
||
redirectUrl.searchParams.delete(key); | ||
|
||
// Stop if | ||
// * the filter was effective | ||
// * the filter was excepted with global exception (1st priority) | ||
if ( | ||
removeparamExceptionFilter === undefined || | ||
removeparamExceptionFilter.removeparam === '' | ||
) { | ||
break; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
// If `result.filter` still remains `undefined`, there | ||
// is some extra logic to handle special cases like | ||
// redirect-rule and redirect=none. | ||
// | ||
// * If redirect=none is found, then cancel all redirects. | ||
// * Else if redirect-rule is found, only redirect if request would be blocked. | ||
// * Else if redirect is found, redirect. | ||
if (result.filter === undefined && redirectFilters.length !== 0) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is the rational behind giving priority to removeparam over redirects? Wouldn't it make sense to do it the other way around? A redirect rule is a block rule + a redirect rule (as per uBO semantics). Hence if a request is blocked it should have priority over keeping the request and remove some of the parameters. |
||
const redirects = redirectFilters | ||
// highest priorty wins | ||
.sort((a, b) => b.getRedirectPriority() - a.getRedirectPriority()); | ||
|
||
if (redirects.length !== 0) { | ||
for (const filter of redirects) { | ||
if (filter.getRedirectResource() === 'none') { | ||
redirectNone = filter; | ||
|
@@ -1503,17 +1575,30 @@ export default class FilterEngine extends EventEmitter<EngineEventHandlers> { | |
// If there was a redirect match and no exception was found, then we | ||
// proceed and process the redirect rule. This means two things: | ||
// | ||
// 1. Check if a redirect=none rule was found, which acts as exception. | ||
// 2. If no exception was found, prepare `result.redirect` response. | ||
// 1. Check if there's a removeparam exception was found, which acts as exception. | ||
// 2. Check if a redirect=none rule was found, which acts as exception. | ||
// 3. If no exception was found, prepare `result.redirect` response. | ||
if ( | ||
result.filter !== undefined && | ||
result.exception === undefined && | ||
result.filter.isRedirect() | ||
result.filter.isRedirectable() | ||
) { | ||
if (redirectNone !== undefined) { | ||
result.exception = redirectNone; | ||
if (result.filter.isRemoveParam()) { | ||
if (removeparamExceptionFilter === undefined) { | ||
result.redirect = { | ||
body: '', | ||
contentType: 'text/plain', | ||
dataUrl: redirectUrl!.toString(), | ||
}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It seems a bit counter-intuitive to me to reuse the dataUrl in order to express the removeparam result. Why not return a list of URL modifiers to be interpreted by the client code and adapted based on available capability of the platform? (iOS, Manifest v2, Manifest v3, might not have the best way to modify request parameters) Ideally the adblocker library should not need to think about these considerations and return a more "abstract" kind of response. |
||
} else { | ||
result.exception = removeparamExceptionFilter; | ||
} | ||
} else { | ||
result.redirect = this.resources.getResource(result.filter.getRedirectResource()); | ||
if (redirectNone !== undefined) { | ||
result.exception = redirectNone; | ||
} else { | ||
result.redirect = this.resources.getResource(result.filter.getRedirectResource()); | ||
} | ||
} | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change | ||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -153,6 +153,7 @@ export const enum NETWORK_FILTER_MASK { | |||||||||||||||
isHostnameAnchor = 1 << 28, | ||||||||||||||||
isRedirectRule = 1 << 29, | ||||||||||||||||
isRedirect = 1 << 30, | ||||||||||||||||
isRemoveParam = 1 << 31, | ||||||||||||||||
// IMPORTANT: the mask is now full, no more options can be added | ||||||||||||||||
// Consider creating a separate fitler type for isReplace if a new | ||||||||||||||||
// network filter option is needed. | ||||||||||||||||
|
@@ -887,6 +888,16 @@ export default class NetworkFilter implements IFilter { | |||||||||||||||
mask = setBit(mask, NETWORK_FILTER_MASK.isReplace); | ||||||||||||||||
optionValue = value; | ||||||||||||||||
|
||||||||||||||||
break; | ||||||||||||||||
case 'removeparam': | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit. Since it would be invalid to have filters with a combination of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's good idea, I think we can do it in a separate PR instead. |
||||||||||||||||
// TODO: Support regex | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How about |
||||||||||||||||
if (negation || value.startsWith('/')) { | ||||||||||||||||
seia-soto marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||
return null; | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
mask = setBit(mask, NETWORK_FILTER_MASK.isRemoveParam); | ||||||||||||||||
optionValue = value; | ||||||||||||||||
|
||||||||||||||||
break; | ||||||||||||||||
default: { | ||||||||||||||||
// Handle content type options separatly | ||||||||||||||||
|
@@ -1151,6 +1162,8 @@ export default class NetworkFilter implements IFilter { | |||||||||||||||
} | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
mask >>>= 0; | ||||||||||||||||
seia-soto marked this conversation as resolved.
Show resolved
Hide resolved
seia-soto marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||
|
||||||||||||||||
return new NetworkFilter({ | ||||||||||||||||
filter, | ||||||||||||||||
hostname, | ||||||||||||||||
|
@@ -1265,6 +1278,14 @@ export default class NetworkFilter implements IFilter { | |||||||||||||||
return this.optionValue; | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
public get removeparam(): string | undefined { | ||||||||||||||||
if (!this.isRemoveParam()) { | ||||||||||||||||
return undefined; | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
return this.optionValue; | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
public isCosmeticFilter(): this is CosmeticFilter { | ||||||||||||||||
return false; | ||||||||||||||||
} | ||||||||||||||||
|
@@ -1535,6 +1556,14 @@ export default class NetworkFilter implements IFilter { | |||||||||||||||
options.push('badfilter'); | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
if (this.isRemoveParam()) { | ||||||||||||||||
if (this.removeparam!.length > 0) { | ||||||||||||||||
seia-soto marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||
options.push(`removeparam=${this.optionValue}`); | ||||||||||||||||
Comment on lines
+1565
to
+1567
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit. Maybe we don't need to mix the use of three different methods/getters.
Suggested change
|
||||||||||||||||
} else { | ||||||||||||||||
options.push('removeparam'); | ||||||||||||||||
seia-soto marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||
} | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
if (options.length > 0) { | ||||||||||||||||
if (typeof modifierReplacer === 'function') { | ||||||||||||||||
filter += `$${options.map(modifierReplacer).join(',')}`; | ||||||||||||||||
|
@@ -1608,6 +1637,14 @@ export default class NetworkFilter implements IFilter { | |||||||||||||||
return getBit(this.getMask(), NETWORK_FILTER_MASK.isReplace); | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
public isRemoveParam(): boolean { | ||||||||||||||||
return getBit(this.getMask(), NETWORK_FILTER_MASK.isRemoveParam); | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
public isRedirectable(): boolean { | ||||||||||||||||
return this.isRedirect() || this.isRemoveParam(); | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
// Expected to be called only with `$replace` modifiers | ||||||||||||||||
public getHtmlModifier(): HTMLModifier | null { | ||||||||||||||||
// Empty `$replace` modifier is to disable all replace modifiers on exception | ||||||||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we store removeparam as redirect rules? Aren't they of different nature?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They're different but removeparam is implemented using redirection.