@adguard/agtree
Version:
Tool set for working with adblock filter lists
312 lines (309 loc) • 18 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 { SEMICOLON, SPACE } from '../../utils/constants.js';
import { createModifierNode } from '../../ast-utils/modifiers.js';
import { BaseConverter } from '../base-interfaces/base-converter.js';
import { RuleConversionError } from '../../errors/rule-conversion-error.js';
import { MultiValueMap } from '../../utils/multi-value-map.js';
import { createConversionResult } from '../base-interfaces/conversion-result.js';
import { cloneModifierListNode } from '../../ast-utils/clone.js';
import { modifiersCompatibilityTable } from '../../compatibility-tables/modifiers.js';
import { redirectsCompatibilityTable } from '../../compatibility-tables/redirects.js';
import '../../compatibility-tables/scriptlets.js';
import { GenericPlatform } from '../../compatibility-tables/platforms.js';
import '../../compatibility-tables/schemas/base.js';
import '../../compatibility-tables/schemas/modifier.js';
import '../../compatibility-tables/schemas/redirect.js';
import '../../compatibility-tables/schemas/scriptlet.js';
import '../../compatibility-tables/schemas/platform.js';
import '../../compatibility-tables/utils/platform-helpers.js';
import '../../compatibility-tables/schemas/resource-type.js';
import { isValidResourceType } from '../../compatibility-tables/utils/resource-type-helpers.js';
import { isUndefined } from '../../utils/type-guards.js';
/**
* @file Network rule modifier list converter.
*/
/**
* @see {@link https://adguard.com/kb/general/ad-filtering/create-own-filters/#csp-modifier}
*/
const CSP_MODIFIER = 'csp';
const CSP_SEPARATOR = SEMICOLON + SPACE;
/**
* @see {@link https://adguard.com/kb/general/ad-filtering/create-own-filters/#csp-modifier}
* @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy}
*/
const COMMON_CSP_PARAMS = '\'self\' \'unsafe-eval\' http: https: data: blob: mediastream: filesystem:';
/**
* @see {@link https://help.adblockplus.org/hc/en-us/articles/360062733293#rewrite}
*/
const ABP_REWRITE_MODIFIER = 'rewrite';
/**
* @see {@link https://adguard.com/kb/general/ad-filtering/create-own-filters/#redirect-modifier}
*/
const REDIRECT_MODIFIER = 'redirect';
/**
* @see {@link https://adguard.com/kb/general/ad-filtering/create-own-filters/#redirect-rule-modifier}
*/
const REDIRECT_RULE_MODIFIER = 'redirect-rule';
/**
* @see {@link https://github.com/gorhill/uBlock/wiki/Resources-Library#empty-redirect-resources}
*/
const UBO_NOOP_TEXT_RESOURCE = 'noop.txt';
/**
* Redirect-related modifiers.
*/
const REDIRECT_MODIFIERS = new Set([
ABP_REWRITE_MODIFIER,
REDIRECT_MODIFIER,
REDIRECT_RULE_MODIFIER,
]);
/**
* Conversion map for ADG network rule modifiers.
*/
const ADG_CONVERSION_MAP = new Map([
['1p', [{ name: () => 'third-party', exception: (actual) => !actual }]],
['3p', [{ name: () => 'third-party' }]],
['css', [{ name: () => 'stylesheet' }]],
['doc', [{ name: () => 'document' }]],
['ehide', [{ name: () => 'elemhide' }]],
['empty', [{ name: () => 'redirect', value: () => 'nooptext' }]],
['first-party', [{ name: () => 'third-party', exception: (actual) => !actual }]],
['frame', [{ name: () => 'subdocument' }]],
['ghide', [{ name: () => 'generichide' }]],
['inline-font', [{ name: () => CSP_MODIFIER, value: () => `font-src ${COMMON_CSP_PARAMS}` }]],
['inline-script', [{ name: () => CSP_MODIFIER, value: () => `script-src ${COMMON_CSP_PARAMS}` }]],
['mp4', [{ name: () => 'redirect', value: () => 'noopmp4-1s' }, { name: () => 'media', value: () => undefined }]],
['queryprune', [{ name: () => 'removeparam' }]],
['shide', [{ name: () => 'specifichide' }]],
['xhr', [{ name: () => 'xmlhttprequest' }]],
]);
/**
* Helper class for converting network rule modifier lists.
*
* @todo Implement `convertToUbo` and `convertToAbp`
*/
class NetworkRuleModifierListConverter extends BaseConverter {
/**
* Converts a network rule modifier list to AdGuard format, if possible.
*
* @param modifierList Network rule modifier list node to convert
* @param isException If `true`, the rule is an exception rule
* @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
* the converted node, and its `isConverted` flag indicates whether the original node was converted.
* If the node was not converted, the result will contain the original node with the same object reference
* @throws If the conversion is not possible
*/
static convertToAdg(modifierList, isException = false) {
const conversionMap = new MultiValueMap();
// Special case: $csp modifier
let cspCount = 0;
modifierList.children.forEach((modifierNode, index) => {
const modifierConversions = ADG_CONVERSION_MAP.get(modifierNode.name.value);
if (modifierConversions) {
for (const modifierConversion of modifierConversions) {
const name = modifierConversion.name(modifierNode.name.value);
const exception = modifierConversion.exception
// If the exception value is undefined in the original modifier, it
// means that the modifier isn't negated
? modifierConversion.exception(modifierNode.exception || false)
: modifierNode.exception;
const value = modifierConversion.value
? modifierConversion.value(modifierNode.value?.value)
: modifierNode.value?.value;
// Check if the name or the value is different from the original modifier
// If so, add the converted modifier to the list
if (name !== modifierNode.name.value || value !== modifierNode.value?.value) {
conversionMap.add(index, createModifierNode(name, value, exception));
}
// Special case: $csp modifier
if (name === CSP_MODIFIER) {
cspCount += 1;
}
}
return;
}
// Handle special case: resource redirection modifiers
if (REDIRECT_MODIFIERS.has(modifierNode.name.value)) {
// Redirect modifiers can't be negated
if (modifierNode.exception === true) {
throw new RuleConversionError(`Modifier '${modifierNode.name.value}' cannot be negated`);
}
// Convert the redirect resource name to ADG format
const redirectResource = modifierNode.value?.value;
// Special case: for exception rules, $redirect without value is allowed,
// and in this case it means an exception for all redirects
if (!redirectResource && !isException) {
throw new RuleConversionError(`No redirect resource specified for '${modifierNode.name.value}' modifier`);
}
// Leave $redirect and $redirect-rule modifiers as is, but convert $rewrite to $redirect
const modifierName = modifierNode.name.value === ABP_REWRITE_MODIFIER
? REDIRECT_MODIFIER
: modifierNode.name.value;
const convertedRedirectResource = redirectResource
? redirectsCompatibilityTable.getFirst(redirectResource, GenericPlatform.AdgAny)?.name
: undefined;
// Check if the modifier name or the redirect resource name is different from the original modifier.
// If so, add the converted modifier to the list
if (modifierName !== modifierNode.name.value
|| (convertedRedirectResource !== undefined && convertedRedirectResource !== redirectResource)) {
conversionMap.add(index, createModifierNode(modifierName,
// If the redirect resource name is unknown, fall back to the original one
// Later, the validator will throw an error if the resource name is invalid
convertedRedirectResource || redirectResource, modifierNode.exception));
}
}
});
// Prepare the result if there are any converted modifiers or $csp modifiers
if (conversionMap.size || cspCount) {
const modifierListClone = cloneModifierListNode(modifierList);
// Replace the original modifiers with the converted ones
// One modifier may be replaced with multiple modifiers, so we need to flatten the array
modifierListClone.children = modifierListClone.children.map((modifierNode, index) => {
const conversionRecord = conversionMap.get(index);
if (conversionRecord) {
return conversionRecord;
}
return modifierNode;
}).flat();
// Special case: $csp modifier: merge multiple $csp modifiers into one
// and put it at the end of the modifier list
if (cspCount) {
const cspValues = [];
modifierListClone.children = modifierListClone.children.filter((modifierNode) => {
if (modifierNode.name.value === CSP_MODIFIER) {
if (!modifierNode.value?.value) {
throw new RuleConversionError('$csp modifier value is missing');
}
cspValues.push(modifierNode.value?.value);
return false;
}
return true;
});
modifierListClone.children.push(createModifierNode(CSP_MODIFIER, cspValues.join(CSP_SEPARATOR)));
}
// Before returning the result, remove duplicated modifiers
modifierListClone.children = modifierListClone.children.filter((modifierNode, index, self) => self.findIndex((m) => m.name.value === modifierNode.name.value
&& m.exception === modifierNode.exception
&& m.value?.value === modifierNode.value?.value) === index);
return createConversionResult(modifierListClone, true);
}
return createConversionResult(modifierList, false);
}
/**
* Converts a network rule modifier list to uBlock format, if possible.
*
* @param modifierList Network rule modifier list node to convert
* @param isException If `true`, the rule is an exception rule
* @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
* the converted node, and its `isConverted` flag indicates whether the original node was converted.
* If the node was not converted, the result will contain the original node with the same object reference
* @throws If the conversion is not possible
*/
// TODO: Optimize
static convertToUbo(modifierList, isException = false) {
const conversionMap = new MultiValueMap();
const resourceTypeModifiersToAdd = new Set();
modifierList.children.forEach((modifierNode, index) => {
const originalModifierName = modifierNode.name.value;
const modifierData = modifiersCompatibilityTable.getFirst(originalModifierName, GenericPlatform.UboAny);
// Handle special case: resource redirection modifiers
if (REDIRECT_MODIFIERS.has(originalModifierName)) {
// Redirect modifiers cannot be negated
if (modifierNode.exception === true) {
throw new RuleConversionError(`Modifier '${modifierNode.name.value}' cannot be negated`);
}
// Convert the redirect resource name to uBO format
const redirectResourceName = modifierNode.value?.value;
// Special case: for exception rules, $redirect without value is allowed,
// and in this case it means an exception for all redirects
if (!redirectResourceName && !isException) {
throw new RuleConversionError(`No redirect resource specified for '${modifierNode.name.value}' modifier`);
}
if (!redirectResourceName) {
// Jump to the next modifier if the redirect resource is not specified
return;
}
// Leave $redirect and $redirect-rule modifiers as is, but convert $rewrite to $redirect
const modifierName = modifierNode.name.value === ABP_REWRITE_MODIFIER
? REDIRECT_MODIFIER
: modifierNode.name.value;
const convertedRedirectResourceData = redirectsCompatibilityTable.getFirst(redirectResourceName, GenericPlatform.UboAny);
const convertedRedirectResourceName = convertedRedirectResourceData?.name ?? redirectResourceName;
// uBlock requires the $redirect modifier to have a resource type
// https://github.com/AdguardTeam/Scriptlets/issues/101
if (convertedRedirectResourceData?.resourceTypes?.length) {
// Convert the resource types to uBO modifiers
const uboResourceTypeModifiers = redirectsCompatibilityTable.getResourceTypeModifiers(convertedRedirectResourceData, GenericPlatform.UboAny);
// Special case: noop text resource
// If any of resource type is already present, we don't need to add other resource types,
// otherwise, add all resource types
// TODO: Optimize this logic
// Check if the current resource is the noop text resource
const isNoopTextResource = convertedRedirectResourceName === UBO_NOOP_TEXT_RESOURCE;
// Determine if there are any valid resource types already present
const hasValidResourceType = modifierList.children.some((modifier) => {
const name = modifier.name.value;
if (!isValidResourceType(name)) {
return false;
}
const convertedModifierData = modifiersCompatibilityTable.getFirst(name, GenericPlatform.UboAny);
return uboResourceTypeModifiers.has(convertedModifierData?.name ?? name);
});
// If it's not the noop text resource or if no valid resource types are present
if (!isNoopTextResource || !hasValidResourceType) {
uboResourceTypeModifiers.forEach((resourceType) => {
resourceTypeModifiersToAdd.add(resourceType);
});
}
}
// Check if the modifier name or the redirect resource name is different from the original modifier.
// If so, add the converted modifier to the list
if (modifierName !== originalModifierName
|| (!isUndefined(convertedRedirectResourceName)
&& convertedRedirectResourceName !== redirectResourceName)) {
conversionMap.add(index, createModifierNode(modifierName,
// If the redirect resource name is unknown, fall back to the original one
// Later, the validator will throw an error if the resource name is invalid
convertedRedirectResourceName || redirectResourceName, modifierNode.exception));
}
return;
}
// Generic modifier conversion
if (modifierData && modifierData.name !== originalModifierName) {
conversionMap.add(index, createModifierNode(modifierData.name, modifierNode.value?.value, modifierNode.exception));
}
});
// Prepare the result if there are any converted modifiers or $csp modifiers
if (conversionMap.size || resourceTypeModifiersToAdd.size) {
const modifierListClone = cloneModifierListNode(modifierList);
// Replace the original modifiers with the converted ones
// One modifier may be replaced with multiple modifiers, so we need to flatten the array
modifierListClone.children = modifierListClone.children.map((modifierNode, index) => {
const conversionRecord = conversionMap.get(index);
if (conversionRecord) {
return conversionRecord;
}
return modifierNode;
}).flat();
// Before returning the result, remove duplicated modifiers
modifierListClone.children = modifierListClone.children.filter((modifierNode, index, self) => self.findIndex((m) => m.name.value === modifierNode.name.value
&& m.exception === modifierNode.exception
&& m.value?.value === modifierNode.value?.value) === index);
if (resourceTypeModifiersToAdd.size) {
const modifierNameSet = new Set(modifierList.children.map((m) => m.name.value));
resourceTypeModifiersToAdd.forEach((resourceType) => {
if (!modifierNameSet.has(resourceType)) {
modifierListClone.children.push(createModifierNode(resourceType));
}
});
}
return createConversionResult(modifierListClone, true);
}
return createConversionResult(modifierList, false);
}
}
export { NetworkRuleModifierListConverter };