@adguard/agtree 1.1.4 → 1.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/dist/agtree.cjs +1375 -453
- package/dist/agtree.d.ts +169 -42
- package/dist/agtree.esm.js +1375 -455
- package/dist/agtree.iife.min.js +5 -5
- package/dist/agtree.umd.min.js +5 -5
- package/dist/build.txt +1 -1
- package/package.json +12 -2
package/dist/agtree.esm.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* AGTree v1.1.
|
|
2
|
+
* AGTree v1.1.6 (build date: Fri, 22 Sep 2023 13:09:45 GMT)
|
|
3
3
|
* (c) 2023 AdGuard Software Ltd.
|
|
4
4
|
* Released under the MIT license
|
|
5
5
|
* https://github.com/AdguardTeam/tsurlfilter/tree/master/packages/agtree#readme
|
|
@@ -7,13 +7,13 @@
|
|
|
7
7
|
import valid from 'semver/functions/valid.js';
|
|
8
8
|
import coerce from 'semver/functions/coerce.js';
|
|
9
9
|
import JSON5 from 'json5';
|
|
10
|
-
import { walk, parse, toPlainObject, find, generate,
|
|
10
|
+
import { walk, parse, toPlainObject, find, generate, List, fromPlainObject } from '@adguard/ecss-tree';
|
|
11
11
|
import * as ecssTree from '@adguard/ecss-tree';
|
|
12
12
|
export { ecssTree as ECSSTree };
|
|
13
13
|
import cloneDeep from 'clone-deep';
|
|
14
14
|
import XRegExp from 'xregexp';
|
|
15
15
|
import { parse as parse$1 } from 'tldts';
|
|
16
|
-
import
|
|
16
|
+
import scriptlets from '@adguard/scriptlets';
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* @file Possible adblock syntaxes are listed here.
|
|
@@ -69,6 +69,9 @@ var AdblockSyntax;
|
|
|
69
69
|
* @file Constant values used by all parts of the library
|
|
70
70
|
*/
|
|
71
71
|
// General
|
|
72
|
+
/**
|
|
73
|
+
* Empty string.
|
|
74
|
+
*/
|
|
72
75
|
const EMPTY = '';
|
|
73
76
|
const SPACE = ' ';
|
|
74
77
|
const TAB = '\t';
|
|
@@ -259,7 +262,7 @@ const NEGATION_MARKER = '~';
|
|
|
259
262
|
/**
|
|
260
263
|
* The wildcard symbol — `*`.
|
|
261
264
|
*/
|
|
262
|
-
const WILDCARD
|
|
265
|
+
const WILDCARD = ASTERISK;
|
|
263
266
|
/**
|
|
264
267
|
* Classic domain separator.
|
|
265
268
|
*
|
|
@@ -2858,7 +2861,7 @@ class ModifierParser {
|
|
|
2858
2861
|
const modifierEnd = Math.max(StringUtils.skipWSBack(raw) + 1, modifierNameStart);
|
|
2859
2862
|
// Modifier name can't be empty
|
|
2860
2863
|
if (modifierNameStart === modifierEnd) {
|
|
2861
|
-
throw new AdblockSyntaxError('Modifier name
|
|
2864
|
+
throw new AdblockSyntaxError('Modifier name cannot be empty', locRange(loc, 0, raw.length));
|
|
2862
2865
|
}
|
|
2863
2866
|
let modifier;
|
|
2864
2867
|
let value;
|
|
@@ -2882,7 +2885,7 @@ class ModifierParser {
|
|
|
2882
2885
|
};
|
|
2883
2886
|
// Value can't be empty
|
|
2884
2887
|
if (assignmentIndex + 1 === modifierEnd) {
|
|
2885
|
-
throw new AdblockSyntaxError('Modifier value
|
|
2888
|
+
throw new AdblockSyntaxError('Modifier value cannot be empty', locRange(loc, 0, raw.length));
|
|
2886
2889
|
}
|
|
2887
2890
|
// Skip whitespace after the assignment operator
|
|
2888
2891
|
const valueStart = StringUtils.skipWS(raw, assignmentIndex + MODIFIER_ASSIGN_OPERATOR.length);
|
|
@@ -3180,8 +3183,29 @@ const FORBIDDEN_CSS_FUNCTIONS = new Set([
|
|
|
3180
3183
|
'url',
|
|
3181
3184
|
]);
|
|
3182
3185
|
|
|
3186
|
+
/**
|
|
3187
|
+
* @file Clone related utilities
|
|
3188
|
+
*
|
|
3189
|
+
* We should keep clone related functions in this file. Thus, we just provide
|
|
3190
|
+
* a simple interface for cloning values, we use it across the AGTree project,
|
|
3191
|
+
* and the implementation "under the hood" can be improved later, if needed.
|
|
3192
|
+
*/
|
|
3193
|
+
/**
|
|
3194
|
+
* Clones an input value to avoid side effects. Use it only in justified cases,
|
|
3195
|
+
* because it can impact performance negatively.
|
|
3196
|
+
*
|
|
3197
|
+
* @param value Value to clone
|
|
3198
|
+
* @returns Cloned value
|
|
3199
|
+
*/
|
|
3200
|
+
function clone(value) {
|
|
3201
|
+
// TODO: Replace cloneDeep with a more efficient implementation
|
|
3202
|
+
return cloneDeep(value);
|
|
3203
|
+
}
|
|
3204
|
+
|
|
3183
3205
|
/**
|
|
3184
3206
|
* @file Additional / helper functions for ECSSTree / CSSTree.
|
|
3207
|
+
*
|
|
3208
|
+
* @note There are no tests for some functions, but during the AGTree optimization we remove them anyway.
|
|
3185
3209
|
*/
|
|
3186
3210
|
/**
|
|
3187
3211
|
* Common CSSTree parsing options.
|
|
@@ -3317,10 +3341,10 @@ class CssTree {
|
|
|
3317
3341
|
ast = CssTree.parse(selectorList, CssTreeParserContext.selectorList);
|
|
3318
3342
|
}
|
|
3319
3343
|
else {
|
|
3320
|
-
ast =
|
|
3344
|
+
ast = clone(selectorList);
|
|
3321
3345
|
}
|
|
3322
3346
|
const nodes = [];
|
|
3323
|
-
// TODO:
|
|
3347
|
+
// TODO: Need to improve CSSTree types, until then we need to use any type here
|
|
3324
3348
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
3325
3349
|
walk(ast, (node) => {
|
|
3326
3350
|
if (CssTree.isExtendedCssNode(node, pseudoClasses, attributeSelectors)) {
|
|
@@ -3349,9 +3373,9 @@ class CssTree {
|
|
|
3349
3373
|
ast = CssTree.parse(selectorList, CssTreeParserContext.selectorList);
|
|
3350
3374
|
}
|
|
3351
3375
|
else {
|
|
3352
|
-
ast =
|
|
3376
|
+
ast = selectorList;
|
|
3353
3377
|
}
|
|
3354
|
-
// TODO:
|
|
3378
|
+
// TODO: Need to improve CSSTree types, until then we need to use any type here
|
|
3355
3379
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
3356
3380
|
return find(ast, (node) => CssTree.isExtendedCssNode(node, pseudoClasses, attributeSelectors)) !== null;
|
|
3357
3381
|
}
|
|
@@ -3388,14 +3412,14 @@ class CssTree {
|
|
|
3388
3412
|
ast = CssTree.parse(declarationList, CssTreeParserContext.declarationList);
|
|
3389
3413
|
}
|
|
3390
3414
|
else {
|
|
3391
|
-
ast =
|
|
3415
|
+
ast = clone(declarationList);
|
|
3392
3416
|
}
|
|
3393
3417
|
const nodes = [];
|
|
3394
3418
|
// While walking the AST we should skip the nested functions,
|
|
3395
3419
|
// for example skip url()s in cross-fade(url(), url()), since
|
|
3396
3420
|
// cross-fade() itself is already a forbidden function
|
|
3397
3421
|
let inForbiddenFunction = false;
|
|
3398
|
-
// TODO:
|
|
3422
|
+
// TODO: Need to improve CSSTree types, until then we need to use any type here
|
|
3399
3423
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
3400
3424
|
walk(ast, {
|
|
3401
3425
|
enter: (node) => {
|
|
@@ -3433,9 +3457,9 @@ class CssTree {
|
|
|
3433
3457
|
ast = CssTree.parse(declarationList, CssTreeParserContext.declarationList);
|
|
3434
3458
|
}
|
|
3435
3459
|
else {
|
|
3436
|
-
ast =
|
|
3460
|
+
ast = clone(declarationList);
|
|
3437
3461
|
}
|
|
3438
|
-
// TODO:
|
|
3462
|
+
// TODO: Need to improve CSSTree types, until then we need to use any type here
|
|
3439
3463
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
3440
3464
|
return find(ast, (node) => CssTree.isForbiddenFunction(node, forbiddenFunctions)) !== null;
|
|
3441
3465
|
}
|
|
@@ -3667,6 +3691,180 @@ class CssTree {
|
|
|
3667
3691
|
});
|
|
3668
3692
|
return result.trim();
|
|
3669
3693
|
}
|
|
3694
|
+
/**
|
|
3695
|
+
* Generates string representation of the selector list.
|
|
3696
|
+
*
|
|
3697
|
+
* @param ast SelectorList AST
|
|
3698
|
+
* @returns String representation of the selector list
|
|
3699
|
+
*/
|
|
3700
|
+
static generateSelectorListPlain(ast) {
|
|
3701
|
+
const result = [];
|
|
3702
|
+
if (!ast.children || ast.children.length === 0) {
|
|
3703
|
+
throw new Error('Selector list cannot be empty');
|
|
3704
|
+
}
|
|
3705
|
+
ast.children.forEach((selector, index, nodeList) => {
|
|
3706
|
+
if (selector.type !== CssTreeNodeType.Selector) {
|
|
3707
|
+
throw new Error(`Unexpected node type: ${selector.type}`);
|
|
3708
|
+
}
|
|
3709
|
+
result.push(this.generateSelectorPlain(selector));
|
|
3710
|
+
// If there is a next node, add a comma and a space after the selector
|
|
3711
|
+
if (nodeList[index + 1]) {
|
|
3712
|
+
result.push(COMMA, SPACE);
|
|
3713
|
+
}
|
|
3714
|
+
});
|
|
3715
|
+
return result.join(EMPTY);
|
|
3716
|
+
}
|
|
3717
|
+
/**
|
|
3718
|
+
* Selector generation based on CSSTree's AST. This is necessary because CSSTree
|
|
3719
|
+
* only adds spaces in some edge cases.
|
|
3720
|
+
*
|
|
3721
|
+
* @param ast CSS Tree AST
|
|
3722
|
+
* @returns CSS selector as string
|
|
3723
|
+
*/
|
|
3724
|
+
static generateSelectorPlain(ast) {
|
|
3725
|
+
let result = EMPTY;
|
|
3726
|
+
let inAttributeSelector = false;
|
|
3727
|
+
let depth = 0;
|
|
3728
|
+
let selectorListDepth = -1;
|
|
3729
|
+
let prevNode = ast;
|
|
3730
|
+
// TODO: Need to improve CSSTree types, until then we need to use any type here
|
|
3731
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
3732
|
+
walk(ast, {
|
|
3733
|
+
// TODO: Need to improve CSSTree types, until then we need to use any type here
|
|
3734
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
3735
|
+
enter: (node) => {
|
|
3736
|
+
depth += 1;
|
|
3737
|
+
// Skip attribute selector / selector list children
|
|
3738
|
+
if (inAttributeSelector || selectorListDepth > -1) {
|
|
3739
|
+
return;
|
|
3740
|
+
}
|
|
3741
|
+
switch (node.type) {
|
|
3742
|
+
// "Trivial" nodes
|
|
3743
|
+
case CssTreeNodeType.TypeSelector:
|
|
3744
|
+
result += node.name;
|
|
3745
|
+
break;
|
|
3746
|
+
case CssTreeNodeType.ClassSelector:
|
|
3747
|
+
result += DOT;
|
|
3748
|
+
result += node.name;
|
|
3749
|
+
break;
|
|
3750
|
+
case CssTreeNodeType.IdSelector:
|
|
3751
|
+
result += HASHMARK;
|
|
3752
|
+
result += node.name;
|
|
3753
|
+
break;
|
|
3754
|
+
case CssTreeNodeType.Identifier:
|
|
3755
|
+
result += node.name;
|
|
3756
|
+
break;
|
|
3757
|
+
case CssTreeNodeType.Raw:
|
|
3758
|
+
result += node.value;
|
|
3759
|
+
break;
|
|
3760
|
+
// "Advanced" nodes
|
|
3761
|
+
case CssTreeNodeType.Nth:
|
|
3762
|
+
// Default generation enough
|
|
3763
|
+
result += generate(node);
|
|
3764
|
+
break;
|
|
3765
|
+
// For example :not([id], [name])
|
|
3766
|
+
case CssTreeNodeType.SelectorList:
|
|
3767
|
+
// eslint-disable-next-line no-case-declarations
|
|
3768
|
+
const selectors = [];
|
|
3769
|
+
node.children.forEach((selector) => {
|
|
3770
|
+
if (selector.type === CssTreeNodeType.Selector) {
|
|
3771
|
+
selectors.push(CssTree.generateSelectorPlain(selector));
|
|
3772
|
+
}
|
|
3773
|
+
else if (selector.type === CssTreeNodeType.Raw) {
|
|
3774
|
+
selectors.push(selector.value);
|
|
3775
|
+
}
|
|
3776
|
+
});
|
|
3777
|
+
// Join selector lists
|
|
3778
|
+
result += selectors.join(COMMA + SPACE);
|
|
3779
|
+
// Skip nodes here
|
|
3780
|
+
selectorListDepth = depth;
|
|
3781
|
+
break;
|
|
3782
|
+
case CssTreeNodeType.Combinator:
|
|
3783
|
+
if (node.name === SPACE) {
|
|
3784
|
+
result += node.name;
|
|
3785
|
+
break;
|
|
3786
|
+
}
|
|
3787
|
+
// Prevent this case (unnecessary space): has( > .something)
|
|
3788
|
+
if (prevNode.type !== CssTreeNodeType.Selector) {
|
|
3789
|
+
result += SPACE;
|
|
3790
|
+
}
|
|
3791
|
+
result += node.name;
|
|
3792
|
+
result += SPACE;
|
|
3793
|
+
break;
|
|
3794
|
+
case CssTreeNodeType.AttributeSelector:
|
|
3795
|
+
result += OPEN_SQUARE_BRACKET;
|
|
3796
|
+
// Identifier name
|
|
3797
|
+
if (node.name) {
|
|
3798
|
+
result += node.name.name;
|
|
3799
|
+
}
|
|
3800
|
+
// Matcher operator, eg =
|
|
3801
|
+
if (node.matcher) {
|
|
3802
|
+
result += node.matcher;
|
|
3803
|
+
// Value can be String, Identifier or null
|
|
3804
|
+
if (node.value !== null) {
|
|
3805
|
+
// String node
|
|
3806
|
+
if (node.value.type === CssTreeNodeType.String) {
|
|
3807
|
+
result += generate(node.value);
|
|
3808
|
+
}
|
|
3809
|
+
else if (node.value.type === CssTreeNodeType.Identifier) {
|
|
3810
|
+
// Identifier node
|
|
3811
|
+
result += node.value.name;
|
|
3812
|
+
}
|
|
3813
|
+
}
|
|
3814
|
+
}
|
|
3815
|
+
// Flags
|
|
3816
|
+
if (node.flags) {
|
|
3817
|
+
// Space before flags
|
|
3818
|
+
result += SPACE;
|
|
3819
|
+
result += node.flags;
|
|
3820
|
+
}
|
|
3821
|
+
result += CLOSE_SQUARE_BRACKET;
|
|
3822
|
+
inAttributeSelector = true;
|
|
3823
|
+
break;
|
|
3824
|
+
case CssTreeNodeType.PseudoElementSelector:
|
|
3825
|
+
result += COLON;
|
|
3826
|
+
result += COLON;
|
|
3827
|
+
result += node.name;
|
|
3828
|
+
if (node.children !== null) {
|
|
3829
|
+
result += OPEN_PARENTHESIS;
|
|
3830
|
+
}
|
|
3831
|
+
break;
|
|
3832
|
+
case CssTreeNodeType.PseudoClassSelector:
|
|
3833
|
+
result += COLON;
|
|
3834
|
+
result += node.name;
|
|
3835
|
+
if (node.children !== null) {
|
|
3836
|
+
result += OPEN_PARENTHESIS;
|
|
3837
|
+
}
|
|
3838
|
+
break;
|
|
3839
|
+
}
|
|
3840
|
+
prevNode = node;
|
|
3841
|
+
},
|
|
3842
|
+
leave: (node) => {
|
|
3843
|
+
depth -= 1;
|
|
3844
|
+
if (node.type === CssTreeNodeType.SelectorList && depth + 1 === selectorListDepth) {
|
|
3845
|
+
selectorListDepth = -1;
|
|
3846
|
+
}
|
|
3847
|
+
if (selectorListDepth > -1) {
|
|
3848
|
+
return;
|
|
3849
|
+
}
|
|
3850
|
+
if (node.type === CssTreeNodeType.AttributeSelector) {
|
|
3851
|
+
inAttributeSelector = false;
|
|
3852
|
+
}
|
|
3853
|
+
if (inAttributeSelector) {
|
|
3854
|
+
return;
|
|
3855
|
+
}
|
|
3856
|
+
switch (node.type) {
|
|
3857
|
+
case CssTreeNodeType.PseudoElementSelector:
|
|
3858
|
+
case CssTreeNodeType.PseudoClassSelector:
|
|
3859
|
+
if (node.children) {
|
|
3860
|
+
result += CLOSE_PARENTHESIS;
|
|
3861
|
+
}
|
|
3862
|
+
break;
|
|
3863
|
+
}
|
|
3864
|
+
},
|
|
3865
|
+
});
|
|
3866
|
+
return result.trim();
|
|
3867
|
+
}
|
|
3670
3868
|
/**
|
|
3671
3869
|
* Block generation based on CSSTree's AST. This is necessary because CSSTree only adds spaces in some edge cases.
|
|
3672
3870
|
*
|
|
@@ -3850,6 +4048,29 @@ class CssTree {
|
|
|
3850
4048
|
});
|
|
3851
4049
|
return result;
|
|
3852
4050
|
}
|
|
4051
|
+
/**
|
|
4052
|
+
* Helper function to generate a raw string from a function selector's children
|
|
4053
|
+
*
|
|
4054
|
+
* @param node Function node
|
|
4055
|
+
* @returns Generated function value
|
|
4056
|
+
* @example `responseheader(name)` -> `name`
|
|
4057
|
+
*/
|
|
4058
|
+
static generateFunctionPlainValue(node) {
|
|
4059
|
+
const result = [];
|
|
4060
|
+
node.children?.forEach((child) => {
|
|
4061
|
+
switch (child.type) {
|
|
4062
|
+
case CssTreeNodeType.Raw:
|
|
4063
|
+
result.push(child.value);
|
|
4064
|
+
break;
|
|
4065
|
+
default:
|
|
4066
|
+
// Fallback to CSSTree's default generate function
|
|
4067
|
+
// TODO: Need to improve CSSTree types, until then we need to use any type here
|
|
4068
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
4069
|
+
result.push(generate(child));
|
|
4070
|
+
}
|
|
4071
|
+
});
|
|
4072
|
+
return result.join(EMPTY);
|
|
4073
|
+
}
|
|
3853
4074
|
}
|
|
3854
4075
|
|
|
3855
4076
|
/**
|
|
@@ -3897,7 +4118,7 @@ class ElementHidingBodyParser {
|
|
|
3897
4118
|
* @throws If the AST is invalid
|
|
3898
4119
|
*/
|
|
3899
4120
|
static generate(ast) {
|
|
3900
|
-
return CssTree.
|
|
4121
|
+
return CssTree.generateSelectorListPlain(ast.selectorList);
|
|
3901
4122
|
}
|
|
3902
4123
|
}
|
|
3903
4124
|
|
|
@@ -4141,7 +4362,7 @@ class CssInjectionBodyParser {
|
|
|
4141
4362
|
if (mediaQueryList || declarationList || remove) {
|
|
4142
4363
|
throw new AdblockSyntaxError(
|
|
4143
4364
|
// eslint-disable-next-line max-len
|
|
4144
|
-
'Invalid selector, regular selector elements
|
|
4365
|
+
'Invalid selector, regular selector elements cannot be used after special pseudo-classes', {
|
|
4145
4366
|
start: node.loc?.start ?? loc,
|
|
4146
4367
|
end: shiftLoc(loc, raw.length),
|
|
4147
4368
|
});
|
|
@@ -4830,7 +5051,7 @@ function createModifierListNode(modifiers = []) {
|
|
|
4830
5051
|
const result = {
|
|
4831
5052
|
type: 'ModifierList',
|
|
4832
5053
|
// We need to clone the modifiers to avoid side effects
|
|
4833
|
-
children:
|
|
5054
|
+
children: modifiers.length ? clone(modifiers) : [],
|
|
4834
5055
|
};
|
|
4835
5056
|
return result;
|
|
4836
5057
|
}
|
|
@@ -4870,8 +5091,9 @@ function hasUboModifierIndicator(rawSelectorList) {
|
|
|
4870
5091
|
* @returns Linked list based selector
|
|
4871
5092
|
*/
|
|
4872
5093
|
function convertSelectorToLinkedList(selector) {
|
|
5094
|
+
// TODO: Need to improve CSSTree types, until then we need to use any type here
|
|
4873
5095
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
4874
|
-
return fromPlainObject(
|
|
5096
|
+
return fromPlainObject(clone(selector));
|
|
4875
5097
|
}
|
|
4876
5098
|
/**
|
|
4877
5099
|
* Helper function that always returns the linked list version of the
|
|
@@ -4881,8 +5103,9 @@ function convertSelectorToLinkedList(selector) {
|
|
|
4881
5103
|
* @returns Linked list based selector list
|
|
4882
5104
|
*/
|
|
4883
5105
|
function convertSelectorListToLinkedList(selectorList) {
|
|
5106
|
+
// TODO: Need to improve CSSTree types, until then we need to use any type here
|
|
4884
5107
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
4885
|
-
return fromPlainObject(
|
|
5108
|
+
return fromPlainObject(clone(selectorList));
|
|
4886
5109
|
}
|
|
4887
5110
|
/**
|
|
4888
5111
|
* Helper function for checking and removing bounding combinators
|
|
@@ -5967,7 +6190,8 @@ class FilterListParser {
|
|
|
5967
6190
|
*/
|
|
5968
6191
|
static generate(ast, preferRaw = false) {
|
|
5969
6192
|
let result = EMPTY;
|
|
5970
|
-
for (
|
|
6193
|
+
for (let i = 0; i < ast.children.length; i += 1) {
|
|
6194
|
+
const rule = ast.children[i];
|
|
5971
6195
|
if (preferRaw && rule.raws?.text) {
|
|
5972
6196
|
result += rule.raws.text;
|
|
5973
6197
|
}
|
|
@@ -5984,6 +6208,11 @@ class FilterListParser {
|
|
|
5984
6208
|
case 'lf':
|
|
5985
6209
|
result += LF;
|
|
5986
6210
|
break;
|
|
6211
|
+
default:
|
|
6212
|
+
if (i !== ast.children.length - 1) {
|
|
6213
|
+
result += LF;
|
|
6214
|
+
}
|
|
6215
|
+
break;
|
|
5987
6216
|
}
|
|
5988
6217
|
}
|
|
5989
6218
|
return result;
|
|
@@ -6125,7 +6354,7 @@ var data$N = { adg_os_any:{ name:"csp",
|
|
|
6125
6354
|
assignable:true,
|
|
6126
6355
|
negatable:false,
|
|
6127
6356
|
value_optional:true,
|
|
6128
|
-
value_format:"
|
|
6357
|
+
value_format:"csp_value" },
|
|
6129
6358
|
adg_ext_any:{ name:"csp",
|
|
6130
6359
|
description:"This modifier completely changes the rule behavior.\nIf it is applied to a rule, it will not block the matching request.\nThe response headers are going to be modified instead.",
|
|
6131
6360
|
docs:"https://adguard.app/kb/general/ad-filtering/create-own-filters/#csp-modifier",
|
|
@@ -6137,7 +6366,7 @@ var data$N = { adg_os_any:{ name:"csp",
|
|
|
6137
6366
|
assignable:true,
|
|
6138
6367
|
negatable:false,
|
|
6139
6368
|
value_optional:true,
|
|
6140
|
-
value_format:"
|
|
6369
|
+
value_format:"csp_value" },
|
|
6141
6370
|
abp_ext_any:{ name:"csp",
|
|
6142
6371
|
description:"This modifier completely changes the rule behavior.\nIf it is applied to a rule, it will not block the matching request.\nThe response headers are going to be modified instead.",
|
|
6143
6372
|
docs:"https://help.adblockplus.org/hc/en-us/articles/360062733293-How-to-write-filters#content-security-policies",
|
|
@@ -6147,7 +6376,7 @@ var data$N = { adg_os_any:{ name:"csp",
|
|
|
6147
6376
|
assignable:true,
|
|
6148
6377
|
negatable:false,
|
|
6149
6378
|
value_optional:true,
|
|
6150
|
-
value_format:"
|
|
6379
|
+
value_format:"csp_value" },
|
|
6151
6380
|
ubo_ext_any:{ name:"csp",
|
|
6152
6381
|
description:"This modifier completely changes the rule behavior.\nIf it is applied to a rule, it will not block the matching request.\nThe response headers are going to be modified instead.",
|
|
6153
6382
|
docs:"https://github.com/gorhill/uBlock/wiki/Static-filter-syntax#csp",
|
|
@@ -6159,7 +6388,7 @@ var data$N = { adg_os_any:{ name:"csp",
|
|
|
6159
6388
|
assignable:true,
|
|
6160
6389
|
negatable:false,
|
|
6161
6390
|
value_optional:true,
|
|
6162
|
-
value_format:"
|
|
6391
|
+
value_format:"csp_value" } };
|
|
6163
6392
|
|
|
6164
6393
|
var data$M = { adg_os_any:{ name:"denyallow",
|
|
6165
6394
|
description:"The `$denyallow` modifier allows to avoid creating additional rules\nwhen it is needed to disable a certain rule for specific domains.\n`$denyallow` matches only target domains and not referrer domains.",
|
|
@@ -6666,7 +6895,8 @@ var data$l = { adg_os_any:{ name:"permissions",
|
|
|
6666
6895
|
inverse_conflicts:true,
|
|
6667
6896
|
assignable:true,
|
|
6668
6897
|
negatable:false,
|
|
6669
|
-
|
|
6898
|
+
value_optional:true,
|
|
6899
|
+
value_format:"permissions_value" } };
|
|
6670
6900
|
|
|
6671
6901
|
var data$k = { adg_any:{ name:"ping",
|
|
6672
6902
|
description:"The rule corresponds to requests caused by either navigator.sendBeacon() or the ping attribute on links.",
|
|
@@ -7327,15 +7557,104 @@ const ALLOWED_STEALTH_OPTIONS = new Set([
|
|
|
7327
7557
|
'xclientdata',
|
|
7328
7558
|
'dpi',
|
|
7329
7559
|
]);
|
|
7560
|
+
/**
|
|
7561
|
+
* Allowed CSP directives for $csp modifier.
|
|
7562
|
+
*
|
|
7563
|
+
* @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy#directives}
|
|
7564
|
+
*/
|
|
7565
|
+
const ALLOWED_CSP_DIRECTIVES = new Set([
|
|
7566
|
+
'base-uri',
|
|
7567
|
+
'child-src',
|
|
7568
|
+
'connect-src',
|
|
7569
|
+
'default-src',
|
|
7570
|
+
'font-src',
|
|
7571
|
+
'form-action',
|
|
7572
|
+
'frame-ancestors',
|
|
7573
|
+
'frame-src',
|
|
7574
|
+
'img-src',
|
|
7575
|
+
'manifest-src',
|
|
7576
|
+
'media-src',
|
|
7577
|
+
'navigate-to',
|
|
7578
|
+
'object-src',
|
|
7579
|
+
'plugin-types',
|
|
7580
|
+
'prefetch-src',
|
|
7581
|
+
'report-to',
|
|
7582
|
+
'report-uri',
|
|
7583
|
+
'sandbox',
|
|
7584
|
+
'script-src',
|
|
7585
|
+
'style-src',
|
|
7586
|
+
'upgrade-insecure-requests',
|
|
7587
|
+
'worker-src',
|
|
7588
|
+
]);
|
|
7589
|
+
/**
|
|
7590
|
+
* Allowed stealth options for $permissions modifier.
|
|
7591
|
+
*
|
|
7592
|
+
* @see {@link https://adguard.app/kb/general/ad-filtering/create-own-filters/#permissions-modifier}
|
|
7593
|
+
*/
|
|
7594
|
+
const ALLOWED_PERMISSION_DIRECTIVES = new Set([
|
|
7595
|
+
'accelerometer',
|
|
7596
|
+
'ambient-light-sensor',
|
|
7597
|
+
'autoplay',
|
|
7598
|
+
'battery',
|
|
7599
|
+
'camera',
|
|
7600
|
+
'display-capture',
|
|
7601
|
+
'document-domain',
|
|
7602
|
+
'encrypted-media',
|
|
7603
|
+
'execution-while-not-rendered',
|
|
7604
|
+
'execution-while-out-of-viewport',
|
|
7605
|
+
'fullscreen',
|
|
7606
|
+
'gamepad',
|
|
7607
|
+
'geolocation',
|
|
7608
|
+
'gyroscope',
|
|
7609
|
+
'hid',
|
|
7610
|
+
'identity-credentials-get',
|
|
7611
|
+
'idle-detection',
|
|
7612
|
+
'local-fonts',
|
|
7613
|
+
'magnetometer',
|
|
7614
|
+
'microphone',
|
|
7615
|
+
'midi',
|
|
7616
|
+
'payment',
|
|
7617
|
+
'picture-in-picture',
|
|
7618
|
+
'publickey-credentials-create',
|
|
7619
|
+
'publickey-credentials-get',
|
|
7620
|
+
'screen-wake-lock',
|
|
7621
|
+
'serial',
|
|
7622
|
+
'speaker-selection',
|
|
7623
|
+
'storage-access',
|
|
7624
|
+
'usb',
|
|
7625
|
+
'web-share',
|
|
7626
|
+
'xr-spatial-tracking',
|
|
7627
|
+
]);
|
|
7628
|
+
/**
|
|
7629
|
+
* One of available tokens for $permission modifier value.
|
|
7630
|
+
*
|
|
7631
|
+
* @see {@link https://w3c.github.io/webappsec-permissions-policy/#structured-header-serialization}
|
|
7632
|
+
*/
|
|
7633
|
+
const PERMISSIONS_TOKEN_SELF = 'self';
|
|
7634
|
+
/**
|
|
7635
|
+
* One of allowlist values for $permissions modifier.
|
|
7636
|
+
*
|
|
7637
|
+
* @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Permissions_Policy#allowlists}
|
|
7638
|
+
*/
|
|
7639
|
+
const EMPTY_PERMISSIONS_ALLOWLIST = `${OPEN_PARENTHESIS}${CLOSE_PARENTHESIS}`;
|
|
7330
7640
|
/**
|
|
7331
7641
|
* Prefixes for error messages used in modifier validation.
|
|
7332
7642
|
*/
|
|
7333
7643
|
const VALIDATION_ERROR_PREFIX = {
|
|
7334
7644
|
BLOCK_ONLY: 'Only blocking rules may contain the modifier',
|
|
7335
7645
|
EXCEPTION_ONLY: 'Only exception rules may contain the modifier',
|
|
7646
|
+
INVALID_CSP_DIRECTIVES: 'Invalid CSP directives for the modifier',
|
|
7336
7647
|
INVALID_LIST_VALUES: 'Invalid values for the modifier',
|
|
7337
7648
|
INVALID_NOOP: 'Invalid noop modifier',
|
|
7649
|
+
INVALID_PERMISSION_DIRECTIVE: 'Invalid Permissions-Policy directive for the modifier',
|
|
7650
|
+
INVALID_PERMISSION_ORIGINS: 'Origins in the value is invalid for the modifier and the directive',
|
|
7651
|
+
INVALID_PERMISSION_ORIGIN_QUOTES: 'Double quotes should be used for origins in the value of the modifier',
|
|
7338
7652
|
MIXED_NEGATIONS: 'Simultaneous usage of negated and not negated values is forbidden for the modifier',
|
|
7653
|
+
NO_CSP_VALUE: 'No CSP value for the modifier and the directive',
|
|
7654
|
+
NO_CSP_DIRECTIVE_QUOTE: 'CSP directives should no be quoted for the modifier',
|
|
7655
|
+
NO_UNESCAPED_PERMISSION_COMMA: 'Unescaped comma in the value is not allowed for the modifier',
|
|
7656
|
+
// TODO: implement later for $scp and $permissions
|
|
7657
|
+
// NO_VALUE_ONLY_FOR_EXCEPTION: 'Modifier without value can be used only in exception rules',
|
|
7339
7658
|
NOT_EXISTENT: 'Non-existent modifier',
|
|
7340
7659
|
NOT_NEGATABLE_MODIFIER: 'Non-negatable modifier',
|
|
7341
7660
|
NOT_NEGATABLE_VALUE: 'Values cannot be negated for the modifier',
|
|
@@ -7448,14 +7767,14 @@ const getSpecificBlockerData = (modifiersData, blockerPrefix, modifierName) => {
|
|
|
7448
7767
|
* @example
|
|
7449
7768
|
* `example.*` — matches with any TLD, e.g. `example.org`, `example.com`, etc.
|
|
7450
7769
|
*/
|
|
7451
|
-
const WILDCARD_TLD = DOT + WILDCARD
|
|
7770
|
+
const WILDCARD_TLD = DOT + WILDCARD;
|
|
7452
7771
|
/**
|
|
7453
7772
|
* Marker for a wildcard subdomain — `*.`.
|
|
7454
7773
|
*
|
|
7455
7774
|
* @example
|
|
7456
7775
|
* `*.example.org` — matches with any subdomain, e.g. `foo.example.org` or `bar.example.org`
|
|
7457
7776
|
*/
|
|
7458
|
-
const WILDCARD_SUBDOMAIN = WILDCARD
|
|
7777
|
+
const WILDCARD_SUBDOMAIN = WILDCARD + DOT;
|
|
7459
7778
|
class DomainUtils {
|
|
7460
7779
|
/**
|
|
7461
7780
|
* Check if the input is a valid domain or hostname.
|
|
@@ -7466,7 +7785,7 @@ class DomainUtils {
|
|
|
7466
7785
|
static isValidDomainOrHostname(domain) {
|
|
7467
7786
|
let domainToCheck = domain;
|
|
7468
7787
|
// Wildcard-only domain, typically a generic rule
|
|
7469
|
-
if (domainToCheck === WILDCARD
|
|
7788
|
+
if (domainToCheck === WILDCARD) {
|
|
7470
7789
|
return true;
|
|
7471
7790
|
}
|
|
7472
7791
|
// https://adguard.com/kb/general/ad-filtering/create-own-filters/#wildcard-for-tld
|
|
@@ -7647,10 +7966,12 @@ class QuoteUtils {
|
|
|
7647
7966
|
var CustomValueFormatValidatorName;
|
|
7648
7967
|
(function (CustomValueFormatValidatorName) {
|
|
7649
7968
|
CustomValueFormatValidatorName["App"] = "pipe_separated_apps";
|
|
7969
|
+
CustomValueFormatValidatorName["Csp"] = "csp_value";
|
|
7650
7970
|
// there are some differences between $domain and $denyallow
|
|
7651
7971
|
CustomValueFormatValidatorName["DenyAllow"] = "pipe_separated_denyallow_domains";
|
|
7652
7972
|
CustomValueFormatValidatorName["Domain"] = "pipe_separated_domains";
|
|
7653
7973
|
CustomValueFormatValidatorName["Method"] = "pipe_separated_methods";
|
|
7974
|
+
CustomValueFormatValidatorName["Permissions"] = "permissions_value";
|
|
7654
7975
|
CustomValueFormatValidatorName["StealthOption"] = "pipe_separated_stealth_options";
|
|
7655
7976
|
})(CustomValueFormatValidatorName || (CustomValueFormatValidatorName = {}));
|
|
7656
7977
|
/**
|
|
@@ -7684,7 +8005,7 @@ const isValidAppNameChunk = (chunk) => {
|
|
|
7684
8005
|
const isValidAppModifierValue = (value) => {
|
|
7685
8006
|
// $app modifier does not support wildcard tld
|
|
7686
8007
|
// https://adguard.app/kb/general/ad-filtering/create-own-filters/#app-modifier
|
|
7687
|
-
if (value.includes(WILDCARD
|
|
8008
|
+
if (value.includes(WILDCARD)) {
|
|
7688
8009
|
return false;
|
|
7689
8010
|
}
|
|
7690
8011
|
return value
|
|
@@ -7711,6 +8032,32 @@ const isValidMethodModifierValue = (value) => {
|
|
|
7711
8032
|
const isValidStealthModifierValue = (value) => {
|
|
7712
8033
|
return ALLOWED_STEALTH_OPTIONS.has(value);
|
|
7713
8034
|
};
|
|
8035
|
+
/**
|
|
8036
|
+
* Checks whether the given `rawOrigin` is valid as Permissions Allowlist origin.
|
|
8037
|
+
*
|
|
8038
|
+
* @see {@link https://w3c.github.io/webappsec-permissions-policy/#allowlists}
|
|
8039
|
+
*
|
|
8040
|
+
* @param rawOrigin The raw origin.
|
|
8041
|
+
*
|
|
8042
|
+
* @returns True if the origin is valid, false otherwise.
|
|
8043
|
+
*/
|
|
8044
|
+
const isValidPermissionsOrigin = (rawOrigin) => {
|
|
8045
|
+
// origins should be quoted by double quote
|
|
8046
|
+
const actualQuoteType = QuoteUtils.getStringQuoteType(rawOrigin);
|
|
8047
|
+
if (actualQuoteType !== QuoteType.Double) {
|
|
8048
|
+
return false;
|
|
8049
|
+
}
|
|
8050
|
+
const origin = QuoteUtils.removeQuotes(rawOrigin);
|
|
8051
|
+
try {
|
|
8052
|
+
// validate the origin by URL constructor
|
|
8053
|
+
// https://w3c.github.io/webappsec-permissions-policy/#algo-parse-policy-directive
|
|
8054
|
+
new URL(origin);
|
|
8055
|
+
}
|
|
8056
|
+
catch (e) {
|
|
8057
|
+
return false;
|
|
8058
|
+
}
|
|
8059
|
+
return true;
|
|
8060
|
+
};
|
|
7714
8061
|
/**
|
|
7715
8062
|
* Checks whether the given `value` is valid domain as $denyallow modifier value.
|
|
7716
8063
|
* Important: wildcard tld are not supported, compared to $domain.
|
|
@@ -7723,7 +8070,7 @@ const isValidDenyAllowModifierValue = (value) => {
|
|
|
7723
8070
|
// $denyallow modifier does not support wildcard tld
|
|
7724
8071
|
// https://adguard.app/kb/general/ad-filtering/create-own-filters/#denyallow-modifier
|
|
7725
8072
|
// but here we are simply checking whether the value contains wildcard `*`, not ends with `.*`
|
|
7726
|
-
if (value.includes(WILDCARD
|
|
8073
|
+
if (value.includes(WILDCARD)) {
|
|
7727
8074
|
return false;
|
|
7728
8075
|
}
|
|
7729
8076
|
// TODO: add cache for domains validation
|
|
@@ -7902,59 +8249,241 @@ const validatePipeSeparatedStealthOptions = (modifier) => {
|
|
|
7902
8249
|
return validateListItemsModifier(modifier, (raw) => StealthOptionListParser.parse(raw), isValidStealthModifierValue, customNoNegatedListItemsValidator);
|
|
7903
8250
|
};
|
|
7904
8251
|
/**
|
|
7905
|
-
*
|
|
7906
|
-
|
|
7907
|
-
const CUSTOM_VALUE_FORMAT_MAP = {
|
|
7908
|
-
[CustomValueFormatValidatorName.App]: validatePipeSeparatedApps,
|
|
7909
|
-
[CustomValueFormatValidatorName.DenyAllow]: validatePipeSeparatedDenyAllowDomains,
|
|
7910
|
-
[CustomValueFormatValidatorName.Domain]: validatePipeSeparatedDomains,
|
|
7911
|
-
[CustomValueFormatValidatorName.Method]: validatePipeSeparatedMethods,
|
|
7912
|
-
[CustomValueFormatValidatorName.StealthOption]: validatePipeSeparatedStealthOptions,
|
|
7913
|
-
};
|
|
7914
|
-
/**
|
|
7915
|
-
* Returns whether the given `valueFormat` is a valid custom value format validator name.
|
|
7916
|
-
*
|
|
7917
|
-
* @param valueFormat Value format for the modifier.
|
|
7918
|
-
*
|
|
7919
|
-
* @returns True if `valueFormat` is a supported pre-defined value format validator name, false otherwise.
|
|
7920
|
-
*/
|
|
7921
|
-
const isCustomValueFormatValidator = (valueFormat) => {
|
|
7922
|
-
return Object.keys(CUSTOM_VALUE_FORMAT_MAP).includes(valueFormat);
|
|
7923
|
-
};
|
|
7924
|
-
/**
|
|
7925
|
-
* Checks whether the value for given `modifier` is valid.
|
|
8252
|
+
* Validates `csp_value` custom value format.
|
|
8253
|
+
* Used for $csp modifier.
|
|
7926
8254
|
*
|
|
7927
8255
|
* @param modifier Modifier AST node.
|
|
7928
|
-
* @param valueFormat Value format for the modifier.
|
|
7929
8256
|
*
|
|
7930
8257
|
* @returns Validation result.
|
|
7931
8258
|
*/
|
|
7932
|
-
const
|
|
7933
|
-
if (isCustomValueFormatValidator(valueFormat)) {
|
|
7934
|
-
const validator = CUSTOM_VALUE_FORMAT_MAP[valueFormat];
|
|
7935
|
-
return validator(modifier);
|
|
7936
|
-
}
|
|
8259
|
+
const validateCspValue = (modifier) => {
|
|
7937
8260
|
const modifierName = modifier.modifier.value;
|
|
7938
8261
|
if (!modifier.value?.value) {
|
|
7939
8262
|
return getValueRequiredValidationResult(modifierName);
|
|
7940
8263
|
}
|
|
7941
|
-
|
|
7942
|
-
|
|
7943
|
-
|
|
7944
|
-
|
|
7945
|
-
|
|
7946
|
-
|
|
7947
|
-
|
|
7948
|
-
|
|
7949
|
-
|
|
7950
|
-
|
|
8264
|
+
// $csp modifier value may contain multiple directives
|
|
8265
|
+
// e.g. "csp=child-src 'none'; frame-src 'self' *; worker-src 'none'"
|
|
8266
|
+
const policyDirectives = modifier.value.value
|
|
8267
|
+
.split(SEMICOLON)
|
|
8268
|
+
// rule with $csp modifier may end with semicolon
|
|
8269
|
+
// e.g. "$csp=sandbox allow-same-origin;"
|
|
8270
|
+
// TODO: add predicate helper for `(i) => !!i`
|
|
8271
|
+
.filter((i) => !!i);
|
|
8272
|
+
const invalidValueValidationResult = getInvalidValidationResult(`${VALIDATION_ERROR_PREFIX.VALUE_INVALID}: '${modifierName}': "${modifier.value.value}"`);
|
|
8273
|
+
if (policyDirectives.length === 0) {
|
|
8274
|
+
return invalidValueValidationResult;
|
|
8275
|
+
}
|
|
8276
|
+
const invalidDirectives = [];
|
|
8277
|
+
for (let i = 0; i < policyDirectives.length; i += 1) {
|
|
8278
|
+
const policyDirective = policyDirectives[i].trim();
|
|
8279
|
+
if (!policyDirective) {
|
|
8280
|
+
return invalidValueValidationResult;
|
|
8281
|
+
}
|
|
8282
|
+
const chunks = policyDirective.split(SPACE);
|
|
8283
|
+
const [directive, ...valueChunks] = chunks;
|
|
8284
|
+
// e.g. "csp=child-src 'none'; ; worker-src 'none'"
|
|
8285
|
+
// validator it here ↑
|
|
8286
|
+
if (!directive) {
|
|
8287
|
+
return invalidValueValidationResult;
|
|
8288
|
+
}
|
|
8289
|
+
if (!ALLOWED_CSP_DIRECTIVES.has(directive)) {
|
|
8290
|
+
// e.g. "csp='child-src' 'none'"
|
|
8291
|
+
if (ALLOWED_CSP_DIRECTIVES.has(QuoteUtils.removeQuotes(directive))) {
|
|
8292
|
+
return getInvalidValidationResult(`${VALIDATION_ERROR_PREFIX.NO_CSP_DIRECTIVE_QUOTE}: '${modifierName}': ${directive}`);
|
|
8293
|
+
}
|
|
8294
|
+
invalidDirectives.push(directive);
|
|
8295
|
+
continue;
|
|
8296
|
+
}
|
|
8297
|
+
if (valueChunks.length === 0) {
|
|
8298
|
+
return getInvalidValidationResult(`${VALIDATION_ERROR_PREFIX.NO_CSP_VALUE}: '${modifierName}': '${directive}'`);
|
|
8299
|
+
}
|
|
8300
|
+
}
|
|
8301
|
+
if (invalidDirectives.length > 0) {
|
|
8302
|
+
const directivesToStr = QuoteUtils.quoteAndJoinStrings(invalidDirectives, QuoteType.Double);
|
|
8303
|
+
return getInvalidValidationResult(`${VALIDATION_ERROR_PREFIX.INVALID_CSP_DIRECTIVES}: '${modifierName}': ${directivesToStr}`);
|
|
7951
8304
|
}
|
|
7952
8305
|
return { valid: true };
|
|
7953
8306
|
};
|
|
7954
|
-
|
|
7955
8307
|
/**
|
|
7956
|
-
*
|
|
7957
|
-
|
|
8308
|
+
* Validates permission allowlist origins in the value of $permissions modifier.
|
|
8309
|
+
*
|
|
8310
|
+
* @see {@link https://w3c.github.io/webappsec-permissions-policy/#allowlists}
|
|
8311
|
+
*
|
|
8312
|
+
* @param allowlistChunks Array of allowlist chunks.
|
|
8313
|
+
* @param directive Permission directive name.
|
|
8314
|
+
* @param modifierName Modifier name.
|
|
8315
|
+
*
|
|
8316
|
+
* @returns Validation result.
|
|
8317
|
+
*/
|
|
8318
|
+
const validatePermissionAllowlistOrigins = (allowlistChunks, directive, modifierName) => {
|
|
8319
|
+
const invalidOrigins = [];
|
|
8320
|
+
for (let i = 0; i < allowlistChunks.length; i += 1) {
|
|
8321
|
+
const chunk = allowlistChunks[i].trim();
|
|
8322
|
+
// skip few spaces between origins (they were splitted by space)
|
|
8323
|
+
// e.g. 'geolocation=("https://example.com" "https://*.example.com")'
|
|
8324
|
+
if (chunk.length === 0) {
|
|
8325
|
+
continue;
|
|
8326
|
+
}
|
|
8327
|
+
/**
|
|
8328
|
+
* 'self' should be checked case-insensitively
|
|
8329
|
+
*
|
|
8330
|
+
* @see {@link https://w3c.github.io/webappsec-permissions-policy/#algo-parse-policy-directive}
|
|
8331
|
+
*
|
|
8332
|
+
* @example 'geolocation=(self)'
|
|
8333
|
+
*/
|
|
8334
|
+
if (chunk.toLowerCase() === PERMISSIONS_TOKEN_SELF) {
|
|
8335
|
+
continue;
|
|
8336
|
+
}
|
|
8337
|
+
if (QuoteUtils.getStringQuoteType(chunk) !== QuoteType.Double) {
|
|
8338
|
+
return getInvalidValidationResult(
|
|
8339
|
+
// eslint-disable-next-line max-len
|
|
8340
|
+
`${VALIDATION_ERROR_PREFIX.INVALID_PERMISSION_ORIGIN_QUOTES}: '${modifierName}': '${directive}': '${QuoteUtils.removeQuotes(chunk)}'`);
|
|
8341
|
+
}
|
|
8342
|
+
if (!isValidPermissionsOrigin(chunk)) {
|
|
8343
|
+
invalidOrigins.push(chunk);
|
|
8344
|
+
}
|
|
8345
|
+
}
|
|
8346
|
+
if (invalidOrigins.length > 0) {
|
|
8347
|
+
const originsToStr = QuoteUtils.quoteAndJoinStrings(invalidOrigins);
|
|
8348
|
+
return getInvalidValidationResult(
|
|
8349
|
+
// eslint-disable-next-line max-len
|
|
8350
|
+
`${VALIDATION_ERROR_PREFIX.INVALID_PERMISSION_ORIGINS}: '${modifierName}': '${directive}': ${originsToStr}`);
|
|
8351
|
+
}
|
|
8352
|
+
return { valid: true };
|
|
8353
|
+
};
|
|
8354
|
+
/**
|
|
8355
|
+
* Validates permission allowlist in the modifier value.
|
|
8356
|
+
*
|
|
8357
|
+
* @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Permissions_Policy#allowlists}
|
|
8358
|
+
* @see {@link https://w3c.github.io/webappsec-permissions-policy/#allowlists}
|
|
8359
|
+
*
|
|
8360
|
+
* @param allowlist Allowlist value.
|
|
8361
|
+
* @param directive Permission directive name.
|
|
8362
|
+
* @param modifierName Modifier name.
|
|
8363
|
+
*
|
|
8364
|
+
* @returns Validation result.
|
|
8365
|
+
*/
|
|
8366
|
+
const validatePermissionAllowlist = (allowlist, directive, modifierName) => {
|
|
8367
|
+
// `*` is one of available permissions tokens
|
|
8368
|
+
// e.g. 'fullscreen=*'
|
|
8369
|
+
// https://w3c.github.io/webappsec-permissions-policy/#structured-header-serialization
|
|
8370
|
+
if (allowlist === WILDCARD
|
|
8371
|
+
// e.g. 'autoplay=()'
|
|
8372
|
+
|| allowlist === EMPTY_PERMISSIONS_ALLOWLIST) {
|
|
8373
|
+
return { valid: true };
|
|
8374
|
+
}
|
|
8375
|
+
if (!(allowlist.startsWith(OPEN_PARENTHESIS) && allowlist.endsWith(CLOSE_PARENTHESIS))) {
|
|
8376
|
+
return getInvalidValidationResult(`${VALIDATION_ERROR_PREFIX.VALUE_INVALID}: '${modifierName}'`);
|
|
8377
|
+
}
|
|
8378
|
+
const allowlistChunks = allowlist.slice(1, -1).split(SPACE);
|
|
8379
|
+
return validatePermissionAllowlistOrigins(allowlistChunks, directive, modifierName);
|
|
8380
|
+
};
|
|
8381
|
+
/**
|
|
8382
|
+
* Validates single permission in the modifier value.
|
|
8383
|
+
*
|
|
8384
|
+
* @param permission Single permission value.
|
|
8385
|
+
* @param modifierName Modifier name.
|
|
8386
|
+
* @param modifierValue Modifier value.
|
|
8387
|
+
*
|
|
8388
|
+
* @returns Validation result.
|
|
8389
|
+
*/
|
|
8390
|
+
const validateSinglePermission = (permission, modifierName, modifierValue) => {
|
|
8391
|
+
// empty permission in the rule
|
|
8392
|
+
// e.g. 'permissions=storage-access=()\\, \\, camera=()'
|
|
8393
|
+
// the validator is here ↑
|
|
8394
|
+
if (!permission) {
|
|
8395
|
+
return getInvalidValidationResult(`${VALIDATION_ERROR_PREFIX.VALUE_INVALID}: '${modifierName}'`);
|
|
8396
|
+
}
|
|
8397
|
+
if (permission.includes(COMMA)) {
|
|
8398
|
+
return getInvalidValidationResult(`${VALIDATION_ERROR_PREFIX.NO_UNESCAPED_PERMISSION_COMMA}: '${modifierName}': '${modifierValue}'`);
|
|
8399
|
+
}
|
|
8400
|
+
const [directive, allowlist] = permission.split(EQUALS);
|
|
8401
|
+
if (!ALLOWED_PERMISSION_DIRECTIVES.has(directive)) {
|
|
8402
|
+
return getInvalidValidationResult(`${VALIDATION_ERROR_PREFIX.INVALID_PERMISSION_DIRECTIVE}: '${modifierName}': '${directive}'`);
|
|
8403
|
+
}
|
|
8404
|
+
return validatePermissionAllowlist(allowlist, directive, modifierName);
|
|
8405
|
+
};
|
|
8406
|
+
/**
|
|
8407
|
+
* Validates `permissions_value` custom value format.
|
|
8408
|
+
* Used for $permissions modifier.
|
|
8409
|
+
*
|
|
8410
|
+
* @param modifier Modifier AST node.
|
|
8411
|
+
*
|
|
8412
|
+
* @returns Validation result.
|
|
8413
|
+
*/
|
|
8414
|
+
const validatePermissions = (modifier) => {
|
|
8415
|
+
if (!modifier.value?.value) {
|
|
8416
|
+
return getValueRequiredValidationResult(modifier.modifier.value);
|
|
8417
|
+
}
|
|
8418
|
+
const modifierName = modifier.modifier.value;
|
|
8419
|
+
const modifierValue = modifier.value.value;
|
|
8420
|
+
// multiple permissions may be separated by escaped commas
|
|
8421
|
+
const permissions = modifier.value.value.split(`${BACKSLASH}${COMMA}`);
|
|
8422
|
+
for (let i = 0; i < permissions.length; i += 1) {
|
|
8423
|
+
const permission = permissions[i].trim();
|
|
8424
|
+
const singlePermissionValidationResult = validateSinglePermission(permission, modifierName, modifierValue);
|
|
8425
|
+
if (!singlePermissionValidationResult.valid) {
|
|
8426
|
+
return singlePermissionValidationResult;
|
|
8427
|
+
}
|
|
8428
|
+
}
|
|
8429
|
+
return { valid: true };
|
|
8430
|
+
};
|
|
8431
|
+
/**
|
|
8432
|
+
* Map of all available pre-defined validators for modifiers with custom `value_format`.
|
|
8433
|
+
*/
|
|
8434
|
+
const CUSTOM_VALUE_FORMAT_MAP = {
|
|
8435
|
+
[CustomValueFormatValidatorName.App]: validatePipeSeparatedApps,
|
|
8436
|
+
[CustomValueFormatValidatorName.Csp]: validateCspValue,
|
|
8437
|
+
[CustomValueFormatValidatorName.DenyAllow]: validatePipeSeparatedDenyAllowDomains,
|
|
8438
|
+
[CustomValueFormatValidatorName.Domain]: validatePipeSeparatedDomains,
|
|
8439
|
+
[CustomValueFormatValidatorName.Method]: validatePipeSeparatedMethods,
|
|
8440
|
+
[CustomValueFormatValidatorName.Permissions]: validatePermissions,
|
|
8441
|
+
[CustomValueFormatValidatorName.StealthOption]: validatePipeSeparatedStealthOptions,
|
|
8442
|
+
};
|
|
8443
|
+
/**
|
|
8444
|
+
* Returns whether the given `valueFormat` is a valid custom value format validator name.
|
|
8445
|
+
*
|
|
8446
|
+
* @param valueFormat Value format for the modifier.
|
|
8447
|
+
*
|
|
8448
|
+
* @returns True if `valueFormat` is a supported pre-defined value format validator name, false otherwise.
|
|
8449
|
+
*/
|
|
8450
|
+
const isCustomValueFormatValidator = (valueFormat) => {
|
|
8451
|
+
return Object.keys(CUSTOM_VALUE_FORMAT_MAP).includes(valueFormat);
|
|
8452
|
+
};
|
|
8453
|
+
/**
|
|
8454
|
+
* Checks whether the value for given `modifier` is valid.
|
|
8455
|
+
*
|
|
8456
|
+
* @param modifier Modifier AST node.
|
|
8457
|
+
* @param valueFormat Value format for the modifier.
|
|
8458
|
+
*
|
|
8459
|
+
* @returns Validation result.
|
|
8460
|
+
*/
|
|
8461
|
+
const validateValue = (modifier, valueFormat) => {
|
|
8462
|
+
if (isCustomValueFormatValidator(valueFormat)) {
|
|
8463
|
+
const validator = CUSTOM_VALUE_FORMAT_MAP[valueFormat];
|
|
8464
|
+
return validator(modifier);
|
|
8465
|
+
}
|
|
8466
|
+
const modifierName = modifier.modifier.value;
|
|
8467
|
+
if (!modifier.value?.value) {
|
|
8468
|
+
return getValueRequiredValidationResult(modifierName);
|
|
8469
|
+
}
|
|
8470
|
+
let xRegExp;
|
|
8471
|
+
try {
|
|
8472
|
+
xRegExp = XRegExp(valueFormat);
|
|
8473
|
+
}
|
|
8474
|
+
catch (e) {
|
|
8475
|
+
throw new Error(`${SOURCE_DATA_ERROR_PREFIX.INVALID_VALUE_FORMAT_REGEXP}: '${modifierName}'`);
|
|
8476
|
+
}
|
|
8477
|
+
const isValid = xRegExp.test(modifier.value?.value);
|
|
8478
|
+
if (!isValid) {
|
|
8479
|
+
return getInvalidValidationResult(`${VALIDATION_ERROR_PREFIX.VALUE_INVALID}: '${modifierName}'`);
|
|
8480
|
+
}
|
|
8481
|
+
return { valid: true };
|
|
8482
|
+
};
|
|
8483
|
+
|
|
8484
|
+
/**
|
|
8485
|
+
* @file Validator for modifiers.
|
|
8486
|
+
*/
|
|
7958
8487
|
/**
|
|
7959
8488
|
* Fully checks whether the given `modifier` valid for given blocker `syntax`:
|
|
7960
8489
|
* is it supported by the blocker, deprecated, assignable, negatable, etc.
|
|
@@ -8011,6 +8540,10 @@ const validateForSpecificSyntax = (modifiersData, syntax, modifier, isException)
|
|
|
8011
8540
|
// e.g. 'domain'
|
|
8012
8541
|
if (specificBlockerData[SpecificKey.Assignable]) {
|
|
8013
8542
|
if (!modifier.value) {
|
|
8543
|
+
// TODO: ditch value_optional after custom validators are implemented for value_format for all modifiers.
|
|
8544
|
+
// This checking should be done in each separate custom validator,
|
|
8545
|
+
// because $csp and $permissions without value can be used only in extension rules,
|
|
8546
|
+
// but $cookie with no value can be used in both blocking and exception rules.
|
|
8014
8547
|
/**
|
|
8015
8548
|
* Some assignable modifiers can be used without a value,
|
|
8016
8549
|
* e.g. '@@||example.com^$cookie'.
|
|
@@ -8098,7 +8631,7 @@ class ModifierValidator {
|
|
|
8098
8631
|
* @returns Result of modifier validation.
|
|
8099
8632
|
*/
|
|
8100
8633
|
validate = (syntax, rawModifier, isException = false) => {
|
|
8101
|
-
const modifier =
|
|
8634
|
+
const modifier = clone(rawModifier);
|
|
8102
8635
|
// special case: handle noop modifier which may be used as multiple underscores (not just one)
|
|
8103
8636
|
// https://adguard.com/kb/general/ad-filtering/create-own-filters/#noop-modifier
|
|
8104
8637
|
if (modifier.modifier.value.startsWith(UNDERSCORE)) {
|
|
@@ -8177,7 +8710,9 @@ class ConverterBase {
|
|
|
8177
8710
|
* Converts some data to AdGuard format
|
|
8178
8711
|
*
|
|
8179
8712
|
* @param data Data to convert
|
|
8180
|
-
* @returns
|
|
8713
|
+
* @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
|
|
8714
|
+
* the converted node, and its `isConverted` flag indicates whether the original node was converted.
|
|
8715
|
+
* If the node was not converted, the result will contain the original node with the same object reference
|
|
8181
8716
|
* @throws If the data is invalid or incompatible
|
|
8182
8717
|
*/
|
|
8183
8718
|
static convertToAdg(data) {
|
|
@@ -8187,7 +8722,9 @@ class ConverterBase {
|
|
|
8187
8722
|
* Converts some data to Adblock Plus format
|
|
8188
8723
|
*
|
|
8189
8724
|
* @param data Data to convert
|
|
8190
|
-
* @returns
|
|
8725
|
+
* @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
|
|
8726
|
+
* the converted node, and its `isConverted` flag indicates whether the original node was converted.
|
|
8727
|
+
* If the node was not converted, the result will contain the original node with the same object reference
|
|
8191
8728
|
* @throws If the data is invalid or incompatible
|
|
8192
8729
|
*/
|
|
8193
8730
|
static convertToAbp(data) {
|
|
@@ -8197,7 +8734,9 @@ class ConverterBase {
|
|
|
8197
8734
|
* Converts some data to uBlock Origin format
|
|
8198
8735
|
*
|
|
8199
8736
|
* @param data Data to convert
|
|
8200
|
-
* @returns
|
|
8737
|
+
* @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
|
|
8738
|
+
* the converted node, and its `isConverted` flag indicates whether the original node was converted.
|
|
8739
|
+
* If the node was not converted, the result will contain the original node with the same object reference
|
|
8201
8740
|
* @throws If the data is invalid or incompatible
|
|
8202
8741
|
*/
|
|
8203
8742
|
static convertToUbo(data) {
|
|
@@ -8221,7 +8760,9 @@ class RuleConverterBase extends ConverterBase {
|
|
|
8221
8760
|
* Converts an adblock filtering rule to AdGuard format, if possible.
|
|
8222
8761
|
*
|
|
8223
8762
|
* @param rule Rule node to convert
|
|
8224
|
-
* @returns
|
|
8763
|
+
* @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
|
|
8764
|
+
* the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
|
|
8765
|
+
* If the rule was not converted, the result array will contain the original node with the same object reference
|
|
8225
8766
|
* @throws If the rule is invalid or cannot be converted
|
|
8226
8767
|
*/
|
|
8227
8768
|
static convertToAdg(rule) {
|
|
@@ -8231,7 +8772,9 @@ class RuleConverterBase extends ConverterBase {
|
|
|
8231
8772
|
* Converts an adblock filtering rule to Adblock Plus format, if possible.
|
|
8232
8773
|
*
|
|
8233
8774
|
* @param rule Rule node to convert
|
|
8234
|
-
* @returns
|
|
8775
|
+
* @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
|
|
8776
|
+
* the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
|
|
8777
|
+
* If the rule was not converted, the result array will contain the original node with the same object reference
|
|
8235
8778
|
* @throws If the rule is invalid or cannot be converted
|
|
8236
8779
|
*/
|
|
8237
8780
|
static convertToAbp(rule) {
|
|
@@ -8241,7 +8784,9 @@ class RuleConverterBase extends ConverterBase {
|
|
|
8241
8784
|
* Converts an adblock filtering rule to uBlock Origin format, if possible.
|
|
8242
8785
|
*
|
|
8243
8786
|
* @param rule Rule node to convert
|
|
8244
|
-
* @returns
|
|
8787
|
+
* @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
|
|
8788
|
+
* the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
|
|
8789
|
+
* If the rule was not converted, the result array will contain the original node with the same object reference
|
|
8245
8790
|
* @throws If the rule is invalid or cannot be converted
|
|
8246
8791
|
*/
|
|
8247
8792
|
static convertToUbo(rule) {
|
|
@@ -8249,6 +8794,37 @@ class RuleConverterBase extends ConverterBase {
|
|
|
8249
8794
|
}
|
|
8250
8795
|
}
|
|
8251
8796
|
|
|
8797
|
+
/**
|
|
8798
|
+
* @file Conversion result interface and helper functions
|
|
8799
|
+
*/
|
|
8800
|
+
/**
|
|
8801
|
+
* Helper function to create a generic conversion result.
|
|
8802
|
+
*
|
|
8803
|
+
* @param result Conversion result
|
|
8804
|
+
* @param isConverted Indicates whether the input item was converted
|
|
8805
|
+
* @template T Type of the item to convert
|
|
8806
|
+
* @template U Type of the conversion result (defaults to `T`, but can be `T[]` as well)
|
|
8807
|
+
* @returns Generic conversion result
|
|
8808
|
+
*/
|
|
8809
|
+
// eslint-disable-next-line max-len
|
|
8810
|
+
function createConversionResult(result, isConverted) {
|
|
8811
|
+
return {
|
|
8812
|
+
result,
|
|
8813
|
+
isConverted,
|
|
8814
|
+
};
|
|
8815
|
+
}
|
|
8816
|
+
/**
|
|
8817
|
+
* Helper function to create a node conversion result.
|
|
8818
|
+
*
|
|
8819
|
+
* @param nodes Array of nodes
|
|
8820
|
+
* @param isConverted Indicates whether the input item was converted
|
|
8821
|
+
* @template T Type of the node (extends `Node`)
|
|
8822
|
+
* @returns Node conversion result
|
|
8823
|
+
*/
|
|
8824
|
+
function createNodeConversionResult(nodes, isConverted) {
|
|
8825
|
+
return createConversionResult(nodes, isConverted);
|
|
8826
|
+
}
|
|
8827
|
+
|
|
8252
8828
|
/**
|
|
8253
8829
|
* @file Comment rule converter
|
|
8254
8830
|
*/
|
|
@@ -8262,27 +8838,30 @@ class CommentRuleConverter extends RuleConverterBase {
|
|
|
8262
8838
|
* Converts a comment rule to AdGuard format, if possible.
|
|
8263
8839
|
*
|
|
8264
8840
|
* @param rule Rule node to convert
|
|
8265
|
-
* @returns
|
|
8841
|
+
* @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
|
|
8842
|
+
* the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
|
|
8843
|
+
* If the rule was not converted, the result array will contain the original node with the same object reference
|
|
8266
8844
|
* @throws If the rule is invalid or cannot be converted
|
|
8267
8845
|
*/
|
|
8268
8846
|
static convertToAdg(rule) {
|
|
8269
|
-
// Clone the provided AST node to avoid side effects
|
|
8270
|
-
const ruleNode = cloneDeep(rule);
|
|
8271
8847
|
// TODO: Add support for other comment types, if needed
|
|
8272
8848
|
// Main task is # -> ! conversion
|
|
8273
|
-
switch (
|
|
8849
|
+
switch (rule.type) {
|
|
8274
8850
|
case CommentRuleType.CommentRule:
|
|
8275
|
-
//
|
|
8276
|
-
if (
|
|
8277
|
-
|
|
8278
|
-
|
|
8279
|
-
|
|
8280
|
-
|
|
8851
|
+
// Check if the rule needs to be converted
|
|
8852
|
+
if (rule.type === CommentRuleType.CommentRule && rule.marker.value === CommentMarker.Hashmark) {
|
|
8853
|
+
// Add a ! to the beginning of the comment
|
|
8854
|
+
// TODO: Replace with custom clone method
|
|
8855
|
+
const ruleClone = clone(rule);
|
|
8856
|
+
ruleClone.marker.value = CommentMarker.Regular;
|
|
8857
|
+
// Add the hashmark to the beginning of the comment text
|
|
8858
|
+
ruleClone.text.value = `${SPACE}${CommentMarker.Hashmark}${ruleClone.text.value}`;
|
|
8859
|
+
return createNodeConversionResult([ruleClone], true);
|
|
8281
8860
|
}
|
|
8282
|
-
return [
|
|
8861
|
+
return createNodeConversionResult([rule], false);
|
|
8283
8862
|
// Leave any other comment rule as is
|
|
8284
8863
|
default:
|
|
8285
|
-
return [
|
|
8864
|
+
return createNodeConversionResult([rule], false);
|
|
8286
8865
|
}
|
|
8287
8866
|
}
|
|
8288
8867
|
}
|
|
@@ -8452,6 +9031,58 @@ class RegExpUtils {
|
|
|
8452
9031
|
}
|
|
8453
9032
|
}
|
|
8454
9033
|
|
|
9034
|
+
/**
|
|
9035
|
+
* @file Custom clone functions for AST nodes, this is probably the most efficient way to clone AST nodes.
|
|
9036
|
+
* @todo Maybe move them to parser classes as 'clone' methods
|
|
9037
|
+
*/
|
|
9038
|
+
/**
|
|
9039
|
+
* Clones a scriptlet rule node.
|
|
9040
|
+
*
|
|
9041
|
+
* @param node Node to clone
|
|
9042
|
+
* @returns Cloned node
|
|
9043
|
+
*/
|
|
9044
|
+
function cloneScriptletRuleNode(node) {
|
|
9045
|
+
return {
|
|
9046
|
+
type: node.type,
|
|
9047
|
+
children: node.children.map((child) => ({ ...child })),
|
|
9048
|
+
};
|
|
9049
|
+
}
|
|
9050
|
+
/**
|
|
9051
|
+
* Clones a domain list node.
|
|
9052
|
+
*
|
|
9053
|
+
* @param node Node to clone
|
|
9054
|
+
* @returns Cloned node
|
|
9055
|
+
*/
|
|
9056
|
+
function cloneDomainListNode(node) {
|
|
9057
|
+
return {
|
|
9058
|
+
type: node.type,
|
|
9059
|
+
separator: node.separator,
|
|
9060
|
+
children: node.children.map((domain) => ({ ...domain })),
|
|
9061
|
+
};
|
|
9062
|
+
}
|
|
9063
|
+
/**
|
|
9064
|
+
* Clones a modifier list node.
|
|
9065
|
+
*
|
|
9066
|
+
* @param node Node to clone
|
|
9067
|
+
* @returns Cloned node
|
|
9068
|
+
*/
|
|
9069
|
+
function cloneModifierListNode(node) {
|
|
9070
|
+
return {
|
|
9071
|
+
type: node.type,
|
|
9072
|
+
children: node.children.map((modifier) => {
|
|
9073
|
+
const res = {
|
|
9074
|
+
type: modifier.type,
|
|
9075
|
+
exception: modifier.exception,
|
|
9076
|
+
modifier: { ...modifier.modifier },
|
|
9077
|
+
};
|
|
9078
|
+
if (modifier.value) {
|
|
9079
|
+
res.value = { ...modifier.value };
|
|
9080
|
+
}
|
|
9081
|
+
return res;
|
|
9082
|
+
}),
|
|
9083
|
+
};
|
|
9084
|
+
}
|
|
9085
|
+
|
|
8455
9086
|
/**
|
|
8456
9087
|
* @file HTML filtering rule converter
|
|
8457
9088
|
*/
|
|
@@ -8464,16 +9095,22 @@ class RegExpUtils {
|
|
|
8464
9095
|
*
|
|
8465
9096
|
* @see {@link https://adguard.com/kb/general/ad-filtering/create-own-filters/#html-filtering-rules}
|
|
8466
9097
|
*/
|
|
8467
|
-
const
|
|
8468
|
-
const
|
|
9098
|
+
const ADG_HTML_DEFAULT_MAX_LENGTH = 8192;
|
|
9099
|
+
const ADG_HTML_CONVERSION_MAX_LENGTH = ADG_HTML_DEFAULT_MAX_LENGTH * 32;
|
|
8469
9100
|
const NOT_SPECIFIED = -1;
|
|
8470
|
-
|
|
8471
|
-
|
|
8472
|
-
|
|
8473
|
-
|
|
8474
|
-
|
|
8475
|
-
|
|
8476
|
-
|
|
9101
|
+
var PseudoClasses$1;
|
|
9102
|
+
(function (PseudoClasses) {
|
|
9103
|
+
PseudoClasses["Contains"] = "contains";
|
|
9104
|
+
PseudoClasses["HasText"] = "has-text";
|
|
9105
|
+
PseudoClasses["MinTextLength"] = "min-text-length";
|
|
9106
|
+
})(PseudoClasses$1 || (PseudoClasses$1 = {}));
|
|
9107
|
+
var AttributeSelectors;
|
|
9108
|
+
(function (AttributeSelectors) {
|
|
9109
|
+
AttributeSelectors["MaxLength"] = "max-length";
|
|
9110
|
+
AttributeSelectors["MinLength"] = "min-length";
|
|
9111
|
+
AttributeSelectors["TagContent"] = "tag-content";
|
|
9112
|
+
AttributeSelectors["Wildcard"] = "wildcard";
|
|
9113
|
+
})(AttributeSelectors || (AttributeSelectors = {}));
|
|
8477
9114
|
/**
|
|
8478
9115
|
* HTML filtering rule converter class
|
|
8479
9116
|
*
|
|
@@ -8496,16 +9133,23 @@ class HtmlRuleConverter extends RuleConverterBase {
|
|
|
8496
9133
|
* ```
|
|
8497
9134
|
*
|
|
8498
9135
|
* @param rule Rule node to convert
|
|
8499
|
-
* @returns
|
|
9136
|
+
* @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
|
|
9137
|
+
* the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
|
|
9138
|
+
* If the rule was not converted, the result array will contain the original node with the same object reference
|
|
8500
9139
|
* @throws If the rule is invalid or cannot be converted
|
|
8501
9140
|
*/
|
|
8502
9141
|
static convertToAdg(rule) {
|
|
8503
|
-
//
|
|
8504
|
-
|
|
9142
|
+
// Ignore AdGuard rules
|
|
9143
|
+
if (rule.syntax === AdblockSyntax.Adg) {
|
|
9144
|
+
return createNodeConversionResult([rule], false);
|
|
9145
|
+
}
|
|
9146
|
+
if (rule.syntax === AdblockSyntax.Abp) {
|
|
9147
|
+
throw new RuleConversionError('Invalid rule, ABP does not support HTML filtering rules');
|
|
9148
|
+
}
|
|
8505
9149
|
// Prepare the conversion result
|
|
8506
9150
|
const conversionResult = [];
|
|
8507
9151
|
// Iterate over selector list
|
|
8508
|
-
for (const selector of
|
|
9152
|
+
for (const selector of rule.body.body.children) {
|
|
8509
9153
|
// Check selector, just in case
|
|
8510
9154
|
if (selector.type !== CssTreeNodeType.Selector) {
|
|
8511
9155
|
throw new RuleConversionError(`Expected selector, got '${selector.type}'`);
|
|
@@ -8532,24 +9176,24 @@ class HtmlRuleConverter extends RuleConverterBase {
|
|
|
8532
9176
|
throw new RuleConversionError('Tag selector should be the first child, if present');
|
|
8533
9177
|
}
|
|
8534
9178
|
// Simply store the tag selector
|
|
8535
|
-
convertedSelector.children.push(
|
|
9179
|
+
convertedSelector.children.push(clone(node));
|
|
8536
9180
|
break;
|
|
8537
9181
|
case CssTreeNodeType.AttributeSelector:
|
|
8538
9182
|
// Check if the attribute selector is a special AdGuard attribute
|
|
8539
9183
|
switch (node.name.name) {
|
|
8540
|
-
case
|
|
9184
|
+
case AttributeSelectors.MinLength:
|
|
8541
9185
|
minLength = CssTree.parseAttributeSelectorValueAsNumber(node);
|
|
8542
9186
|
break;
|
|
8543
|
-
case
|
|
9187
|
+
case AttributeSelectors.MaxLength:
|
|
8544
9188
|
maxLength = CssTree.parseAttributeSelectorValueAsNumber(node);
|
|
8545
9189
|
break;
|
|
8546
|
-
case
|
|
8547
|
-
case
|
|
9190
|
+
case AttributeSelectors.TagContent:
|
|
9191
|
+
case AttributeSelectors.Wildcard:
|
|
8548
9192
|
CssTree.assertAttributeSelectorHasStringValue(node);
|
|
8549
|
-
convertedSelector.children.push(
|
|
9193
|
+
convertedSelector.children.push(clone(node));
|
|
8550
9194
|
break;
|
|
8551
9195
|
default:
|
|
8552
|
-
convertedSelector.children.push(
|
|
9196
|
+
convertedSelector.children.push(clone(node));
|
|
8553
9197
|
}
|
|
8554
9198
|
break;
|
|
8555
9199
|
case CssTreeNodeType.PseudoClassSelector:
|
|
@@ -8563,18 +9207,18 @@ class HtmlRuleConverter extends RuleConverterBase {
|
|
|
8563
9207
|
}
|
|
8564
9208
|
// Process the pseudo class based on its name
|
|
8565
9209
|
switch (node.name) {
|
|
8566
|
-
case
|
|
8567
|
-
case
|
|
9210
|
+
case PseudoClasses$1.HasText:
|
|
9211
|
+
case PseudoClasses$1.Contains:
|
|
8568
9212
|
// Check if the argument is a RegExp
|
|
8569
9213
|
if (RegExpUtils.isRegexPattern(arg.value)) {
|
|
8570
9214
|
// TODO: Add some support for RegExp patterns later
|
|
8571
9215
|
// Need to find a way to convert some RegExp patterns to glob patterns
|
|
8572
9216
|
throw new RuleConversionError('Conversion of RegExp patterns is not yet supported');
|
|
8573
9217
|
}
|
|
8574
|
-
convertedSelector.children.push(CssTree.createAttributeSelectorNode(
|
|
9218
|
+
convertedSelector.children.push(CssTree.createAttributeSelectorNode(AttributeSelectors.TagContent, arg.value));
|
|
8575
9219
|
break;
|
|
8576
9220
|
// https://github.com/gorhill/uBlock/wiki/Procedural-cosmetic-filters#subjectmin-text-lengthn
|
|
8577
|
-
case
|
|
9221
|
+
case PseudoClasses$1.MinTextLength:
|
|
8578
9222
|
minLength = CssTree.parsePseudoClassArgumentAsNumber(node);
|
|
8579
9223
|
break;
|
|
8580
9224
|
default:
|
|
@@ -8586,10 +9230,10 @@ class HtmlRuleConverter extends RuleConverterBase {
|
|
|
8586
9230
|
}
|
|
8587
9231
|
}
|
|
8588
9232
|
if (minLength !== NOT_SPECIFIED) {
|
|
8589
|
-
convertedSelector.children.push(CssTree.createAttributeSelectorNode(
|
|
9233
|
+
convertedSelector.children.push(CssTree.createAttributeSelectorNode(AttributeSelectors.MinLength, String(minLength)));
|
|
8590
9234
|
}
|
|
8591
|
-
convertedSelector.children.push(CssTree.createAttributeSelectorNode(
|
|
8592
|
-
?
|
|
9235
|
+
convertedSelector.children.push(CssTree.createAttributeSelectorNode(AttributeSelectors.MaxLength, String(maxLength === NOT_SPECIFIED
|
|
9236
|
+
? ADG_HTML_CONVERSION_MAX_LENGTH
|
|
8593
9237
|
: maxLength)));
|
|
8594
9238
|
// Create the converted rule
|
|
8595
9239
|
conversionResult.push({
|
|
@@ -8599,7 +9243,7 @@ class HtmlRuleConverter extends RuleConverterBase {
|
|
|
8599
9243
|
// Convert the separator based on the exception status
|
|
8600
9244
|
separator: {
|
|
8601
9245
|
type: 'Value',
|
|
8602
|
-
value:
|
|
9246
|
+
value: rule.exception
|
|
8603
9247
|
? CosmeticRuleSeparator.AdgHtmlFilteringException
|
|
8604
9248
|
: CosmeticRuleSeparator.AdgHtmlFiltering,
|
|
8605
9249
|
},
|
|
@@ -8614,11 +9258,11 @@ class HtmlRuleConverter extends RuleConverterBase {
|
|
|
8614
9258
|
}],
|
|
8615
9259
|
},
|
|
8616
9260
|
},
|
|
8617
|
-
exception:
|
|
8618
|
-
domains:
|
|
9261
|
+
exception: rule.exception,
|
|
9262
|
+
domains: cloneDomainListNode(rule.domains),
|
|
8619
9263
|
});
|
|
8620
9264
|
}
|
|
8621
|
-
return conversionResult;
|
|
9265
|
+
return createNodeConversionResult(conversionResult, true);
|
|
8622
9266
|
}
|
|
8623
9267
|
}
|
|
8624
9268
|
|
|
@@ -8639,96 +9283,38 @@ function getScriptletName(scriptletNode) {
|
|
|
8639
9283
|
return scriptletNode.children[0].value;
|
|
8640
9284
|
}
|
|
8641
9285
|
/**
|
|
8642
|
-
* Set name of the scriptlet
|
|
9286
|
+
* Set name of the scriptlet.
|
|
9287
|
+
* Modifies input `scriptletNode` if needed.
|
|
8643
9288
|
*
|
|
8644
9289
|
* @param scriptletNode Scriptlet node to set name of
|
|
8645
9290
|
* @param name Name to set
|
|
8646
|
-
* @returns Scriptlet node with the specified name
|
|
8647
|
-
* @throws If the scriptlet is empty
|
|
8648
9291
|
*/
|
|
8649
9292
|
function setScriptletName(scriptletNode, name) {
|
|
8650
|
-
if (scriptletNode.children.length
|
|
8651
|
-
|
|
9293
|
+
if (scriptletNode.children.length > 0) {
|
|
9294
|
+
// eslint-disable-next-line no-param-reassign
|
|
9295
|
+
scriptletNode.children[0].value = name;
|
|
8652
9296
|
}
|
|
8653
|
-
const scriptletNodeClone = cloneDeep(scriptletNode);
|
|
8654
|
-
scriptletNodeClone.children[0].value = name;
|
|
8655
|
-
return scriptletNodeClone;
|
|
8656
9297
|
}
|
|
8657
9298
|
/**
|
|
8658
9299
|
* Set quote type of the scriptlet parameters
|
|
8659
9300
|
*
|
|
8660
9301
|
* @param scriptletNode Scriptlet node to set quote type of
|
|
8661
9302
|
* @param quoteType Preferred quote type
|
|
8662
|
-
* @returns Scriptlet node with the specified quote type
|
|
8663
9303
|
*/
|
|
8664
9304
|
function setScriptletQuoteType(scriptletNode, quoteType) {
|
|
8665
|
-
if (scriptletNode.children.length
|
|
8666
|
-
|
|
8667
|
-
|
|
8668
|
-
|
|
8669
|
-
for (let i = 0; i < scriptletNodeClone.children.length; i += 1) {
|
|
8670
|
-
scriptletNodeClone.children[i].value = QuoteUtils.setStringQuoteType(scriptletNodeClone.children[i].value, quoteType);
|
|
8671
|
-
}
|
|
8672
|
-
return scriptletNodeClone;
|
|
8673
|
-
}
|
|
8674
|
-
|
|
8675
|
-
/**
|
|
8676
|
-
* @file Scriptlet conversions from ABP and uBO to ADG
|
|
8677
|
-
*/
|
|
8678
|
-
const ABP_SCRIPTLET_PREFIX = 'abp-';
|
|
8679
|
-
const UBO_SCRIPTLET_PREFIX = 'ubo-';
|
|
8680
|
-
/**
|
|
8681
|
-
* Helper class for converting scriptlets from ABP and uBO to ADG
|
|
8682
|
-
*/
|
|
8683
|
-
class AdgScriptletConverter {
|
|
8684
|
-
/**
|
|
8685
|
-
* Helper function to convert scriptlets to ADG. We implement the core
|
|
8686
|
-
* logic here to avoid code duplication.
|
|
8687
|
-
*
|
|
8688
|
-
* @param scriptletNode Scriptlet parameter list node to convert
|
|
8689
|
-
* @param prefix Prefix to add to the scriptlet name
|
|
8690
|
-
* @returns Converted scriptlet parameter list node
|
|
8691
|
-
*/
|
|
8692
|
-
static convertToAdg(scriptletNode, prefix) {
|
|
8693
|
-
// Remove possible quotes just to make it easier to work with the scriptlet name
|
|
8694
|
-
const scriptletName = QuoteUtils.setStringQuoteType(getScriptletName(scriptletNode), QuoteType.None);
|
|
8695
|
-
// Clone the node to avoid any side effects
|
|
8696
|
-
let result = cloneDeep(scriptletNode);
|
|
8697
|
-
// Only add prefix if it's not already there
|
|
8698
|
-
if (!scriptletName.startsWith(prefix)) {
|
|
8699
|
-
result = setScriptletName(scriptletNode, `${prefix}${scriptletName}`);
|
|
9305
|
+
if (scriptletNode.children.length > 0) {
|
|
9306
|
+
for (let i = 0; i < scriptletNode.children.length; i += 1) {
|
|
9307
|
+
// eslint-disable-next-line no-param-reassign
|
|
9308
|
+
scriptletNode.children[i].value = QuoteUtils.setStringQuoteType(scriptletNode.children[i].value, quoteType);
|
|
8700
9309
|
}
|
|
8701
|
-
// ADG scriptlet parameters should be quoted, and single quoted are preferred
|
|
8702
|
-
result = setScriptletQuoteType(result, QuoteType.Single);
|
|
8703
|
-
return result;
|
|
8704
9310
|
}
|
|
8705
|
-
/**
|
|
8706
|
-
* Converts an ABP snippet node to ADG scriptlet node, if possible.
|
|
8707
|
-
*
|
|
8708
|
-
* @param scriptletNode Scriptlet node to convert
|
|
8709
|
-
* @returns Converted scriptlet node
|
|
8710
|
-
* @throws If the scriptlet isn't supported by ADG or is invalid
|
|
8711
|
-
* @see {@link https://help.adblockplus.org/hc/en-us/articles/1500002338501#snippets-ref}
|
|
8712
|
-
*/
|
|
8713
|
-
static convertFromAbp = (scriptletNode) => {
|
|
8714
|
-
return AdgScriptletConverter.convertToAdg(scriptletNode, ABP_SCRIPTLET_PREFIX);
|
|
8715
|
-
};
|
|
8716
|
-
/**
|
|
8717
|
-
* Convert a uBO scriptlet node to ADG scriptlet node, if possible.
|
|
8718
|
-
*
|
|
8719
|
-
* @param scriptletNode Scriptlet node to convert
|
|
8720
|
-
* @returns Converted scriptlet node
|
|
8721
|
-
* @throws If the scriptlet isn't supported by ADG or is invalid
|
|
8722
|
-
* @see {@link https://github.com/gorhill/uBlock/wiki/Resources-Library#available-general-purpose-scriptlets}
|
|
8723
|
-
*/
|
|
8724
|
-
static convertFromUbo = (scriptletNode) => {
|
|
8725
|
-
return AdgScriptletConverter.convertToAdg(scriptletNode, UBO_SCRIPTLET_PREFIX);
|
|
8726
|
-
};
|
|
8727
9311
|
}
|
|
8728
9312
|
|
|
8729
9313
|
/**
|
|
8730
9314
|
* @file Scriptlet injection rule converter
|
|
8731
9315
|
*/
|
|
9316
|
+
const ABP_SCRIPTLET_PREFIX = 'abp-';
|
|
9317
|
+
const UBO_SCRIPTLET_PREFIX = 'ubo-';
|
|
8732
9318
|
/**
|
|
8733
9319
|
* Scriptlet injection rule converter class
|
|
8734
9320
|
*
|
|
@@ -8739,38 +9325,91 @@ class ScriptletRuleConverter extends RuleConverterBase {
|
|
|
8739
9325
|
* Converts a scriptlet injection rule to AdGuard format, if possible.
|
|
8740
9326
|
*
|
|
8741
9327
|
* @param rule Rule node to convert
|
|
8742
|
-
* @returns
|
|
9328
|
+
* @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
|
|
9329
|
+
* the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
|
|
9330
|
+
* If the rule was not converted, the result array will contain the original node with the same object reference
|
|
8743
9331
|
* @throws If the rule is invalid or cannot be converted
|
|
8744
9332
|
*/
|
|
8745
9333
|
static convertToAdg(rule) {
|
|
8746
|
-
//
|
|
8747
|
-
|
|
9334
|
+
// Ignore AdGuard rules
|
|
9335
|
+
if (rule.syntax === AdblockSyntax.Adg) {
|
|
9336
|
+
return createNodeConversionResult([rule], false);
|
|
9337
|
+
}
|
|
9338
|
+
const separator = rule.separator.value;
|
|
9339
|
+
let convertedSeparator = separator;
|
|
9340
|
+
convertedSeparator = rule.exception
|
|
9341
|
+
? CosmeticRuleSeparator.AdgJsInjectionException
|
|
9342
|
+
: CosmeticRuleSeparator.AdgJsInjection;
|
|
8748
9343
|
const convertedScriptlets = [];
|
|
8749
|
-
for (const scriptlet of
|
|
8750
|
-
|
|
8751
|
-
|
|
8752
|
-
|
|
8753
|
-
|
|
8754
|
-
|
|
9344
|
+
for (const scriptlet of rule.body.children) {
|
|
9345
|
+
// Clone the node to avoid any side effects
|
|
9346
|
+
const scriptletClone = cloneScriptletRuleNode(scriptlet);
|
|
9347
|
+
// Remove possible quotes just to make it easier to work with the scriptlet name
|
|
9348
|
+
const scriptletName = QuoteUtils.setStringQuoteType(getScriptletName(scriptletClone), QuoteType.None);
|
|
9349
|
+
// Add prefix if it's not already there
|
|
9350
|
+
let prefix;
|
|
9351
|
+
switch (rule.syntax) {
|
|
9352
|
+
case AdblockSyntax.Abp:
|
|
9353
|
+
prefix = ABP_SCRIPTLET_PREFIX;
|
|
9354
|
+
break;
|
|
9355
|
+
case AdblockSyntax.Ubo:
|
|
9356
|
+
prefix = UBO_SCRIPTLET_PREFIX;
|
|
9357
|
+
break;
|
|
9358
|
+
default:
|
|
9359
|
+
prefix = EMPTY;
|
|
8755
9360
|
}
|
|
8756
|
-
|
|
8757
|
-
|
|
9361
|
+
if (!scriptletName.startsWith(prefix)) {
|
|
9362
|
+
setScriptletName(scriptletClone, `${prefix}${scriptletName}`);
|
|
8758
9363
|
}
|
|
9364
|
+
// ADG scriptlet parameters should be quoted, and single quoted are preferred
|
|
9365
|
+
setScriptletQuoteType(scriptletClone, QuoteType.Single);
|
|
9366
|
+
convertedScriptlets.push(scriptletClone);
|
|
8759
9367
|
}
|
|
8760
|
-
|
|
8761
|
-
|
|
8762
|
-
|
|
8763
|
-
|
|
8764
|
-
return convertedScriptlets.map((scriptlet) => {
|
|
8765
|
-
return {
|
|
8766
|
-
...ruleNode,
|
|
9368
|
+
return createNodeConversionResult(convertedScriptlets.map((scriptlet) => {
|
|
9369
|
+
const res = {
|
|
9370
|
+
category: rule.category,
|
|
9371
|
+
type: rule.type,
|
|
8767
9372
|
syntax: AdblockSyntax.Adg,
|
|
9373
|
+
exception: rule.exception,
|
|
9374
|
+
domains: cloneDomainListNode(rule.domains),
|
|
9375
|
+
separator: {
|
|
9376
|
+
type: 'Value',
|
|
9377
|
+
value: convertedSeparator,
|
|
9378
|
+
},
|
|
8768
9379
|
body: {
|
|
8769
|
-
|
|
9380
|
+
type: rule.body.type,
|
|
8770
9381
|
children: [scriptlet],
|
|
8771
9382
|
},
|
|
8772
9383
|
};
|
|
8773
|
-
|
|
9384
|
+
if (rule.modifiers) {
|
|
9385
|
+
res.modifiers = cloneModifierListNode(rule.modifiers);
|
|
9386
|
+
}
|
|
9387
|
+
return res;
|
|
9388
|
+
}), true);
|
|
9389
|
+
}
|
|
9390
|
+
}
|
|
9391
|
+
|
|
9392
|
+
/**
|
|
9393
|
+
* A very simple map extension that allows to store multiple values for the same key
|
|
9394
|
+
* by storing them in an array.
|
|
9395
|
+
*
|
|
9396
|
+
* @todo Add more methods if needed
|
|
9397
|
+
*/
|
|
9398
|
+
class MultiValueMap extends Map {
|
|
9399
|
+
/**
|
|
9400
|
+
* Adds a value to the map. If the key already exists, the value will be appended to the existing array,
|
|
9401
|
+
* otherwise a new array will be created for the key.
|
|
9402
|
+
*
|
|
9403
|
+
* @param key Key to add
|
|
9404
|
+
* @param values Value(s) to add
|
|
9405
|
+
*/
|
|
9406
|
+
add(key, ...values) {
|
|
9407
|
+
let currentValues = super.get(key);
|
|
9408
|
+
if (isUndefined(currentValues)) {
|
|
9409
|
+
currentValues = [];
|
|
9410
|
+
super.set(key, values);
|
|
9411
|
+
}
|
|
9412
|
+
currentValues.push(...values);
|
|
8774
9413
|
}
|
|
8775
9414
|
}
|
|
8776
9415
|
|
|
@@ -8796,69 +9435,115 @@ class AdgCosmeticRuleModifierConverter {
|
|
|
8796
9435
|
* Converts a uBO cosmetic rule modifier list to ADG, if possible.
|
|
8797
9436
|
*
|
|
8798
9437
|
* @param modifierList Cosmetic rule modifier list node to convert
|
|
8799
|
-
* @returns
|
|
9438
|
+
* @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
|
|
9439
|
+
* the converted node, and its `isConverted` flag indicates whether the original node was converted.
|
|
9440
|
+
* If the node was not converted, the result will contain the original node with the same object reference
|
|
8800
9441
|
* @throws If the modifier list cannot be converted
|
|
8801
9442
|
* @see {@link https://github.com/gorhill/uBlock/wiki/Procedural-cosmetic-filters#cosmetic-filter-operators}
|
|
8802
9443
|
*/
|
|
8803
|
-
static convertFromUbo
|
|
8804
|
-
const
|
|
8805
|
-
|
|
8806
|
-
|
|
8807
|
-
|
|
8808
|
-
|
|
8809
|
-
|
|
8810
|
-
|
|
8811
|
-
|
|
8812
|
-
|
|
8813
|
-
|
|
8814
|
-
|
|
8815
|
-
|
|
8816
|
-
|
|
8817
|
-
|
|
8818
|
-
//
|
|
8819
|
-
|
|
8820
|
-
|
|
8821
|
-
? `${REGEX_MARKER}${RegExpUtils.negateRegexPattern(RegExpUtils.patternToRegexp(modifierValue))}${REGEX_MARKER}`
|
|
8822
|
-
: modifierValue));
|
|
8823
|
-
break;
|
|
8824
|
-
default:
|
|
8825
|
-
// Leave the modifier as-is
|
|
8826
|
-
convertedModifierList.children.push(modifier);
|
|
9444
|
+
static convertFromUbo(modifierList) {
|
|
9445
|
+
const conversionMap = new MultiValueMap();
|
|
9446
|
+
modifierList.children.forEach((modifier, index) => {
|
|
9447
|
+
// :matches-path
|
|
9448
|
+
if (modifier.modifier.value === UBO_MATCHES_PATH_OPERATOR) {
|
|
9449
|
+
if (!modifier.value) {
|
|
9450
|
+
throw new RuleConversionError(`'${UBO_MATCHES_PATH_OPERATOR}' operator requires a value`);
|
|
9451
|
+
}
|
|
9452
|
+
const value = RegExpUtils.isRegexPattern(modifier.value.value)
|
|
9453
|
+
? StringUtils.escapeCharacters(modifier.value.value, SPECIAL_MODIFIER_REGEX_CHARS)
|
|
9454
|
+
: modifier.value.value;
|
|
9455
|
+
// Convert uBO's `:matches-path(...)` operator to ADG's `$path=...` modifier
|
|
9456
|
+
conversionMap.add(index, createModifierNode(ADG_PATH_MODIFIER,
|
|
9457
|
+
// We should negate the regexp if the modifier is an exception
|
|
9458
|
+
modifier.exception
|
|
9459
|
+
// eslint-disable-next-line max-len
|
|
9460
|
+
? `${REGEX_MARKER}${RegExpUtils.negateRegexPattern(RegExpUtils.patternToRegexp(value))}${REGEX_MARKER}`
|
|
9461
|
+
: value));
|
|
8827
9462
|
}
|
|
8828
|
-
}
|
|
8829
|
-
|
|
8830
|
-
|
|
9463
|
+
});
|
|
9464
|
+
// Check if we have any converted modifiers
|
|
9465
|
+
if (conversionMap.size) {
|
|
9466
|
+
const modifierListClone = clone(modifierList);
|
|
9467
|
+
// Replace the original modifiers with the converted ones
|
|
9468
|
+
modifierListClone.children = modifierListClone.children.map((modifier, index) => {
|
|
9469
|
+
const convertedModifier = conversionMap.get(index);
|
|
9470
|
+
return convertedModifier ?? modifier;
|
|
9471
|
+
}).flat();
|
|
9472
|
+
return createConversionResult(modifierListClone, true);
|
|
9473
|
+
}
|
|
9474
|
+
// Otherwise, just return the original modifier list
|
|
9475
|
+
return createConversionResult(modifierList, false);
|
|
9476
|
+
}
|
|
8831
9477
|
}
|
|
8832
9478
|
|
|
8833
|
-
|
|
8834
|
-
|
|
8835
|
-
|
|
8836
|
-
|
|
8837
|
-
|
|
8838
|
-
|
|
8839
|
-
|
|
8840
|
-
|
|
8841
|
-
|
|
8842
|
-
|
|
8843
|
-
|
|
8844
|
-
|
|
8845
|
-
|
|
9479
|
+
var PseudoClasses;
|
|
9480
|
+
(function (PseudoClasses) {
|
|
9481
|
+
PseudoClasses["AbpContains"] = "-abp-contains";
|
|
9482
|
+
PseudoClasses["AbpHas"] = "-abp-has";
|
|
9483
|
+
PseudoClasses["Contains"] = "contains";
|
|
9484
|
+
PseudoClasses["Has"] = "has";
|
|
9485
|
+
PseudoClasses["HasText"] = "has-text";
|
|
9486
|
+
PseudoClasses["MatchesCss"] = "matches-css";
|
|
9487
|
+
PseudoClasses["MatchesCssAfter"] = "matches-css-after";
|
|
9488
|
+
PseudoClasses["MatchesCssBefore"] = "matches-css-before";
|
|
9489
|
+
PseudoClasses["Not"] = "not";
|
|
9490
|
+
})(PseudoClasses || (PseudoClasses = {}));
|
|
9491
|
+
var PseudoElements;
|
|
9492
|
+
(function (PseudoElements) {
|
|
9493
|
+
PseudoElements["After"] = "after";
|
|
9494
|
+
PseudoElements["Before"] = "before";
|
|
9495
|
+
})(PseudoElements || (PseudoElements = {}));
|
|
9496
|
+
const PSEUDO_ELEMENT_NAMES = new Set([
|
|
9497
|
+
PseudoElements.After,
|
|
9498
|
+
PseudoElements.Before,
|
|
9499
|
+
]);
|
|
9500
|
+
const LEGACY_MATCHES_CSS_NAMES = new Set([
|
|
9501
|
+
PseudoClasses.MatchesCssAfter,
|
|
9502
|
+
PseudoClasses.MatchesCssBefore,
|
|
9503
|
+
]);
|
|
9504
|
+
const LEGACY_EXT_CSS_INDICATOR_PSEUDO_NAMES = new Set([
|
|
9505
|
+
PseudoClasses.Not,
|
|
9506
|
+
PseudoClasses.MatchesCssBefore,
|
|
9507
|
+
PseudoClasses.MatchesCssAfter,
|
|
9508
|
+
]);
|
|
9509
|
+
const CSS_CONVERSION_INDICATOR_PSEUDO_NAMES = new Set([
|
|
9510
|
+
PseudoClasses.AbpContains,
|
|
9511
|
+
PseudoClasses.AbpHas,
|
|
9512
|
+
PseudoClasses.HasText,
|
|
9513
|
+
]);
|
|
8846
9514
|
/**
|
|
8847
9515
|
* Converts some pseudo-classes to pseudo-elements. For example:
|
|
8848
9516
|
* - `:before` → `::before`
|
|
8849
9517
|
*
|
|
8850
9518
|
* @param selectorList Selector list to convert
|
|
8851
|
-
* @returns
|
|
9519
|
+
* @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
|
|
9520
|
+
* the converted node, and its `isConverted` flag indicates whether the original node was converted.
|
|
9521
|
+
* If the node was not converted, the result will contain the original node with the same object reference
|
|
8852
9522
|
*/
|
|
8853
9523
|
function convertToPseudoElements(selectorList) {
|
|
8854
|
-
//
|
|
8855
|
-
const
|
|
9524
|
+
// Check conversion indications before doing any heavy work
|
|
9525
|
+
const hasIndicator = find(
|
|
9526
|
+
// TODO: Need to improve CSSTree types, until then we need to use any type here
|
|
9527
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
9528
|
+
selectorList, (node) => node.type === CssTreeNodeType.PseudoClassSelector && PSEUDO_ELEMENT_NAMES.has(node.name));
|
|
9529
|
+
if (!hasIndicator) {
|
|
9530
|
+
return createConversionResult(selectorList, false);
|
|
9531
|
+
}
|
|
9532
|
+
// Make a clone of the selector list to avoid modifying the original one,
|
|
9533
|
+
// then convert & return the cloned version
|
|
9534
|
+
const selectorListClone = clone(selectorList);
|
|
9535
|
+
// TODO: Need to improve CSSTree types, until then we need to use any type here
|
|
9536
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
8856
9537
|
walk(selectorListClone, {
|
|
9538
|
+
// TODO: Need to improve CSSTree types, until then we need to use any type here
|
|
9539
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
8857
9540
|
leave: (node) => {
|
|
8858
9541
|
if (node.type === CssTreeNodeType.PseudoClassSelector) {
|
|
8859
|
-
//
|
|
8860
|
-
//
|
|
8861
|
-
|
|
9542
|
+
// If the pseudo-class is `:before` or `:after`, then we should
|
|
9543
|
+
// convert the node type to pseudo-element:
|
|
9544
|
+
// :after → ::after
|
|
9545
|
+
// :before → ::before
|
|
9546
|
+
if (PSEUDO_ELEMENT_NAMES.has(node.name)) {
|
|
8862
9547
|
Object.assign(node, {
|
|
8863
9548
|
...node,
|
|
8864
9549
|
type: CssTreeNodeType.PseudoElementSelector,
|
|
@@ -8867,7 +9552,7 @@ function convertToPseudoElements(selectorList) {
|
|
|
8867
9552
|
}
|
|
8868
9553
|
},
|
|
8869
9554
|
});
|
|
8870
|
-
return selectorListClone;
|
|
9555
|
+
return createConversionResult(selectorListClone, true);
|
|
8871
9556
|
}
|
|
8872
9557
|
/**
|
|
8873
9558
|
* Converts legacy Extended CSS `matches-css-before` and `matches-css-after`
|
|
@@ -8876,33 +9561,36 @@ function convertToPseudoElements(selectorList) {
|
|
|
8876
9561
|
* - `:matches-css-after(...)` → `:matches-css(after, ...)`
|
|
8877
9562
|
*
|
|
8878
9563
|
* @param node Node to convert
|
|
9564
|
+
* @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
|
|
9565
|
+
* the converted node, and its `isConverted` flag indicates whether the original node was converted.
|
|
9566
|
+
* If the node was not converted, the result will contain the original node with the same object reference
|
|
8879
9567
|
* @throws If the node is invalid
|
|
8880
9568
|
*/
|
|
8881
9569
|
function convertLegacyMatchesCss(node) {
|
|
8882
|
-
|
|
8883
|
-
if (
|
|
8884
|
-
|
|
8885
|
-
|
|
8886
|
-
|
|
8887
|
-
|
|
8888
|
-
|
|
8889
|
-
|
|
8890
|
-
|
|
8891
|
-
|
|
8892
|
-
|
|
8893
|
-
|
|
8894
|
-
|
|
8895
|
-
|
|
8896
|
-
|
|
8897
|
-
|
|
8898
|
-
|
|
8899
|
-
|
|
8900
|
-
|
|
8901
|
-
|
|
8902
|
-
|
|
8903
|
-
|
|
8904
|
-
|
|
8905
|
-
|
|
9570
|
+
// Check conversion indications before doing any heavy work
|
|
9571
|
+
if (node.type !== CssTreeNodeType.PseudoClassSelector || !LEGACY_MATCHES_CSS_NAMES.has(node.name)) {
|
|
9572
|
+
return createConversionResult(node, false);
|
|
9573
|
+
}
|
|
9574
|
+
const nodeClone = clone(node);
|
|
9575
|
+
if (!nodeClone.children || nodeClone.children.length < 1) {
|
|
9576
|
+
throw new Error(`Invalid ${node.name} pseudo-class: missing argument`);
|
|
9577
|
+
}
|
|
9578
|
+
// Rename the pseudo-class
|
|
9579
|
+
nodeClone.name = PseudoClasses.MatchesCss;
|
|
9580
|
+
// Remove the 'matches-css-' prefix to get the direction
|
|
9581
|
+
const direction = node.name.substring(PseudoClasses.MatchesCss.length + 1);
|
|
9582
|
+
// Add the direction to the first raw argument
|
|
9583
|
+
const arg = nodeClone.children[0];
|
|
9584
|
+
// Check argument
|
|
9585
|
+
if (!arg) {
|
|
9586
|
+
throw new Error(`Invalid ${node.name} pseudo-class: argument shouldn't be null`);
|
|
9587
|
+
}
|
|
9588
|
+
if (arg.type !== CssTreeNodeType.Raw) {
|
|
9589
|
+
throw new Error(`Invalid ${node.name} pseudo-class: unexpected argument type`);
|
|
9590
|
+
}
|
|
9591
|
+
// Add the direction as the first argument
|
|
9592
|
+
arg.value = `${direction},${arg.value}`;
|
|
9593
|
+
return createConversionResult(nodeClone, true);
|
|
8906
9594
|
}
|
|
8907
9595
|
/**
|
|
8908
9596
|
* Converts legacy Extended CSS selectors to the modern Extended CSS syntax.
|
|
@@ -8912,16 +9600,40 @@ function convertLegacyMatchesCss(node) {
|
|
|
8912
9600
|
* - `[-ext-matches-css-before=...]` → `:matches-css(before, ...)`
|
|
8913
9601
|
*
|
|
8914
9602
|
* @param selectorList Selector list AST to convert
|
|
8915
|
-
* @returns
|
|
9603
|
+
* @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
|
|
9604
|
+
* the converted node, and its `isConverted` flag indicates whether the original node was converted.
|
|
9605
|
+
* If the node was not converted, the result will contain the original node with the same object reference
|
|
8916
9606
|
*/
|
|
8917
9607
|
function convertFromLegacyExtendedCss(selectorList) {
|
|
8918
|
-
//
|
|
8919
|
-
const
|
|
9608
|
+
// Check conversion indications before doing any heavy work
|
|
9609
|
+
const hasIndicator = find(
|
|
9610
|
+
// TODO: Need to improve CSSTree types, until then we need to use any type here
|
|
9611
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
9612
|
+
selectorList, (node) => {
|
|
9613
|
+
if (node.type === CssTreeNodeType.PseudoClassSelector) {
|
|
9614
|
+
return LEGACY_EXT_CSS_INDICATOR_PSEUDO_NAMES.has(node.name);
|
|
9615
|
+
}
|
|
9616
|
+
if (node.type === CssTreeNodeType.AttributeSelector) {
|
|
9617
|
+
return node.name.name.startsWith(LEGACY_EXT_CSS_ATTRIBUTE_PREFIX);
|
|
9618
|
+
}
|
|
9619
|
+
return false;
|
|
9620
|
+
});
|
|
9621
|
+
if (!hasIndicator) {
|
|
9622
|
+
return createConversionResult(selectorList, false);
|
|
9623
|
+
}
|
|
9624
|
+
const selectorListClone = clone(selectorList);
|
|
9625
|
+
// TODO: Need to improve CSSTree types, until then we need to use any type here
|
|
9626
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
8920
9627
|
walk(selectorListClone, {
|
|
9628
|
+
// TODO: Need to improve CSSTree types, until then we need to use any type here
|
|
9629
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
8921
9630
|
leave: (node) => {
|
|
8922
9631
|
// :matches-css-before(arg) → :matches-css(before,arg)
|
|
8923
9632
|
// :matches-css-after(arg) → :matches-css(after,arg)
|
|
8924
|
-
convertLegacyMatchesCss(node);
|
|
9633
|
+
const convertedLegacyExtCss = convertLegacyMatchesCss(node);
|
|
9634
|
+
if (convertedLegacyExtCss.isConverted) {
|
|
9635
|
+
Object.assign(node, convertedLegacyExtCss.result);
|
|
9636
|
+
}
|
|
8925
9637
|
// [-ext-name=...] → :name(...)
|
|
8926
9638
|
// [-ext-name='...'] → :name(...)
|
|
8927
9639
|
// [-ext-name="..."] → :name(...)
|
|
@@ -8935,7 +9647,7 @@ function convertFromLegacyExtendedCss(selectorList) {
|
|
|
8935
9647
|
// Remove the '-ext-' prefix to get the pseudo-class name
|
|
8936
9648
|
const name = node.name.name.substring(LEGACY_EXT_CSS_ATTRIBUTE_PREFIX.length);
|
|
8937
9649
|
// Prepare the children list for the pseudo-class node
|
|
8938
|
-
const children =
|
|
9650
|
+
const children = [];
|
|
8939
9651
|
// TODO: Change String node to Raw node to drop the quotes.
|
|
8940
9652
|
// The structure of the node is the same, just the type
|
|
8941
9653
|
// is different and generate() will generate the quotes
|
|
@@ -8948,7 +9660,7 @@ function convertFromLegacyExtendedCss(selectorList) {
|
|
|
8948
9660
|
// For example, if the input is [-ext-has="> .selector"], then
|
|
8949
9661
|
// we need to parse "> .selector" as a selector instead of string
|
|
8950
9662
|
// it as a raw value
|
|
8951
|
-
if ([
|
|
9663
|
+
if ([PseudoClasses.Has, PseudoClasses.Not].includes(name)) {
|
|
8952
9664
|
// Get the value of the attribute selector
|
|
8953
9665
|
const { value } = node;
|
|
8954
9666
|
// If the value is an identifier, then simply push it to the
|
|
@@ -8958,10 +9670,12 @@ function convertFromLegacyExtendedCss(selectorList) {
|
|
|
8958
9670
|
}
|
|
8959
9671
|
else if (value.type === CssTreeNodeType.String) {
|
|
8960
9672
|
// Parse the value as a selector
|
|
8961
|
-
const parsedChildren = CssTree.
|
|
9673
|
+
const parsedChildren = CssTree.parsePlain(value.value, CssTreeParserContext.selectorList);
|
|
8962
9674
|
// Don't forget convert the parsed AST again, because
|
|
8963
9675
|
// it was a raw string before
|
|
8964
|
-
|
|
9676
|
+
const convertedChildren = convertFromLegacyExtendedCss(parsedChildren);
|
|
9677
|
+
// Push the converted children to the list
|
|
9678
|
+
children.push(convertedChildren.result);
|
|
8965
9679
|
}
|
|
8966
9680
|
}
|
|
8967
9681
|
else {
|
|
@@ -8988,14 +9702,12 @@ function convertFromLegacyExtendedCss(selectorList) {
|
|
|
8988
9702
|
children,
|
|
8989
9703
|
};
|
|
8990
9704
|
// Handle this case: [-ext-matches-css-before=...] → :matches-css(before,...)
|
|
8991
|
-
convertLegacyMatchesCss(pseudoNode);
|
|
8992
|
-
|
|
8993
|
-
// keep the reference to the original node
|
|
8994
|
-
Object.assign(node, pseudoNode);
|
|
9705
|
+
const convertedPseudoNode = convertLegacyMatchesCss(pseudoNode);
|
|
9706
|
+
Object.assign(node, convertedPseudoNode.isConverted ? convertedPseudoNode.result : pseudoNode);
|
|
8995
9707
|
}
|
|
8996
9708
|
},
|
|
8997
9709
|
});
|
|
8998
|
-
return selectorListClone;
|
|
9710
|
+
return createConversionResult(selectorListClone, true);
|
|
8999
9711
|
}
|
|
9000
9712
|
/**
|
|
9001
9713
|
* CSS selector converter
|
|
@@ -9007,32 +9719,51 @@ class CssSelectorConverter extends ConverterBase {
|
|
|
9007
9719
|
* Converts Extended CSS elements to AdGuard-compatible ones
|
|
9008
9720
|
*
|
|
9009
9721
|
* @param selectorList Selector list to convert
|
|
9010
|
-
* @returns
|
|
9722
|
+
* @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
|
|
9723
|
+
* the converted node, and its `isConverted` flag indicates whether the original node was converted.
|
|
9724
|
+
* If the node was not converted, the result will contain the original node with the same object reference
|
|
9011
9725
|
* @throws If the rule is invalid or incompatible
|
|
9012
9726
|
*/
|
|
9013
9727
|
static convertToAdg(selectorList) {
|
|
9014
9728
|
// First, convert
|
|
9015
9729
|
// - legacy Extended CSS selectors to the modern Extended CSS syntax and
|
|
9016
9730
|
// - some pseudo-classes to pseudo-elements
|
|
9017
|
-
const
|
|
9731
|
+
const legacyExtCssConverted = convertFromLegacyExtendedCss(selectorList);
|
|
9732
|
+
const pseudoElementsConverted = convertToPseudoElements(legacyExtCssConverted.result);
|
|
9733
|
+
const hasIndicator = legacyExtCssConverted.isConverted || pseudoElementsConverted.isConverted || find(
|
|
9734
|
+
// TODO: Need to improve CSSTree types, until then we need to use any type here
|
|
9735
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
9736
|
+
selectorList,
|
|
9737
|
+
// eslint-disable-next-line max-len
|
|
9738
|
+
(node) => node.type === CssTreeNodeType.PseudoClassSelector && CSS_CONVERSION_INDICATOR_PSEUDO_NAMES.has(node.name));
|
|
9739
|
+
if (!hasIndicator) {
|
|
9740
|
+
return createConversionResult(selectorList, false);
|
|
9741
|
+
}
|
|
9742
|
+
const selectorListClone = legacyExtCssConverted.isConverted || pseudoElementsConverted.isConverted
|
|
9743
|
+
? pseudoElementsConverted.result
|
|
9744
|
+
: clone(selectorList);
|
|
9018
9745
|
// Then, convert some Extended CSS pseudo-classes to AdGuard-compatible ones
|
|
9746
|
+
// TODO: Need to improve CSSTree types, until then we need to use any type here
|
|
9747
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
9019
9748
|
walk(selectorListClone, {
|
|
9749
|
+
// TODO: Need to improve CSSTree types, until then we need to use any type here
|
|
9750
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
9020
9751
|
leave: (node) => {
|
|
9021
9752
|
if (node.type === CssTreeNodeType.PseudoClassSelector) {
|
|
9022
9753
|
// :-abp-contains(...) → :contains(...)
|
|
9023
9754
|
// :has-text(...) → :contains(...)
|
|
9024
|
-
if (node.name ===
|
|
9025
|
-
CssTree.renamePseudoClass(node,
|
|
9755
|
+
if (node.name === PseudoClasses.AbpContains || node.name === PseudoClasses.HasText) {
|
|
9756
|
+
CssTree.renamePseudoClass(node, PseudoClasses.Contains);
|
|
9026
9757
|
}
|
|
9027
9758
|
// :-abp-has(...) → :has(...)
|
|
9028
|
-
if (node.name ===
|
|
9029
|
-
CssTree.renamePseudoClass(node,
|
|
9759
|
+
if (node.name === PseudoClasses.AbpHas) {
|
|
9760
|
+
CssTree.renamePseudoClass(node, PseudoClasses.Has);
|
|
9030
9761
|
}
|
|
9031
9762
|
// TODO: check uBO's `:others()` and `:watch-attr()` pseudo-classes
|
|
9032
9763
|
}
|
|
9033
9764
|
},
|
|
9034
9765
|
});
|
|
9035
|
-
return selectorListClone;
|
|
9766
|
+
return createConversionResult(selectorListClone, true);
|
|
9036
9767
|
}
|
|
9037
9768
|
}
|
|
9038
9769
|
|
|
@@ -9049,27 +9780,39 @@ class CssInjectionRuleConverter extends RuleConverterBase {
|
|
|
9049
9780
|
* Converts a CSS injection rule to AdGuard format, if possible.
|
|
9050
9781
|
*
|
|
9051
9782
|
* @param rule Rule node to convert
|
|
9052
|
-
* @returns
|
|
9783
|
+
* @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
|
|
9784
|
+
* the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
|
|
9785
|
+
* If the rule was not converted, the result array will contain the original node with the same object reference
|
|
9053
9786
|
* @throws If the rule is invalid or cannot be converted
|
|
9054
9787
|
*/
|
|
9055
9788
|
static convertToAdg(rule) {
|
|
9056
|
-
|
|
9057
|
-
|
|
9789
|
+
const separator = rule.separator.value;
|
|
9790
|
+
let convertedSeparator = separator;
|
|
9058
9791
|
// Change the separator if the rule contains ExtendedCSS selectors
|
|
9059
|
-
if (CssTree.hasAnySelectorExtendedCssNode(
|
|
9060
|
-
|
|
9792
|
+
if (CssTree.hasAnySelectorExtendedCssNode(rule.body.selectorList) || rule.body.remove) {
|
|
9793
|
+
convertedSeparator = rule.exception
|
|
9061
9794
|
? CosmeticRuleSeparator.AdgExtendedCssInjectionException
|
|
9062
9795
|
: CosmeticRuleSeparator.AdgExtendedCssInjection;
|
|
9063
9796
|
}
|
|
9064
9797
|
else {
|
|
9065
|
-
|
|
9798
|
+
convertedSeparator = rule.exception
|
|
9066
9799
|
? CosmeticRuleSeparator.AdgCssInjectionException
|
|
9067
9800
|
: CosmeticRuleSeparator.AdgCssInjection;
|
|
9068
9801
|
}
|
|
9069
|
-
|
|
9070
|
-
|
|
9071
|
-
|
|
9072
|
-
|
|
9802
|
+
const convertedSelectorList = CssSelectorConverter.convertToAdg(rule.body.selectorList);
|
|
9803
|
+
// Check if the rule needs to be converted
|
|
9804
|
+
if (!(rule.syntax === AdblockSyntax.Common || rule.syntax === AdblockSyntax.Adg)
|
|
9805
|
+
|| separator !== convertedSeparator
|
|
9806
|
+
|| convertedSelectorList.isConverted) {
|
|
9807
|
+
// TODO: Replace with custom clone method
|
|
9808
|
+
const ruleClone = clone(rule);
|
|
9809
|
+
ruleClone.syntax = AdblockSyntax.Adg;
|
|
9810
|
+
ruleClone.separator.value = convertedSeparator;
|
|
9811
|
+
ruleClone.body.selectorList = convertedSelectorList.result;
|
|
9812
|
+
return createNodeConversionResult([ruleClone], true);
|
|
9813
|
+
}
|
|
9814
|
+
// Otherwise, return the original rule
|
|
9815
|
+
return createNodeConversionResult([rule], false);
|
|
9073
9816
|
}
|
|
9074
9817
|
}
|
|
9075
9818
|
|
|
@@ -9086,27 +9829,39 @@ class ElementHidingRuleConverter extends RuleConverterBase {
|
|
|
9086
9829
|
* Converts an element hiding rule to AdGuard format, if possible.
|
|
9087
9830
|
*
|
|
9088
9831
|
* @param rule Rule node to convert
|
|
9089
|
-
* @returns
|
|
9832
|
+
* @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
|
|
9833
|
+
* the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
|
|
9834
|
+
* If the rule was not converted, the result array will contain the original node with the same object reference
|
|
9090
9835
|
* @throws If the rule is invalid or cannot be converted
|
|
9091
9836
|
*/
|
|
9092
9837
|
static convertToAdg(rule) {
|
|
9093
|
-
|
|
9094
|
-
|
|
9838
|
+
const separator = rule.separator.value;
|
|
9839
|
+
let convertedSeparator = separator;
|
|
9095
9840
|
// Change the separator if the rule contains ExtendedCSS selectors
|
|
9096
|
-
if (CssTree.hasAnySelectorExtendedCssNode(
|
|
9097
|
-
|
|
9841
|
+
if (CssTree.hasAnySelectorExtendedCssNode(rule.body.selectorList)) {
|
|
9842
|
+
convertedSeparator = rule.exception
|
|
9098
9843
|
? CosmeticRuleSeparator.ExtendedElementHidingException
|
|
9099
9844
|
: CosmeticRuleSeparator.ExtendedElementHiding;
|
|
9100
9845
|
}
|
|
9101
9846
|
else {
|
|
9102
|
-
|
|
9847
|
+
convertedSeparator = rule.exception
|
|
9103
9848
|
? CosmeticRuleSeparator.ElementHidingException
|
|
9104
9849
|
: CosmeticRuleSeparator.ElementHiding;
|
|
9105
9850
|
}
|
|
9106
|
-
|
|
9107
|
-
|
|
9108
|
-
|
|
9109
|
-
|
|
9851
|
+
const convertedSelectorList = CssSelectorConverter.convertToAdg(rule.body.selectorList);
|
|
9852
|
+
// Check if the rule needs to be converted
|
|
9853
|
+
if (!(rule.syntax === AdblockSyntax.Common || rule.syntax === AdblockSyntax.Adg)
|
|
9854
|
+
|| separator !== convertedSeparator
|
|
9855
|
+
|| convertedSelectorList.isConverted) {
|
|
9856
|
+
// TODO: Replace with custom clone method
|
|
9857
|
+
const ruleClone = clone(rule);
|
|
9858
|
+
ruleClone.syntax = AdblockSyntax.Adg;
|
|
9859
|
+
ruleClone.separator.value = convertedSeparator;
|
|
9860
|
+
ruleClone.body.selectorList = convertedSelectorList.result;
|
|
9861
|
+
return createNodeConversionResult([ruleClone], true);
|
|
9862
|
+
}
|
|
9863
|
+
// Otherwise, return the original rule
|
|
9864
|
+
return createNodeConversionResult([rule], false);
|
|
9110
9865
|
}
|
|
9111
9866
|
}
|
|
9112
9867
|
|
|
@@ -9134,7 +9889,7 @@ function createNetworkRuleNode(pattern, modifiers = undefined, exception = false
|
|
|
9134
9889
|
},
|
|
9135
9890
|
};
|
|
9136
9891
|
if (!isUndefined(modifiers)) {
|
|
9137
|
-
result.modifiers =
|
|
9892
|
+
result.modifiers = clone(modifiers);
|
|
9138
9893
|
}
|
|
9139
9894
|
return result;
|
|
9140
9895
|
}
|
|
@@ -9154,32 +9909,37 @@ class HeaderRemovalRuleConverter extends RuleConverterBase {
|
|
|
9154
9909
|
* Converts a header removal rule to AdGuard syntax, if possible.
|
|
9155
9910
|
*
|
|
9156
9911
|
* @param rule Rule node to convert
|
|
9157
|
-
* @returns
|
|
9912
|
+
* @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
|
|
9913
|
+
* the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
|
|
9914
|
+
* If the rule was not converted, the result array will contain the original node with the same object reference
|
|
9158
9915
|
* @throws If the rule is invalid or cannot be converted
|
|
9916
|
+
* @example
|
|
9917
|
+
* If the input rule is:
|
|
9918
|
+
* ```adblock
|
|
9919
|
+
* example.com##^responseheader(header-name)
|
|
9920
|
+
* ```
|
|
9921
|
+
* The output will be:
|
|
9922
|
+
* ```adblock
|
|
9923
|
+
* ||example.com^$removeheader=header-name
|
|
9924
|
+
* ```
|
|
9159
9925
|
*/
|
|
9160
9926
|
static convertToAdg(rule) {
|
|
9161
|
-
// Clone the provided AST node to avoid side effects
|
|
9162
|
-
const ruleNode = cloneDeep(rule);
|
|
9163
9927
|
// TODO: Add support for ABP syntax once it starts supporting header removal rules
|
|
9164
|
-
//
|
|
9165
|
-
if (
|
|
9166
|
-
||
|
|
9167
|
-
||
|
|
9168
|
-
||
|
|
9169
|
-
|
|
9928
|
+
// Leave the rule as is if it's not a header removal rule
|
|
9929
|
+
if (rule.category !== RuleCategory.Cosmetic
|
|
9930
|
+
|| rule.type !== CosmeticRuleType.HtmlFilteringRule
|
|
9931
|
+
|| rule.body.body.type !== CssTreeNodeType.Function
|
|
9932
|
+
|| rule.body.body.name !== UBO_RESPONSEHEADER_MARKER) {
|
|
9933
|
+
return createNodeConversionResult([rule], false);
|
|
9170
9934
|
}
|
|
9171
9935
|
// Prepare network rule pattern
|
|
9172
|
-
|
|
9173
|
-
if (
|
|
9936
|
+
const pattern = [];
|
|
9937
|
+
if (rule.domains.children.length === 1) {
|
|
9174
9938
|
// If the rule has only one domain, we can use a simple network rule pattern:
|
|
9175
9939
|
// ||single-domain-from-the-rule^
|
|
9176
|
-
pattern
|
|
9177
|
-
ADBLOCK_URL_START,
|
|
9178
|
-
ruleNode.domains.children[0].value,
|
|
9179
|
-
ADBLOCK_URL_SEPARATOR,
|
|
9180
|
-
].join(EMPTY);
|
|
9940
|
+
pattern.push(ADBLOCK_URL_START, rule.domains.children[0].value, ADBLOCK_URL_SEPARATOR);
|
|
9181
9941
|
}
|
|
9182
|
-
else if (
|
|
9942
|
+
else if (rule.domains.children.length > 1) {
|
|
9183
9943
|
// TODO: Add support for multiple domains, for example:
|
|
9184
9944
|
// example.com,example.org,example.net##^responseheader(header-name)
|
|
9185
9945
|
// We should consider allowing $domain with $removeheader modifier,
|
|
@@ -9189,13 +9949,13 @@ class HeaderRemovalRuleConverter extends RuleConverterBase {
|
|
|
9189
9949
|
}
|
|
9190
9950
|
// Prepare network rule modifiers
|
|
9191
9951
|
const modifiers = createModifierListNode();
|
|
9192
|
-
modifiers.children.push(createModifierNode(ADG_REMOVEHEADER_MODIFIER, CssTree.
|
|
9952
|
+
modifiers.children.push(createModifierNode(ADG_REMOVEHEADER_MODIFIER, CssTree.generateFunctionPlainValue(rule.body.body)));
|
|
9193
9953
|
// Construct the network rule
|
|
9194
|
-
return [
|
|
9195
|
-
createNetworkRuleNode(pattern, modifiers,
|
|
9954
|
+
return createNodeConversionResult([
|
|
9955
|
+
createNetworkRuleNode(pattern.join(EMPTY), modifiers,
|
|
9196
9956
|
// Copy the exception flag
|
|
9197
|
-
|
|
9198
|
-
];
|
|
9957
|
+
rule.exception, AdblockSyntax.Adg),
|
|
9958
|
+
], true);
|
|
9199
9959
|
}
|
|
9200
9960
|
}
|
|
9201
9961
|
|
|
@@ -9212,54 +9972,80 @@ class CosmeticRuleConverter extends RuleConverterBase {
|
|
|
9212
9972
|
* Converts a cosmetic rule to AdGuard syntax, if possible.
|
|
9213
9973
|
*
|
|
9214
9974
|
* @param rule Rule node to convert
|
|
9215
|
-
* @returns
|
|
9975
|
+
* @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
|
|
9976
|
+
* the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
|
|
9977
|
+
* If the rule was not converted, the result array will contain the original node with the same object reference
|
|
9216
9978
|
* @throws If the rule is invalid or cannot be converted
|
|
9217
9979
|
*/
|
|
9218
9980
|
static convertToAdg(rule) {
|
|
9219
|
-
|
|
9220
|
-
const ruleNode = cloneDeep(rule);
|
|
9221
|
-
// Convert cosmetic rule modifiers
|
|
9222
|
-
if (ruleNode.modifiers) {
|
|
9223
|
-
if (ruleNode.syntax === AdblockSyntax.Ubo) {
|
|
9224
|
-
// uBO doesn't support this rule:
|
|
9225
|
-
// example.com##+js(set-constant.js, foo, bar):matches-path(/baz)
|
|
9226
|
-
if (ruleNode.type === CosmeticRuleType.ScriptletInjectionRule) {
|
|
9227
|
-
throw new RuleConversionError('uBO scriptlet injection rules don\'t support cosmetic rule modifiers');
|
|
9228
|
-
}
|
|
9229
|
-
ruleNode.modifiers = AdgCosmeticRuleModifierConverter.convertFromUbo(ruleNode.modifiers);
|
|
9230
|
-
}
|
|
9231
|
-
else if (ruleNode.syntax === AdblockSyntax.Abp) {
|
|
9232
|
-
// TODO: Implement once ABP starts supporting cosmetic rule modifiers
|
|
9233
|
-
throw new RuleConversionError('ABP don\'t support cosmetic rule modifiers');
|
|
9234
|
-
}
|
|
9235
|
-
}
|
|
9981
|
+
let subconverterResult;
|
|
9236
9982
|
// Convert cosmetic rule based on its type
|
|
9237
|
-
switch (
|
|
9983
|
+
switch (rule.type) {
|
|
9238
9984
|
case CosmeticRuleType.ElementHidingRule:
|
|
9239
|
-
|
|
9985
|
+
subconverterResult = ElementHidingRuleConverter.convertToAdg(rule);
|
|
9986
|
+
break;
|
|
9240
9987
|
case CosmeticRuleType.ScriptletInjectionRule:
|
|
9241
|
-
|
|
9988
|
+
subconverterResult = ScriptletRuleConverter.convertToAdg(rule);
|
|
9989
|
+
break;
|
|
9242
9990
|
case CosmeticRuleType.CssInjectionRule:
|
|
9243
|
-
|
|
9991
|
+
subconverterResult = CssInjectionRuleConverter.convertToAdg(rule);
|
|
9992
|
+
break;
|
|
9244
9993
|
case CosmeticRuleType.HtmlFilteringRule:
|
|
9245
9994
|
// Handle special case: uBO response header filtering rule
|
|
9246
|
-
if (
|
|
9247
|
-
&&
|
|
9248
|
-
|
|
9995
|
+
if (rule.body.body.type === CssTreeNodeType.Function
|
|
9996
|
+
&& rule.body.body.name === UBO_RESPONSEHEADER_MARKER) {
|
|
9997
|
+
subconverterResult = HeaderRemovalRuleConverter.convertToAdg(rule);
|
|
9249
9998
|
}
|
|
9250
|
-
|
|
9251
|
-
|
|
9999
|
+
else {
|
|
10000
|
+
subconverterResult = HtmlRuleConverter.convertToAdg(rule);
|
|
10001
|
+
}
|
|
10002
|
+
break;
|
|
10003
|
+
// Note: Currently, only ADG supports JS injection rules, so we don't need to convert them
|
|
9252
10004
|
case CosmeticRuleType.JsInjectionRule:
|
|
9253
|
-
|
|
10005
|
+
subconverterResult = createNodeConversionResult([rule], false);
|
|
10006
|
+
break;
|
|
9254
10007
|
default:
|
|
9255
10008
|
throw new RuleConversionError('Unsupported cosmetic rule type');
|
|
9256
10009
|
}
|
|
10010
|
+
let convertedModifiers;
|
|
10011
|
+
// Convert cosmetic rule modifiers, if any
|
|
10012
|
+
if (rule.modifiers) {
|
|
10013
|
+
if (rule.syntax === AdblockSyntax.Ubo) {
|
|
10014
|
+
// uBO doesn't support this rule:
|
|
10015
|
+
// example.com##+js(set-constant.js, foo, bar):matches-path(/baz)
|
|
10016
|
+
if (rule.type === CosmeticRuleType.ScriptletInjectionRule) {
|
|
10017
|
+
throw new RuleConversionError('uBO scriptlet injection rules don\'t support cosmetic rule modifiers');
|
|
10018
|
+
}
|
|
10019
|
+
convertedModifiers = AdgCosmeticRuleModifierConverter.convertFromUbo(rule.modifiers);
|
|
10020
|
+
}
|
|
10021
|
+
else if (rule.syntax === AdblockSyntax.Abp) {
|
|
10022
|
+
// TODO: Implement once ABP starts supporting cosmetic rule modifiers
|
|
10023
|
+
throw new RuleConversionError('ABP don\'t support cosmetic rule modifiers');
|
|
10024
|
+
}
|
|
10025
|
+
}
|
|
10026
|
+
if ((subconverterResult.result.length > 1 || subconverterResult.isConverted)
|
|
10027
|
+
|| (convertedModifiers && convertedModifiers.isConverted)) {
|
|
10028
|
+
// Add modifier list to the subconverter result rules
|
|
10029
|
+
subconverterResult.result.forEach((subconverterRule) => {
|
|
10030
|
+
if (convertedModifiers && subconverterRule.category === RuleCategory.Cosmetic) {
|
|
10031
|
+
// eslint-disable-next-line no-param-reassign
|
|
10032
|
+
subconverterRule.modifiers = convertedModifiers.result;
|
|
10033
|
+
}
|
|
10034
|
+
});
|
|
10035
|
+
return subconverterResult;
|
|
10036
|
+
}
|
|
10037
|
+
return createNodeConversionResult([rule], false);
|
|
9257
10038
|
}
|
|
9258
10039
|
}
|
|
9259
10040
|
|
|
9260
10041
|
/**
|
|
9261
10042
|
* @file Network rule modifier list converter.
|
|
9262
10043
|
*/
|
|
10044
|
+
// Since scriptlets library doesn't have ESM exports, we should import
|
|
10045
|
+
// the whole module and then extract the required functions from it here.
|
|
10046
|
+
// Otherwise importing AGTree will cause an error in ESM environment,
|
|
10047
|
+
// because scriptlets library doesn't support named exports.
|
|
10048
|
+
const { redirects } = scriptlets;
|
|
9263
10049
|
/**
|
|
9264
10050
|
* @see {@link https://adguard.com/kb/general/ad-filtering/create-own-filters/#csp-modifier}
|
|
9265
10051
|
*/
|
|
@@ -9320,17 +10106,16 @@ class NetworkRuleModifierListConverter extends ConverterBase {
|
|
|
9320
10106
|
* Converts a network rule modifier list to AdGuard format, if possible.
|
|
9321
10107
|
*
|
|
9322
10108
|
* @param modifierList Network rule modifier list node to convert
|
|
9323
|
-
* @returns
|
|
10109
|
+
* @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
|
|
10110
|
+
* the converted node, and its `isConverted` flag indicates whether the original node was converted.
|
|
10111
|
+
* If the node was not converted, the result will contain the original node with the same object reference
|
|
9324
10112
|
* @throws If the conversion is not possible
|
|
9325
10113
|
*/
|
|
9326
10114
|
static convertToAdg(modifierList) {
|
|
9327
|
-
|
|
9328
|
-
|
|
9329
|
-
|
|
9330
|
-
|
|
9331
|
-
const cspValues = [];
|
|
9332
|
-
modifierListNode.children.forEach((modifierNode) => {
|
|
9333
|
-
// Handle regular modifiers conversion and $csp modifiers collection
|
|
10115
|
+
const conversionMap = new MultiValueMap();
|
|
10116
|
+
// Special case: $csp modifier
|
|
10117
|
+
let cspCount = 0;
|
|
10118
|
+
modifierList.children.forEach((modifierNode, index) => {
|
|
9334
10119
|
const modifierConversions = ADG_CONVERSION_MAP.get(modifierNode.modifier.value);
|
|
9335
10120
|
if (modifierConversions) {
|
|
9336
10121
|
for (const modifierConversion of modifierConversions) {
|
|
@@ -9343,17 +10128,14 @@ class NetworkRuleModifierListConverter extends ConverterBase {
|
|
|
9343
10128
|
const value = modifierConversion.value
|
|
9344
10129
|
? modifierConversion.value(modifierNode.value?.value)
|
|
9345
10130
|
: modifierNode.value?.value;
|
|
9346
|
-
if
|
|
9347
|
-
|
|
9348
|
-
|
|
10131
|
+
// Check if the name or the value is different from the original modifier
|
|
10132
|
+
// If so, add the converted modifier to the list
|
|
10133
|
+
if (name !== modifierNode.modifier.value || value !== modifierNode.value?.value) {
|
|
10134
|
+
conversionMap.add(index, createModifierNode(name, value, exception));
|
|
9349
10135
|
}
|
|
9350
|
-
|
|
9351
|
-
|
|
9352
|
-
|
|
9353
|
-
const existingModifier = convertedModifierList.children.find((m) => m.modifier.value === name && m.exception === exception && m.value?.value === value);
|
|
9354
|
-
if (!existingModifier) {
|
|
9355
|
-
convertedModifierList.children.push(createModifierNode(name, value, exception));
|
|
9356
|
-
}
|
|
10136
|
+
// Special case: $csp modifier
|
|
10137
|
+
if (name === CSP_MODIFIER) {
|
|
10138
|
+
cspCount += 1;
|
|
9357
10139
|
}
|
|
9358
10140
|
}
|
|
9359
10141
|
return;
|
|
@@ -9376,26 +10158,52 @@ class NetworkRuleModifierListConverter extends ConverterBase {
|
|
|
9376
10158
|
// Try to convert the redirect resource name to ADG format
|
|
9377
10159
|
// This function returns undefined if the resource name is unknown
|
|
9378
10160
|
const convertedRedirectResource = redirects.convertRedirectNameToAdg(redirectResource);
|
|
9379
|
-
|
|
9380
|
-
// If
|
|
9381
|
-
|
|
9382
|
-
|
|
9383
|
-
|
|
9384
|
-
|
|
9385
|
-
|
|
9386
|
-
|
|
9387
|
-
|
|
9388
|
-
&& m.exception === modifierNode.exception
|
|
9389
|
-
&& m.value?.value === modifierNode.value?.value);
|
|
9390
|
-
if (!existingModifier) {
|
|
9391
|
-
convertedModifierList.children.push(modifierNode);
|
|
10161
|
+
// Check if the modifier name or the redirect resource name is different from the original modifier
|
|
10162
|
+
// If so, add the converted modifier to the list
|
|
10163
|
+
if (modifierName !== modifierNode.modifier.value
|
|
10164
|
+
|| (convertedRedirectResource !== undefined && convertedRedirectResource !== redirectResource)) {
|
|
10165
|
+
conversionMap.add(index, createModifierNode(modifierName,
|
|
10166
|
+
// If the redirect resource name is unknown, fall back to the original one
|
|
10167
|
+
// Later, the validator will throw an error if the resource name is invalid
|
|
10168
|
+
convertedRedirectResource || redirectResource, modifierNode.exception));
|
|
10169
|
+
}
|
|
9392
10170
|
}
|
|
9393
10171
|
});
|
|
9394
|
-
//
|
|
9395
|
-
if (
|
|
9396
|
-
|
|
10172
|
+
// Prepare the result if there are any converted modifiers or $csp modifiers
|
|
10173
|
+
if (conversionMap.size || cspCount) {
|
|
10174
|
+
const modifierListClone = cloneModifierListNode(modifierList);
|
|
10175
|
+
// Replace the original modifiers with the converted ones
|
|
10176
|
+
// One modifier may be replaced with multiple modifiers, so we need to flatten the array
|
|
10177
|
+
modifierListClone.children = modifierListClone.children.map((modifierNode, index) => {
|
|
10178
|
+
const conversionRecord = conversionMap.get(index);
|
|
10179
|
+
if (conversionRecord) {
|
|
10180
|
+
return conversionRecord;
|
|
10181
|
+
}
|
|
10182
|
+
return modifierNode;
|
|
10183
|
+
}).flat();
|
|
10184
|
+
// Special case: $csp modifier: merge multiple $csp modifiers into one
|
|
10185
|
+
// and put it at the end of the modifier list
|
|
10186
|
+
if (cspCount) {
|
|
10187
|
+
const cspValues = [];
|
|
10188
|
+
modifierListClone.children = modifierListClone.children.filter((modifierNode) => {
|
|
10189
|
+
if (modifierNode.modifier.value === CSP_MODIFIER) {
|
|
10190
|
+
if (!modifierNode.value?.value) {
|
|
10191
|
+
throw new RuleConversionError('$csp modifier value is missing');
|
|
10192
|
+
}
|
|
10193
|
+
cspValues.push(modifierNode.value?.value);
|
|
10194
|
+
return false;
|
|
10195
|
+
}
|
|
10196
|
+
return true;
|
|
10197
|
+
});
|
|
10198
|
+
modifierListClone.children.push(createModifierNode(CSP_MODIFIER, cspValues.join(CSP_SEPARATOR)));
|
|
10199
|
+
}
|
|
10200
|
+
// Before returning the result, remove duplicated modifiers
|
|
10201
|
+
modifierListClone.children = modifierListClone.children.filter((modifierNode, index, self) => self.findIndex((m) => m.modifier.value === modifierNode.modifier.value
|
|
10202
|
+
&& m.exception === modifierNode.exception
|
|
10203
|
+
&& m.value?.value === modifierNode.value?.value) === index);
|
|
10204
|
+
return createConversionResult(modifierListClone, true);
|
|
9397
10205
|
}
|
|
9398
|
-
return
|
|
10206
|
+
return createConversionResult(modifierList, false);
|
|
9399
10207
|
}
|
|
9400
10208
|
}
|
|
9401
10209
|
|
|
@@ -9412,17 +10220,35 @@ class NetworkRuleConverter extends RuleConverterBase {
|
|
|
9412
10220
|
* Converts a network rule to AdGuard format, if possible.
|
|
9413
10221
|
*
|
|
9414
10222
|
* @param rule Rule node to convert
|
|
9415
|
-
* @returns
|
|
10223
|
+
* @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
|
|
10224
|
+
* the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
|
|
10225
|
+
* If the rule was not converted, the result array will contain the original node with the same object reference
|
|
9416
10226
|
* @throws If the rule is invalid or cannot be converted
|
|
9417
10227
|
*/
|
|
9418
10228
|
static convertToAdg(rule) {
|
|
9419
|
-
|
|
9420
|
-
|
|
9421
|
-
|
|
9422
|
-
|
|
9423
|
-
|
|
10229
|
+
if (rule.modifiers) {
|
|
10230
|
+
const modifiers = NetworkRuleModifierListConverter.convertToAdg(rule.modifiers);
|
|
10231
|
+
// If the object reference is different, it means that the modifiers were converted
|
|
10232
|
+
// In this case, we should clone the entire rule and replace the modifiers with the converted ones
|
|
10233
|
+
if (modifiers.isConverted) {
|
|
10234
|
+
return {
|
|
10235
|
+
result: [{
|
|
10236
|
+
category: RuleCategory.Network,
|
|
10237
|
+
type: 'NetworkRule',
|
|
10238
|
+
syntax: rule.syntax,
|
|
10239
|
+
exception: rule.exception,
|
|
10240
|
+
pattern: {
|
|
10241
|
+
type: 'Value',
|
|
10242
|
+
value: rule.pattern.value,
|
|
10243
|
+
},
|
|
10244
|
+
modifiers: modifiers.result,
|
|
10245
|
+
}],
|
|
10246
|
+
isConverted: true,
|
|
10247
|
+
};
|
|
10248
|
+
}
|
|
9424
10249
|
}
|
|
9425
|
-
return
|
|
10250
|
+
// If the modifiers were not converted, return the original rule
|
|
10251
|
+
return createNodeConversionResult([rule], false);
|
|
9426
10252
|
}
|
|
9427
10253
|
}
|
|
9428
10254
|
|
|
@@ -9443,48 +10269,27 @@ class RuleConverter extends RuleConverterBase {
|
|
|
9443
10269
|
* Converts an adblock filtering rule to AdGuard format, if possible.
|
|
9444
10270
|
*
|
|
9445
10271
|
* @param rule Rule node to convert
|
|
9446
|
-
* @returns
|
|
10272
|
+
* @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
|
|
10273
|
+
* the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
|
|
10274
|
+
* If the rule was not converted, the result array will contain the original node with the same object reference
|
|
9447
10275
|
* @throws If the rule is invalid or cannot be converted
|
|
9448
10276
|
*/
|
|
9449
10277
|
static convertToAdg(rule) {
|
|
9450
|
-
// Clone the provided AST node to avoid side effects
|
|
9451
|
-
const ruleNode = cloneDeep(rule);
|
|
9452
10278
|
// Delegate conversion to the corresponding sub-converter
|
|
9453
10279
|
// based on the rule category
|
|
9454
|
-
switch (
|
|
10280
|
+
switch (rule.category) {
|
|
9455
10281
|
case RuleCategory.Comment:
|
|
9456
|
-
return CommentRuleConverter.convertToAdg(
|
|
10282
|
+
return CommentRuleConverter.convertToAdg(rule);
|
|
9457
10283
|
case RuleCategory.Cosmetic:
|
|
9458
|
-
return CosmeticRuleConverter.convertToAdg(
|
|
10284
|
+
return CosmeticRuleConverter.convertToAdg(rule);
|
|
9459
10285
|
case RuleCategory.Network:
|
|
9460
|
-
return NetworkRuleConverter.convertToAdg(
|
|
10286
|
+
return NetworkRuleConverter.convertToAdg(rule);
|
|
9461
10287
|
default:
|
|
9462
|
-
throw new RuleConversionError(`Unknown rule category: ${
|
|
10288
|
+
throw new RuleConversionError(`Unknown rule category: ${rule.category}`);
|
|
9463
10289
|
}
|
|
9464
10290
|
}
|
|
9465
10291
|
}
|
|
9466
10292
|
|
|
9467
|
-
/**
|
|
9468
|
-
* @file Utility functions for working with filter list nodes
|
|
9469
|
-
*/
|
|
9470
|
-
/**
|
|
9471
|
-
* Creates a filter list node
|
|
9472
|
-
*
|
|
9473
|
-
* @param rules Rules to put in the list (optional, defaults to an empty list)
|
|
9474
|
-
* @returns Filter list node
|
|
9475
|
-
*/
|
|
9476
|
-
function createFilterListNode(rules = []) {
|
|
9477
|
-
const result = {
|
|
9478
|
-
type: 'FilterList',
|
|
9479
|
-
children: [],
|
|
9480
|
-
};
|
|
9481
|
-
// We need to clone the rules to avoid side effects
|
|
9482
|
-
if (rules.length > 0) {
|
|
9483
|
-
result.children = cloneDeep(rules);
|
|
9484
|
-
}
|
|
9485
|
-
return result;
|
|
9486
|
-
}
|
|
9487
|
-
|
|
9488
10293
|
/**
|
|
9489
10294
|
* @file Adblock filter list converter
|
|
9490
10295
|
*/
|
|
@@ -9503,18 +10308,133 @@ class FilterListConverter extends ConverterBase {
|
|
|
9503
10308
|
* Converts an adblock filter list to AdGuard format, if possible.
|
|
9504
10309
|
*
|
|
9505
10310
|
* @param filterListNode Filter list node to convert
|
|
9506
|
-
* @
|
|
9507
|
-
*
|
|
10311
|
+
* @param tolerant Indicates whether the converter should be tolerant to invalid rules. If enabled and a rule is
|
|
10312
|
+
* invalid, it will be left as is. If disabled and a rule is invalid, the whole filter list will be failed.
|
|
10313
|
+
* Defaults to `true`.
|
|
10314
|
+
* @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
|
|
10315
|
+
* the converted node, and its `isConverted` flag indicates whether the original node was converted.
|
|
10316
|
+
* If the node was not converted, the result will contain the original node with the same object reference
|
|
10317
|
+
* @throws If the filter list is invalid or cannot be converted (if the tolerant mode is disabled)
|
|
10318
|
+
*/
|
|
10319
|
+
static convertToAdg(filterListNode, tolerant = true) {
|
|
10320
|
+
// Prepare a map to store the converted rules by their index in the filter list
|
|
10321
|
+
const conversionMap = new MultiValueMap();
|
|
10322
|
+
// Iterate over the filtering rules and convert them one by one, then add them to the result (one conversion may
|
|
10323
|
+
// result in multiple rules)
|
|
10324
|
+
for (let i = 0; i < filterListNode.children.length; i += 1) {
|
|
10325
|
+
try {
|
|
10326
|
+
const convertedRules = RuleConverter.convertToAdg(filterListNode.children[i]);
|
|
10327
|
+
// Add the converted rules to the map if they were converted
|
|
10328
|
+
if (convertedRules.isConverted) {
|
|
10329
|
+
conversionMap.add(i, ...convertedRules.result);
|
|
10330
|
+
}
|
|
10331
|
+
}
|
|
10332
|
+
catch (error) {
|
|
10333
|
+
// If the tolerant mode is disabled, we should throw an error, this will fail the whole filter list
|
|
10334
|
+
// conversion.
|
|
10335
|
+
// Otherwise, we just ignore the error and leave the rule as is
|
|
10336
|
+
if (!tolerant) {
|
|
10337
|
+
throw error;
|
|
10338
|
+
}
|
|
10339
|
+
}
|
|
10340
|
+
}
|
|
10341
|
+
// If the conversion map is empty, it means that no rules were converted, so we can return the original filter
|
|
10342
|
+
// list
|
|
10343
|
+
if (conversionMap.size === 0) {
|
|
10344
|
+
return createConversionResult(filterListNode, false);
|
|
10345
|
+
}
|
|
10346
|
+
// Otherwise, create a new filter list node with the converted rules
|
|
10347
|
+
const convertedFilterList = {
|
|
10348
|
+
type: 'FilterList',
|
|
10349
|
+
children: [],
|
|
10350
|
+
};
|
|
10351
|
+
// Iterate over the original rules again and add them to the converted filter list, replacing the converted
|
|
10352
|
+
// rules with the new ones at the specified indexes
|
|
10353
|
+
for (let i = 0; i < filterListNode.children.length; i += 1) {
|
|
10354
|
+
const rules = conversionMap.get(i);
|
|
10355
|
+
if (rules) {
|
|
10356
|
+
convertedFilterList.children.push(...rules);
|
|
10357
|
+
}
|
|
10358
|
+
else {
|
|
10359
|
+
// We clone the unconverted rules to avoid mutating the original filter list if we return the converted
|
|
10360
|
+
// one
|
|
10361
|
+
convertedFilterList.children.push(clone(filterListNode.children[i]));
|
|
10362
|
+
}
|
|
10363
|
+
}
|
|
10364
|
+
return createConversionResult(convertedFilterList, true);
|
|
10365
|
+
}
|
|
10366
|
+
}
|
|
10367
|
+
|
|
10368
|
+
/**
|
|
10369
|
+
* @file Filter list converter for raw filter lists
|
|
10370
|
+
*
|
|
10371
|
+
* Technically, this is a wrapper around `FilterListConverter` that works with nodes instead of strings.
|
|
10372
|
+
*/
|
|
10373
|
+
/**
|
|
10374
|
+
* Adblock filter list converter class.
|
|
10375
|
+
*
|
|
10376
|
+
* You can use this class to convert string-based filter lists, since most of the converters work with nodes.
|
|
10377
|
+
* This class just provides an extra layer on top of the {@link FilterListConverter} and calls the parser/serializer
|
|
10378
|
+
* before/after the conversion internally.
|
|
10379
|
+
*
|
|
10380
|
+
* @todo Implement `convertToUbo` and `convertToAbp`
|
|
10381
|
+
*/
|
|
10382
|
+
class RawFilterListConverter extends ConverterBase {
|
|
10383
|
+
/**
|
|
10384
|
+
* Converts an adblock filter list text to AdGuard format, if possible.
|
|
10385
|
+
*
|
|
10386
|
+
* @param rawFilterList Raw filter list text to convert
|
|
10387
|
+
* @param tolerant Indicates whether the converter should be tolerant to invalid rules. If enabled and a rule is
|
|
10388
|
+
* invalid, it will be left as is. If disabled and a rule is invalid, the whole filter list will be failed.
|
|
10389
|
+
* Defaults to `true`.
|
|
10390
|
+
* @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
|
|
10391
|
+
* the array of converted filter list text, and its `isConverted` flag indicates whether the original rule was
|
|
10392
|
+
* converted. If the rule was not converted, the original filter list text will be returned
|
|
10393
|
+
* @throws If the filter list is invalid or cannot be converted (if the tolerant mode is disabled)
|
|
9508
10394
|
*/
|
|
9509
|
-
static convertToAdg(
|
|
9510
|
-
const
|
|
9511
|
-
//
|
|
9512
|
-
|
|
9513
|
-
|
|
9514
|
-
const convertedRules = RuleConverter.convertToAdg(ruleNode);
|
|
9515
|
-
result.children.push(...convertedRules);
|
|
10395
|
+
static convertToAdg(rawFilterList, tolerant = true) {
|
|
10396
|
+
const conversionResult = FilterListConverter.convertToAdg(FilterListParser.parse(rawFilterList, tolerant), tolerant);
|
|
10397
|
+
// If the filter list was not converted, return the original text
|
|
10398
|
+
if (!conversionResult.isConverted) {
|
|
10399
|
+
return createConversionResult(rawFilterList, false);
|
|
9516
10400
|
}
|
|
9517
|
-
return result
|
|
10401
|
+
// Otherwise, serialize the filter list and return the result
|
|
10402
|
+
return createConversionResult(FilterListParser.generate(conversionResult.result), true);
|
|
10403
|
+
}
|
|
10404
|
+
}
|
|
10405
|
+
|
|
10406
|
+
/**
|
|
10407
|
+
* @file Rule converter for raw rules
|
|
10408
|
+
*
|
|
10409
|
+
* Technically, this is a wrapper around `RuleConverter` that works with nodes instead of strings.
|
|
10410
|
+
*/
|
|
10411
|
+
/**
|
|
10412
|
+
* Adblock filtering rule converter class.
|
|
10413
|
+
*
|
|
10414
|
+
* You can use this class to convert string-based adblock rules, since most of the converters work with nodes.
|
|
10415
|
+
* This class just provides an extra layer on top of the {@link RuleConverter} and calls the parser/serializer
|
|
10416
|
+
* before/after the conversion internally.
|
|
10417
|
+
*
|
|
10418
|
+
* @todo Implement `convertToUbo` and `convertToAbp`
|
|
10419
|
+
*/
|
|
10420
|
+
class RawRuleConverter extends ConverterBase {
|
|
10421
|
+
/**
|
|
10422
|
+
* Converts an adblock filtering rule to AdGuard format, if possible.
|
|
10423
|
+
*
|
|
10424
|
+
* @param rawRule Raw rule text to convert
|
|
10425
|
+
* @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
|
|
10426
|
+
* the array of converted rule texts, and its `isConverted` flag indicates whether the original rule was converted.
|
|
10427
|
+
* If the rule was not converted, the original rule text will be returned
|
|
10428
|
+
* @throws If the rule is invalid or cannot be converted
|
|
10429
|
+
*/
|
|
10430
|
+
static convertToAdg(rawRule) {
|
|
10431
|
+
const conversionResult = RuleConverter.convertToAdg(RuleParser.parse(rawRule));
|
|
10432
|
+
// If the rule was not converted, return the original rule text
|
|
10433
|
+
if (!conversionResult.isConverted) {
|
|
10434
|
+
return createConversionResult([rawRule], false);
|
|
10435
|
+
}
|
|
10436
|
+
// Otherwise, serialize the converted rule nodes
|
|
10437
|
+
return createConversionResult(conversionResult.result.map(RuleParser.generate), true);
|
|
9518
10438
|
}
|
|
9519
10439
|
}
|
|
9520
10440
|
|
|
@@ -9596,7 +10516,7 @@ class LogicalExpressionUtils {
|
|
|
9596
10516
|
}
|
|
9597
10517
|
}
|
|
9598
10518
|
|
|
9599
|
-
const version$1 = "1.1.
|
|
10519
|
+
const version$1 = "1.1.6";
|
|
9600
10520
|
|
|
9601
10521
|
/**
|
|
9602
10522
|
* @file AGTree version
|
|
@@ -9607,4 +10527,4 @@ const version$1 = "1.1.4";
|
|
|
9607
10527
|
// with wrong relative path to `package.json`. So we need this little "hack"
|
|
9608
10528
|
const version = version$1;
|
|
9609
10529
|
|
|
9610
|
-
export { ADBLOCK_URL_SEPARATOR, ADBLOCK_URL_SEPARATOR_REGEX, ADBLOCK_URL_START, ADBLOCK_URL_START_REGEX, ADBLOCK_WILDCARD, ADBLOCK_WILDCARD_REGEX, ADG_SCRIPTLET_MASK, AGLINT_COMMAND_PREFIX, AdblockSyntax, AdblockSyntaxError, AgentCommentRuleParser, AgentParser, AppListParser, COMMA_DOMAIN_LIST_SEPARATOR, CommentMarker, CommentRuleParser, CommentRuleType, ConfigCommentRuleParser, CosmeticRuleParser, CosmeticRuleSeparator, CosmeticRuleSeparatorUtils, CosmeticRuleType, CssTree, CssTreeNodeType, CssTreeParserContext, DomainListParser, DomainUtils, EXT_CSS_LEGACY_ATTRIBUTES, EXT_CSS_PSEUDO_CLASSES, FORBIDDEN_CSS_FUNCTIONS, FilterListConverter, FilterListParser, HINT_MARKER, HintCommentRuleParser, HintParser, IF, INCLUDE, LogicalExpressionParser, LogicalExpressionUtils, METADATA_HEADERS, MODIFIERS_SEPARATOR, MODIFIER_ASSIGN_OPERATOR, MetadataCommentRuleParser, MethodListParser, ModifierListParser, ModifierParser, NEGATION_MARKER, NETWORK_RULE_EXCEPTION_MARKER, NETWORK_RULE_SEPARATOR, NetworkRuleParser, NotImplementedError, PIPE_MODIFIER_SEPARATOR, PREPROCESSOR_MARKER, ParameterListParser, PreProcessorCommentRuleParser, QuoteType, QuoteUtils, RegExpUtils, RuleCategory, RuleConversionError, RuleConverter, RuleParser, SAFARI_CB_AFFINITY, SPECIAL_REGEX_SYMBOLS, StealthOptionListParser, UBO_SCRIPTLET_MASK, locRange, modifierValidator, shiftLoc, version };
|
|
10530
|
+
export { ADBLOCK_URL_SEPARATOR, ADBLOCK_URL_SEPARATOR_REGEX, ADBLOCK_URL_START, ADBLOCK_URL_START_REGEX, ADBLOCK_WILDCARD, ADBLOCK_WILDCARD_REGEX, ADG_SCRIPTLET_MASK, AGLINT_COMMAND_PREFIX, AdblockSyntax, AdblockSyntaxError, AgentCommentRuleParser, AgentParser, AppListParser, COMMA_DOMAIN_LIST_SEPARATOR, CommentMarker, CommentRuleParser, CommentRuleType, ConfigCommentRuleParser, CosmeticRuleParser, CosmeticRuleSeparator, CosmeticRuleSeparatorUtils, CosmeticRuleType, CssTree, CssTreeNodeType, CssTreeParserContext, DomainListParser, DomainUtils, EXT_CSS_LEGACY_ATTRIBUTES, EXT_CSS_PSEUDO_CLASSES, FORBIDDEN_CSS_FUNCTIONS, FilterListConverter, FilterListParser, HINT_MARKER, HintCommentRuleParser, HintParser, IF, INCLUDE, LogicalExpressionParser, LogicalExpressionUtils, METADATA_HEADERS, MODIFIERS_SEPARATOR, MODIFIER_ASSIGN_OPERATOR, MetadataCommentRuleParser, MethodListParser, ModifierListParser, ModifierParser, NEGATION_MARKER, NETWORK_RULE_EXCEPTION_MARKER, NETWORK_RULE_SEPARATOR, NetworkRuleParser, NotImplementedError, PIPE_MODIFIER_SEPARATOR, PREPROCESSOR_MARKER, ParameterListParser, PreProcessorCommentRuleParser, QuoteType, QuoteUtils, RawFilterListConverter, RawRuleConverter, RegExpUtils, RuleCategory, RuleConversionError, RuleConverter, RuleParser, SAFARI_CB_AFFINITY, SPECIAL_REGEX_SYMBOLS, StealthOptionListParser, UBO_SCRIPTLET_MASK, locRange, modifierValidator, shiftLoc, version };
|