@adguard/agtree
Version:
Tool set for working with adblock filter lists
124 lines (121 loc) • 5.83 kB
JavaScript
/*
* AGTree v3.2.2 (build date: Tue, 08 Jul 2025 13:39:47 GMT)
* (c) 2025 Adguard Software Ltd.
* Released under the MIT license
* https://github.com/AdguardTeam/tsurlfilter/tree/master/packages/agtree#readme
*/
import { TokenType, getFormattedTokenName } from '@adguard/css-tokenizer';
import { sprintf } from 'sprintf-js';
import { RuleConversionError } from '../../errors/rule-conversion-error.js';
import { RuleCategory, CosmeticRuleType } from '../../nodes/index.js';
import { RuleConverterBase } from '../base-interfaces/rule-converter-base.js';
import { createModifierListNode, createModifierNode } from '../../ast-utils/modifiers.js';
import { UBO_HTML_MASK, EMPTY } from '../../utils/constants.js';
import { ADBLOCK_URL_START, ADBLOCK_URL_SEPARATOR } from '../../utils/regexp.js';
import { createNetworkRuleNode } from '../../ast-utils/network-rules.js';
import { AdblockSyntax } from '../../utils/adblockers.js';
import { createNodeConversionResult } from '../base-interfaces/conversion-result.js';
import { CssTokenStream } from '../../parser/css/css-token-stream.js';
/**
* @file Converter for request header removal rules
*/
const UBO_RESPONSEHEADER_FN = 'responseheader';
const ADG_REMOVEHEADER_MODIFIER = 'removeheader';
const ERROR_MESSAGES = {
EMPTY_PARAMETER: `Empty parameter for '${UBO_RESPONSEHEADER_FN}' function`,
EXPECTED_END_OF_RULE: "Expected end of rule, but got '%s'",
MULTIPLE_DOMAINS_NOT_SUPPORTED: 'Multiple domains are not supported yet',
};
/**
* Converter for request header removal rules
*
* @todo Implement `convertToUbo` (ABP currently doesn't support header removal rules)
*/
class HeaderRemovalRuleConverter extends RuleConverterBase {
/**
* Converts a header removal rule to AdGuard syntax, if possible.
*
* @param rule Rule node to convert
* @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
* the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
* If the rule was not converted, the result array will contain the original node with the same object reference
* @throws If the rule is invalid or cannot be converted
* @example
* If the input rule is:
* ```adblock
* example.com##^responseheader(header-name)
* ```
* The output will be:
* ```adblock
* ||example.com^$removeheader=header-name
* ```
*/
static convertToAdg(rule) {
// TODO: Add support for ABP syntax once it starts supporting header removal rules
// Leave the rule as is if it's not a header removal rule
if (rule.category !== RuleCategory.Cosmetic || rule.type !== CosmeticRuleType.HtmlFilteringRule) {
return createNodeConversionResult([rule], false);
}
const stream = new CssTokenStream(rule.body.value);
let token;
// Skip leading whitespace
stream.skipWhitespace();
// Next token should be the `^` followed by a `responseheader` function
token = stream.get();
if (!token || token.type !== TokenType.Delim || rule.body.value[token.start] !== UBO_HTML_MASK) {
return createNodeConversionResult([rule], false);
}
stream.advance();
token = stream.get();
if (!token) {
return createNodeConversionResult([rule], false);
}
const functionName = rule.body.value.slice(token.start, token.end - 1);
if (functionName !== UBO_RESPONSEHEADER_FN) {
return createNodeConversionResult([rule], false);
}
// Parse the parameter
const paramStart = token.end;
stream.skipUntilBalanced();
const paramEnd = stream.getOrFail().end;
const param = rule.body.value.slice(paramStart, paramEnd - 1).trim();
// Do not allow empty parameter
if (param.length === 0) {
throw new RuleConversionError(ERROR_MESSAGES.EMPTY_PARAMETER);
}
stream.expect(TokenType.CloseParenthesis);
stream.advance();
// Skip trailing whitespace after the function call
stream.skipWhitespace();
// Expect the end of the rule - so nothing should be left in the stream
if (!stream.isEof()) {
token = stream.getOrFail();
throw new RuleConversionError(sprintf(ERROR_MESSAGES.EXPECTED_END_OF_RULE, getFormattedTokenName(token.type)));
}
// Prepare network rule pattern
const pattern = [];
if (rule.domains.children.length === 1) {
// If the rule has only one domain, we can use a simple network rule pattern:
// ||single-domain-from-the-rule^
pattern.push(ADBLOCK_URL_START, rule.domains.children[0].value, ADBLOCK_URL_SEPARATOR);
}
else if (rule.domains.children.length > 1) {
// TODO: Add support for multiple domains, for example:
// example.com,example.org,example.net##^responseheader(header-name)
// We should consider allowing $domain with $removeheader modifier,
// for example:
// $removeheader=header-name,domain=example.com|example.org|example.net
throw new RuleConversionError(ERROR_MESSAGES.MULTIPLE_DOMAINS_NOT_SUPPORTED);
}
// Prepare network rule modifiers
const modifiers = createModifierListNode();
modifiers.children.push(createModifierNode(ADG_REMOVEHEADER_MODIFIER, param));
// Construct the network rule
return createNodeConversionResult([
createNetworkRuleNode(pattern.join(EMPTY), modifiers,
// Copy the exception flag
rule.exception, AdblockSyntax.Adg),
], true);
}
}
export { ERROR_MESSAGES, HeaderRemovalRuleConverter };