UNPKG

@adguard/agtree

Version:
171 lines (168 loc) 8.37 kB
/* * AGTree v3.4.3 (build date: Thu, 11 Dec 2025 13:43:19 GMT) * (c) 2025 Adguard Software Ltd. * Released under the MIT license * https://github.com/AdguardTeam/tsurlfilter/tree/master/packages/agtree#readme */ import { sprintf } from 'sprintf-js'; import { UNDERSCORE, NEWLINE, SPACE } from '../utils/constants.js'; import { VALIDATION_ERROR_PREFIX, SOURCE_DATA_ERROR_PREFIX } from './constants.js'; import { getInvalidValidationResult, getValueRequiredValidationResult } from './helpers.js'; import { validateValue } from './value.js'; import { modifiersCompatibilityTable } from '../compatibility-tables/modifiers.js'; import '../compatibility-tables/redirects.js'; import '../compatibility-tables/scriptlets.js'; import '../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 { hasPlatformMultipleProducts, getHumanReadablePlatformName, isGenericPlatform } from '../compatibility-tables/utils/platform-helpers.js'; import '../compatibility-tables/schemas/resource-type.js'; import '../compatibility-tables/utils/resource-type-helpers.js'; import { isValidNoopModifier } from '../utils/noop-modifier.js'; /** * @file Validator for modifiers. */ /** * Fully checks whether the given `modifier` is valid for a specific product platform: * is it supported by the product, deprecated, assignable, negatable, etc. * * @param platform Platform to check the modifier for. Must be a specific platform (e.g., AdgExtChrome) * or a generic platform for a single product (e.g., AdgAny, UboExtChromium). * If multiple products are specified, validation is skipped and returns valid. * @param modifier Parsed modifier AST node. * @param isException Whether the modifier is used in exception rule. * Needed to check whether the modifier is allowed only in blocking or exception rules. * * @returns Result of modifier validation. */ const validateForSpecificProduct = (platform, modifier, isException) => { if (platform === 0) { throw new Error('No platforms specified'); } const isGeneric = isGenericPlatform(platform); // Skip validation if multiple products are specified if (isGeneric && hasPlatformMultipleProducts(platform)) { return { valid: true }; } const modifierName = modifier.name.value; // needed for validation of negation, assignment, etc. // Use getSingle for specific platforms, getFirst for generic platforms const specificBlockerData = isGeneric ? modifiersCompatibilityTable.getFirst(modifierName, platform) : modifiersCompatibilityTable.getSingle(modifierName, platform); // if no specific blocker data is found if (!specificBlockerData) { return getInvalidValidationResult(sprintf(VALIDATION_ERROR_PREFIX.NOT_SUPPORTED, getHumanReadablePlatformName(platform))); } // e.g. 'object-subrequest' if (specificBlockerData.removed) { return getInvalidValidationResult(`${VALIDATION_ERROR_PREFIX.REMOVED}: '${modifierName}'`); } if (specificBlockerData.deprecated) { if (!specificBlockerData.deprecationMessage) { throw new Error(`${SOURCE_DATA_ERROR_PREFIX.NO_DEPRECATION_MESSAGE}: '${modifierName}'`); } // prepare the message which is multiline in the yaml file const warn = specificBlockerData.deprecationMessage.replace(NEWLINE, SPACE); return { valid: true, warn, }; } if (specificBlockerData.blockOnly && isException) { return getInvalidValidationResult(`${VALIDATION_ERROR_PREFIX.BLOCK_ONLY}: '${modifierName}'`); } if (specificBlockerData.exceptionOnly && !isException) { return getInvalidValidationResult(`${VALIDATION_ERROR_PREFIX.EXCEPTION_ONLY}: '${modifierName}'`); } // e.g. '~domain=example.com' if (!specificBlockerData.negatable && modifier.exception) { return getInvalidValidationResult(`${VALIDATION_ERROR_PREFIX.NOT_NEGATABLE_MODIFIER}: '${modifierName}'`); } // e.g. 'domain' if (specificBlockerData.assignable) { if (!modifier.value) { // TODO: ditch value_optional after custom validators are implemented for value_format for all modifiers. // This checking should be done in each separate custom validator, // because $csp and $permissions without value can be used only in extension rules, // but $cookie with no value can be used in both blocking and exception rules. /** * Some assignable modifiers can be used without a value, * e.g. '@@||example.com^$cookie'. */ if (specificBlockerData.valueOptional) { return { valid: true }; } // for other assignable modifiers the value is required return getValueRequiredValidationResult(modifierName); } /** * TODO: consider to return `{ valid: true, warn: 'Modifier value may be specified' }` (???) * for $stealth modifier without a value * but only after the extension will support value for $stealth: * https://github.com/AdguardTeam/AdguardBrowserExtension/issues/2107 */ if (!specificBlockerData.valueFormat) { throw new Error(`${SOURCE_DATA_ERROR_PREFIX.NO_VALUE_FORMAT_FOR_ASSIGNABLE}: '${modifierName}'`); } return validateValue(modifier, specificBlockerData.valueFormat, specificBlockerData.valueFormatFlags); } if (modifier?.value) { // e.g. 'third-party=true' return getInvalidValidationResult(`${VALIDATION_ERROR_PREFIX.VALUE_FORBIDDEN}: '${modifierName}'`); } return { valid: true }; }; // TODO: move to modifier.ts and use index.ts only for exporting /** * Modifier validator class. */ class ModifierValidator { /** * Simply checks whether the modifier exists in any adblocker. * * **Deprecated** and **removed** modifiers are considered as **existent**. * * @param modifier Already parsed modifier AST node. * * @returns True if modifier exists, false otherwise. */ // eslint-disable-next-line class-methods-use-this exists = (modifier) => { return modifiersCompatibilityTable.existsAny(modifier.name.value); }; /** * Checks whether the given `modifier` is valid for specified `platforms`. * It checks whether the modifier is supported by the product, deprecated, assignable, negatable, etc. * * @param platforms Platforms to check the modifier for. Can be a specific platform (e.g., AdgExtChrome) * or a generic platform (e.g., AdgAny, UboExtChromium, or combination of multiple products). * @param modifier Modifier AST node. * @param isException Whether the modifier is used in exception rule, default to false. * Needed to check whether the modifier is allowed only in blocking or exception rules. * * @note For single product: specific platforms use exact lookup, generic platforms use first match. * If multiple products are specified (e.g., AdgAny | UboAny), validation is skipped and returns valid. * * @returns Result of modifier validation. */ validate = (platforms, modifier, isException = false) => { // special case: handle noop modifier which may be used as multiple underscores (not just one) // https://adguard.com/kb/general/ad-filtering/create-own-filters/#noop-modifier if (modifier.name.value.startsWith(UNDERSCORE)) { // check whether the modifier value contains something else besides underscores if (!isValidNoopModifier(modifier.name.value)) { return getInvalidValidationResult(`${VALIDATION_ERROR_PREFIX.INVALID_NOOP}: '${modifier.name.value}'`); } } if (!this.exists(modifier)) { return getInvalidValidationResult(`${VALIDATION_ERROR_PREFIX.NOT_EXISTENT}: '${modifier.name.value}'`); } return validateForSpecificProduct(platforms, modifier, isException); }; } const modifierValidator = new ModifierValidator(); export { modifierValidator };