@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.cjs
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
|
|
@@ -88,6 +88,9 @@ exports.AdblockSyntax = void 0;
|
|
|
88
88
|
* @file Constant values used by all parts of the library
|
|
89
89
|
*/
|
|
90
90
|
// General
|
|
91
|
+
/**
|
|
92
|
+
* Empty string.
|
|
93
|
+
*/
|
|
91
94
|
const EMPTY = '';
|
|
92
95
|
const SPACE = ' ';
|
|
93
96
|
const TAB = '\t';
|
|
@@ -278,7 +281,7 @@ const NEGATION_MARKER = '~';
|
|
|
278
281
|
/**
|
|
279
282
|
* The wildcard symbol — `*`.
|
|
280
283
|
*/
|
|
281
|
-
const WILDCARD
|
|
284
|
+
const WILDCARD = ASTERISK;
|
|
282
285
|
/**
|
|
283
286
|
* Classic domain separator.
|
|
284
287
|
*
|
|
@@ -2877,7 +2880,7 @@ class ModifierParser {
|
|
|
2877
2880
|
const modifierEnd = Math.max(StringUtils.skipWSBack(raw) + 1, modifierNameStart);
|
|
2878
2881
|
// Modifier name can't be empty
|
|
2879
2882
|
if (modifierNameStart === modifierEnd) {
|
|
2880
|
-
throw new AdblockSyntaxError('Modifier name
|
|
2883
|
+
throw new AdblockSyntaxError('Modifier name cannot be empty', locRange(loc, 0, raw.length));
|
|
2881
2884
|
}
|
|
2882
2885
|
let modifier;
|
|
2883
2886
|
let value;
|
|
@@ -2901,7 +2904,7 @@ class ModifierParser {
|
|
|
2901
2904
|
};
|
|
2902
2905
|
// Value can't be empty
|
|
2903
2906
|
if (assignmentIndex + 1 === modifierEnd) {
|
|
2904
|
-
throw new AdblockSyntaxError('Modifier value
|
|
2907
|
+
throw new AdblockSyntaxError('Modifier value cannot be empty', locRange(loc, 0, raw.length));
|
|
2905
2908
|
}
|
|
2906
2909
|
// Skip whitespace after the assignment operator
|
|
2907
2910
|
const valueStart = StringUtils.skipWS(raw, assignmentIndex + MODIFIER_ASSIGN_OPERATOR.length);
|
|
@@ -3199,8 +3202,29 @@ const FORBIDDEN_CSS_FUNCTIONS = new Set([
|
|
|
3199
3202
|
'url',
|
|
3200
3203
|
]);
|
|
3201
3204
|
|
|
3205
|
+
/**
|
|
3206
|
+
* @file Clone related utilities
|
|
3207
|
+
*
|
|
3208
|
+
* We should keep clone related functions in this file. Thus, we just provide
|
|
3209
|
+
* a simple interface for cloning values, we use it across the AGTree project,
|
|
3210
|
+
* and the implementation "under the hood" can be improved later, if needed.
|
|
3211
|
+
*/
|
|
3212
|
+
/**
|
|
3213
|
+
* Clones an input value to avoid side effects. Use it only in justified cases,
|
|
3214
|
+
* because it can impact performance negatively.
|
|
3215
|
+
*
|
|
3216
|
+
* @param value Value to clone
|
|
3217
|
+
* @returns Cloned value
|
|
3218
|
+
*/
|
|
3219
|
+
function clone(value) {
|
|
3220
|
+
// TODO: Replace cloneDeep with a more efficient implementation
|
|
3221
|
+
return cloneDeep(value);
|
|
3222
|
+
}
|
|
3223
|
+
|
|
3202
3224
|
/**
|
|
3203
3225
|
* @file Additional / helper functions for ECSSTree / CSSTree.
|
|
3226
|
+
*
|
|
3227
|
+
* @note There are no tests for some functions, but during the AGTree optimization we remove them anyway.
|
|
3204
3228
|
*/
|
|
3205
3229
|
/**
|
|
3206
3230
|
* Common CSSTree parsing options.
|
|
@@ -3336,10 +3360,10 @@ class CssTree {
|
|
|
3336
3360
|
ast = CssTree.parse(selectorList, exports.CssTreeParserContext.selectorList);
|
|
3337
3361
|
}
|
|
3338
3362
|
else {
|
|
3339
|
-
ast =
|
|
3363
|
+
ast = clone(selectorList);
|
|
3340
3364
|
}
|
|
3341
3365
|
const nodes = [];
|
|
3342
|
-
// TODO:
|
|
3366
|
+
// TODO: Need to improve CSSTree types, until then we need to use any type here
|
|
3343
3367
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
3344
3368
|
ecssTree.walk(ast, (node) => {
|
|
3345
3369
|
if (CssTree.isExtendedCssNode(node, pseudoClasses, attributeSelectors)) {
|
|
@@ -3368,9 +3392,9 @@ class CssTree {
|
|
|
3368
3392
|
ast = CssTree.parse(selectorList, exports.CssTreeParserContext.selectorList);
|
|
3369
3393
|
}
|
|
3370
3394
|
else {
|
|
3371
|
-
ast =
|
|
3395
|
+
ast = selectorList;
|
|
3372
3396
|
}
|
|
3373
|
-
// TODO:
|
|
3397
|
+
// TODO: Need to improve CSSTree types, until then we need to use any type here
|
|
3374
3398
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
3375
3399
|
return ecssTree.find(ast, (node) => CssTree.isExtendedCssNode(node, pseudoClasses, attributeSelectors)) !== null;
|
|
3376
3400
|
}
|
|
@@ -3407,14 +3431,14 @@ class CssTree {
|
|
|
3407
3431
|
ast = CssTree.parse(declarationList, exports.CssTreeParserContext.declarationList);
|
|
3408
3432
|
}
|
|
3409
3433
|
else {
|
|
3410
|
-
ast =
|
|
3434
|
+
ast = clone(declarationList);
|
|
3411
3435
|
}
|
|
3412
3436
|
const nodes = [];
|
|
3413
3437
|
// While walking the AST we should skip the nested functions,
|
|
3414
3438
|
// for example skip url()s in cross-fade(url(), url()), since
|
|
3415
3439
|
// cross-fade() itself is already a forbidden function
|
|
3416
3440
|
let inForbiddenFunction = false;
|
|
3417
|
-
// TODO:
|
|
3441
|
+
// TODO: Need to improve CSSTree types, until then we need to use any type here
|
|
3418
3442
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
3419
3443
|
ecssTree.walk(ast, {
|
|
3420
3444
|
enter: (node) => {
|
|
@@ -3452,9 +3476,9 @@ class CssTree {
|
|
|
3452
3476
|
ast = CssTree.parse(declarationList, exports.CssTreeParserContext.declarationList);
|
|
3453
3477
|
}
|
|
3454
3478
|
else {
|
|
3455
|
-
ast =
|
|
3479
|
+
ast = clone(declarationList);
|
|
3456
3480
|
}
|
|
3457
|
-
// TODO:
|
|
3481
|
+
// TODO: Need to improve CSSTree types, until then we need to use any type here
|
|
3458
3482
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
3459
3483
|
return ecssTree.find(ast, (node) => CssTree.isForbiddenFunction(node, forbiddenFunctions)) !== null;
|
|
3460
3484
|
}
|
|
@@ -3686,6 +3710,180 @@ class CssTree {
|
|
|
3686
3710
|
});
|
|
3687
3711
|
return result.trim();
|
|
3688
3712
|
}
|
|
3713
|
+
/**
|
|
3714
|
+
* Generates string representation of the selector list.
|
|
3715
|
+
*
|
|
3716
|
+
* @param ast SelectorList AST
|
|
3717
|
+
* @returns String representation of the selector list
|
|
3718
|
+
*/
|
|
3719
|
+
static generateSelectorListPlain(ast) {
|
|
3720
|
+
const result = [];
|
|
3721
|
+
if (!ast.children || ast.children.length === 0) {
|
|
3722
|
+
throw new Error('Selector list cannot be empty');
|
|
3723
|
+
}
|
|
3724
|
+
ast.children.forEach((selector, index, nodeList) => {
|
|
3725
|
+
if (selector.type !== exports.CssTreeNodeType.Selector) {
|
|
3726
|
+
throw new Error(`Unexpected node type: ${selector.type}`);
|
|
3727
|
+
}
|
|
3728
|
+
result.push(this.generateSelectorPlain(selector));
|
|
3729
|
+
// If there is a next node, add a comma and a space after the selector
|
|
3730
|
+
if (nodeList[index + 1]) {
|
|
3731
|
+
result.push(COMMA, SPACE);
|
|
3732
|
+
}
|
|
3733
|
+
});
|
|
3734
|
+
return result.join(EMPTY);
|
|
3735
|
+
}
|
|
3736
|
+
/**
|
|
3737
|
+
* Selector generation based on CSSTree's AST. This is necessary because CSSTree
|
|
3738
|
+
* only adds spaces in some edge cases.
|
|
3739
|
+
*
|
|
3740
|
+
* @param ast CSS Tree AST
|
|
3741
|
+
* @returns CSS selector as string
|
|
3742
|
+
*/
|
|
3743
|
+
static generateSelectorPlain(ast) {
|
|
3744
|
+
let result = EMPTY;
|
|
3745
|
+
let inAttributeSelector = false;
|
|
3746
|
+
let depth = 0;
|
|
3747
|
+
let selectorListDepth = -1;
|
|
3748
|
+
let prevNode = ast;
|
|
3749
|
+
// TODO: Need to improve CSSTree types, until then we need to use any type here
|
|
3750
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
3751
|
+
ecssTree.walk(ast, {
|
|
3752
|
+
// TODO: Need to improve CSSTree types, until then we need to use any type here
|
|
3753
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
3754
|
+
enter: (node) => {
|
|
3755
|
+
depth += 1;
|
|
3756
|
+
// Skip attribute selector / selector list children
|
|
3757
|
+
if (inAttributeSelector || selectorListDepth > -1) {
|
|
3758
|
+
return;
|
|
3759
|
+
}
|
|
3760
|
+
switch (node.type) {
|
|
3761
|
+
// "Trivial" nodes
|
|
3762
|
+
case exports.CssTreeNodeType.TypeSelector:
|
|
3763
|
+
result += node.name;
|
|
3764
|
+
break;
|
|
3765
|
+
case exports.CssTreeNodeType.ClassSelector:
|
|
3766
|
+
result += DOT;
|
|
3767
|
+
result += node.name;
|
|
3768
|
+
break;
|
|
3769
|
+
case exports.CssTreeNodeType.IdSelector:
|
|
3770
|
+
result += HASHMARK;
|
|
3771
|
+
result += node.name;
|
|
3772
|
+
break;
|
|
3773
|
+
case exports.CssTreeNodeType.Identifier:
|
|
3774
|
+
result += node.name;
|
|
3775
|
+
break;
|
|
3776
|
+
case exports.CssTreeNodeType.Raw:
|
|
3777
|
+
result += node.value;
|
|
3778
|
+
break;
|
|
3779
|
+
// "Advanced" nodes
|
|
3780
|
+
case exports.CssTreeNodeType.Nth:
|
|
3781
|
+
// Default generation enough
|
|
3782
|
+
result += ecssTree.generate(node);
|
|
3783
|
+
break;
|
|
3784
|
+
// For example :not([id], [name])
|
|
3785
|
+
case exports.CssTreeNodeType.SelectorList:
|
|
3786
|
+
// eslint-disable-next-line no-case-declarations
|
|
3787
|
+
const selectors = [];
|
|
3788
|
+
node.children.forEach((selector) => {
|
|
3789
|
+
if (selector.type === exports.CssTreeNodeType.Selector) {
|
|
3790
|
+
selectors.push(CssTree.generateSelectorPlain(selector));
|
|
3791
|
+
}
|
|
3792
|
+
else if (selector.type === exports.CssTreeNodeType.Raw) {
|
|
3793
|
+
selectors.push(selector.value);
|
|
3794
|
+
}
|
|
3795
|
+
});
|
|
3796
|
+
// Join selector lists
|
|
3797
|
+
result += selectors.join(COMMA + SPACE);
|
|
3798
|
+
// Skip nodes here
|
|
3799
|
+
selectorListDepth = depth;
|
|
3800
|
+
break;
|
|
3801
|
+
case exports.CssTreeNodeType.Combinator:
|
|
3802
|
+
if (node.name === SPACE) {
|
|
3803
|
+
result += node.name;
|
|
3804
|
+
break;
|
|
3805
|
+
}
|
|
3806
|
+
// Prevent this case (unnecessary space): has( > .something)
|
|
3807
|
+
if (prevNode.type !== exports.CssTreeNodeType.Selector) {
|
|
3808
|
+
result += SPACE;
|
|
3809
|
+
}
|
|
3810
|
+
result += node.name;
|
|
3811
|
+
result += SPACE;
|
|
3812
|
+
break;
|
|
3813
|
+
case exports.CssTreeNodeType.AttributeSelector:
|
|
3814
|
+
result += OPEN_SQUARE_BRACKET;
|
|
3815
|
+
// Identifier name
|
|
3816
|
+
if (node.name) {
|
|
3817
|
+
result += node.name.name;
|
|
3818
|
+
}
|
|
3819
|
+
// Matcher operator, eg =
|
|
3820
|
+
if (node.matcher) {
|
|
3821
|
+
result += node.matcher;
|
|
3822
|
+
// Value can be String, Identifier or null
|
|
3823
|
+
if (node.value !== null) {
|
|
3824
|
+
// String node
|
|
3825
|
+
if (node.value.type === exports.CssTreeNodeType.String) {
|
|
3826
|
+
result += ecssTree.generate(node.value);
|
|
3827
|
+
}
|
|
3828
|
+
else if (node.value.type === exports.CssTreeNodeType.Identifier) {
|
|
3829
|
+
// Identifier node
|
|
3830
|
+
result += node.value.name;
|
|
3831
|
+
}
|
|
3832
|
+
}
|
|
3833
|
+
}
|
|
3834
|
+
// Flags
|
|
3835
|
+
if (node.flags) {
|
|
3836
|
+
// Space before flags
|
|
3837
|
+
result += SPACE;
|
|
3838
|
+
result += node.flags;
|
|
3839
|
+
}
|
|
3840
|
+
result += CLOSE_SQUARE_BRACKET;
|
|
3841
|
+
inAttributeSelector = true;
|
|
3842
|
+
break;
|
|
3843
|
+
case exports.CssTreeNodeType.PseudoElementSelector:
|
|
3844
|
+
result += COLON;
|
|
3845
|
+
result += COLON;
|
|
3846
|
+
result += node.name;
|
|
3847
|
+
if (node.children !== null) {
|
|
3848
|
+
result += OPEN_PARENTHESIS;
|
|
3849
|
+
}
|
|
3850
|
+
break;
|
|
3851
|
+
case exports.CssTreeNodeType.PseudoClassSelector:
|
|
3852
|
+
result += COLON;
|
|
3853
|
+
result += node.name;
|
|
3854
|
+
if (node.children !== null) {
|
|
3855
|
+
result += OPEN_PARENTHESIS;
|
|
3856
|
+
}
|
|
3857
|
+
break;
|
|
3858
|
+
}
|
|
3859
|
+
prevNode = node;
|
|
3860
|
+
},
|
|
3861
|
+
leave: (node) => {
|
|
3862
|
+
depth -= 1;
|
|
3863
|
+
if (node.type === exports.CssTreeNodeType.SelectorList && depth + 1 === selectorListDepth) {
|
|
3864
|
+
selectorListDepth = -1;
|
|
3865
|
+
}
|
|
3866
|
+
if (selectorListDepth > -1) {
|
|
3867
|
+
return;
|
|
3868
|
+
}
|
|
3869
|
+
if (node.type === exports.CssTreeNodeType.AttributeSelector) {
|
|
3870
|
+
inAttributeSelector = false;
|
|
3871
|
+
}
|
|
3872
|
+
if (inAttributeSelector) {
|
|
3873
|
+
return;
|
|
3874
|
+
}
|
|
3875
|
+
switch (node.type) {
|
|
3876
|
+
case exports.CssTreeNodeType.PseudoElementSelector:
|
|
3877
|
+
case exports.CssTreeNodeType.PseudoClassSelector:
|
|
3878
|
+
if (node.children) {
|
|
3879
|
+
result += CLOSE_PARENTHESIS;
|
|
3880
|
+
}
|
|
3881
|
+
break;
|
|
3882
|
+
}
|
|
3883
|
+
},
|
|
3884
|
+
});
|
|
3885
|
+
return result.trim();
|
|
3886
|
+
}
|
|
3689
3887
|
/**
|
|
3690
3888
|
* Block generation based on CSSTree's AST. This is necessary because CSSTree only adds spaces in some edge cases.
|
|
3691
3889
|
*
|
|
@@ -3869,6 +4067,29 @@ class CssTree {
|
|
|
3869
4067
|
});
|
|
3870
4068
|
return result;
|
|
3871
4069
|
}
|
|
4070
|
+
/**
|
|
4071
|
+
* Helper function to generate a raw string from a function selector's children
|
|
4072
|
+
*
|
|
4073
|
+
* @param node Function node
|
|
4074
|
+
* @returns Generated function value
|
|
4075
|
+
* @example `responseheader(name)` -> `name`
|
|
4076
|
+
*/
|
|
4077
|
+
static generateFunctionPlainValue(node) {
|
|
4078
|
+
const result = [];
|
|
4079
|
+
node.children?.forEach((child) => {
|
|
4080
|
+
switch (child.type) {
|
|
4081
|
+
case exports.CssTreeNodeType.Raw:
|
|
4082
|
+
result.push(child.value);
|
|
4083
|
+
break;
|
|
4084
|
+
default:
|
|
4085
|
+
// Fallback to CSSTree's default generate function
|
|
4086
|
+
// TODO: Need to improve CSSTree types, until then we need to use any type here
|
|
4087
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
4088
|
+
result.push(ecssTree.generate(child));
|
|
4089
|
+
}
|
|
4090
|
+
});
|
|
4091
|
+
return result.join(EMPTY);
|
|
4092
|
+
}
|
|
3872
4093
|
}
|
|
3873
4094
|
|
|
3874
4095
|
/**
|
|
@@ -3916,7 +4137,7 @@ class ElementHidingBodyParser {
|
|
|
3916
4137
|
* @throws If the AST is invalid
|
|
3917
4138
|
*/
|
|
3918
4139
|
static generate(ast) {
|
|
3919
|
-
return CssTree.
|
|
4140
|
+
return CssTree.generateSelectorListPlain(ast.selectorList);
|
|
3920
4141
|
}
|
|
3921
4142
|
}
|
|
3922
4143
|
|
|
@@ -4160,7 +4381,7 @@ class CssInjectionBodyParser {
|
|
|
4160
4381
|
if (mediaQueryList || declarationList || remove) {
|
|
4161
4382
|
throw new AdblockSyntaxError(
|
|
4162
4383
|
// eslint-disable-next-line max-len
|
|
4163
|
-
'Invalid selector, regular selector elements
|
|
4384
|
+
'Invalid selector, regular selector elements cannot be used after special pseudo-classes', {
|
|
4164
4385
|
start: node.loc?.start ?? loc,
|
|
4165
4386
|
end: shiftLoc(loc, raw.length),
|
|
4166
4387
|
});
|
|
@@ -4849,7 +5070,7 @@ function createModifierListNode(modifiers = []) {
|
|
|
4849
5070
|
const result = {
|
|
4850
5071
|
type: 'ModifierList',
|
|
4851
5072
|
// We need to clone the modifiers to avoid side effects
|
|
4852
|
-
children:
|
|
5073
|
+
children: modifiers.length ? clone(modifiers) : [],
|
|
4853
5074
|
};
|
|
4854
5075
|
return result;
|
|
4855
5076
|
}
|
|
@@ -4889,8 +5110,9 @@ function hasUboModifierIndicator(rawSelectorList) {
|
|
|
4889
5110
|
* @returns Linked list based selector
|
|
4890
5111
|
*/
|
|
4891
5112
|
function convertSelectorToLinkedList(selector) {
|
|
5113
|
+
// TODO: Need to improve CSSTree types, until then we need to use any type here
|
|
4892
5114
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
4893
|
-
return ecssTree.fromPlainObject(
|
|
5115
|
+
return ecssTree.fromPlainObject(clone(selector));
|
|
4894
5116
|
}
|
|
4895
5117
|
/**
|
|
4896
5118
|
* Helper function that always returns the linked list version of the
|
|
@@ -4900,8 +5122,9 @@ function convertSelectorToLinkedList(selector) {
|
|
|
4900
5122
|
* @returns Linked list based selector list
|
|
4901
5123
|
*/
|
|
4902
5124
|
function convertSelectorListToLinkedList(selectorList) {
|
|
5125
|
+
// TODO: Need to improve CSSTree types, until then we need to use any type here
|
|
4903
5126
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
4904
|
-
return ecssTree.fromPlainObject(
|
|
5127
|
+
return ecssTree.fromPlainObject(clone(selectorList));
|
|
4905
5128
|
}
|
|
4906
5129
|
/**
|
|
4907
5130
|
* Helper function for checking and removing bounding combinators
|
|
@@ -5986,7 +6209,8 @@ class FilterListParser {
|
|
|
5986
6209
|
*/
|
|
5987
6210
|
static generate(ast, preferRaw = false) {
|
|
5988
6211
|
let result = EMPTY;
|
|
5989
|
-
for (
|
|
6212
|
+
for (let i = 0; i < ast.children.length; i += 1) {
|
|
6213
|
+
const rule = ast.children[i];
|
|
5990
6214
|
if (preferRaw && rule.raws?.text) {
|
|
5991
6215
|
result += rule.raws.text;
|
|
5992
6216
|
}
|
|
@@ -6003,6 +6227,11 @@ class FilterListParser {
|
|
|
6003
6227
|
case 'lf':
|
|
6004
6228
|
result += LF;
|
|
6005
6229
|
break;
|
|
6230
|
+
default:
|
|
6231
|
+
if (i !== ast.children.length - 1) {
|
|
6232
|
+
result += LF;
|
|
6233
|
+
}
|
|
6234
|
+
break;
|
|
6006
6235
|
}
|
|
6007
6236
|
}
|
|
6008
6237
|
return result;
|
|
@@ -6144,7 +6373,7 @@ var data$N = { adg_os_any:{ name:"csp",
|
|
|
6144
6373
|
assignable:true,
|
|
6145
6374
|
negatable:false,
|
|
6146
6375
|
value_optional:true,
|
|
6147
|
-
value_format:"
|
|
6376
|
+
value_format:"csp_value" },
|
|
6148
6377
|
adg_ext_any:{ name:"csp",
|
|
6149
6378
|
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.",
|
|
6150
6379
|
docs:"https://adguard.app/kb/general/ad-filtering/create-own-filters/#csp-modifier",
|
|
@@ -6156,7 +6385,7 @@ var data$N = { adg_os_any:{ name:"csp",
|
|
|
6156
6385
|
assignable:true,
|
|
6157
6386
|
negatable:false,
|
|
6158
6387
|
value_optional:true,
|
|
6159
|
-
value_format:"
|
|
6388
|
+
value_format:"csp_value" },
|
|
6160
6389
|
abp_ext_any:{ name:"csp",
|
|
6161
6390
|
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.",
|
|
6162
6391
|
docs:"https://help.adblockplus.org/hc/en-us/articles/360062733293-How-to-write-filters#content-security-policies",
|
|
@@ -6166,7 +6395,7 @@ var data$N = { adg_os_any:{ name:"csp",
|
|
|
6166
6395
|
assignable:true,
|
|
6167
6396
|
negatable:false,
|
|
6168
6397
|
value_optional:true,
|
|
6169
|
-
value_format:"
|
|
6398
|
+
value_format:"csp_value" },
|
|
6170
6399
|
ubo_ext_any:{ name:"csp",
|
|
6171
6400
|
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.",
|
|
6172
6401
|
docs:"https://github.com/gorhill/uBlock/wiki/Static-filter-syntax#csp",
|
|
@@ -6178,7 +6407,7 @@ var data$N = { adg_os_any:{ name:"csp",
|
|
|
6178
6407
|
assignable:true,
|
|
6179
6408
|
negatable:false,
|
|
6180
6409
|
value_optional:true,
|
|
6181
|
-
value_format:"
|
|
6410
|
+
value_format:"csp_value" } };
|
|
6182
6411
|
|
|
6183
6412
|
var data$M = { adg_os_any:{ name:"denyallow",
|
|
6184
6413
|
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.",
|
|
@@ -6685,7 +6914,8 @@ var data$l = { adg_os_any:{ name:"permissions",
|
|
|
6685
6914
|
inverse_conflicts:true,
|
|
6686
6915
|
assignable:true,
|
|
6687
6916
|
negatable:false,
|
|
6688
|
-
|
|
6917
|
+
value_optional:true,
|
|
6918
|
+
value_format:"permissions_value" } };
|
|
6689
6919
|
|
|
6690
6920
|
var data$k = { adg_any:{ name:"ping",
|
|
6691
6921
|
description:"The rule corresponds to requests caused by either navigator.sendBeacon() or the ping attribute on links.",
|
|
@@ -7346,15 +7576,104 @@ const ALLOWED_STEALTH_OPTIONS = new Set([
|
|
|
7346
7576
|
'xclientdata',
|
|
7347
7577
|
'dpi',
|
|
7348
7578
|
]);
|
|
7579
|
+
/**
|
|
7580
|
+
* Allowed CSP directives for $csp modifier.
|
|
7581
|
+
*
|
|
7582
|
+
* @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy#directives}
|
|
7583
|
+
*/
|
|
7584
|
+
const ALLOWED_CSP_DIRECTIVES = new Set([
|
|
7585
|
+
'base-uri',
|
|
7586
|
+
'child-src',
|
|
7587
|
+
'connect-src',
|
|
7588
|
+
'default-src',
|
|
7589
|
+
'font-src',
|
|
7590
|
+
'form-action',
|
|
7591
|
+
'frame-ancestors',
|
|
7592
|
+
'frame-src',
|
|
7593
|
+
'img-src',
|
|
7594
|
+
'manifest-src',
|
|
7595
|
+
'media-src',
|
|
7596
|
+
'navigate-to',
|
|
7597
|
+
'object-src',
|
|
7598
|
+
'plugin-types',
|
|
7599
|
+
'prefetch-src',
|
|
7600
|
+
'report-to',
|
|
7601
|
+
'report-uri',
|
|
7602
|
+
'sandbox',
|
|
7603
|
+
'script-src',
|
|
7604
|
+
'style-src',
|
|
7605
|
+
'upgrade-insecure-requests',
|
|
7606
|
+
'worker-src',
|
|
7607
|
+
]);
|
|
7608
|
+
/**
|
|
7609
|
+
* Allowed stealth options for $permissions modifier.
|
|
7610
|
+
*
|
|
7611
|
+
* @see {@link https://adguard.app/kb/general/ad-filtering/create-own-filters/#permissions-modifier}
|
|
7612
|
+
*/
|
|
7613
|
+
const ALLOWED_PERMISSION_DIRECTIVES = new Set([
|
|
7614
|
+
'accelerometer',
|
|
7615
|
+
'ambient-light-sensor',
|
|
7616
|
+
'autoplay',
|
|
7617
|
+
'battery',
|
|
7618
|
+
'camera',
|
|
7619
|
+
'display-capture',
|
|
7620
|
+
'document-domain',
|
|
7621
|
+
'encrypted-media',
|
|
7622
|
+
'execution-while-not-rendered',
|
|
7623
|
+
'execution-while-out-of-viewport',
|
|
7624
|
+
'fullscreen',
|
|
7625
|
+
'gamepad',
|
|
7626
|
+
'geolocation',
|
|
7627
|
+
'gyroscope',
|
|
7628
|
+
'hid',
|
|
7629
|
+
'identity-credentials-get',
|
|
7630
|
+
'idle-detection',
|
|
7631
|
+
'local-fonts',
|
|
7632
|
+
'magnetometer',
|
|
7633
|
+
'microphone',
|
|
7634
|
+
'midi',
|
|
7635
|
+
'payment',
|
|
7636
|
+
'picture-in-picture',
|
|
7637
|
+
'publickey-credentials-create',
|
|
7638
|
+
'publickey-credentials-get',
|
|
7639
|
+
'screen-wake-lock',
|
|
7640
|
+
'serial',
|
|
7641
|
+
'speaker-selection',
|
|
7642
|
+
'storage-access',
|
|
7643
|
+
'usb',
|
|
7644
|
+
'web-share',
|
|
7645
|
+
'xr-spatial-tracking',
|
|
7646
|
+
]);
|
|
7647
|
+
/**
|
|
7648
|
+
* One of available tokens for $permission modifier value.
|
|
7649
|
+
*
|
|
7650
|
+
* @see {@link https://w3c.github.io/webappsec-permissions-policy/#structured-header-serialization}
|
|
7651
|
+
*/
|
|
7652
|
+
const PERMISSIONS_TOKEN_SELF = 'self';
|
|
7653
|
+
/**
|
|
7654
|
+
* One of allowlist values for $permissions modifier.
|
|
7655
|
+
*
|
|
7656
|
+
* @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Permissions_Policy#allowlists}
|
|
7657
|
+
*/
|
|
7658
|
+
const EMPTY_PERMISSIONS_ALLOWLIST = `${OPEN_PARENTHESIS}${CLOSE_PARENTHESIS}`;
|
|
7349
7659
|
/**
|
|
7350
7660
|
* Prefixes for error messages used in modifier validation.
|
|
7351
7661
|
*/
|
|
7352
7662
|
const VALIDATION_ERROR_PREFIX = {
|
|
7353
7663
|
BLOCK_ONLY: 'Only blocking rules may contain the modifier',
|
|
7354
7664
|
EXCEPTION_ONLY: 'Only exception rules may contain the modifier',
|
|
7665
|
+
INVALID_CSP_DIRECTIVES: 'Invalid CSP directives for the modifier',
|
|
7355
7666
|
INVALID_LIST_VALUES: 'Invalid values for the modifier',
|
|
7356
7667
|
INVALID_NOOP: 'Invalid noop modifier',
|
|
7668
|
+
INVALID_PERMISSION_DIRECTIVE: 'Invalid Permissions-Policy directive for the modifier',
|
|
7669
|
+
INVALID_PERMISSION_ORIGINS: 'Origins in the value is invalid for the modifier and the directive',
|
|
7670
|
+
INVALID_PERMISSION_ORIGIN_QUOTES: 'Double quotes should be used for origins in the value of the modifier',
|
|
7357
7671
|
MIXED_NEGATIONS: 'Simultaneous usage of negated and not negated values is forbidden for the modifier',
|
|
7672
|
+
NO_CSP_VALUE: 'No CSP value for the modifier and the directive',
|
|
7673
|
+
NO_CSP_DIRECTIVE_QUOTE: 'CSP directives should no be quoted for the modifier',
|
|
7674
|
+
NO_UNESCAPED_PERMISSION_COMMA: 'Unescaped comma in the value is not allowed for the modifier',
|
|
7675
|
+
// TODO: implement later for $scp and $permissions
|
|
7676
|
+
// NO_VALUE_ONLY_FOR_EXCEPTION: 'Modifier without value can be used only in exception rules',
|
|
7358
7677
|
NOT_EXISTENT: 'Non-existent modifier',
|
|
7359
7678
|
NOT_NEGATABLE_MODIFIER: 'Non-negatable modifier',
|
|
7360
7679
|
NOT_NEGATABLE_VALUE: 'Values cannot be negated for the modifier',
|
|
@@ -7467,14 +7786,14 @@ const getSpecificBlockerData = (modifiersData, blockerPrefix, modifierName) => {
|
|
|
7467
7786
|
* @example
|
|
7468
7787
|
* `example.*` — matches with any TLD, e.g. `example.org`, `example.com`, etc.
|
|
7469
7788
|
*/
|
|
7470
|
-
const WILDCARD_TLD = DOT + WILDCARD
|
|
7789
|
+
const WILDCARD_TLD = DOT + WILDCARD;
|
|
7471
7790
|
/**
|
|
7472
7791
|
* Marker for a wildcard subdomain — `*.`.
|
|
7473
7792
|
*
|
|
7474
7793
|
* @example
|
|
7475
7794
|
* `*.example.org` — matches with any subdomain, e.g. `foo.example.org` or `bar.example.org`
|
|
7476
7795
|
*/
|
|
7477
|
-
const WILDCARD_SUBDOMAIN = WILDCARD
|
|
7796
|
+
const WILDCARD_SUBDOMAIN = WILDCARD + DOT;
|
|
7478
7797
|
class DomainUtils {
|
|
7479
7798
|
/**
|
|
7480
7799
|
* Check if the input is a valid domain or hostname.
|
|
@@ -7485,7 +7804,7 @@ class DomainUtils {
|
|
|
7485
7804
|
static isValidDomainOrHostname(domain) {
|
|
7486
7805
|
let domainToCheck = domain;
|
|
7487
7806
|
// Wildcard-only domain, typically a generic rule
|
|
7488
|
-
if (domainToCheck === WILDCARD
|
|
7807
|
+
if (domainToCheck === WILDCARD) {
|
|
7489
7808
|
return true;
|
|
7490
7809
|
}
|
|
7491
7810
|
// https://adguard.com/kb/general/ad-filtering/create-own-filters/#wildcard-for-tld
|
|
@@ -7666,10 +7985,12 @@ class QuoteUtils {
|
|
|
7666
7985
|
var CustomValueFormatValidatorName;
|
|
7667
7986
|
(function (CustomValueFormatValidatorName) {
|
|
7668
7987
|
CustomValueFormatValidatorName["App"] = "pipe_separated_apps";
|
|
7988
|
+
CustomValueFormatValidatorName["Csp"] = "csp_value";
|
|
7669
7989
|
// there are some differences between $domain and $denyallow
|
|
7670
7990
|
CustomValueFormatValidatorName["DenyAllow"] = "pipe_separated_denyallow_domains";
|
|
7671
7991
|
CustomValueFormatValidatorName["Domain"] = "pipe_separated_domains";
|
|
7672
7992
|
CustomValueFormatValidatorName["Method"] = "pipe_separated_methods";
|
|
7993
|
+
CustomValueFormatValidatorName["Permissions"] = "permissions_value";
|
|
7673
7994
|
CustomValueFormatValidatorName["StealthOption"] = "pipe_separated_stealth_options";
|
|
7674
7995
|
})(CustomValueFormatValidatorName || (CustomValueFormatValidatorName = {}));
|
|
7675
7996
|
/**
|
|
@@ -7703,7 +8024,7 @@ const isValidAppNameChunk = (chunk) => {
|
|
|
7703
8024
|
const isValidAppModifierValue = (value) => {
|
|
7704
8025
|
// $app modifier does not support wildcard tld
|
|
7705
8026
|
// https://adguard.app/kb/general/ad-filtering/create-own-filters/#app-modifier
|
|
7706
|
-
if (value.includes(WILDCARD
|
|
8027
|
+
if (value.includes(WILDCARD)) {
|
|
7707
8028
|
return false;
|
|
7708
8029
|
}
|
|
7709
8030
|
return value
|
|
@@ -7730,6 +8051,32 @@ const isValidMethodModifierValue = (value) => {
|
|
|
7730
8051
|
const isValidStealthModifierValue = (value) => {
|
|
7731
8052
|
return ALLOWED_STEALTH_OPTIONS.has(value);
|
|
7732
8053
|
};
|
|
8054
|
+
/**
|
|
8055
|
+
* Checks whether the given `rawOrigin` is valid as Permissions Allowlist origin.
|
|
8056
|
+
*
|
|
8057
|
+
* @see {@link https://w3c.github.io/webappsec-permissions-policy/#allowlists}
|
|
8058
|
+
*
|
|
8059
|
+
* @param rawOrigin The raw origin.
|
|
8060
|
+
*
|
|
8061
|
+
* @returns True if the origin is valid, false otherwise.
|
|
8062
|
+
*/
|
|
8063
|
+
const isValidPermissionsOrigin = (rawOrigin) => {
|
|
8064
|
+
// origins should be quoted by double quote
|
|
8065
|
+
const actualQuoteType = QuoteUtils.getStringQuoteType(rawOrigin);
|
|
8066
|
+
if (actualQuoteType !== exports.QuoteType.Double) {
|
|
8067
|
+
return false;
|
|
8068
|
+
}
|
|
8069
|
+
const origin = QuoteUtils.removeQuotes(rawOrigin);
|
|
8070
|
+
try {
|
|
8071
|
+
// validate the origin by URL constructor
|
|
8072
|
+
// https://w3c.github.io/webappsec-permissions-policy/#algo-parse-policy-directive
|
|
8073
|
+
new URL(origin);
|
|
8074
|
+
}
|
|
8075
|
+
catch (e) {
|
|
8076
|
+
return false;
|
|
8077
|
+
}
|
|
8078
|
+
return true;
|
|
8079
|
+
};
|
|
7733
8080
|
/**
|
|
7734
8081
|
* Checks whether the given `value` is valid domain as $denyallow modifier value.
|
|
7735
8082
|
* Important: wildcard tld are not supported, compared to $domain.
|
|
@@ -7742,7 +8089,7 @@ const isValidDenyAllowModifierValue = (value) => {
|
|
|
7742
8089
|
// $denyallow modifier does not support wildcard tld
|
|
7743
8090
|
// https://adguard.app/kb/general/ad-filtering/create-own-filters/#denyallow-modifier
|
|
7744
8091
|
// but here we are simply checking whether the value contains wildcard `*`, not ends with `.*`
|
|
7745
|
-
if (value.includes(WILDCARD
|
|
8092
|
+
if (value.includes(WILDCARD)) {
|
|
7746
8093
|
return false;
|
|
7747
8094
|
}
|
|
7748
8095
|
// TODO: add cache for domains validation
|
|
@@ -7921,59 +8268,241 @@ const validatePipeSeparatedStealthOptions = (modifier) => {
|
|
|
7921
8268
|
return validateListItemsModifier(modifier, (raw) => StealthOptionListParser.parse(raw), isValidStealthModifierValue, customNoNegatedListItemsValidator);
|
|
7922
8269
|
};
|
|
7923
8270
|
/**
|
|
7924
|
-
*
|
|
7925
|
-
|
|
7926
|
-
const CUSTOM_VALUE_FORMAT_MAP = {
|
|
7927
|
-
[CustomValueFormatValidatorName.App]: validatePipeSeparatedApps,
|
|
7928
|
-
[CustomValueFormatValidatorName.DenyAllow]: validatePipeSeparatedDenyAllowDomains,
|
|
7929
|
-
[CustomValueFormatValidatorName.Domain]: validatePipeSeparatedDomains,
|
|
7930
|
-
[CustomValueFormatValidatorName.Method]: validatePipeSeparatedMethods,
|
|
7931
|
-
[CustomValueFormatValidatorName.StealthOption]: validatePipeSeparatedStealthOptions,
|
|
7932
|
-
};
|
|
7933
|
-
/**
|
|
7934
|
-
* Returns whether the given `valueFormat` is a valid custom value format validator name.
|
|
7935
|
-
*
|
|
7936
|
-
* @param valueFormat Value format for the modifier.
|
|
7937
|
-
*
|
|
7938
|
-
* @returns True if `valueFormat` is a supported pre-defined value format validator name, false otherwise.
|
|
7939
|
-
*/
|
|
7940
|
-
const isCustomValueFormatValidator = (valueFormat) => {
|
|
7941
|
-
return Object.keys(CUSTOM_VALUE_FORMAT_MAP).includes(valueFormat);
|
|
7942
|
-
};
|
|
7943
|
-
/**
|
|
7944
|
-
* Checks whether the value for given `modifier` is valid.
|
|
8271
|
+
* Validates `csp_value` custom value format.
|
|
8272
|
+
* Used for $csp modifier.
|
|
7945
8273
|
*
|
|
7946
8274
|
* @param modifier Modifier AST node.
|
|
7947
|
-
* @param valueFormat Value format for the modifier.
|
|
7948
8275
|
*
|
|
7949
8276
|
* @returns Validation result.
|
|
7950
8277
|
*/
|
|
7951
|
-
const
|
|
7952
|
-
if (isCustomValueFormatValidator(valueFormat)) {
|
|
7953
|
-
const validator = CUSTOM_VALUE_FORMAT_MAP[valueFormat];
|
|
7954
|
-
return validator(modifier);
|
|
7955
|
-
}
|
|
8278
|
+
const validateCspValue = (modifier) => {
|
|
7956
8279
|
const modifierName = modifier.modifier.value;
|
|
7957
8280
|
if (!modifier.value?.value) {
|
|
7958
8281
|
return getValueRequiredValidationResult(modifierName);
|
|
7959
8282
|
}
|
|
7960
|
-
|
|
7961
|
-
|
|
7962
|
-
|
|
7963
|
-
|
|
7964
|
-
|
|
7965
|
-
|
|
7966
|
-
|
|
7967
|
-
|
|
7968
|
-
|
|
7969
|
-
|
|
8283
|
+
// $csp modifier value may contain multiple directives
|
|
8284
|
+
// e.g. "csp=child-src 'none'; frame-src 'self' *; worker-src 'none'"
|
|
8285
|
+
const policyDirectives = modifier.value.value
|
|
8286
|
+
.split(SEMICOLON)
|
|
8287
|
+
// rule with $csp modifier may end with semicolon
|
|
8288
|
+
// e.g. "$csp=sandbox allow-same-origin;"
|
|
8289
|
+
// TODO: add predicate helper for `(i) => !!i`
|
|
8290
|
+
.filter((i) => !!i);
|
|
8291
|
+
const invalidValueValidationResult = getInvalidValidationResult(`${VALIDATION_ERROR_PREFIX.VALUE_INVALID}: '${modifierName}': "${modifier.value.value}"`);
|
|
8292
|
+
if (policyDirectives.length === 0) {
|
|
8293
|
+
return invalidValueValidationResult;
|
|
8294
|
+
}
|
|
8295
|
+
const invalidDirectives = [];
|
|
8296
|
+
for (let i = 0; i < policyDirectives.length; i += 1) {
|
|
8297
|
+
const policyDirective = policyDirectives[i].trim();
|
|
8298
|
+
if (!policyDirective) {
|
|
8299
|
+
return invalidValueValidationResult;
|
|
8300
|
+
}
|
|
8301
|
+
const chunks = policyDirective.split(SPACE);
|
|
8302
|
+
const [directive, ...valueChunks] = chunks;
|
|
8303
|
+
// e.g. "csp=child-src 'none'; ; worker-src 'none'"
|
|
8304
|
+
// validator it here ↑
|
|
8305
|
+
if (!directive) {
|
|
8306
|
+
return invalidValueValidationResult;
|
|
8307
|
+
}
|
|
8308
|
+
if (!ALLOWED_CSP_DIRECTIVES.has(directive)) {
|
|
8309
|
+
// e.g. "csp='child-src' 'none'"
|
|
8310
|
+
if (ALLOWED_CSP_DIRECTIVES.has(QuoteUtils.removeQuotes(directive))) {
|
|
8311
|
+
return getInvalidValidationResult(`${VALIDATION_ERROR_PREFIX.NO_CSP_DIRECTIVE_QUOTE}: '${modifierName}': ${directive}`);
|
|
8312
|
+
}
|
|
8313
|
+
invalidDirectives.push(directive);
|
|
8314
|
+
continue;
|
|
8315
|
+
}
|
|
8316
|
+
if (valueChunks.length === 0) {
|
|
8317
|
+
return getInvalidValidationResult(`${VALIDATION_ERROR_PREFIX.NO_CSP_VALUE}: '${modifierName}': '${directive}'`);
|
|
8318
|
+
}
|
|
8319
|
+
}
|
|
8320
|
+
if (invalidDirectives.length > 0) {
|
|
8321
|
+
const directivesToStr = QuoteUtils.quoteAndJoinStrings(invalidDirectives, exports.QuoteType.Double);
|
|
8322
|
+
return getInvalidValidationResult(`${VALIDATION_ERROR_PREFIX.INVALID_CSP_DIRECTIVES}: '${modifierName}': ${directivesToStr}`);
|
|
7970
8323
|
}
|
|
7971
8324
|
return { valid: true };
|
|
7972
8325
|
};
|
|
7973
|
-
|
|
7974
8326
|
/**
|
|
7975
|
-
*
|
|
7976
|
-
|
|
8327
|
+
* Validates permission allowlist origins in the value of $permissions modifier.
|
|
8328
|
+
*
|
|
8329
|
+
* @see {@link https://w3c.github.io/webappsec-permissions-policy/#allowlists}
|
|
8330
|
+
*
|
|
8331
|
+
* @param allowlistChunks Array of allowlist chunks.
|
|
8332
|
+
* @param directive Permission directive name.
|
|
8333
|
+
* @param modifierName Modifier name.
|
|
8334
|
+
*
|
|
8335
|
+
* @returns Validation result.
|
|
8336
|
+
*/
|
|
8337
|
+
const validatePermissionAllowlistOrigins = (allowlistChunks, directive, modifierName) => {
|
|
8338
|
+
const invalidOrigins = [];
|
|
8339
|
+
for (let i = 0; i < allowlistChunks.length; i += 1) {
|
|
8340
|
+
const chunk = allowlistChunks[i].trim();
|
|
8341
|
+
// skip few spaces between origins (they were splitted by space)
|
|
8342
|
+
// e.g. 'geolocation=("https://example.com" "https://*.example.com")'
|
|
8343
|
+
if (chunk.length === 0) {
|
|
8344
|
+
continue;
|
|
8345
|
+
}
|
|
8346
|
+
/**
|
|
8347
|
+
* 'self' should be checked case-insensitively
|
|
8348
|
+
*
|
|
8349
|
+
* @see {@link https://w3c.github.io/webappsec-permissions-policy/#algo-parse-policy-directive}
|
|
8350
|
+
*
|
|
8351
|
+
* @example 'geolocation=(self)'
|
|
8352
|
+
*/
|
|
8353
|
+
if (chunk.toLowerCase() === PERMISSIONS_TOKEN_SELF) {
|
|
8354
|
+
continue;
|
|
8355
|
+
}
|
|
8356
|
+
if (QuoteUtils.getStringQuoteType(chunk) !== exports.QuoteType.Double) {
|
|
8357
|
+
return getInvalidValidationResult(
|
|
8358
|
+
// eslint-disable-next-line max-len
|
|
8359
|
+
`${VALIDATION_ERROR_PREFIX.INVALID_PERMISSION_ORIGIN_QUOTES}: '${modifierName}': '${directive}': '${QuoteUtils.removeQuotes(chunk)}'`);
|
|
8360
|
+
}
|
|
8361
|
+
if (!isValidPermissionsOrigin(chunk)) {
|
|
8362
|
+
invalidOrigins.push(chunk);
|
|
8363
|
+
}
|
|
8364
|
+
}
|
|
8365
|
+
if (invalidOrigins.length > 0) {
|
|
8366
|
+
const originsToStr = QuoteUtils.quoteAndJoinStrings(invalidOrigins);
|
|
8367
|
+
return getInvalidValidationResult(
|
|
8368
|
+
// eslint-disable-next-line max-len
|
|
8369
|
+
`${VALIDATION_ERROR_PREFIX.INVALID_PERMISSION_ORIGINS}: '${modifierName}': '${directive}': ${originsToStr}`);
|
|
8370
|
+
}
|
|
8371
|
+
return { valid: true };
|
|
8372
|
+
};
|
|
8373
|
+
/**
|
|
8374
|
+
* Validates permission allowlist in the modifier value.
|
|
8375
|
+
*
|
|
8376
|
+
* @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Permissions_Policy#allowlists}
|
|
8377
|
+
* @see {@link https://w3c.github.io/webappsec-permissions-policy/#allowlists}
|
|
8378
|
+
*
|
|
8379
|
+
* @param allowlist Allowlist value.
|
|
8380
|
+
* @param directive Permission directive name.
|
|
8381
|
+
* @param modifierName Modifier name.
|
|
8382
|
+
*
|
|
8383
|
+
* @returns Validation result.
|
|
8384
|
+
*/
|
|
8385
|
+
const validatePermissionAllowlist = (allowlist, directive, modifierName) => {
|
|
8386
|
+
// `*` is one of available permissions tokens
|
|
8387
|
+
// e.g. 'fullscreen=*'
|
|
8388
|
+
// https://w3c.github.io/webappsec-permissions-policy/#structured-header-serialization
|
|
8389
|
+
if (allowlist === WILDCARD
|
|
8390
|
+
// e.g. 'autoplay=()'
|
|
8391
|
+
|| allowlist === EMPTY_PERMISSIONS_ALLOWLIST) {
|
|
8392
|
+
return { valid: true };
|
|
8393
|
+
}
|
|
8394
|
+
if (!(allowlist.startsWith(OPEN_PARENTHESIS) && allowlist.endsWith(CLOSE_PARENTHESIS))) {
|
|
8395
|
+
return getInvalidValidationResult(`${VALIDATION_ERROR_PREFIX.VALUE_INVALID}: '${modifierName}'`);
|
|
8396
|
+
}
|
|
8397
|
+
const allowlistChunks = allowlist.slice(1, -1).split(SPACE);
|
|
8398
|
+
return validatePermissionAllowlistOrigins(allowlistChunks, directive, modifierName);
|
|
8399
|
+
};
|
|
8400
|
+
/**
|
|
8401
|
+
* Validates single permission in the modifier value.
|
|
8402
|
+
*
|
|
8403
|
+
* @param permission Single permission value.
|
|
8404
|
+
* @param modifierName Modifier name.
|
|
8405
|
+
* @param modifierValue Modifier value.
|
|
8406
|
+
*
|
|
8407
|
+
* @returns Validation result.
|
|
8408
|
+
*/
|
|
8409
|
+
const validateSinglePermission = (permission, modifierName, modifierValue) => {
|
|
8410
|
+
// empty permission in the rule
|
|
8411
|
+
// e.g. 'permissions=storage-access=()\\, \\, camera=()'
|
|
8412
|
+
// the validator is here ↑
|
|
8413
|
+
if (!permission) {
|
|
8414
|
+
return getInvalidValidationResult(`${VALIDATION_ERROR_PREFIX.VALUE_INVALID}: '${modifierName}'`);
|
|
8415
|
+
}
|
|
8416
|
+
if (permission.includes(COMMA)) {
|
|
8417
|
+
return getInvalidValidationResult(`${VALIDATION_ERROR_PREFIX.NO_UNESCAPED_PERMISSION_COMMA}: '${modifierName}': '${modifierValue}'`);
|
|
8418
|
+
}
|
|
8419
|
+
const [directive, allowlist] = permission.split(EQUALS);
|
|
8420
|
+
if (!ALLOWED_PERMISSION_DIRECTIVES.has(directive)) {
|
|
8421
|
+
return getInvalidValidationResult(`${VALIDATION_ERROR_PREFIX.INVALID_PERMISSION_DIRECTIVE}: '${modifierName}': '${directive}'`);
|
|
8422
|
+
}
|
|
8423
|
+
return validatePermissionAllowlist(allowlist, directive, modifierName);
|
|
8424
|
+
};
|
|
8425
|
+
/**
|
|
8426
|
+
* Validates `permissions_value` custom value format.
|
|
8427
|
+
* Used for $permissions modifier.
|
|
8428
|
+
*
|
|
8429
|
+
* @param modifier Modifier AST node.
|
|
8430
|
+
*
|
|
8431
|
+
* @returns Validation result.
|
|
8432
|
+
*/
|
|
8433
|
+
const validatePermissions = (modifier) => {
|
|
8434
|
+
if (!modifier.value?.value) {
|
|
8435
|
+
return getValueRequiredValidationResult(modifier.modifier.value);
|
|
8436
|
+
}
|
|
8437
|
+
const modifierName = modifier.modifier.value;
|
|
8438
|
+
const modifierValue = modifier.value.value;
|
|
8439
|
+
// multiple permissions may be separated by escaped commas
|
|
8440
|
+
const permissions = modifier.value.value.split(`${BACKSLASH}${COMMA}`);
|
|
8441
|
+
for (let i = 0; i < permissions.length; i += 1) {
|
|
8442
|
+
const permission = permissions[i].trim();
|
|
8443
|
+
const singlePermissionValidationResult = validateSinglePermission(permission, modifierName, modifierValue);
|
|
8444
|
+
if (!singlePermissionValidationResult.valid) {
|
|
8445
|
+
return singlePermissionValidationResult;
|
|
8446
|
+
}
|
|
8447
|
+
}
|
|
8448
|
+
return { valid: true };
|
|
8449
|
+
};
|
|
8450
|
+
/**
|
|
8451
|
+
* Map of all available pre-defined validators for modifiers with custom `value_format`.
|
|
8452
|
+
*/
|
|
8453
|
+
const CUSTOM_VALUE_FORMAT_MAP = {
|
|
8454
|
+
[CustomValueFormatValidatorName.App]: validatePipeSeparatedApps,
|
|
8455
|
+
[CustomValueFormatValidatorName.Csp]: validateCspValue,
|
|
8456
|
+
[CustomValueFormatValidatorName.DenyAllow]: validatePipeSeparatedDenyAllowDomains,
|
|
8457
|
+
[CustomValueFormatValidatorName.Domain]: validatePipeSeparatedDomains,
|
|
8458
|
+
[CustomValueFormatValidatorName.Method]: validatePipeSeparatedMethods,
|
|
8459
|
+
[CustomValueFormatValidatorName.Permissions]: validatePermissions,
|
|
8460
|
+
[CustomValueFormatValidatorName.StealthOption]: validatePipeSeparatedStealthOptions,
|
|
8461
|
+
};
|
|
8462
|
+
/**
|
|
8463
|
+
* Returns whether the given `valueFormat` is a valid custom value format validator name.
|
|
8464
|
+
*
|
|
8465
|
+
* @param valueFormat Value format for the modifier.
|
|
8466
|
+
*
|
|
8467
|
+
* @returns True if `valueFormat` is a supported pre-defined value format validator name, false otherwise.
|
|
8468
|
+
*/
|
|
8469
|
+
const isCustomValueFormatValidator = (valueFormat) => {
|
|
8470
|
+
return Object.keys(CUSTOM_VALUE_FORMAT_MAP).includes(valueFormat);
|
|
8471
|
+
};
|
|
8472
|
+
/**
|
|
8473
|
+
* Checks whether the value for given `modifier` is valid.
|
|
8474
|
+
*
|
|
8475
|
+
* @param modifier Modifier AST node.
|
|
8476
|
+
* @param valueFormat Value format for the modifier.
|
|
8477
|
+
*
|
|
8478
|
+
* @returns Validation result.
|
|
8479
|
+
*/
|
|
8480
|
+
const validateValue = (modifier, valueFormat) => {
|
|
8481
|
+
if (isCustomValueFormatValidator(valueFormat)) {
|
|
8482
|
+
const validator = CUSTOM_VALUE_FORMAT_MAP[valueFormat];
|
|
8483
|
+
return validator(modifier);
|
|
8484
|
+
}
|
|
8485
|
+
const modifierName = modifier.modifier.value;
|
|
8486
|
+
if (!modifier.value?.value) {
|
|
8487
|
+
return getValueRequiredValidationResult(modifierName);
|
|
8488
|
+
}
|
|
8489
|
+
let xRegExp;
|
|
8490
|
+
try {
|
|
8491
|
+
xRegExp = XRegExp(valueFormat);
|
|
8492
|
+
}
|
|
8493
|
+
catch (e) {
|
|
8494
|
+
throw new Error(`${SOURCE_DATA_ERROR_PREFIX.INVALID_VALUE_FORMAT_REGEXP}: '${modifierName}'`);
|
|
8495
|
+
}
|
|
8496
|
+
const isValid = xRegExp.test(modifier.value?.value);
|
|
8497
|
+
if (!isValid) {
|
|
8498
|
+
return getInvalidValidationResult(`${VALIDATION_ERROR_PREFIX.VALUE_INVALID}: '${modifierName}'`);
|
|
8499
|
+
}
|
|
8500
|
+
return { valid: true };
|
|
8501
|
+
};
|
|
8502
|
+
|
|
8503
|
+
/**
|
|
8504
|
+
* @file Validator for modifiers.
|
|
8505
|
+
*/
|
|
7977
8506
|
/**
|
|
7978
8507
|
* Fully checks whether the given `modifier` valid for given blocker `syntax`:
|
|
7979
8508
|
* is it supported by the blocker, deprecated, assignable, negatable, etc.
|
|
@@ -8030,6 +8559,10 @@ const validateForSpecificSyntax = (modifiersData, syntax, modifier, isException)
|
|
|
8030
8559
|
// e.g. 'domain'
|
|
8031
8560
|
if (specificBlockerData[SpecificKey.Assignable]) {
|
|
8032
8561
|
if (!modifier.value) {
|
|
8562
|
+
// TODO: ditch value_optional after custom validators are implemented for value_format for all modifiers.
|
|
8563
|
+
// This checking should be done in each separate custom validator,
|
|
8564
|
+
// because $csp and $permissions without value can be used only in extension rules,
|
|
8565
|
+
// but $cookie with no value can be used in both blocking and exception rules.
|
|
8033
8566
|
/**
|
|
8034
8567
|
* Some assignable modifiers can be used without a value,
|
|
8035
8568
|
* e.g. '@@||example.com^$cookie'.
|
|
@@ -8117,7 +8650,7 @@ class ModifierValidator {
|
|
|
8117
8650
|
* @returns Result of modifier validation.
|
|
8118
8651
|
*/
|
|
8119
8652
|
validate = (syntax, rawModifier, isException = false) => {
|
|
8120
|
-
const modifier =
|
|
8653
|
+
const modifier = clone(rawModifier);
|
|
8121
8654
|
// special case: handle noop modifier which may be used as multiple underscores (not just one)
|
|
8122
8655
|
// https://adguard.com/kb/general/ad-filtering/create-own-filters/#noop-modifier
|
|
8123
8656
|
if (modifier.modifier.value.startsWith(UNDERSCORE)) {
|
|
@@ -8196,7 +8729,9 @@ class ConverterBase {
|
|
|
8196
8729
|
* Converts some data to AdGuard format
|
|
8197
8730
|
*
|
|
8198
8731
|
* @param data Data to convert
|
|
8199
|
-
* @returns
|
|
8732
|
+
* @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
|
|
8733
|
+
* the converted node, and its `isConverted` flag indicates whether the original node was converted.
|
|
8734
|
+
* If the node was not converted, the result will contain the original node with the same object reference
|
|
8200
8735
|
* @throws If the data is invalid or incompatible
|
|
8201
8736
|
*/
|
|
8202
8737
|
static convertToAdg(data) {
|
|
@@ -8206,7 +8741,9 @@ class ConverterBase {
|
|
|
8206
8741
|
* Converts some data to Adblock Plus format
|
|
8207
8742
|
*
|
|
8208
8743
|
* @param data Data to convert
|
|
8209
|
-
* @returns
|
|
8744
|
+
* @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
|
|
8745
|
+
* the converted node, and its `isConverted` flag indicates whether the original node was converted.
|
|
8746
|
+
* If the node was not converted, the result will contain the original node with the same object reference
|
|
8210
8747
|
* @throws If the data is invalid or incompatible
|
|
8211
8748
|
*/
|
|
8212
8749
|
static convertToAbp(data) {
|
|
@@ -8216,7 +8753,9 @@ class ConverterBase {
|
|
|
8216
8753
|
* Converts some data to uBlock Origin format
|
|
8217
8754
|
*
|
|
8218
8755
|
* @param data Data to convert
|
|
8219
|
-
* @returns
|
|
8756
|
+
* @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
|
|
8757
|
+
* the converted node, and its `isConverted` flag indicates whether the original node was converted.
|
|
8758
|
+
* If the node was not converted, the result will contain the original node with the same object reference
|
|
8220
8759
|
* @throws If the data is invalid or incompatible
|
|
8221
8760
|
*/
|
|
8222
8761
|
static convertToUbo(data) {
|
|
@@ -8240,7 +8779,9 @@ class RuleConverterBase extends ConverterBase {
|
|
|
8240
8779
|
* Converts an adblock filtering rule to AdGuard format, if possible.
|
|
8241
8780
|
*
|
|
8242
8781
|
* @param rule Rule node to convert
|
|
8243
|
-
* @returns
|
|
8782
|
+
* @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
|
|
8783
|
+
* the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
|
|
8784
|
+
* If the rule was not converted, the result array will contain the original node with the same object reference
|
|
8244
8785
|
* @throws If the rule is invalid or cannot be converted
|
|
8245
8786
|
*/
|
|
8246
8787
|
static convertToAdg(rule) {
|
|
@@ -8250,7 +8791,9 @@ class RuleConverterBase extends ConverterBase {
|
|
|
8250
8791
|
* Converts an adblock filtering rule to Adblock Plus format, if possible.
|
|
8251
8792
|
*
|
|
8252
8793
|
* @param rule Rule node to convert
|
|
8253
|
-
* @returns
|
|
8794
|
+
* @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
|
|
8795
|
+
* the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
|
|
8796
|
+
* If the rule was not converted, the result array will contain the original node with the same object reference
|
|
8254
8797
|
* @throws If the rule is invalid or cannot be converted
|
|
8255
8798
|
*/
|
|
8256
8799
|
static convertToAbp(rule) {
|
|
@@ -8260,7 +8803,9 @@ class RuleConverterBase extends ConverterBase {
|
|
|
8260
8803
|
* Converts an adblock filtering rule to uBlock Origin format, if possible.
|
|
8261
8804
|
*
|
|
8262
8805
|
* @param rule Rule node to convert
|
|
8263
|
-
* @returns
|
|
8806
|
+
* @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
|
|
8807
|
+
* the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
|
|
8808
|
+
* If the rule was not converted, the result array will contain the original node with the same object reference
|
|
8264
8809
|
* @throws If the rule is invalid or cannot be converted
|
|
8265
8810
|
*/
|
|
8266
8811
|
static convertToUbo(rule) {
|
|
@@ -8268,6 +8813,37 @@ class RuleConverterBase extends ConverterBase {
|
|
|
8268
8813
|
}
|
|
8269
8814
|
}
|
|
8270
8815
|
|
|
8816
|
+
/**
|
|
8817
|
+
* @file Conversion result interface and helper functions
|
|
8818
|
+
*/
|
|
8819
|
+
/**
|
|
8820
|
+
* Helper function to create a generic conversion result.
|
|
8821
|
+
*
|
|
8822
|
+
* @param result Conversion result
|
|
8823
|
+
* @param isConverted Indicates whether the input item was converted
|
|
8824
|
+
* @template T Type of the item to convert
|
|
8825
|
+
* @template U Type of the conversion result (defaults to `T`, but can be `T[]` as well)
|
|
8826
|
+
* @returns Generic conversion result
|
|
8827
|
+
*/
|
|
8828
|
+
// eslint-disable-next-line max-len
|
|
8829
|
+
function createConversionResult(result, isConverted) {
|
|
8830
|
+
return {
|
|
8831
|
+
result,
|
|
8832
|
+
isConverted,
|
|
8833
|
+
};
|
|
8834
|
+
}
|
|
8835
|
+
/**
|
|
8836
|
+
* Helper function to create a node conversion result.
|
|
8837
|
+
*
|
|
8838
|
+
* @param nodes Array of nodes
|
|
8839
|
+
* @param isConverted Indicates whether the input item was converted
|
|
8840
|
+
* @template T Type of the node (extends `Node`)
|
|
8841
|
+
* @returns Node conversion result
|
|
8842
|
+
*/
|
|
8843
|
+
function createNodeConversionResult(nodes, isConverted) {
|
|
8844
|
+
return createConversionResult(nodes, isConverted);
|
|
8845
|
+
}
|
|
8846
|
+
|
|
8271
8847
|
/**
|
|
8272
8848
|
* @file Comment rule converter
|
|
8273
8849
|
*/
|
|
@@ -8281,27 +8857,30 @@ class CommentRuleConverter extends RuleConverterBase {
|
|
|
8281
8857
|
* Converts a comment rule to AdGuard format, if possible.
|
|
8282
8858
|
*
|
|
8283
8859
|
* @param rule Rule node to convert
|
|
8284
|
-
* @returns
|
|
8860
|
+
* @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
|
|
8861
|
+
* the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
|
|
8862
|
+
* If the rule was not converted, the result array will contain the original node with the same object reference
|
|
8285
8863
|
* @throws If the rule is invalid or cannot be converted
|
|
8286
8864
|
*/
|
|
8287
8865
|
static convertToAdg(rule) {
|
|
8288
|
-
// Clone the provided AST node to avoid side effects
|
|
8289
|
-
const ruleNode = cloneDeep(rule);
|
|
8290
8866
|
// TODO: Add support for other comment types, if needed
|
|
8291
8867
|
// Main task is # -> ! conversion
|
|
8292
|
-
switch (
|
|
8868
|
+
switch (rule.type) {
|
|
8293
8869
|
case exports.CommentRuleType.CommentRule:
|
|
8294
|
-
//
|
|
8295
|
-
if (
|
|
8296
|
-
|
|
8297
|
-
|
|
8298
|
-
|
|
8299
|
-
|
|
8870
|
+
// Check if the rule needs to be converted
|
|
8871
|
+
if (rule.type === exports.CommentRuleType.CommentRule && rule.marker.value === exports.CommentMarker.Hashmark) {
|
|
8872
|
+
// Add a ! to the beginning of the comment
|
|
8873
|
+
// TODO: Replace with custom clone method
|
|
8874
|
+
const ruleClone = clone(rule);
|
|
8875
|
+
ruleClone.marker.value = exports.CommentMarker.Regular;
|
|
8876
|
+
// Add the hashmark to the beginning of the comment text
|
|
8877
|
+
ruleClone.text.value = `${SPACE}${exports.CommentMarker.Hashmark}${ruleClone.text.value}`;
|
|
8878
|
+
return createNodeConversionResult([ruleClone], true);
|
|
8300
8879
|
}
|
|
8301
|
-
return [
|
|
8880
|
+
return createNodeConversionResult([rule], false);
|
|
8302
8881
|
// Leave any other comment rule as is
|
|
8303
8882
|
default:
|
|
8304
|
-
return [
|
|
8883
|
+
return createNodeConversionResult([rule], false);
|
|
8305
8884
|
}
|
|
8306
8885
|
}
|
|
8307
8886
|
}
|
|
@@ -8471,6 +9050,58 @@ class RegExpUtils {
|
|
|
8471
9050
|
}
|
|
8472
9051
|
}
|
|
8473
9052
|
|
|
9053
|
+
/**
|
|
9054
|
+
* @file Custom clone functions for AST nodes, this is probably the most efficient way to clone AST nodes.
|
|
9055
|
+
* @todo Maybe move them to parser classes as 'clone' methods
|
|
9056
|
+
*/
|
|
9057
|
+
/**
|
|
9058
|
+
* Clones a scriptlet rule node.
|
|
9059
|
+
*
|
|
9060
|
+
* @param node Node to clone
|
|
9061
|
+
* @returns Cloned node
|
|
9062
|
+
*/
|
|
9063
|
+
function cloneScriptletRuleNode(node) {
|
|
9064
|
+
return {
|
|
9065
|
+
type: node.type,
|
|
9066
|
+
children: node.children.map((child) => ({ ...child })),
|
|
9067
|
+
};
|
|
9068
|
+
}
|
|
9069
|
+
/**
|
|
9070
|
+
* Clones a domain list node.
|
|
9071
|
+
*
|
|
9072
|
+
* @param node Node to clone
|
|
9073
|
+
* @returns Cloned node
|
|
9074
|
+
*/
|
|
9075
|
+
function cloneDomainListNode(node) {
|
|
9076
|
+
return {
|
|
9077
|
+
type: node.type,
|
|
9078
|
+
separator: node.separator,
|
|
9079
|
+
children: node.children.map((domain) => ({ ...domain })),
|
|
9080
|
+
};
|
|
9081
|
+
}
|
|
9082
|
+
/**
|
|
9083
|
+
* Clones a modifier list node.
|
|
9084
|
+
*
|
|
9085
|
+
* @param node Node to clone
|
|
9086
|
+
* @returns Cloned node
|
|
9087
|
+
*/
|
|
9088
|
+
function cloneModifierListNode(node) {
|
|
9089
|
+
return {
|
|
9090
|
+
type: node.type,
|
|
9091
|
+
children: node.children.map((modifier) => {
|
|
9092
|
+
const res = {
|
|
9093
|
+
type: modifier.type,
|
|
9094
|
+
exception: modifier.exception,
|
|
9095
|
+
modifier: { ...modifier.modifier },
|
|
9096
|
+
};
|
|
9097
|
+
if (modifier.value) {
|
|
9098
|
+
res.value = { ...modifier.value };
|
|
9099
|
+
}
|
|
9100
|
+
return res;
|
|
9101
|
+
}),
|
|
9102
|
+
};
|
|
9103
|
+
}
|
|
9104
|
+
|
|
8474
9105
|
/**
|
|
8475
9106
|
* @file HTML filtering rule converter
|
|
8476
9107
|
*/
|
|
@@ -8483,16 +9114,22 @@ class RegExpUtils {
|
|
|
8483
9114
|
*
|
|
8484
9115
|
* @see {@link https://adguard.com/kb/general/ad-filtering/create-own-filters/#html-filtering-rules}
|
|
8485
9116
|
*/
|
|
8486
|
-
const
|
|
8487
|
-
const
|
|
9117
|
+
const ADG_HTML_DEFAULT_MAX_LENGTH = 8192;
|
|
9118
|
+
const ADG_HTML_CONVERSION_MAX_LENGTH = ADG_HTML_DEFAULT_MAX_LENGTH * 32;
|
|
8488
9119
|
const NOT_SPECIFIED = -1;
|
|
8489
|
-
|
|
8490
|
-
|
|
8491
|
-
|
|
8492
|
-
|
|
8493
|
-
|
|
8494
|
-
|
|
8495
|
-
|
|
9120
|
+
var PseudoClasses$1;
|
|
9121
|
+
(function (PseudoClasses) {
|
|
9122
|
+
PseudoClasses["Contains"] = "contains";
|
|
9123
|
+
PseudoClasses["HasText"] = "has-text";
|
|
9124
|
+
PseudoClasses["MinTextLength"] = "min-text-length";
|
|
9125
|
+
})(PseudoClasses$1 || (PseudoClasses$1 = {}));
|
|
9126
|
+
var AttributeSelectors;
|
|
9127
|
+
(function (AttributeSelectors) {
|
|
9128
|
+
AttributeSelectors["MaxLength"] = "max-length";
|
|
9129
|
+
AttributeSelectors["MinLength"] = "min-length";
|
|
9130
|
+
AttributeSelectors["TagContent"] = "tag-content";
|
|
9131
|
+
AttributeSelectors["Wildcard"] = "wildcard";
|
|
9132
|
+
})(AttributeSelectors || (AttributeSelectors = {}));
|
|
8496
9133
|
/**
|
|
8497
9134
|
* HTML filtering rule converter class
|
|
8498
9135
|
*
|
|
@@ -8515,16 +9152,23 @@ class HtmlRuleConverter extends RuleConverterBase {
|
|
|
8515
9152
|
* ```
|
|
8516
9153
|
*
|
|
8517
9154
|
* @param rule Rule node to convert
|
|
8518
|
-
* @returns
|
|
9155
|
+
* @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
|
|
9156
|
+
* the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
|
|
9157
|
+
* If the rule was not converted, the result array will contain the original node with the same object reference
|
|
8519
9158
|
* @throws If the rule is invalid or cannot be converted
|
|
8520
9159
|
*/
|
|
8521
9160
|
static convertToAdg(rule) {
|
|
8522
|
-
//
|
|
8523
|
-
|
|
9161
|
+
// Ignore AdGuard rules
|
|
9162
|
+
if (rule.syntax === exports.AdblockSyntax.Adg) {
|
|
9163
|
+
return createNodeConversionResult([rule], false);
|
|
9164
|
+
}
|
|
9165
|
+
if (rule.syntax === exports.AdblockSyntax.Abp) {
|
|
9166
|
+
throw new RuleConversionError('Invalid rule, ABP does not support HTML filtering rules');
|
|
9167
|
+
}
|
|
8524
9168
|
// Prepare the conversion result
|
|
8525
9169
|
const conversionResult = [];
|
|
8526
9170
|
// Iterate over selector list
|
|
8527
|
-
for (const selector of
|
|
9171
|
+
for (const selector of rule.body.body.children) {
|
|
8528
9172
|
// Check selector, just in case
|
|
8529
9173
|
if (selector.type !== exports.CssTreeNodeType.Selector) {
|
|
8530
9174
|
throw new RuleConversionError(`Expected selector, got '${selector.type}'`);
|
|
@@ -8551,24 +9195,24 @@ class HtmlRuleConverter extends RuleConverterBase {
|
|
|
8551
9195
|
throw new RuleConversionError('Tag selector should be the first child, if present');
|
|
8552
9196
|
}
|
|
8553
9197
|
// Simply store the tag selector
|
|
8554
|
-
convertedSelector.children.push(
|
|
9198
|
+
convertedSelector.children.push(clone(node));
|
|
8555
9199
|
break;
|
|
8556
9200
|
case exports.CssTreeNodeType.AttributeSelector:
|
|
8557
9201
|
// Check if the attribute selector is a special AdGuard attribute
|
|
8558
9202
|
switch (node.name.name) {
|
|
8559
|
-
case
|
|
9203
|
+
case AttributeSelectors.MinLength:
|
|
8560
9204
|
minLength = CssTree.parseAttributeSelectorValueAsNumber(node);
|
|
8561
9205
|
break;
|
|
8562
|
-
case
|
|
9206
|
+
case AttributeSelectors.MaxLength:
|
|
8563
9207
|
maxLength = CssTree.parseAttributeSelectorValueAsNumber(node);
|
|
8564
9208
|
break;
|
|
8565
|
-
case
|
|
8566
|
-
case
|
|
9209
|
+
case AttributeSelectors.TagContent:
|
|
9210
|
+
case AttributeSelectors.Wildcard:
|
|
8567
9211
|
CssTree.assertAttributeSelectorHasStringValue(node);
|
|
8568
|
-
convertedSelector.children.push(
|
|
9212
|
+
convertedSelector.children.push(clone(node));
|
|
8569
9213
|
break;
|
|
8570
9214
|
default:
|
|
8571
|
-
convertedSelector.children.push(
|
|
9215
|
+
convertedSelector.children.push(clone(node));
|
|
8572
9216
|
}
|
|
8573
9217
|
break;
|
|
8574
9218
|
case exports.CssTreeNodeType.PseudoClassSelector:
|
|
@@ -8582,18 +9226,18 @@ class HtmlRuleConverter extends RuleConverterBase {
|
|
|
8582
9226
|
}
|
|
8583
9227
|
// Process the pseudo class based on its name
|
|
8584
9228
|
switch (node.name) {
|
|
8585
|
-
case
|
|
8586
|
-
case
|
|
9229
|
+
case PseudoClasses$1.HasText:
|
|
9230
|
+
case PseudoClasses$1.Contains:
|
|
8587
9231
|
// Check if the argument is a RegExp
|
|
8588
9232
|
if (RegExpUtils.isRegexPattern(arg.value)) {
|
|
8589
9233
|
// TODO: Add some support for RegExp patterns later
|
|
8590
9234
|
// Need to find a way to convert some RegExp patterns to glob patterns
|
|
8591
9235
|
throw new RuleConversionError('Conversion of RegExp patterns is not yet supported');
|
|
8592
9236
|
}
|
|
8593
|
-
convertedSelector.children.push(CssTree.createAttributeSelectorNode(
|
|
9237
|
+
convertedSelector.children.push(CssTree.createAttributeSelectorNode(AttributeSelectors.TagContent, arg.value));
|
|
8594
9238
|
break;
|
|
8595
9239
|
// https://github.com/gorhill/uBlock/wiki/Procedural-cosmetic-filters#subjectmin-text-lengthn
|
|
8596
|
-
case
|
|
9240
|
+
case PseudoClasses$1.MinTextLength:
|
|
8597
9241
|
minLength = CssTree.parsePseudoClassArgumentAsNumber(node);
|
|
8598
9242
|
break;
|
|
8599
9243
|
default:
|
|
@@ -8605,10 +9249,10 @@ class HtmlRuleConverter extends RuleConverterBase {
|
|
|
8605
9249
|
}
|
|
8606
9250
|
}
|
|
8607
9251
|
if (minLength !== NOT_SPECIFIED) {
|
|
8608
|
-
convertedSelector.children.push(CssTree.createAttributeSelectorNode(
|
|
9252
|
+
convertedSelector.children.push(CssTree.createAttributeSelectorNode(AttributeSelectors.MinLength, String(minLength)));
|
|
8609
9253
|
}
|
|
8610
|
-
convertedSelector.children.push(CssTree.createAttributeSelectorNode(
|
|
8611
|
-
?
|
|
9254
|
+
convertedSelector.children.push(CssTree.createAttributeSelectorNode(AttributeSelectors.MaxLength, String(maxLength === NOT_SPECIFIED
|
|
9255
|
+
? ADG_HTML_CONVERSION_MAX_LENGTH
|
|
8612
9256
|
: maxLength)));
|
|
8613
9257
|
// Create the converted rule
|
|
8614
9258
|
conversionResult.push({
|
|
@@ -8618,7 +9262,7 @@ class HtmlRuleConverter extends RuleConverterBase {
|
|
|
8618
9262
|
// Convert the separator based on the exception status
|
|
8619
9263
|
separator: {
|
|
8620
9264
|
type: 'Value',
|
|
8621
|
-
value:
|
|
9265
|
+
value: rule.exception
|
|
8622
9266
|
? exports.CosmeticRuleSeparator.AdgHtmlFilteringException
|
|
8623
9267
|
: exports.CosmeticRuleSeparator.AdgHtmlFiltering,
|
|
8624
9268
|
},
|
|
@@ -8633,11 +9277,11 @@ class HtmlRuleConverter extends RuleConverterBase {
|
|
|
8633
9277
|
}],
|
|
8634
9278
|
},
|
|
8635
9279
|
},
|
|
8636
|
-
exception:
|
|
8637
|
-
domains:
|
|
9280
|
+
exception: rule.exception,
|
|
9281
|
+
domains: cloneDomainListNode(rule.domains),
|
|
8638
9282
|
});
|
|
8639
9283
|
}
|
|
8640
|
-
return conversionResult;
|
|
9284
|
+
return createNodeConversionResult(conversionResult, true);
|
|
8641
9285
|
}
|
|
8642
9286
|
}
|
|
8643
9287
|
|
|
@@ -8658,96 +9302,38 @@ function getScriptletName(scriptletNode) {
|
|
|
8658
9302
|
return scriptletNode.children[0].value;
|
|
8659
9303
|
}
|
|
8660
9304
|
/**
|
|
8661
|
-
* Set name of the scriptlet
|
|
9305
|
+
* Set name of the scriptlet.
|
|
9306
|
+
* Modifies input `scriptletNode` if needed.
|
|
8662
9307
|
*
|
|
8663
9308
|
* @param scriptletNode Scriptlet node to set name of
|
|
8664
9309
|
* @param name Name to set
|
|
8665
|
-
* @returns Scriptlet node with the specified name
|
|
8666
|
-
* @throws If the scriptlet is empty
|
|
8667
9310
|
*/
|
|
8668
9311
|
function setScriptletName(scriptletNode, name) {
|
|
8669
|
-
if (scriptletNode.children.length
|
|
8670
|
-
|
|
9312
|
+
if (scriptletNode.children.length > 0) {
|
|
9313
|
+
// eslint-disable-next-line no-param-reassign
|
|
9314
|
+
scriptletNode.children[0].value = name;
|
|
8671
9315
|
}
|
|
8672
|
-
const scriptletNodeClone = cloneDeep(scriptletNode);
|
|
8673
|
-
scriptletNodeClone.children[0].value = name;
|
|
8674
|
-
return scriptletNodeClone;
|
|
8675
9316
|
}
|
|
8676
9317
|
/**
|
|
8677
9318
|
* Set quote type of the scriptlet parameters
|
|
8678
9319
|
*
|
|
8679
9320
|
* @param scriptletNode Scriptlet node to set quote type of
|
|
8680
9321
|
* @param quoteType Preferred quote type
|
|
8681
|
-
* @returns Scriptlet node with the specified quote type
|
|
8682
9322
|
*/
|
|
8683
9323
|
function setScriptletQuoteType(scriptletNode, quoteType) {
|
|
8684
|
-
if (scriptletNode.children.length
|
|
8685
|
-
|
|
8686
|
-
|
|
8687
|
-
|
|
8688
|
-
for (let i = 0; i < scriptletNodeClone.children.length; i += 1) {
|
|
8689
|
-
scriptletNodeClone.children[i].value = QuoteUtils.setStringQuoteType(scriptletNodeClone.children[i].value, quoteType);
|
|
8690
|
-
}
|
|
8691
|
-
return scriptletNodeClone;
|
|
8692
|
-
}
|
|
8693
|
-
|
|
8694
|
-
/**
|
|
8695
|
-
* @file Scriptlet conversions from ABP and uBO to ADG
|
|
8696
|
-
*/
|
|
8697
|
-
const ABP_SCRIPTLET_PREFIX = 'abp-';
|
|
8698
|
-
const UBO_SCRIPTLET_PREFIX = 'ubo-';
|
|
8699
|
-
/**
|
|
8700
|
-
* Helper class for converting scriptlets from ABP and uBO to ADG
|
|
8701
|
-
*/
|
|
8702
|
-
class AdgScriptletConverter {
|
|
8703
|
-
/**
|
|
8704
|
-
* Helper function to convert scriptlets to ADG. We implement the core
|
|
8705
|
-
* logic here to avoid code duplication.
|
|
8706
|
-
*
|
|
8707
|
-
* @param scriptletNode Scriptlet parameter list node to convert
|
|
8708
|
-
* @param prefix Prefix to add to the scriptlet name
|
|
8709
|
-
* @returns Converted scriptlet parameter list node
|
|
8710
|
-
*/
|
|
8711
|
-
static convertToAdg(scriptletNode, prefix) {
|
|
8712
|
-
// Remove possible quotes just to make it easier to work with the scriptlet name
|
|
8713
|
-
const scriptletName = QuoteUtils.setStringQuoteType(getScriptletName(scriptletNode), exports.QuoteType.None);
|
|
8714
|
-
// Clone the node to avoid any side effects
|
|
8715
|
-
let result = cloneDeep(scriptletNode);
|
|
8716
|
-
// Only add prefix if it's not already there
|
|
8717
|
-
if (!scriptletName.startsWith(prefix)) {
|
|
8718
|
-
result = setScriptletName(scriptletNode, `${prefix}${scriptletName}`);
|
|
9324
|
+
if (scriptletNode.children.length > 0) {
|
|
9325
|
+
for (let i = 0; i < scriptletNode.children.length; i += 1) {
|
|
9326
|
+
// eslint-disable-next-line no-param-reassign
|
|
9327
|
+
scriptletNode.children[i].value = QuoteUtils.setStringQuoteType(scriptletNode.children[i].value, quoteType);
|
|
8719
9328
|
}
|
|
8720
|
-
// ADG scriptlet parameters should be quoted, and single quoted are preferred
|
|
8721
|
-
result = setScriptletQuoteType(result, exports.QuoteType.Single);
|
|
8722
|
-
return result;
|
|
8723
9329
|
}
|
|
8724
|
-
/**
|
|
8725
|
-
* Converts an ABP snippet node to ADG scriptlet node, if possible.
|
|
8726
|
-
*
|
|
8727
|
-
* @param scriptletNode Scriptlet node to convert
|
|
8728
|
-
* @returns Converted scriptlet node
|
|
8729
|
-
* @throws If the scriptlet isn't supported by ADG or is invalid
|
|
8730
|
-
* @see {@link https://help.adblockplus.org/hc/en-us/articles/1500002338501#snippets-ref}
|
|
8731
|
-
*/
|
|
8732
|
-
static convertFromAbp = (scriptletNode) => {
|
|
8733
|
-
return AdgScriptletConverter.convertToAdg(scriptletNode, ABP_SCRIPTLET_PREFIX);
|
|
8734
|
-
};
|
|
8735
|
-
/**
|
|
8736
|
-
* Convert a uBO scriptlet node to ADG scriptlet node, if possible.
|
|
8737
|
-
*
|
|
8738
|
-
* @param scriptletNode Scriptlet node to convert
|
|
8739
|
-
* @returns Converted scriptlet node
|
|
8740
|
-
* @throws If the scriptlet isn't supported by ADG or is invalid
|
|
8741
|
-
* @see {@link https://github.com/gorhill/uBlock/wiki/Resources-Library#available-general-purpose-scriptlets}
|
|
8742
|
-
*/
|
|
8743
|
-
static convertFromUbo = (scriptletNode) => {
|
|
8744
|
-
return AdgScriptletConverter.convertToAdg(scriptletNode, UBO_SCRIPTLET_PREFIX);
|
|
8745
|
-
};
|
|
8746
9330
|
}
|
|
8747
9331
|
|
|
8748
9332
|
/**
|
|
8749
9333
|
* @file Scriptlet injection rule converter
|
|
8750
9334
|
*/
|
|
9335
|
+
const ABP_SCRIPTLET_PREFIX = 'abp-';
|
|
9336
|
+
const UBO_SCRIPTLET_PREFIX = 'ubo-';
|
|
8751
9337
|
/**
|
|
8752
9338
|
* Scriptlet injection rule converter class
|
|
8753
9339
|
*
|
|
@@ -8758,38 +9344,91 @@ class ScriptletRuleConverter extends RuleConverterBase {
|
|
|
8758
9344
|
* Converts a scriptlet injection rule to AdGuard format, if possible.
|
|
8759
9345
|
*
|
|
8760
9346
|
* @param rule Rule node to convert
|
|
8761
|
-
* @returns
|
|
9347
|
+
* @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
|
|
9348
|
+
* the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
|
|
9349
|
+
* If the rule was not converted, the result array will contain the original node with the same object reference
|
|
8762
9350
|
* @throws If the rule is invalid or cannot be converted
|
|
8763
9351
|
*/
|
|
8764
9352
|
static convertToAdg(rule) {
|
|
8765
|
-
//
|
|
8766
|
-
|
|
9353
|
+
// Ignore AdGuard rules
|
|
9354
|
+
if (rule.syntax === exports.AdblockSyntax.Adg) {
|
|
9355
|
+
return createNodeConversionResult([rule], false);
|
|
9356
|
+
}
|
|
9357
|
+
const separator = rule.separator.value;
|
|
9358
|
+
let convertedSeparator = separator;
|
|
9359
|
+
convertedSeparator = rule.exception
|
|
9360
|
+
? exports.CosmeticRuleSeparator.AdgJsInjectionException
|
|
9361
|
+
: exports.CosmeticRuleSeparator.AdgJsInjection;
|
|
8767
9362
|
const convertedScriptlets = [];
|
|
8768
|
-
for (const scriptlet of
|
|
8769
|
-
|
|
8770
|
-
|
|
8771
|
-
|
|
8772
|
-
|
|
8773
|
-
|
|
9363
|
+
for (const scriptlet of rule.body.children) {
|
|
9364
|
+
// Clone the node to avoid any side effects
|
|
9365
|
+
const scriptletClone = cloneScriptletRuleNode(scriptlet);
|
|
9366
|
+
// Remove possible quotes just to make it easier to work with the scriptlet name
|
|
9367
|
+
const scriptletName = QuoteUtils.setStringQuoteType(getScriptletName(scriptletClone), exports.QuoteType.None);
|
|
9368
|
+
// Add prefix if it's not already there
|
|
9369
|
+
let prefix;
|
|
9370
|
+
switch (rule.syntax) {
|
|
9371
|
+
case exports.AdblockSyntax.Abp:
|
|
9372
|
+
prefix = ABP_SCRIPTLET_PREFIX;
|
|
9373
|
+
break;
|
|
9374
|
+
case exports.AdblockSyntax.Ubo:
|
|
9375
|
+
prefix = UBO_SCRIPTLET_PREFIX;
|
|
9376
|
+
break;
|
|
9377
|
+
default:
|
|
9378
|
+
prefix = EMPTY;
|
|
8774
9379
|
}
|
|
8775
|
-
|
|
8776
|
-
|
|
9380
|
+
if (!scriptletName.startsWith(prefix)) {
|
|
9381
|
+
setScriptletName(scriptletClone, `${prefix}${scriptletName}`);
|
|
8777
9382
|
}
|
|
9383
|
+
// ADG scriptlet parameters should be quoted, and single quoted are preferred
|
|
9384
|
+
setScriptletQuoteType(scriptletClone, exports.QuoteType.Single);
|
|
9385
|
+
convertedScriptlets.push(scriptletClone);
|
|
8778
9386
|
}
|
|
8779
|
-
|
|
8780
|
-
|
|
8781
|
-
|
|
8782
|
-
|
|
8783
|
-
return convertedScriptlets.map((scriptlet) => {
|
|
8784
|
-
return {
|
|
8785
|
-
...ruleNode,
|
|
9387
|
+
return createNodeConversionResult(convertedScriptlets.map((scriptlet) => {
|
|
9388
|
+
const res = {
|
|
9389
|
+
category: rule.category,
|
|
9390
|
+
type: rule.type,
|
|
8786
9391
|
syntax: exports.AdblockSyntax.Adg,
|
|
9392
|
+
exception: rule.exception,
|
|
9393
|
+
domains: cloneDomainListNode(rule.domains),
|
|
9394
|
+
separator: {
|
|
9395
|
+
type: 'Value',
|
|
9396
|
+
value: convertedSeparator,
|
|
9397
|
+
},
|
|
8787
9398
|
body: {
|
|
8788
|
-
|
|
9399
|
+
type: rule.body.type,
|
|
8789
9400
|
children: [scriptlet],
|
|
8790
9401
|
},
|
|
8791
9402
|
};
|
|
8792
|
-
|
|
9403
|
+
if (rule.modifiers) {
|
|
9404
|
+
res.modifiers = cloneModifierListNode(rule.modifiers);
|
|
9405
|
+
}
|
|
9406
|
+
return res;
|
|
9407
|
+
}), true);
|
|
9408
|
+
}
|
|
9409
|
+
}
|
|
9410
|
+
|
|
9411
|
+
/**
|
|
9412
|
+
* A very simple map extension that allows to store multiple values for the same key
|
|
9413
|
+
* by storing them in an array.
|
|
9414
|
+
*
|
|
9415
|
+
* @todo Add more methods if needed
|
|
9416
|
+
*/
|
|
9417
|
+
class MultiValueMap extends Map {
|
|
9418
|
+
/**
|
|
9419
|
+
* Adds a value to the map. If the key already exists, the value will be appended to the existing array,
|
|
9420
|
+
* otherwise a new array will be created for the key.
|
|
9421
|
+
*
|
|
9422
|
+
* @param key Key to add
|
|
9423
|
+
* @param values Value(s) to add
|
|
9424
|
+
*/
|
|
9425
|
+
add(key, ...values) {
|
|
9426
|
+
let currentValues = super.get(key);
|
|
9427
|
+
if (isUndefined(currentValues)) {
|
|
9428
|
+
currentValues = [];
|
|
9429
|
+
super.set(key, values);
|
|
9430
|
+
}
|
|
9431
|
+
currentValues.push(...values);
|
|
8793
9432
|
}
|
|
8794
9433
|
}
|
|
8795
9434
|
|
|
@@ -8815,69 +9454,115 @@ class AdgCosmeticRuleModifierConverter {
|
|
|
8815
9454
|
* Converts a uBO cosmetic rule modifier list to ADG, if possible.
|
|
8816
9455
|
*
|
|
8817
9456
|
* @param modifierList Cosmetic rule modifier list node to convert
|
|
8818
|
-
* @returns
|
|
9457
|
+
* @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
|
|
9458
|
+
* the converted node, and its `isConverted` flag indicates whether the original node was converted.
|
|
9459
|
+
* If the node was not converted, the result will contain the original node with the same object reference
|
|
8819
9460
|
* @throws If the modifier list cannot be converted
|
|
8820
9461
|
* @see {@link https://github.com/gorhill/uBlock/wiki/Procedural-cosmetic-filters#cosmetic-filter-operators}
|
|
8821
9462
|
*/
|
|
8822
|
-
static convertFromUbo
|
|
8823
|
-
const
|
|
8824
|
-
|
|
8825
|
-
|
|
8826
|
-
|
|
8827
|
-
|
|
8828
|
-
|
|
8829
|
-
|
|
8830
|
-
|
|
8831
|
-
|
|
8832
|
-
|
|
8833
|
-
|
|
8834
|
-
|
|
8835
|
-
|
|
8836
|
-
|
|
8837
|
-
//
|
|
8838
|
-
|
|
8839
|
-
|
|
8840
|
-
? `${REGEX_MARKER}${RegExpUtils.negateRegexPattern(RegExpUtils.patternToRegexp(modifierValue))}${REGEX_MARKER}`
|
|
8841
|
-
: modifierValue));
|
|
8842
|
-
break;
|
|
8843
|
-
default:
|
|
8844
|
-
// Leave the modifier as-is
|
|
8845
|
-
convertedModifierList.children.push(modifier);
|
|
9463
|
+
static convertFromUbo(modifierList) {
|
|
9464
|
+
const conversionMap = new MultiValueMap();
|
|
9465
|
+
modifierList.children.forEach((modifier, index) => {
|
|
9466
|
+
// :matches-path
|
|
9467
|
+
if (modifier.modifier.value === UBO_MATCHES_PATH_OPERATOR) {
|
|
9468
|
+
if (!modifier.value) {
|
|
9469
|
+
throw new RuleConversionError(`'${UBO_MATCHES_PATH_OPERATOR}' operator requires a value`);
|
|
9470
|
+
}
|
|
9471
|
+
const value = RegExpUtils.isRegexPattern(modifier.value.value)
|
|
9472
|
+
? StringUtils.escapeCharacters(modifier.value.value, SPECIAL_MODIFIER_REGEX_CHARS)
|
|
9473
|
+
: modifier.value.value;
|
|
9474
|
+
// Convert uBO's `:matches-path(...)` operator to ADG's `$path=...` modifier
|
|
9475
|
+
conversionMap.add(index, createModifierNode(ADG_PATH_MODIFIER,
|
|
9476
|
+
// We should negate the regexp if the modifier is an exception
|
|
9477
|
+
modifier.exception
|
|
9478
|
+
// eslint-disable-next-line max-len
|
|
9479
|
+
? `${REGEX_MARKER}${RegExpUtils.negateRegexPattern(RegExpUtils.patternToRegexp(value))}${REGEX_MARKER}`
|
|
9480
|
+
: value));
|
|
8846
9481
|
}
|
|
8847
|
-
}
|
|
8848
|
-
|
|
8849
|
-
|
|
9482
|
+
});
|
|
9483
|
+
// Check if we have any converted modifiers
|
|
9484
|
+
if (conversionMap.size) {
|
|
9485
|
+
const modifierListClone = clone(modifierList);
|
|
9486
|
+
// Replace the original modifiers with the converted ones
|
|
9487
|
+
modifierListClone.children = modifierListClone.children.map((modifier, index) => {
|
|
9488
|
+
const convertedModifier = conversionMap.get(index);
|
|
9489
|
+
return convertedModifier ?? modifier;
|
|
9490
|
+
}).flat();
|
|
9491
|
+
return createConversionResult(modifierListClone, true);
|
|
9492
|
+
}
|
|
9493
|
+
// Otherwise, just return the original modifier list
|
|
9494
|
+
return createConversionResult(modifierList, false);
|
|
9495
|
+
}
|
|
8850
9496
|
}
|
|
8851
9497
|
|
|
8852
|
-
|
|
8853
|
-
|
|
8854
|
-
|
|
8855
|
-
|
|
8856
|
-
|
|
8857
|
-
|
|
8858
|
-
|
|
8859
|
-
|
|
8860
|
-
|
|
8861
|
-
|
|
8862
|
-
|
|
8863
|
-
|
|
8864
|
-
|
|
9498
|
+
var PseudoClasses;
|
|
9499
|
+
(function (PseudoClasses) {
|
|
9500
|
+
PseudoClasses["AbpContains"] = "-abp-contains";
|
|
9501
|
+
PseudoClasses["AbpHas"] = "-abp-has";
|
|
9502
|
+
PseudoClasses["Contains"] = "contains";
|
|
9503
|
+
PseudoClasses["Has"] = "has";
|
|
9504
|
+
PseudoClasses["HasText"] = "has-text";
|
|
9505
|
+
PseudoClasses["MatchesCss"] = "matches-css";
|
|
9506
|
+
PseudoClasses["MatchesCssAfter"] = "matches-css-after";
|
|
9507
|
+
PseudoClasses["MatchesCssBefore"] = "matches-css-before";
|
|
9508
|
+
PseudoClasses["Not"] = "not";
|
|
9509
|
+
})(PseudoClasses || (PseudoClasses = {}));
|
|
9510
|
+
var PseudoElements;
|
|
9511
|
+
(function (PseudoElements) {
|
|
9512
|
+
PseudoElements["After"] = "after";
|
|
9513
|
+
PseudoElements["Before"] = "before";
|
|
9514
|
+
})(PseudoElements || (PseudoElements = {}));
|
|
9515
|
+
const PSEUDO_ELEMENT_NAMES = new Set([
|
|
9516
|
+
PseudoElements.After,
|
|
9517
|
+
PseudoElements.Before,
|
|
9518
|
+
]);
|
|
9519
|
+
const LEGACY_MATCHES_CSS_NAMES = new Set([
|
|
9520
|
+
PseudoClasses.MatchesCssAfter,
|
|
9521
|
+
PseudoClasses.MatchesCssBefore,
|
|
9522
|
+
]);
|
|
9523
|
+
const LEGACY_EXT_CSS_INDICATOR_PSEUDO_NAMES = new Set([
|
|
9524
|
+
PseudoClasses.Not,
|
|
9525
|
+
PseudoClasses.MatchesCssBefore,
|
|
9526
|
+
PseudoClasses.MatchesCssAfter,
|
|
9527
|
+
]);
|
|
9528
|
+
const CSS_CONVERSION_INDICATOR_PSEUDO_NAMES = new Set([
|
|
9529
|
+
PseudoClasses.AbpContains,
|
|
9530
|
+
PseudoClasses.AbpHas,
|
|
9531
|
+
PseudoClasses.HasText,
|
|
9532
|
+
]);
|
|
8865
9533
|
/**
|
|
8866
9534
|
* Converts some pseudo-classes to pseudo-elements. For example:
|
|
8867
9535
|
* - `:before` → `::before`
|
|
8868
9536
|
*
|
|
8869
9537
|
* @param selectorList Selector list to convert
|
|
8870
|
-
* @returns
|
|
9538
|
+
* @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
|
|
9539
|
+
* the converted node, and its `isConverted` flag indicates whether the original node was converted.
|
|
9540
|
+
* If the node was not converted, the result will contain the original node with the same object reference
|
|
8871
9541
|
*/
|
|
8872
9542
|
function convertToPseudoElements(selectorList) {
|
|
8873
|
-
//
|
|
8874
|
-
const
|
|
9543
|
+
// Check conversion indications before doing any heavy work
|
|
9544
|
+
const hasIndicator = ecssTree.find(
|
|
9545
|
+
// TODO: Need to improve CSSTree types, until then we need to use any type here
|
|
9546
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
9547
|
+
selectorList, (node) => node.type === exports.CssTreeNodeType.PseudoClassSelector && PSEUDO_ELEMENT_NAMES.has(node.name));
|
|
9548
|
+
if (!hasIndicator) {
|
|
9549
|
+
return createConversionResult(selectorList, false);
|
|
9550
|
+
}
|
|
9551
|
+
// Make a clone of the selector list to avoid modifying the original one,
|
|
9552
|
+
// then convert & return the cloned version
|
|
9553
|
+
const selectorListClone = clone(selectorList);
|
|
9554
|
+
// TODO: Need to improve CSSTree types, until then we need to use any type here
|
|
9555
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
8875
9556
|
ecssTree.walk(selectorListClone, {
|
|
9557
|
+
// TODO: Need to improve CSSTree types, until then we need to use any type here
|
|
9558
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
8876
9559
|
leave: (node) => {
|
|
8877
9560
|
if (node.type === exports.CssTreeNodeType.PseudoClassSelector) {
|
|
8878
|
-
//
|
|
8879
|
-
//
|
|
8880
|
-
|
|
9561
|
+
// If the pseudo-class is `:before` or `:after`, then we should
|
|
9562
|
+
// convert the node type to pseudo-element:
|
|
9563
|
+
// :after → ::after
|
|
9564
|
+
// :before → ::before
|
|
9565
|
+
if (PSEUDO_ELEMENT_NAMES.has(node.name)) {
|
|
8881
9566
|
Object.assign(node, {
|
|
8882
9567
|
...node,
|
|
8883
9568
|
type: exports.CssTreeNodeType.PseudoElementSelector,
|
|
@@ -8886,7 +9571,7 @@ function convertToPseudoElements(selectorList) {
|
|
|
8886
9571
|
}
|
|
8887
9572
|
},
|
|
8888
9573
|
});
|
|
8889
|
-
return selectorListClone;
|
|
9574
|
+
return createConversionResult(selectorListClone, true);
|
|
8890
9575
|
}
|
|
8891
9576
|
/**
|
|
8892
9577
|
* Converts legacy Extended CSS `matches-css-before` and `matches-css-after`
|
|
@@ -8895,33 +9580,36 @@ function convertToPseudoElements(selectorList) {
|
|
|
8895
9580
|
* - `:matches-css-after(...)` → `:matches-css(after, ...)`
|
|
8896
9581
|
*
|
|
8897
9582
|
* @param node Node to convert
|
|
9583
|
+
* @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
|
|
9584
|
+
* the converted node, and its `isConverted` flag indicates whether the original node was converted.
|
|
9585
|
+
* If the node was not converted, the result will contain the original node with the same object reference
|
|
8898
9586
|
* @throws If the node is invalid
|
|
8899
9587
|
*/
|
|
8900
9588
|
function convertLegacyMatchesCss(node) {
|
|
8901
|
-
|
|
8902
|
-
if (
|
|
8903
|
-
|
|
8904
|
-
|
|
8905
|
-
|
|
8906
|
-
|
|
8907
|
-
|
|
8908
|
-
|
|
8909
|
-
|
|
8910
|
-
|
|
8911
|
-
|
|
8912
|
-
|
|
8913
|
-
|
|
8914
|
-
|
|
8915
|
-
|
|
8916
|
-
|
|
8917
|
-
|
|
8918
|
-
|
|
8919
|
-
|
|
8920
|
-
|
|
8921
|
-
|
|
8922
|
-
|
|
8923
|
-
|
|
8924
|
-
|
|
9589
|
+
// Check conversion indications before doing any heavy work
|
|
9590
|
+
if (node.type !== exports.CssTreeNodeType.PseudoClassSelector || !LEGACY_MATCHES_CSS_NAMES.has(node.name)) {
|
|
9591
|
+
return createConversionResult(node, false);
|
|
9592
|
+
}
|
|
9593
|
+
const nodeClone = clone(node);
|
|
9594
|
+
if (!nodeClone.children || nodeClone.children.length < 1) {
|
|
9595
|
+
throw new Error(`Invalid ${node.name} pseudo-class: missing argument`);
|
|
9596
|
+
}
|
|
9597
|
+
// Rename the pseudo-class
|
|
9598
|
+
nodeClone.name = PseudoClasses.MatchesCss;
|
|
9599
|
+
// Remove the 'matches-css-' prefix to get the direction
|
|
9600
|
+
const direction = node.name.substring(PseudoClasses.MatchesCss.length + 1);
|
|
9601
|
+
// Add the direction to the first raw argument
|
|
9602
|
+
const arg = nodeClone.children[0];
|
|
9603
|
+
// Check argument
|
|
9604
|
+
if (!arg) {
|
|
9605
|
+
throw new Error(`Invalid ${node.name} pseudo-class: argument shouldn't be null`);
|
|
9606
|
+
}
|
|
9607
|
+
if (arg.type !== exports.CssTreeNodeType.Raw) {
|
|
9608
|
+
throw new Error(`Invalid ${node.name} pseudo-class: unexpected argument type`);
|
|
9609
|
+
}
|
|
9610
|
+
// Add the direction as the first argument
|
|
9611
|
+
arg.value = `${direction},${arg.value}`;
|
|
9612
|
+
return createConversionResult(nodeClone, true);
|
|
8925
9613
|
}
|
|
8926
9614
|
/**
|
|
8927
9615
|
* Converts legacy Extended CSS selectors to the modern Extended CSS syntax.
|
|
@@ -8931,16 +9619,40 @@ function convertLegacyMatchesCss(node) {
|
|
|
8931
9619
|
* - `[-ext-matches-css-before=...]` → `:matches-css(before, ...)`
|
|
8932
9620
|
*
|
|
8933
9621
|
* @param selectorList Selector list AST to convert
|
|
8934
|
-
* @returns
|
|
9622
|
+
* @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
|
|
9623
|
+
* the converted node, and its `isConverted` flag indicates whether the original node was converted.
|
|
9624
|
+
* If the node was not converted, the result will contain the original node with the same object reference
|
|
8935
9625
|
*/
|
|
8936
9626
|
function convertFromLegacyExtendedCss(selectorList) {
|
|
8937
|
-
//
|
|
8938
|
-
const
|
|
9627
|
+
// Check conversion indications before doing any heavy work
|
|
9628
|
+
const hasIndicator = ecssTree.find(
|
|
9629
|
+
// TODO: Need to improve CSSTree types, until then we need to use any type here
|
|
9630
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
9631
|
+
selectorList, (node) => {
|
|
9632
|
+
if (node.type === exports.CssTreeNodeType.PseudoClassSelector) {
|
|
9633
|
+
return LEGACY_EXT_CSS_INDICATOR_PSEUDO_NAMES.has(node.name);
|
|
9634
|
+
}
|
|
9635
|
+
if (node.type === exports.CssTreeNodeType.AttributeSelector) {
|
|
9636
|
+
return node.name.name.startsWith(LEGACY_EXT_CSS_ATTRIBUTE_PREFIX);
|
|
9637
|
+
}
|
|
9638
|
+
return false;
|
|
9639
|
+
});
|
|
9640
|
+
if (!hasIndicator) {
|
|
9641
|
+
return createConversionResult(selectorList, false);
|
|
9642
|
+
}
|
|
9643
|
+
const selectorListClone = clone(selectorList);
|
|
9644
|
+
// TODO: Need to improve CSSTree types, until then we need to use any type here
|
|
9645
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
8939
9646
|
ecssTree.walk(selectorListClone, {
|
|
9647
|
+
// TODO: Need to improve CSSTree types, until then we need to use any type here
|
|
9648
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
8940
9649
|
leave: (node) => {
|
|
8941
9650
|
// :matches-css-before(arg) → :matches-css(before,arg)
|
|
8942
9651
|
// :matches-css-after(arg) → :matches-css(after,arg)
|
|
8943
|
-
convertLegacyMatchesCss(node);
|
|
9652
|
+
const convertedLegacyExtCss = convertLegacyMatchesCss(node);
|
|
9653
|
+
if (convertedLegacyExtCss.isConverted) {
|
|
9654
|
+
Object.assign(node, convertedLegacyExtCss.result);
|
|
9655
|
+
}
|
|
8944
9656
|
// [-ext-name=...] → :name(...)
|
|
8945
9657
|
// [-ext-name='...'] → :name(...)
|
|
8946
9658
|
// [-ext-name="..."] → :name(...)
|
|
@@ -8954,7 +9666,7 @@ function convertFromLegacyExtendedCss(selectorList) {
|
|
|
8954
9666
|
// Remove the '-ext-' prefix to get the pseudo-class name
|
|
8955
9667
|
const name = node.name.name.substring(LEGACY_EXT_CSS_ATTRIBUTE_PREFIX.length);
|
|
8956
9668
|
// Prepare the children list for the pseudo-class node
|
|
8957
|
-
const children =
|
|
9669
|
+
const children = [];
|
|
8958
9670
|
// TODO: Change String node to Raw node to drop the quotes.
|
|
8959
9671
|
// The structure of the node is the same, just the type
|
|
8960
9672
|
// is different and generate() will generate the quotes
|
|
@@ -8967,7 +9679,7 @@ function convertFromLegacyExtendedCss(selectorList) {
|
|
|
8967
9679
|
// For example, if the input is [-ext-has="> .selector"], then
|
|
8968
9680
|
// we need to parse "> .selector" as a selector instead of string
|
|
8969
9681
|
// it as a raw value
|
|
8970
|
-
if ([
|
|
9682
|
+
if ([PseudoClasses.Has, PseudoClasses.Not].includes(name)) {
|
|
8971
9683
|
// Get the value of the attribute selector
|
|
8972
9684
|
const { value } = node;
|
|
8973
9685
|
// If the value is an identifier, then simply push it to the
|
|
@@ -8977,10 +9689,12 @@ function convertFromLegacyExtendedCss(selectorList) {
|
|
|
8977
9689
|
}
|
|
8978
9690
|
else if (value.type === exports.CssTreeNodeType.String) {
|
|
8979
9691
|
// Parse the value as a selector
|
|
8980
|
-
const parsedChildren = CssTree.
|
|
9692
|
+
const parsedChildren = CssTree.parsePlain(value.value, exports.CssTreeParserContext.selectorList);
|
|
8981
9693
|
// Don't forget convert the parsed AST again, because
|
|
8982
9694
|
// it was a raw string before
|
|
8983
|
-
|
|
9695
|
+
const convertedChildren = convertFromLegacyExtendedCss(parsedChildren);
|
|
9696
|
+
// Push the converted children to the list
|
|
9697
|
+
children.push(convertedChildren.result);
|
|
8984
9698
|
}
|
|
8985
9699
|
}
|
|
8986
9700
|
else {
|
|
@@ -9007,14 +9721,12 @@ function convertFromLegacyExtendedCss(selectorList) {
|
|
|
9007
9721
|
children,
|
|
9008
9722
|
};
|
|
9009
9723
|
// Handle this case: [-ext-matches-css-before=...] → :matches-css(before,...)
|
|
9010
|
-
convertLegacyMatchesCss(pseudoNode);
|
|
9011
|
-
|
|
9012
|
-
// keep the reference to the original node
|
|
9013
|
-
Object.assign(node, pseudoNode);
|
|
9724
|
+
const convertedPseudoNode = convertLegacyMatchesCss(pseudoNode);
|
|
9725
|
+
Object.assign(node, convertedPseudoNode.isConverted ? convertedPseudoNode.result : pseudoNode);
|
|
9014
9726
|
}
|
|
9015
9727
|
},
|
|
9016
9728
|
});
|
|
9017
|
-
return selectorListClone;
|
|
9729
|
+
return createConversionResult(selectorListClone, true);
|
|
9018
9730
|
}
|
|
9019
9731
|
/**
|
|
9020
9732
|
* CSS selector converter
|
|
@@ -9026,32 +9738,51 @@ class CssSelectorConverter extends ConverterBase {
|
|
|
9026
9738
|
* Converts Extended CSS elements to AdGuard-compatible ones
|
|
9027
9739
|
*
|
|
9028
9740
|
* @param selectorList Selector list to convert
|
|
9029
|
-
* @returns
|
|
9741
|
+
* @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
|
|
9742
|
+
* the converted node, and its `isConverted` flag indicates whether the original node was converted.
|
|
9743
|
+
* If the node was not converted, the result will contain the original node with the same object reference
|
|
9030
9744
|
* @throws If the rule is invalid or incompatible
|
|
9031
9745
|
*/
|
|
9032
9746
|
static convertToAdg(selectorList) {
|
|
9033
9747
|
// First, convert
|
|
9034
9748
|
// - legacy Extended CSS selectors to the modern Extended CSS syntax and
|
|
9035
9749
|
// - some pseudo-classes to pseudo-elements
|
|
9036
|
-
const
|
|
9750
|
+
const legacyExtCssConverted = convertFromLegacyExtendedCss(selectorList);
|
|
9751
|
+
const pseudoElementsConverted = convertToPseudoElements(legacyExtCssConverted.result);
|
|
9752
|
+
const hasIndicator = legacyExtCssConverted.isConverted || pseudoElementsConverted.isConverted || ecssTree.find(
|
|
9753
|
+
// TODO: Need to improve CSSTree types, until then we need to use any type here
|
|
9754
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
9755
|
+
selectorList,
|
|
9756
|
+
// eslint-disable-next-line max-len
|
|
9757
|
+
(node) => node.type === exports.CssTreeNodeType.PseudoClassSelector && CSS_CONVERSION_INDICATOR_PSEUDO_NAMES.has(node.name));
|
|
9758
|
+
if (!hasIndicator) {
|
|
9759
|
+
return createConversionResult(selectorList, false);
|
|
9760
|
+
}
|
|
9761
|
+
const selectorListClone = legacyExtCssConverted.isConverted || pseudoElementsConverted.isConverted
|
|
9762
|
+
? pseudoElementsConverted.result
|
|
9763
|
+
: clone(selectorList);
|
|
9037
9764
|
// Then, convert some Extended CSS pseudo-classes to AdGuard-compatible ones
|
|
9765
|
+
// TODO: Need to improve CSSTree types, until then we need to use any type here
|
|
9766
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
9038
9767
|
ecssTree.walk(selectorListClone, {
|
|
9768
|
+
// TODO: Need to improve CSSTree types, until then we need to use any type here
|
|
9769
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
9039
9770
|
leave: (node) => {
|
|
9040
9771
|
if (node.type === exports.CssTreeNodeType.PseudoClassSelector) {
|
|
9041
9772
|
// :-abp-contains(...) → :contains(...)
|
|
9042
9773
|
// :has-text(...) → :contains(...)
|
|
9043
|
-
if (node.name ===
|
|
9044
|
-
CssTree.renamePseudoClass(node,
|
|
9774
|
+
if (node.name === PseudoClasses.AbpContains || node.name === PseudoClasses.HasText) {
|
|
9775
|
+
CssTree.renamePseudoClass(node, PseudoClasses.Contains);
|
|
9045
9776
|
}
|
|
9046
9777
|
// :-abp-has(...) → :has(...)
|
|
9047
|
-
if (node.name ===
|
|
9048
|
-
CssTree.renamePseudoClass(node,
|
|
9778
|
+
if (node.name === PseudoClasses.AbpHas) {
|
|
9779
|
+
CssTree.renamePseudoClass(node, PseudoClasses.Has);
|
|
9049
9780
|
}
|
|
9050
9781
|
// TODO: check uBO's `:others()` and `:watch-attr()` pseudo-classes
|
|
9051
9782
|
}
|
|
9052
9783
|
},
|
|
9053
9784
|
});
|
|
9054
|
-
return selectorListClone;
|
|
9785
|
+
return createConversionResult(selectorListClone, true);
|
|
9055
9786
|
}
|
|
9056
9787
|
}
|
|
9057
9788
|
|
|
@@ -9068,27 +9799,39 @@ class CssInjectionRuleConverter extends RuleConverterBase {
|
|
|
9068
9799
|
* Converts a CSS injection rule to AdGuard format, if possible.
|
|
9069
9800
|
*
|
|
9070
9801
|
* @param rule Rule node to convert
|
|
9071
|
-
* @returns
|
|
9802
|
+
* @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
|
|
9803
|
+
* the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
|
|
9804
|
+
* If the rule was not converted, the result array will contain the original node with the same object reference
|
|
9072
9805
|
* @throws If the rule is invalid or cannot be converted
|
|
9073
9806
|
*/
|
|
9074
9807
|
static convertToAdg(rule) {
|
|
9075
|
-
|
|
9076
|
-
|
|
9808
|
+
const separator = rule.separator.value;
|
|
9809
|
+
let convertedSeparator = separator;
|
|
9077
9810
|
// Change the separator if the rule contains ExtendedCSS selectors
|
|
9078
|
-
if (CssTree.hasAnySelectorExtendedCssNode(
|
|
9079
|
-
|
|
9811
|
+
if (CssTree.hasAnySelectorExtendedCssNode(rule.body.selectorList) || rule.body.remove) {
|
|
9812
|
+
convertedSeparator = rule.exception
|
|
9080
9813
|
? exports.CosmeticRuleSeparator.AdgExtendedCssInjectionException
|
|
9081
9814
|
: exports.CosmeticRuleSeparator.AdgExtendedCssInjection;
|
|
9082
9815
|
}
|
|
9083
9816
|
else {
|
|
9084
|
-
|
|
9817
|
+
convertedSeparator = rule.exception
|
|
9085
9818
|
? exports.CosmeticRuleSeparator.AdgCssInjectionException
|
|
9086
9819
|
: exports.CosmeticRuleSeparator.AdgCssInjection;
|
|
9087
9820
|
}
|
|
9088
|
-
|
|
9089
|
-
|
|
9090
|
-
|
|
9091
|
-
|
|
9821
|
+
const convertedSelectorList = CssSelectorConverter.convertToAdg(rule.body.selectorList);
|
|
9822
|
+
// Check if the rule needs to be converted
|
|
9823
|
+
if (!(rule.syntax === exports.AdblockSyntax.Common || rule.syntax === exports.AdblockSyntax.Adg)
|
|
9824
|
+
|| separator !== convertedSeparator
|
|
9825
|
+
|| convertedSelectorList.isConverted) {
|
|
9826
|
+
// TODO: Replace with custom clone method
|
|
9827
|
+
const ruleClone = clone(rule);
|
|
9828
|
+
ruleClone.syntax = exports.AdblockSyntax.Adg;
|
|
9829
|
+
ruleClone.separator.value = convertedSeparator;
|
|
9830
|
+
ruleClone.body.selectorList = convertedSelectorList.result;
|
|
9831
|
+
return createNodeConversionResult([ruleClone], true);
|
|
9832
|
+
}
|
|
9833
|
+
// Otherwise, return the original rule
|
|
9834
|
+
return createNodeConversionResult([rule], false);
|
|
9092
9835
|
}
|
|
9093
9836
|
}
|
|
9094
9837
|
|
|
@@ -9105,27 +9848,39 @@ class ElementHidingRuleConverter extends RuleConverterBase {
|
|
|
9105
9848
|
* Converts an element hiding rule to AdGuard format, if possible.
|
|
9106
9849
|
*
|
|
9107
9850
|
* @param rule Rule node to convert
|
|
9108
|
-
* @returns
|
|
9851
|
+
* @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
|
|
9852
|
+
* the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
|
|
9853
|
+
* If the rule was not converted, the result array will contain the original node with the same object reference
|
|
9109
9854
|
* @throws If the rule is invalid or cannot be converted
|
|
9110
9855
|
*/
|
|
9111
9856
|
static convertToAdg(rule) {
|
|
9112
|
-
|
|
9113
|
-
|
|
9857
|
+
const separator = rule.separator.value;
|
|
9858
|
+
let convertedSeparator = separator;
|
|
9114
9859
|
// Change the separator if the rule contains ExtendedCSS selectors
|
|
9115
|
-
if (CssTree.hasAnySelectorExtendedCssNode(
|
|
9116
|
-
|
|
9860
|
+
if (CssTree.hasAnySelectorExtendedCssNode(rule.body.selectorList)) {
|
|
9861
|
+
convertedSeparator = rule.exception
|
|
9117
9862
|
? exports.CosmeticRuleSeparator.ExtendedElementHidingException
|
|
9118
9863
|
: exports.CosmeticRuleSeparator.ExtendedElementHiding;
|
|
9119
9864
|
}
|
|
9120
9865
|
else {
|
|
9121
|
-
|
|
9866
|
+
convertedSeparator = rule.exception
|
|
9122
9867
|
? exports.CosmeticRuleSeparator.ElementHidingException
|
|
9123
9868
|
: exports.CosmeticRuleSeparator.ElementHiding;
|
|
9124
9869
|
}
|
|
9125
|
-
|
|
9126
|
-
|
|
9127
|
-
|
|
9128
|
-
|
|
9870
|
+
const convertedSelectorList = CssSelectorConverter.convertToAdg(rule.body.selectorList);
|
|
9871
|
+
// Check if the rule needs to be converted
|
|
9872
|
+
if (!(rule.syntax === exports.AdblockSyntax.Common || rule.syntax === exports.AdblockSyntax.Adg)
|
|
9873
|
+
|| separator !== convertedSeparator
|
|
9874
|
+
|| convertedSelectorList.isConverted) {
|
|
9875
|
+
// TODO: Replace with custom clone method
|
|
9876
|
+
const ruleClone = clone(rule);
|
|
9877
|
+
ruleClone.syntax = exports.AdblockSyntax.Adg;
|
|
9878
|
+
ruleClone.separator.value = convertedSeparator;
|
|
9879
|
+
ruleClone.body.selectorList = convertedSelectorList.result;
|
|
9880
|
+
return createNodeConversionResult([ruleClone], true);
|
|
9881
|
+
}
|
|
9882
|
+
// Otherwise, return the original rule
|
|
9883
|
+
return createNodeConversionResult([rule], false);
|
|
9129
9884
|
}
|
|
9130
9885
|
}
|
|
9131
9886
|
|
|
@@ -9153,7 +9908,7 @@ function createNetworkRuleNode(pattern, modifiers = undefined, exception = false
|
|
|
9153
9908
|
},
|
|
9154
9909
|
};
|
|
9155
9910
|
if (!isUndefined(modifiers)) {
|
|
9156
|
-
result.modifiers =
|
|
9911
|
+
result.modifiers = clone(modifiers);
|
|
9157
9912
|
}
|
|
9158
9913
|
return result;
|
|
9159
9914
|
}
|
|
@@ -9173,32 +9928,37 @@ class HeaderRemovalRuleConverter extends RuleConverterBase {
|
|
|
9173
9928
|
* Converts a header removal rule to AdGuard syntax, if possible.
|
|
9174
9929
|
*
|
|
9175
9930
|
* @param rule Rule node to convert
|
|
9176
|
-
* @returns
|
|
9931
|
+
* @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
|
|
9932
|
+
* the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
|
|
9933
|
+
* If the rule was not converted, the result array will contain the original node with the same object reference
|
|
9177
9934
|
* @throws If the rule is invalid or cannot be converted
|
|
9935
|
+
* @example
|
|
9936
|
+
* If the input rule is:
|
|
9937
|
+
* ```adblock
|
|
9938
|
+
* example.com##^responseheader(header-name)
|
|
9939
|
+
* ```
|
|
9940
|
+
* The output will be:
|
|
9941
|
+
* ```adblock
|
|
9942
|
+
* ||example.com^$removeheader=header-name
|
|
9943
|
+
* ```
|
|
9178
9944
|
*/
|
|
9179
9945
|
static convertToAdg(rule) {
|
|
9180
|
-
// Clone the provided AST node to avoid side effects
|
|
9181
|
-
const ruleNode = cloneDeep(rule);
|
|
9182
9946
|
// TODO: Add support for ABP syntax once it starts supporting header removal rules
|
|
9183
|
-
//
|
|
9184
|
-
if (
|
|
9185
|
-
||
|
|
9186
|
-
||
|
|
9187
|
-
||
|
|
9188
|
-
|
|
9947
|
+
// Leave the rule as is if it's not a header removal rule
|
|
9948
|
+
if (rule.category !== exports.RuleCategory.Cosmetic
|
|
9949
|
+
|| rule.type !== exports.CosmeticRuleType.HtmlFilteringRule
|
|
9950
|
+
|| rule.body.body.type !== exports.CssTreeNodeType.Function
|
|
9951
|
+
|| rule.body.body.name !== UBO_RESPONSEHEADER_MARKER) {
|
|
9952
|
+
return createNodeConversionResult([rule], false);
|
|
9189
9953
|
}
|
|
9190
9954
|
// Prepare network rule pattern
|
|
9191
|
-
|
|
9192
|
-
if (
|
|
9955
|
+
const pattern = [];
|
|
9956
|
+
if (rule.domains.children.length === 1) {
|
|
9193
9957
|
// If the rule has only one domain, we can use a simple network rule pattern:
|
|
9194
9958
|
// ||single-domain-from-the-rule^
|
|
9195
|
-
pattern
|
|
9196
|
-
ADBLOCK_URL_START,
|
|
9197
|
-
ruleNode.domains.children[0].value,
|
|
9198
|
-
ADBLOCK_URL_SEPARATOR,
|
|
9199
|
-
].join(EMPTY);
|
|
9959
|
+
pattern.push(ADBLOCK_URL_START, rule.domains.children[0].value, ADBLOCK_URL_SEPARATOR);
|
|
9200
9960
|
}
|
|
9201
|
-
else if (
|
|
9961
|
+
else if (rule.domains.children.length > 1) {
|
|
9202
9962
|
// TODO: Add support for multiple domains, for example:
|
|
9203
9963
|
// example.com,example.org,example.net##^responseheader(header-name)
|
|
9204
9964
|
// We should consider allowing $domain with $removeheader modifier,
|
|
@@ -9208,13 +9968,13 @@ class HeaderRemovalRuleConverter extends RuleConverterBase {
|
|
|
9208
9968
|
}
|
|
9209
9969
|
// Prepare network rule modifiers
|
|
9210
9970
|
const modifiers = createModifierListNode();
|
|
9211
|
-
modifiers.children.push(createModifierNode(ADG_REMOVEHEADER_MODIFIER, CssTree.
|
|
9971
|
+
modifiers.children.push(createModifierNode(ADG_REMOVEHEADER_MODIFIER, CssTree.generateFunctionPlainValue(rule.body.body)));
|
|
9212
9972
|
// Construct the network rule
|
|
9213
|
-
return [
|
|
9214
|
-
createNetworkRuleNode(pattern, modifiers,
|
|
9973
|
+
return createNodeConversionResult([
|
|
9974
|
+
createNetworkRuleNode(pattern.join(EMPTY), modifiers,
|
|
9215
9975
|
// Copy the exception flag
|
|
9216
|
-
|
|
9217
|
-
];
|
|
9976
|
+
rule.exception, exports.AdblockSyntax.Adg),
|
|
9977
|
+
], true);
|
|
9218
9978
|
}
|
|
9219
9979
|
}
|
|
9220
9980
|
|
|
@@ -9231,54 +9991,80 @@ class CosmeticRuleConverter extends RuleConverterBase {
|
|
|
9231
9991
|
* Converts a cosmetic rule to AdGuard syntax, if possible.
|
|
9232
9992
|
*
|
|
9233
9993
|
* @param rule Rule node to convert
|
|
9234
|
-
* @returns
|
|
9994
|
+
* @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
|
|
9995
|
+
* the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
|
|
9996
|
+
* If the rule was not converted, the result array will contain the original node with the same object reference
|
|
9235
9997
|
* @throws If the rule is invalid or cannot be converted
|
|
9236
9998
|
*/
|
|
9237
9999
|
static convertToAdg(rule) {
|
|
9238
|
-
|
|
9239
|
-
const ruleNode = cloneDeep(rule);
|
|
9240
|
-
// Convert cosmetic rule modifiers
|
|
9241
|
-
if (ruleNode.modifiers) {
|
|
9242
|
-
if (ruleNode.syntax === exports.AdblockSyntax.Ubo) {
|
|
9243
|
-
// uBO doesn't support this rule:
|
|
9244
|
-
// example.com##+js(set-constant.js, foo, bar):matches-path(/baz)
|
|
9245
|
-
if (ruleNode.type === exports.CosmeticRuleType.ScriptletInjectionRule) {
|
|
9246
|
-
throw new RuleConversionError('uBO scriptlet injection rules don\'t support cosmetic rule modifiers');
|
|
9247
|
-
}
|
|
9248
|
-
ruleNode.modifiers = AdgCosmeticRuleModifierConverter.convertFromUbo(ruleNode.modifiers);
|
|
9249
|
-
}
|
|
9250
|
-
else if (ruleNode.syntax === exports.AdblockSyntax.Abp) {
|
|
9251
|
-
// TODO: Implement once ABP starts supporting cosmetic rule modifiers
|
|
9252
|
-
throw new RuleConversionError('ABP don\'t support cosmetic rule modifiers');
|
|
9253
|
-
}
|
|
9254
|
-
}
|
|
10000
|
+
let subconverterResult;
|
|
9255
10001
|
// Convert cosmetic rule based on its type
|
|
9256
|
-
switch (
|
|
10002
|
+
switch (rule.type) {
|
|
9257
10003
|
case exports.CosmeticRuleType.ElementHidingRule:
|
|
9258
|
-
|
|
10004
|
+
subconverterResult = ElementHidingRuleConverter.convertToAdg(rule);
|
|
10005
|
+
break;
|
|
9259
10006
|
case exports.CosmeticRuleType.ScriptletInjectionRule:
|
|
9260
|
-
|
|
10007
|
+
subconverterResult = ScriptletRuleConverter.convertToAdg(rule);
|
|
10008
|
+
break;
|
|
9261
10009
|
case exports.CosmeticRuleType.CssInjectionRule:
|
|
9262
|
-
|
|
10010
|
+
subconverterResult = CssInjectionRuleConverter.convertToAdg(rule);
|
|
10011
|
+
break;
|
|
9263
10012
|
case exports.CosmeticRuleType.HtmlFilteringRule:
|
|
9264
10013
|
// Handle special case: uBO response header filtering rule
|
|
9265
|
-
if (
|
|
9266
|
-
&&
|
|
9267
|
-
|
|
10014
|
+
if (rule.body.body.type === exports.CssTreeNodeType.Function
|
|
10015
|
+
&& rule.body.body.name === UBO_RESPONSEHEADER_MARKER) {
|
|
10016
|
+
subconverterResult = HeaderRemovalRuleConverter.convertToAdg(rule);
|
|
9268
10017
|
}
|
|
9269
|
-
|
|
9270
|
-
|
|
10018
|
+
else {
|
|
10019
|
+
subconverterResult = HtmlRuleConverter.convertToAdg(rule);
|
|
10020
|
+
}
|
|
10021
|
+
break;
|
|
10022
|
+
// Note: Currently, only ADG supports JS injection rules, so we don't need to convert them
|
|
9271
10023
|
case exports.CosmeticRuleType.JsInjectionRule:
|
|
9272
|
-
|
|
10024
|
+
subconverterResult = createNodeConversionResult([rule], false);
|
|
10025
|
+
break;
|
|
9273
10026
|
default:
|
|
9274
10027
|
throw new RuleConversionError('Unsupported cosmetic rule type');
|
|
9275
10028
|
}
|
|
10029
|
+
let convertedModifiers;
|
|
10030
|
+
// Convert cosmetic rule modifiers, if any
|
|
10031
|
+
if (rule.modifiers) {
|
|
10032
|
+
if (rule.syntax === exports.AdblockSyntax.Ubo) {
|
|
10033
|
+
// uBO doesn't support this rule:
|
|
10034
|
+
// example.com##+js(set-constant.js, foo, bar):matches-path(/baz)
|
|
10035
|
+
if (rule.type === exports.CosmeticRuleType.ScriptletInjectionRule) {
|
|
10036
|
+
throw new RuleConversionError('uBO scriptlet injection rules don\'t support cosmetic rule modifiers');
|
|
10037
|
+
}
|
|
10038
|
+
convertedModifiers = AdgCosmeticRuleModifierConverter.convertFromUbo(rule.modifiers);
|
|
10039
|
+
}
|
|
10040
|
+
else if (rule.syntax === exports.AdblockSyntax.Abp) {
|
|
10041
|
+
// TODO: Implement once ABP starts supporting cosmetic rule modifiers
|
|
10042
|
+
throw new RuleConversionError('ABP don\'t support cosmetic rule modifiers');
|
|
10043
|
+
}
|
|
10044
|
+
}
|
|
10045
|
+
if ((subconverterResult.result.length > 1 || subconverterResult.isConverted)
|
|
10046
|
+
|| (convertedModifiers && convertedModifiers.isConverted)) {
|
|
10047
|
+
// Add modifier list to the subconverter result rules
|
|
10048
|
+
subconverterResult.result.forEach((subconverterRule) => {
|
|
10049
|
+
if (convertedModifiers && subconverterRule.category === exports.RuleCategory.Cosmetic) {
|
|
10050
|
+
// eslint-disable-next-line no-param-reassign
|
|
10051
|
+
subconverterRule.modifiers = convertedModifiers.result;
|
|
10052
|
+
}
|
|
10053
|
+
});
|
|
10054
|
+
return subconverterResult;
|
|
10055
|
+
}
|
|
10056
|
+
return createNodeConversionResult([rule], false);
|
|
9276
10057
|
}
|
|
9277
10058
|
}
|
|
9278
10059
|
|
|
9279
10060
|
/**
|
|
9280
10061
|
* @file Network rule modifier list converter.
|
|
9281
10062
|
*/
|
|
10063
|
+
// Since scriptlets library doesn't have ESM exports, we should import
|
|
10064
|
+
// the whole module and then extract the required functions from it here.
|
|
10065
|
+
// Otherwise importing AGTree will cause an error in ESM environment,
|
|
10066
|
+
// because scriptlets library doesn't support named exports.
|
|
10067
|
+
const { redirects } = scriptlets;
|
|
9282
10068
|
/**
|
|
9283
10069
|
* @see {@link https://adguard.com/kb/general/ad-filtering/create-own-filters/#csp-modifier}
|
|
9284
10070
|
*/
|
|
@@ -9339,17 +10125,16 @@ class NetworkRuleModifierListConverter extends ConverterBase {
|
|
|
9339
10125
|
* Converts a network rule modifier list to AdGuard format, if possible.
|
|
9340
10126
|
*
|
|
9341
10127
|
* @param modifierList Network rule modifier list node to convert
|
|
9342
|
-
* @returns
|
|
10128
|
+
* @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
|
|
10129
|
+
* the converted node, and its `isConverted` flag indicates whether the original node was converted.
|
|
10130
|
+
* If the node was not converted, the result will contain the original node with the same object reference
|
|
9343
10131
|
* @throws If the conversion is not possible
|
|
9344
10132
|
*/
|
|
9345
10133
|
static convertToAdg(modifierList) {
|
|
9346
|
-
|
|
9347
|
-
|
|
9348
|
-
|
|
9349
|
-
|
|
9350
|
-
const cspValues = [];
|
|
9351
|
-
modifierListNode.children.forEach((modifierNode) => {
|
|
9352
|
-
// Handle regular modifiers conversion and $csp modifiers collection
|
|
10134
|
+
const conversionMap = new MultiValueMap();
|
|
10135
|
+
// Special case: $csp modifier
|
|
10136
|
+
let cspCount = 0;
|
|
10137
|
+
modifierList.children.forEach((modifierNode, index) => {
|
|
9353
10138
|
const modifierConversions = ADG_CONVERSION_MAP.get(modifierNode.modifier.value);
|
|
9354
10139
|
if (modifierConversions) {
|
|
9355
10140
|
for (const modifierConversion of modifierConversions) {
|
|
@@ -9362,17 +10147,14 @@ class NetworkRuleModifierListConverter extends ConverterBase {
|
|
|
9362
10147
|
const value = modifierConversion.value
|
|
9363
10148
|
? modifierConversion.value(modifierNode.value?.value)
|
|
9364
10149
|
: modifierNode.value?.value;
|
|
9365
|
-
if
|
|
9366
|
-
|
|
9367
|
-
|
|
10150
|
+
// Check if the name or the value is different from the original modifier
|
|
10151
|
+
// If so, add the converted modifier to the list
|
|
10152
|
+
if (name !== modifierNode.modifier.value || value !== modifierNode.value?.value) {
|
|
10153
|
+
conversionMap.add(index, createModifierNode(name, value, exception));
|
|
9368
10154
|
}
|
|
9369
|
-
|
|
9370
|
-
|
|
9371
|
-
|
|
9372
|
-
const existingModifier = convertedModifierList.children.find((m) => m.modifier.value === name && m.exception === exception && m.value?.value === value);
|
|
9373
|
-
if (!existingModifier) {
|
|
9374
|
-
convertedModifierList.children.push(createModifierNode(name, value, exception));
|
|
9375
|
-
}
|
|
10155
|
+
// Special case: $csp modifier
|
|
10156
|
+
if (name === CSP_MODIFIER) {
|
|
10157
|
+
cspCount += 1;
|
|
9376
10158
|
}
|
|
9377
10159
|
}
|
|
9378
10160
|
return;
|
|
@@ -9394,27 +10176,53 @@ class NetworkRuleModifierListConverter extends ConverterBase {
|
|
|
9394
10176
|
: modifierNode.modifier.value;
|
|
9395
10177
|
// Try to convert the redirect resource name to ADG format
|
|
9396
10178
|
// This function returns undefined if the resource name is unknown
|
|
9397
|
-
const convertedRedirectResource =
|
|
9398
|
-
|
|
9399
|
-
// If
|
|
9400
|
-
|
|
9401
|
-
|
|
9402
|
-
|
|
9403
|
-
|
|
9404
|
-
|
|
9405
|
-
|
|
9406
|
-
|
|
9407
|
-
&& m.exception === modifierNode.exception
|
|
9408
|
-
&& m.value?.value === modifierNode.value?.value);
|
|
9409
|
-
if (!existingModifier) {
|
|
9410
|
-
convertedModifierList.children.push(modifierNode);
|
|
10179
|
+
const convertedRedirectResource = redirects.convertRedirectNameToAdg(redirectResource);
|
|
10180
|
+
// Check if the modifier name or the redirect resource name is different from the original modifier
|
|
10181
|
+
// If so, add the converted modifier to the list
|
|
10182
|
+
if (modifierName !== modifierNode.modifier.value
|
|
10183
|
+
|| (convertedRedirectResource !== undefined && convertedRedirectResource !== redirectResource)) {
|
|
10184
|
+
conversionMap.add(index, createModifierNode(modifierName,
|
|
10185
|
+
// If the redirect resource name is unknown, fall back to the original one
|
|
10186
|
+
// Later, the validator will throw an error if the resource name is invalid
|
|
10187
|
+
convertedRedirectResource || redirectResource, modifierNode.exception));
|
|
10188
|
+
}
|
|
9411
10189
|
}
|
|
9412
10190
|
});
|
|
9413
|
-
//
|
|
9414
|
-
if (
|
|
9415
|
-
|
|
10191
|
+
// Prepare the result if there are any converted modifiers or $csp modifiers
|
|
10192
|
+
if (conversionMap.size || cspCount) {
|
|
10193
|
+
const modifierListClone = cloneModifierListNode(modifierList);
|
|
10194
|
+
// Replace the original modifiers with the converted ones
|
|
10195
|
+
// One modifier may be replaced with multiple modifiers, so we need to flatten the array
|
|
10196
|
+
modifierListClone.children = modifierListClone.children.map((modifierNode, index) => {
|
|
10197
|
+
const conversionRecord = conversionMap.get(index);
|
|
10198
|
+
if (conversionRecord) {
|
|
10199
|
+
return conversionRecord;
|
|
10200
|
+
}
|
|
10201
|
+
return modifierNode;
|
|
10202
|
+
}).flat();
|
|
10203
|
+
// Special case: $csp modifier: merge multiple $csp modifiers into one
|
|
10204
|
+
// and put it at the end of the modifier list
|
|
10205
|
+
if (cspCount) {
|
|
10206
|
+
const cspValues = [];
|
|
10207
|
+
modifierListClone.children = modifierListClone.children.filter((modifierNode) => {
|
|
10208
|
+
if (modifierNode.modifier.value === CSP_MODIFIER) {
|
|
10209
|
+
if (!modifierNode.value?.value) {
|
|
10210
|
+
throw new RuleConversionError('$csp modifier value is missing');
|
|
10211
|
+
}
|
|
10212
|
+
cspValues.push(modifierNode.value?.value);
|
|
10213
|
+
return false;
|
|
10214
|
+
}
|
|
10215
|
+
return true;
|
|
10216
|
+
});
|
|
10217
|
+
modifierListClone.children.push(createModifierNode(CSP_MODIFIER, cspValues.join(CSP_SEPARATOR)));
|
|
10218
|
+
}
|
|
10219
|
+
// Before returning the result, remove duplicated modifiers
|
|
10220
|
+
modifierListClone.children = modifierListClone.children.filter((modifierNode, index, self) => self.findIndex((m) => m.modifier.value === modifierNode.modifier.value
|
|
10221
|
+
&& m.exception === modifierNode.exception
|
|
10222
|
+
&& m.value?.value === modifierNode.value?.value) === index);
|
|
10223
|
+
return createConversionResult(modifierListClone, true);
|
|
9416
10224
|
}
|
|
9417
|
-
return
|
|
10225
|
+
return createConversionResult(modifierList, false);
|
|
9418
10226
|
}
|
|
9419
10227
|
}
|
|
9420
10228
|
|
|
@@ -9431,17 +10239,35 @@ class NetworkRuleConverter extends RuleConverterBase {
|
|
|
9431
10239
|
* Converts a network rule to AdGuard format, if possible.
|
|
9432
10240
|
*
|
|
9433
10241
|
* @param rule Rule node to convert
|
|
9434
|
-
* @returns
|
|
10242
|
+
* @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
|
|
10243
|
+
* the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
|
|
10244
|
+
* If the rule was not converted, the result array will contain the original node with the same object reference
|
|
9435
10245
|
* @throws If the rule is invalid or cannot be converted
|
|
9436
10246
|
*/
|
|
9437
10247
|
static convertToAdg(rule) {
|
|
9438
|
-
|
|
9439
|
-
|
|
9440
|
-
|
|
9441
|
-
|
|
9442
|
-
|
|
10248
|
+
if (rule.modifiers) {
|
|
10249
|
+
const modifiers = NetworkRuleModifierListConverter.convertToAdg(rule.modifiers);
|
|
10250
|
+
// If the object reference is different, it means that the modifiers were converted
|
|
10251
|
+
// In this case, we should clone the entire rule and replace the modifiers with the converted ones
|
|
10252
|
+
if (modifiers.isConverted) {
|
|
10253
|
+
return {
|
|
10254
|
+
result: [{
|
|
10255
|
+
category: exports.RuleCategory.Network,
|
|
10256
|
+
type: 'NetworkRule',
|
|
10257
|
+
syntax: rule.syntax,
|
|
10258
|
+
exception: rule.exception,
|
|
10259
|
+
pattern: {
|
|
10260
|
+
type: 'Value',
|
|
10261
|
+
value: rule.pattern.value,
|
|
10262
|
+
},
|
|
10263
|
+
modifiers: modifiers.result,
|
|
10264
|
+
}],
|
|
10265
|
+
isConverted: true,
|
|
10266
|
+
};
|
|
10267
|
+
}
|
|
9443
10268
|
}
|
|
9444
|
-
return
|
|
10269
|
+
// If the modifiers were not converted, return the original rule
|
|
10270
|
+
return createNodeConversionResult([rule], false);
|
|
9445
10271
|
}
|
|
9446
10272
|
}
|
|
9447
10273
|
|
|
@@ -9462,48 +10288,27 @@ class RuleConverter extends RuleConverterBase {
|
|
|
9462
10288
|
* Converts an adblock filtering rule to AdGuard format, if possible.
|
|
9463
10289
|
*
|
|
9464
10290
|
* @param rule Rule node to convert
|
|
9465
|
-
* @returns
|
|
10291
|
+
* @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
|
|
10292
|
+
* the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
|
|
10293
|
+
* If the rule was not converted, the result array will contain the original node with the same object reference
|
|
9466
10294
|
* @throws If the rule is invalid or cannot be converted
|
|
9467
10295
|
*/
|
|
9468
10296
|
static convertToAdg(rule) {
|
|
9469
|
-
// Clone the provided AST node to avoid side effects
|
|
9470
|
-
const ruleNode = cloneDeep(rule);
|
|
9471
10297
|
// Delegate conversion to the corresponding sub-converter
|
|
9472
10298
|
// based on the rule category
|
|
9473
|
-
switch (
|
|
10299
|
+
switch (rule.category) {
|
|
9474
10300
|
case exports.RuleCategory.Comment:
|
|
9475
|
-
return CommentRuleConverter.convertToAdg(
|
|
10301
|
+
return CommentRuleConverter.convertToAdg(rule);
|
|
9476
10302
|
case exports.RuleCategory.Cosmetic:
|
|
9477
|
-
return CosmeticRuleConverter.convertToAdg(
|
|
10303
|
+
return CosmeticRuleConverter.convertToAdg(rule);
|
|
9478
10304
|
case exports.RuleCategory.Network:
|
|
9479
|
-
return NetworkRuleConverter.convertToAdg(
|
|
10305
|
+
return NetworkRuleConverter.convertToAdg(rule);
|
|
9480
10306
|
default:
|
|
9481
|
-
throw new RuleConversionError(`Unknown rule category: ${
|
|
10307
|
+
throw new RuleConversionError(`Unknown rule category: ${rule.category}`);
|
|
9482
10308
|
}
|
|
9483
10309
|
}
|
|
9484
10310
|
}
|
|
9485
10311
|
|
|
9486
|
-
/**
|
|
9487
|
-
* @file Utility functions for working with filter list nodes
|
|
9488
|
-
*/
|
|
9489
|
-
/**
|
|
9490
|
-
* Creates a filter list node
|
|
9491
|
-
*
|
|
9492
|
-
* @param rules Rules to put in the list (optional, defaults to an empty list)
|
|
9493
|
-
* @returns Filter list node
|
|
9494
|
-
*/
|
|
9495
|
-
function createFilterListNode(rules = []) {
|
|
9496
|
-
const result = {
|
|
9497
|
-
type: 'FilterList',
|
|
9498
|
-
children: [],
|
|
9499
|
-
};
|
|
9500
|
-
// We need to clone the rules to avoid side effects
|
|
9501
|
-
if (rules.length > 0) {
|
|
9502
|
-
result.children = cloneDeep(rules);
|
|
9503
|
-
}
|
|
9504
|
-
return result;
|
|
9505
|
-
}
|
|
9506
|
-
|
|
9507
10312
|
/**
|
|
9508
10313
|
* @file Adblock filter list converter
|
|
9509
10314
|
*/
|
|
@@ -9522,18 +10327,133 @@ class FilterListConverter extends ConverterBase {
|
|
|
9522
10327
|
* Converts an adblock filter list to AdGuard format, if possible.
|
|
9523
10328
|
*
|
|
9524
10329
|
* @param filterListNode Filter list node to convert
|
|
9525
|
-
* @
|
|
9526
|
-
*
|
|
10330
|
+
* @param tolerant Indicates whether the converter should be tolerant to invalid rules. If enabled and a rule is
|
|
10331
|
+
* invalid, it will be left as is. If disabled and a rule is invalid, the whole filter list will be failed.
|
|
10332
|
+
* Defaults to `true`.
|
|
10333
|
+
* @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
|
|
10334
|
+
* the converted node, and its `isConverted` flag indicates whether the original node was converted.
|
|
10335
|
+
* If the node was not converted, the result will contain the original node with the same object reference
|
|
10336
|
+
* @throws If the filter list is invalid or cannot be converted (if the tolerant mode is disabled)
|
|
10337
|
+
*/
|
|
10338
|
+
static convertToAdg(filterListNode, tolerant = true) {
|
|
10339
|
+
// Prepare a map to store the converted rules by their index in the filter list
|
|
10340
|
+
const conversionMap = new MultiValueMap();
|
|
10341
|
+
// Iterate over the filtering rules and convert them one by one, then add them to the result (one conversion may
|
|
10342
|
+
// result in multiple rules)
|
|
10343
|
+
for (let i = 0; i < filterListNode.children.length; i += 1) {
|
|
10344
|
+
try {
|
|
10345
|
+
const convertedRules = RuleConverter.convertToAdg(filterListNode.children[i]);
|
|
10346
|
+
// Add the converted rules to the map if they were converted
|
|
10347
|
+
if (convertedRules.isConverted) {
|
|
10348
|
+
conversionMap.add(i, ...convertedRules.result);
|
|
10349
|
+
}
|
|
10350
|
+
}
|
|
10351
|
+
catch (error) {
|
|
10352
|
+
// If the tolerant mode is disabled, we should throw an error, this will fail the whole filter list
|
|
10353
|
+
// conversion.
|
|
10354
|
+
// Otherwise, we just ignore the error and leave the rule as is
|
|
10355
|
+
if (!tolerant) {
|
|
10356
|
+
throw error;
|
|
10357
|
+
}
|
|
10358
|
+
}
|
|
10359
|
+
}
|
|
10360
|
+
// If the conversion map is empty, it means that no rules were converted, so we can return the original filter
|
|
10361
|
+
// list
|
|
10362
|
+
if (conversionMap.size === 0) {
|
|
10363
|
+
return createConversionResult(filterListNode, false);
|
|
10364
|
+
}
|
|
10365
|
+
// Otherwise, create a new filter list node with the converted rules
|
|
10366
|
+
const convertedFilterList = {
|
|
10367
|
+
type: 'FilterList',
|
|
10368
|
+
children: [],
|
|
10369
|
+
};
|
|
10370
|
+
// Iterate over the original rules again and add them to the converted filter list, replacing the converted
|
|
10371
|
+
// rules with the new ones at the specified indexes
|
|
10372
|
+
for (let i = 0; i < filterListNode.children.length; i += 1) {
|
|
10373
|
+
const rules = conversionMap.get(i);
|
|
10374
|
+
if (rules) {
|
|
10375
|
+
convertedFilterList.children.push(...rules);
|
|
10376
|
+
}
|
|
10377
|
+
else {
|
|
10378
|
+
// We clone the unconverted rules to avoid mutating the original filter list if we return the converted
|
|
10379
|
+
// one
|
|
10380
|
+
convertedFilterList.children.push(clone(filterListNode.children[i]));
|
|
10381
|
+
}
|
|
10382
|
+
}
|
|
10383
|
+
return createConversionResult(convertedFilterList, true);
|
|
10384
|
+
}
|
|
10385
|
+
}
|
|
10386
|
+
|
|
10387
|
+
/**
|
|
10388
|
+
* @file Filter list converter for raw filter lists
|
|
10389
|
+
*
|
|
10390
|
+
* Technically, this is a wrapper around `FilterListConverter` that works with nodes instead of strings.
|
|
10391
|
+
*/
|
|
10392
|
+
/**
|
|
10393
|
+
* Adblock filter list converter class.
|
|
10394
|
+
*
|
|
10395
|
+
* You can use this class to convert string-based filter lists, since most of the converters work with nodes.
|
|
10396
|
+
* This class just provides an extra layer on top of the {@link FilterListConverter} and calls the parser/serializer
|
|
10397
|
+
* before/after the conversion internally.
|
|
10398
|
+
*
|
|
10399
|
+
* @todo Implement `convertToUbo` and `convertToAbp`
|
|
10400
|
+
*/
|
|
10401
|
+
class RawFilterListConverter extends ConverterBase {
|
|
10402
|
+
/**
|
|
10403
|
+
* Converts an adblock filter list text to AdGuard format, if possible.
|
|
10404
|
+
*
|
|
10405
|
+
* @param rawFilterList Raw filter list text to convert
|
|
10406
|
+
* @param tolerant Indicates whether the converter should be tolerant to invalid rules. If enabled and a rule is
|
|
10407
|
+
* invalid, it will be left as is. If disabled and a rule is invalid, the whole filter list will be failed.
|
|
10408
|
+
* Defaults to `true`.
|
|
10409
|
+
* @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
|
|
10410
|
+
* the array of converted filter list text, and its `isConverted` flag indicates whether the original rule was
|
|
10411
|
+
* converted. If the rule was not converted, the original filter list text will be returned
|
|
10412
|
+
* @throws If the filter list is invalid or cannot be converted (if the tolerant mode is disabled)
|
|
9527
10413
|
*/
|
|
9528
|
-
static convertToAdg(
|
|
9529
|
-
const
|
|
9530
|
-
//
|
|
9531
|
-
|
|
9532
|
-
|
|
9533
|
-
const convertedRules = RuleConverter.convertToAdg(ruleNode);
|
|
9534
|
-
result.children.push(...convertedRules);
|
|
10414
|
+
static convertToAdg(rawFilterList, tolerant = true) {
|
|
10415
|
+
const conversionResult = FilterListConverter.convertToAdg(FilterListParser.parse(rawFilterList, tolerant), tolerant);
|
|
10416
|
+
// If the filter list was not converted, return the original text
|
|
10417
|
+
if (!conversionResult.isConverted) {
|
|
10418
|
+
return createConversionResult(rawFilterList, false);
|
|
9535
10419
|
}
|
|
9536
|
-
return result
|
|
10420
|
+
// Otherwise, serialize the filter list and return the result
|
|
10421
|
+
return createConversionResult(FilterListParser.generate(conversionResult.result), true);
|
|
10422
|
+
}
|
|
10423
|
+
}
|
|
10424
|
+
|
|
10425
|
+
/**
|
|
10426
|
+
* @file Rule converter for raw rules
|
|
10427
|
+
*
|
|
10428
|
+
* Technically, this is a wrapper around `RuleConverter` that works with nodes instead of strings.
|
|
10429
|
+
*/
|
|
10430
|
+
/**
|
|
10431
|
+
* Adblock filtering rule converter class.
|
|
10432
|
+
*
|
|
10433
|
+
* You can use this class to convert string-based adblock rules, since most of the converters work with nodes.
|
|
10434
|
+
* This class just provides an extra layer on top of the {@link RuleConverter} and calls the parser/serializer
|
|
10435
|
+
* before/after the conversion internally.
|
|
10436
|
+
*
|
|
10437
|
+
* @todo Implement `convertToUbo` and `convertToAbp`
|
|
10438
|
+
*/
|
|
10439
|
+
class RawRuleConverter extends ConverterBase {
|
|
10440
|
+
/**
|
|
10441
|
+
* Converts an adblock filtering rule to AdGuard format, if possible.
|
|
10442
|
+
*
|
|
10443
|
+
* @param rawRule Raw rule text to convert
|
|
10444
|
+
* @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
|
|
10445
|
+
* the array of converted rule texts, and its `isConverted` flag indicates whether the original rule was converted.
|
|
10446
|
+
* If the rule was not converted, the original rule text will be returned
|
|
10447
|
+
* @throws If the rule is invalid or cannot be converted
|
|
10448
|
+
*/
|
|
10449
|
+
static convertToAdg(rawRule) {
|
|
10450
|
+
const conversionResult = RuleConverter.convertToAdg(RuleParser.parse(rawRule));
|
|
10451
|
+
// If the rule was not converted, return the original rule text
|
|
10452
|
+
if (!conversionResult.isConverted) {
|
|
10453
|
+
return createConversionResult([rawRule], false);
|
|
10454
|
+
}
|
|
10455
|
+
// Otherwise, serialize the converted rule nodes
|
|
10456
|
+
return createConversionResult(conversionResult.result.map(RuleParser.generate), true);
|
|
9537
10457
|
}
|
|
9538
10458
|
}
|
|
9539
10459
|
|
|
@@ -9615,7 +10535,7 @@ class LogicalExpressionUtils {
|
|
|
9615
10535
|
}
|
|
9616
10536
|
}
|
|
9617
10537
|
|
|
9618
|
-
const version$1 = "1.1.
|
|
10538
|
+
const version$1 = "1.1.6";
|
|
9619
10539
|
|
|
9620
10540
|
/**
|
|
9621
10541
|
* @file AGTree version
|
|
@@ -9676,6 +10596,8 @@ exports.PREPROCESSOR_MARKER = PREPROCESSOR_MARKER;
|
|
|
9676
10596
|
exports.ParameterListParser = ParameterListParser;
|
|
9677
10597
|
exports.PreProcessorCommentRuleParser = PreProcessorCommentRuleParser;
|
|
9678
10598
|
exports.QuoteUtils = QuoteUtils;
|
|
10599
|
+
exports.RawFilterListConverter = RawFilterListConverter;
|
|
10600
|
+
exports.RawRuleConverter = RawRuleConverter;
|
|
9679
10601
|
exports.RegExpUtils = RegExpUtils;
|
|
9680
10602
|
exports.RuleConversionError = RuleConversionError;
|
|
9681
10603
|
exports.RuleConverter = RuleConverter;
|