@adguard/agtree 2.0.0-alpha.0 → 2.0.0
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/README.md +12 -0
- package/dist/agtree.cjs +1460 -1274
- package/dist/agtree.d.ts +29 -8
- package/dist/agtree.esm.js +1460 -1274
- 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 +2 -2
package/dist/agtree.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* AGTree v2.0.0
|
|
2
|
+
* AGTree v2.0.0 (build date: Thu, 15 Aug 2024 15:06:58 GMT)
|
|
3
3
|
* (c) 2024 Adguard Software Ltd.
|
|
4
4
|
* Released under the MIT license
|
|
5
5
|
* https://github.com/AdguardTeam/tsurlfilter/tree/master/packages/agtree#readme
|
|
@@ -763,6 +763,15 @@ class StringUtils {
|
|
|
763
763
|
}
|
|
764
764
|
}
|
|
765
765
|
|
|
766
|
+
/**
|
|
767
|
+
* Possible operators in the logical expression.
|
|
768
|
+
*/
|
|
769
|
+
var OperatorValue;
|
|
770
|
+
(function (OperatorValue) {
|
|
771
|
+
OperatorValue["Not"] = "!";
|
|
772
|
+
OperatorValue["And"] = "&&";
|
|
773
|
+
OperatorValue["Or"] = "||";
|
|
774
|
+
})(OperatorValue || (OperatorValue = {}));
|
|
766
775
|
/**
|
|
767
776
|
* Represents the different comment markers that can be used in an adblock rule.
|
|
768
777
|
*
|
|
@@ -2958,15 +2967,6 @@ var ParenthesisNodeBinaryPropMap;
|
|
|
2958
2967
|
ParenthesisNodeBinaryPropMap[ParenthesisNodeBinaryPropMap["Start"] = 2] = "Start";
|
|
2959
2968
|
ParenthesisNodeBinaryPropMap[ParenthesisNodeBinaryPropMap["End"] = 3] = "End";
|
|
2960
2969
|
})(ParenthesisNodeBinaryPropMap || (ParenthesisNodeBinaryPropMap = {}));
|
|
2961
|
-
/**
|
|
2962
|
-
* Possible operators in the logical expression.
|
|
2963
|
-
*/
|
|
2964
|
-
var OperatorValue;
|
|
2965
|
-
(function (OperatorValue) {
|
|
2966
|
-
OperatorValue["Not"] = "!";
|
|
2967
|
-
OperatorValue["And"] = "&&";
|
|
2968
|
-
OperatorValue["Or"] = "||";
|
|
2969
|
-
})(OperatorValue || (OperatorValue = {}));
|
|
2970
2970
|
/**
|
|
2971
2971
|
* Possible token types in the logical expression.
|
|
2972
2972
|
*/
|
|
@@ -3012,7 +3012,9 @@ const getOperatorOrFail = binary => {
|
|
|
3012
3012
|
/**
|
|
3013
3013
|
* Serialization map for known variables.
|
|
3014
3014
|
*/
|
|
3015
|
-
const KNOWN_VARIABLES_MAP = new Map([['ext_abp', 0], ['ext_ublock', 1], ['ext_ubol', 2], ['ext_devbuild', 3], ['env_chromium', 4], ['env_edge', 5], ['env_firefox', 6], ['env_mobile', 7], ['env_safari', 8], ['env_mv3', 9], ['false', 10], ['cap_html_filtering', 11], ['cap_user_stylesheet', 12], ['adguard', 13], ['adguard_app_windows', 14], ['adguard_app_mac', 15], ['adguard_app_android', 16], ['adguard_app_ios', 17], ['adguard_ext_safari', 18], ['adguard_ext_chromium', 19], ['adguard_ext_firefox', 20], ['adguard_ext_edge', 21], ['adguard_ext_opera', 22], ['adguard_ext_android_cb', 23]
|
|
3015
|
+
const KNOWN_VARIABLES_MAP = new Map([['ext_abp', 0], ['ext_ublock', 1], ['ext_ubol', 2], ['ext_devbuild', 3], ['env_chromium', 4], ['env_edge', 5], ['env_firefox', 6], ['env_mobile', 7], ['env_safari', 8], ['env_mv3', 9], ['false', 10], ['cap_html_filtering', 11], ['cap_user_stylesheet', 12], ['adguard', 13], ['adguard_app_windows', 14], ['adguard_app_mac', 15], ['adguard_app_android', 16], ['adguard_app_ios', 17], ['adguard_ext_safari', 18], ['adguard_ext_chromium', 19], ['adguard_ext_firefox', 20], ['adguard_ext_edge', 21], ['adguard_ext_opera', 22], ['adguard_ext_android_cb', 23]
|
|
3016
|
+
// TODO: Add 'adguard_ext_chromium_mv3' to the list
|
|
3017
|
+
]);
|
|
3016
3018
|
/**
|
|
3017
3019
|
* Deserialization map for known variables.
|
|
3018
3020
|
*/
|
|
@@ -5716,7 +5718,7 @@ class CssTokenStream {
|
|
|
5716
5718
|
* @returns An array containing the number of tokens skipped and the number of tokens skipped without leading and
|
|
5717
5719
|
* trailing whitespace tokens.
|
|
5718
5720
|
*/
|
|
5719
|
-
|
|
5721
|
+
skipUntilExt(type, balance) {
|
|
5720
5722
|
let i = this.index;
|
|
5721
5723
|
let firstNonWsToken = -1; // -1 means no non-whitespace token found yet
|
|
5722
5724
|
let lastNonWsToken = -1; // -1 means no non-whitespace token found yet
|
|
@@ -5944,7 +5946,7 @@ class AdgCssInjectionParser extends ParserBase {
|
|
|
5944
5946
|
// └ this one
|
|
5945
5947
|
const {
|
|
5946
5948
|
skippedTrimmed: selectorTokensLength
|
|
5947
|
-
} = stream.
|
|
5949
|
+
} = stream.skipUntilExt(cssTokenizer.TokenType.OpenCurlyBracket, balanceShift + 1);
|
|
5948
5950
|
stream.expect(cssTokenizer.TokenType.OpenCurlyBracket);
|
|
5949
5951
|
// If the skipped tokens count is 0 without leading and trailing whitespace characters, then the selector list
|
|
5950
5952
|
// is empty
|
|
@@ -6184,6 +6186,7 @@ class AbpSnippetInjectionBodyParser extends ParserBase {
|
|
|
6184
6186
|
*
|
|
6185
6187
|
* @note Only 256 values can be represented this way.
|
|
6186
6188
|
*/
|
|
6189
|
+
// TODO: Update this map with the actual values
|
|
6187
6190
|
static FREQUENT_ARGS_SERIALIZATION_MAP = new Map([['abort-current-inline-script', 0], ['abort-on-property-read', 1], ['abort-on-property-write', 2], ['json-prune', 3], ['log', 4], ['prevent-listener', 5], ['cookie-remover', 6], ['override-property-read', 7], ['abort-on-iframe-property-read', 8], ['abort-on-iframe-property-write', 9], ['freeze-element', 10], ['json-override', 11], ['simulate-mouse-event', 12], ['strip-fetch-query-parameter', 13], ['hide-if-contains', 14], ['hide-if-contains-image', 15], ['hide-if-contains-image-hash', 16], ['hide-if-contains-similar-text', 17], ['hide-if-contains-visible-text', 18], ['hide-if-contains-and-matches-style', 19], ['hide-if-graph-matches', 20], ['hide-if-has-and-matches-style', 21], ['hide-if-labelled-by', 22], ['hide-if-matches-xpath', 23], ['hide-if-matches-computed-xpath', 24], ['hide-if-shadow-contains', 25], ['debug', 26], ['trace', 27], ['race', 28]]);
|
|
6188
6191
|
/**
|
|
6189
6192
|
* Value map for binary deserialization. This helps to reduce the size of the serialized data,
|
|
@@ -6311,6 +6314,7 @@ class UboScriptletInjectionBodyParser extends ParserBase {
|
|
|
6311
6314
|
*
|
|
6312
6315
|
* @note Only 256 values can be represented this way.
|
|
6313
6316
|
*/
|
|
6317
|
+
// TODO: Update this map with the actual values
|
|
6314
6318
|
static FREQUENT_ARGS_SERIALIZATION_MAP = new Map([['abort-current-script.js', 0], ['acs.js', 1], ['abort-current-inline-script.js', 2], ['acis.js', 3], ['abort-on-property-read.js', 4], ['aopr.js', 5], ['abort-on-property-write.js', 6], ['aopw.js', 7], ['abort-on-stack-trace.js', 8], ['aost.js', 9], ['adjust-setInterval.js', 10], ['nano-setInterval-booster.js', 11], ['nano-sib.js', 12], ['adjust-setTimeout.js', 13], ['nano-setTimeout-booster.js', 14], ['nano-stb.js', 15], ['close-window.js', 16], ['window-close-if.js', 17], ['disable-newtab-links.js', 18], ['evaldata-prune.js', 19], ['json-prune.js', 20], ['addEventListener-logger.js', 21], ['aell.js', 22], ['m3u-prune.js', 23], ['nowebrtc.js', 24], ['addEventListener-defuser.js', 25], ['aeld.js', 26], ['prevent-addEventListener.js', 27], ['adfly-defuser.js', 28], ['noeval-if.js', 29], ['prevent-eval-if.js', 30], ['no-fetch-if.js', 31], ['prevent-fetch.js', 32], ['no-xhr-if.js', 33], ['prevent-xhr.js', 34], ['prevent-refresh.js', 35], ['refresh-defuser.js', 36], ['no-requestAnimationFrame-if.js', 37], ['norafif.js', 38], ['prevent-requestAnimationFrame.js', 39], ['no-setInterval-if.js', 40], ['nosiif.js', 41], ['prevent-setInterval.js', 42], ['setInterval-defuser.js', 43], ['no-setTimeout-if.js', 44], ['nostif.js', 45], ['prevent-setTimeout.js', 46], ['setTimeout-defuser.js', 47], ['no-window-open-if.js', 48], ['nowoif.js', 49], ['prevent-window-open.js', 50], ['window.open-defuser.js', 51], ['remove-attr.js', 52], ['ra.js', 53], ['remove-class.js', 54], ['rc.js', 55], ['remove-cookie.js', 56], ['cookie-remover.js', 57], ['remove-node-text.js', 58], ['rmnt.js', 59], ['set-attr.js', 60], ['set-constant.js', 61], ['set.js', 62], ['set-cookie.js', 63], ['set-local-storage-item.js', 64], ['set-session-storage-item.js', 65], ['xml-prune.js', 66], ['webrtc-if.js', 67], ['overlay-buster.js', 68], ['alert-buster.js', 69], ['golem.de.js', 70], ['href-sanitizer.js', 71], ['call-nothrow.js', 72], ['window.name-defuser.js', 73], ['spoof-css.js', 74], ['trusted-set-constant.js', 75], ['trusted-set.js', 76], ['trusted-set-cookie.js', 77], ['trusted-set-local-storage-item.js', 78], ['trusted-replace-fetch-response.js', 79], ['json-prune-fetch-response.js', 80], ['json-prune-xhr-response.js', 81], ['trusted-replace-xhr-response.js', 82], ['multiup.js', 83], ['prevent-canvas.js', 84], ['set-cookie-reload.js', 85], ['trusted-set-cookie-reload.js', 86], ['trusted-click-element.js', 87], ['trusted-prune-inbound-object.js', 88], ['trusted-prune-outbound-object.js', 89], ['trusted-set-session-storage-item.js', 90], ['trusted-replace-node-text.js', 91], ['trusted-rpnt.js', 92], ['replace-node-text.js', 93], ['rpnt.js', 94]]);
|
|
6315
6319
|
/**
|
|
6316
6320
|
* Value map for binary deserialization. This helps to reduce the size of the serialized data,
|
|
@@ -6448,6 +6452,7 @@ class AdgScriptletInjectionBodyParser extends ParserBase {
|
|
|
6448
6452
|
*
|
|
6449
6453
|
* @note Only 256 values can be represented this way.
|
|
6450
6454
|
*/
|
|
6455
|
+
// TODO: Update this map with the actual values
|
|
6451
6456
|
static FREQUENT_ARGS_SERIALIZATION_MAP = new Map([['abort-current-inline-script', 0], ['abort-on-property-read', 1], ['abort-on-property-write', 2], ['abort-on-stack-trace', 3], ['adjust-setInterval', 4], ['adjust-setTimeout', 5], ['close-window', 6], ['debug-current-inline-script', 7], ['debug-on-property-read', 8], ['debug-on-property-write', 9], ['dir-string', 10], ['disable-newtab-links', 11], ['evaldata-prune', 12], ['json-prune', 13], ['log', 14], ['log-addEventListener', 15], ['log-eval', 16], ['log-on-stack-trace', 17], ['m3u-prune', 18], ['noeval', 19], ['nowebrtc', 20], ['no-topics', 21], ['prevent-addEventListener', 22], ['prevent-adfly', 23], ['prevent-bab', 24], ['prevent-eval-if', 25], ['prevent-fab-3.2.0', 26], ['prevent-fetch', 27], ['prevent-xhr', 28], ['prevent-popads-net', 29], ['prevent-refresh', 30], ['prevent-requestAnimationFrame', 31], ['prevent-setInterval', 32], ['prevent-setTimeout', 33], ['prevent-window-open', 34], ['remove-attr', 35], ['remove-class', 36], ['remove-cookie', 37], ['remove-node-text', 38], ['set-attr', 39], ['set-constant', 40], ['set-cookie', 41], ['set-cookie-reload', 42], ['set-local-storage-item', 43], ['set-popads-dummy', 44], ['set-session-storage-item', 45], ['xml-prune', 46]]);
|
|
6452
6457
|
/**
|
|
6453
6458
|
* Value map for binary deserialization. This helps to reduce the size of the serialized data,
|
|
@@ -21527,6 +21532,30 @@ function getScriptletName(scriptletNode) {
|
|
|
21527
21532
|
}
|
|
21528
21533
|
return scriptletNode.children[0]?.value ?? EMPTY;
|
|
21529
21534
|
}
|
|
21535
|
+
/**
|
|
21536
|
+
* Transform the nth argument of the scriptlet node
|
|
21537
|
+
*
|
|
21538
|
+
* @param scriptletNode Scriptlet node to transform argument of
|
|
21539
|
+
* @param index Index of the argument to transform (index 0 is the scriptlet name)
|
|
21540
|
+
* @param transform Function to transform the argument
|
|
21541
|
+
*/
|
|
21542
|
+
function transformNthScriptletArgument(scriptletNode, index, transform) {
|
|
21543
|
+
const child = scriptletNode.children[index];
|
|
21544
|
+
if (!isUndefined(child) && !isNull(child)) {
|
|
21545
|
+
child.value = transform(child.value);
|
|
21546
|
+
}
|
|
21547
|
+
}
|
|
21548
|
+
/**
|
|
21549
|
+
* Transform all arguments of the scriptlet node
|
|
21550
|
+
*
|
|
21551
|
+
* @param scriptletNode Scriptlet node to transform arguments of
|
|
21552
|
+
* @param transform Function to transform the arguments
|
|
21553
|
+
*/
|
|
21554
|
+
function transformAllScriptletArguments(scriptletNode, transform) {
|
|
21555
|
+
for (let i = 0; i < scriptletNode.children.length; i += 1) {
|
|
21556
|
+
transformNthScriptletArgument(scriptletNode, i, transform);
|
|
21557
|
+
}
|
|
21558
|
+
}
|
|
21530
21559
|
/**
|
|
21531
21560
|
* Set name of the scriptlet.
|
|
21532
21561
|
* Modifies input `scriptletNode` if needed.
|
|
@@ -21535,10 +21564,7 @@ function getScriptletName(scriptletNode) {
|
|
|
21535
21564
|
* @param name Name to set
|
|
21536
21565
|
*/
|
|
21537
21566
|
function setScriptletName(scriptletNode, name) {
|
|
21538
|
-
|
|
21539
|
-
// eslint-disable-next-line no-param-reassign
|
|
21540
|
-
scriptletNode.children[0].value = name;
|
|
21541
|
-
}
|
|
21567
|
+
transformNthScriptletArgument(scriptletNode, 0, () => name);
|
|
21542
21568
|
}
|
|
21543
21569
|
/**
|
|
21544
21570
|
* Set quote type of the scriptlet parameters
|
|
@@ -21547,1422 +21573,1559 @@ function setScriptletName(scriptletNode, name) {
|
|
|
21547
21573
|
* @param quoteType Preferred quote type
|
|
21548
21574
|
*/
|
|
21549
21575
|
function setScriptletQuoteType(scriptletNode, quoteType) {
|
|
21550
|
-
|
|
21551
|
-
for (let i = 0; i < scriptletNode.children.length; i += 1) {
|
|
21552
|
-
const child = scriptletNode.children[i];
|
|
21553
|
-
if (isNull(child)) {
|
|
21554
|
-
continue;
|
|
21555
|
-
}
|
|
21556
|
-
// eslint-disable-next-line no-param-reassign
|
|
21557
|
-
child.value = QuoteUtils.setStringQuoteType(child.value, quoteType);
|
|
21558
|
-
}
|
|
21559
|
-
}
|
|
21576
|
+
transformAllScriptletArguments(scriptletNode, value => QuoteUtils.setStringQuoteType(value, quoteType));
|
|
21560
21577
|
}
|
|
21561
21578
|
|
|
21562
21579
|
/**
|
|
21563
|
-
* @file
|
|
21580
|
+
* @file Compatibility tables for redirects.
|
|
21564
21581
|
*/
|
|
21565
|
-
const ABP_SCRIPTLET_PREFIX = 'abp-';
|
|
21566
|
-
const UBO_SCRIPTLET_PREFIX = 'ubo-';
|
|
21567
21582
|
/**
|
|
21568
|
-
*
|
|
21583
|
+
* Prefix for resource redirection names.
|
|
21584
|
+
*/
|
|
21585
|
+
const ABP_RESOURCE_PREFIX = 'abp-resource:';
|
|
21586
|
+
const ABP_RESOURCE_PREFIX_LENGTH = ABP_RESOURCE_PREFIX.length;
|
|
21587
|
+
/**
|
|
21588
|
+
* Transforms the name of an ABP redirect to a normalized form.
|
|
21569
21589
|
*
|
|
21570
|
-
* @
|
|
21590
|
+
* @param name Redirect name to normalize.
|
|
21591
|
+
*
|
|
21592
|
+
* @returns Normalized redirect name.
|
|
21593
|
+
*
|
|
21594
|
+
* @example
|
|
21595
|
+
* abpRedirectNameNormalizer('abp-resource:my-resource') // => 'my-resource'
|
|
21571
21596
|
*/
|
|
21572
|
-
|
|
21597
|
+
const abpRedirectNameNormalizer = name => {
|
|
21598
|
+
if (name.startsWith(ABP_RESOURCE_PREFIX)) {
|
|
21599
|
+
return name.slice(ABP_RESOURCE_PREFIX_LENGTH);
|
|
21600
|
+
}
|
|
21601
|
+
return name;
|
|
21602
|
+
};
|
|
21603
|
+
/**
|
|
21604
|
+
* Compatibility table for redirects.
|
|
21605
|
+
*/
|
|
21606
|
+
class RedirectsCompatibilityTable extends CompatibilityTableBase {
|
|
21573
21607
|
/**
|
|
21574
|
-
*
|
|
21608
|
+
* Creates a new instance of the compatibility table for redirects.
|
|
21575
21609
|
*
|
|
21576
|
-
* @param
|
|
21577
|
-
* @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
|
|
21578
|
-
* the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
|
|
21579
|
-
* If the rule was not converted, the result array will contain the original node with the same object reference
|
|
21580
|
-
* @throws If the rule is invalid or cannot be converted
|
|
21610
|
+
* @param data Compatibility table data.
|
|
21581
21611
|
*/
|
|
21582
|
-
|
|
21583
|
-
|
|
21584
|
-
if (rule.syntax === exports.AdblockSyntax.Adg) {
|
|
21585
|
-
return createNodeConversionResult([rule], false);
|
|
21586
|
-
}
|
|
21587
|
-
const separator = rule.separator.value;
|
|
21588
|
-
let convertedSeparator = separator;
|
|
21589
|
-
convertedSeparator = rule.exception ? exports.CosmeticRuleSeparator.AdgJsInjectionException : exports.CosmeticRuleSeparator.AdgJsInjection;
|
|
21590
|
-
const convertedScriptlets = [];
|
|
21591
|
-
for (const scriptlet of rule.body.children) {
|
|
21592
|
-
// Clone the node to avoid any side effects
|
|
21593
|
-
const scriptletClone = cloneScriptletRuleNode(scriptlet);
|
|
21594
|
-
// Remove possible quotes just to make it easier to work with the scriptlet name
|
|
21595
|
-
const scriptletName = QuoteUtils.setStringQuoteType(getScriptletName(scriptletClone), exports.QuoteType.None);
|
|
21596
|
-
// Add prefix if it's not already there
|
|
21597
|
-
let prefix;
|
|
21598
|
-
switch (rule.syntax) {
|
|
21599
|
-
case exports.AdblockSyntax.Abp:
|
|
21600
|
-
prefix = ABP_SCRIPTLET_PREFIX;
|
|
21601
|
-
break;
|
|
21602
|
-
case exports.AdblockSyntax.Ubo:
|
|
21603
|
-
prefix = UBO_SCRIPTLET_PREFIX;
|
|
21604
|
-
break;
|
|
21605
|
-
default:
|
|
21606
|
-
prefix = EMPTY;
|
|
21607
|
-
}
|
|
21608
|
-
if (!scriptletName.startsWith(prefix)) {
|
|
21609
|
-
setScriptletName(scriptletClone, `${prefix}${scriptletName}`);
|
|
21610
|
-
}
|
|
21611
|
-
// ADG scriptlet parameters should be quoted, and single quoted are preferred
|
|
21612
|
-
setScriptletQuoteType(scriptletClone, exports.QuoteType.Single);
|
|
21613
|
-
convertedScriptlets.push(scriptletClone);
|
|
21614
|
-
}
|
|
21615
|
-
return createNodeConversionResult(convertedScriptlets.map(scriptlet => {
|
|
21616
|
-
const res = {
|
|
21617
|
-
category: rule.category,
|
|
21618
|
-
type: rule.type,
|
|
21619
|
-
syntax: exports.AdblockSyntax.Adg,
|
|
21620
|
-
exception: rule.exception,
|
|
21621
|
-
domains: cloneDomainListNode(rule.domains),
|
|
21622
|
-
separator: {
|
|
21623
|
-
type: 'Value',
|
|
21624
|
-
value: convertedSeparator
|
|
21625
|
-
},
|
|
21626
|
-
body: {
|
|
21627
|
-
type: rule.body.type,
|
|
21628
|
-
children: [scriptlet]
|
|
21629
|
-
}
|
|
21630
|
-
};
|
|
21631
|
-
if (rule.modifiers) {
|
|
21632
|
-
res.modifiers = cloneModifierListNode(rule.modifiers);
|
|
21633
|
-
}
|
|
21634
|
-
return res;
|
|
21635
|
-
}), true);
|
|
21612
|
+
constructor(data) {
|
|
21613
|
+
super(data, abpRedirectNameNormalizer);
|
|
21636
21614
|
}
|
|
21637
21615
|
}
|
|
21616
|
+
/**
|
|
21617
|
+
* Deep freeze the compatibility table data to avoid accidental modifications.
|
|
21618
|
+
*/
|
|
21619
|
+
deepFreeze(redirectsCompatibilityTableData);
|
|
21620
|
+
/**
|
|
21621
|
+
* Compatibility table instance for redirects.
|
|
21622
|
+
*/
|
|
21623
|
+
const redirectsCompatibilityTable = new RedirectsCompatibilityTable(redirectsCompatibilityTableData);
|
|
21638
21624
|
|
|
21639
21625
|
/**
|
|
21640
|
-
* @file
|
|
21626
|
+
* @file Compatibility tables for scriptlets.
|
|
21641
21627
|
*/
|
|
21642
21628
|
/**
|
|
21643
|
-
*
|
|
21644
|
-
*
|
|
21645
|
-
* @param name Name of the modifier
|
|
21646
|
-
* @param value Value of the modifier
|
|
21647
|
-
* @param exception Whether the modifier is an exception
|
|
21648
|
-
* @returns Modifier node
|
|
21629
|
+
* Compatibility table for scriptlets.
|
|
21649
21630
|
*/
|
|
21650
|
-
|
|
21651
|
-
const result = {
|
|
21652
|
-
type: 'Modifier',
|
|
21653
|
-
exception,
|
|
21654
|
-
name: {
|
|
21655
|
-
type: 'Value',
|
|
21656
|
-
value: name
|
|
21657
|
-
}
|
|
21658
|
-
};
|
|
21659
|
-
if (!isUndefined(value)) {
|
|
21660
|
-
result.value = {
|
|
21661
|
-
type: 'Value',
|
|
21662
|
-
value
|
|
21663
|
-
};
|
|
21664
|
-
}
|
|
21665
|
-
return result;
|
|
21666
|
-
}
|
|
21631
|
+
class ScriptletsCompatibilityTable extends CompatibilityTableBase {}
|
|
21667
21632
|
/**
|
|
21668
|
-
*
|
|
21669
|
-
*
|
|
21670
|
-
* @param modifiers Modifiers to put in the list (optional, defaults to an empty list)
|
|
21671
|
-
* @returns Modifier list node
|
|
21633
|
+
* Deep freeze the compatibility table data to avoid accidental modifications.
|
|
21672
21634
|
*/
|
|
21673
|
-
|
|
21674
|
-
const result = {
|
|
21675
|
-
type: 'ModifierList',
|
|
21676
|
-
// We need to clone the modifiers to avoid side effects
|
|
21677
|
-
children: modifiers.length ? clone(modifiers) : []
|
|
21678
|
-
};
|
|
21679
|
-
return result;
|
|
21680
|
-
}
|
|
21681
|
-
|
|
21635
|
+
deepFreeze(scriptletsCompatibilityTableData);
|
|
21682
21636
|
/**
|
|
21683
|
-
*
|
|
21684
|
-
* by storing them in an array.
|
|
21685
|
-
*
|
|
21686
|
-
* @todo Add more methods if needed
|
|
21637
|
+
* Compatibility table instance for scriptlets.
|
|
21687
21638
|
*/
|
|
21688
|
-
|
|
21689
|
-
/**
|
|
21690
|
-
* Adds a value to the map. If the key already exists, the value will be appended to the existing array,
|
|
21691
|
-
* otherwise a new array will be created for the key.
|
|
21692
|
-
*
|
|
21693
|
-
* @param key Key to add
|
|
21694
|
-
* @param values Value(s) to add
|
|
21695
|
-
*/
|
|
21696
|
-
add(key, ...values) {
|
|
21697
|
-
let currentValues = super.get(key);
|
|
21698
|
-
if (isUndefined(currentValues)) {
|
|
21699
|
-
currentValues = [];
|
|
21700
|
-
super.set(key, values);
|
|
21701
|
-
}
|
|
21702
|
-
currentValues.push(...values);
|
|
21703
|
-
}
|
|
21704
|
-
}
|
|
21639
|
+
const scriptletsCompatibilityTable = new ScriptletsCompatibilityTable(scriptletsCompatibilityTableData);
|
|
21705
21640
|
|
|
21641
|
+
/* eslint-disable no-bitwise */
|
|
21706
21642
|
/**
|
|
21707
|
-
* @file
|
|
21643
|
+
* @file Platform schema.
|
|
21708
21644
|
*/
|
|
21709
|
-
const UBO_MATCHES_PATH_OPERATOR = 'matches-path';
|
|
21710
|
-
const ADG_PATH_MODIFIER = 'path';
|
|
21711
21645
|
/**
|
|
21712
|
-
*
|
|
21646
|
+
* Platform separator, e.g. 'adg_os_any|adg_safari_any' means any AdGuard OS platform and
|
|
21647
|
+
* any AdGuard Safari content blocker platform.
|
|
21713
21648
|
*/
|
|
21714
|
-
const
|
|
21649
|
+
const PLATFORM_SEPARATOR = '|';
|
|
21715
21650
|
/**
|
|
21716
|
-
*
|
|
21651
|
+
* Platform negation character, e.g. 'adg_any|~adg_safari_any' means any AdGuard product except
|
|
21652
|
+
* Safari content blockers.
|
|
21717
21653
|
*/
|
|
21718
|
-
|
|
21719
|
-
|
|
21720
|
-
|
|
21721
|
-
|
|
21722
|
-
|
|
21723
|
-
|
|
21724
|
-
|
|
21725
|
-
|
|
21726
|
-
|
|
21727
|
-
|
|
21728
|
-
|
|
21729
|
-
|
|
21730
|
-
|
|
21731
|
-
|
|
21732
|
-
|
|
21733
|
-
|
|
21734
|
-
|
|
21735
|
-
|
|
21736
|
-
}
|
|
21737
|
-
const value = RegExpUtils.isRegexPattern(modifier.value.value) ? StringUtils.escapeCharacters(modifier.value.value, SPECIAL_MODIFIER_REGEX_CHARS) : modifier.value.value;
|
|
21738
|
-
// Convert uBO's `:matches-path(...)` operator to ADG's `$path=...` modifier
|
|
21739
|
-
conversionMap.add(index, createModifierNode(ADG_PATH_MODIFIER,
|
|
21740
|
-
// We should negate the regexp if the modifier is an exception
|
|
21741
|
-
modifier.exception
|
|
21742
|
-
// eslint-disable-next-line max-len
|
|
21743
|
-
? `${REGEX_MARKER}${RegExpUtils.negateRegexPattern(RegExpUtils.patternToRegexp(value))}${REGEX_MARKER}` : value));
|
|
21744
|
-
}
|
|
21745
|
-
});
|
|
21746
|
-
// Check if we have any converted modifiers
|
|
21747
|
-
if (conversionMap.size) {
|
|
21748
|
-
const modifierListClone = clone(modifierList);
|
|
21749
|
-
// Replace the original modifiers with the converted ones
|
|
21750
|
-
modifierListClone.children = modifierListClone.children.map((modifier, index) => {
|
|
21751
|
-
const convertedModifier = conversionMap.get(index);
|
|
21752
|
-
return convertedModifier ?? modifier;
|
|
21753
|
-
}).flat();
|
|
21754
|
-
return createConversionResult(modifierListClone, true);
|
|
21654
|
+
const PLATFORM_NEGATION = '~';
|
|
21655
|
+
/**
|
|
21656
|
+
* Parses a raw platform string into a platform bitmask.
|
|
21657
|
+
*
|
|
21658
|
+
* @param rawPlatforms Raw platform string, e.g. 'adg_safari_any|adg_os_any'.
|
|
21659
|
+
*
|
|
21660
|
+
* @returns Platform bitmask.
|
|
21661
|
+
*/
|
|
21662
|
+
const parseRawPlatforms = rawPlatforms => {
|
|
21663
|
+
// e.g. 'adg_safari_any|adg_os_any'
|
|
21664
|
+
const rawPlatformList = rawPlatforms.split(PLATFORM_SEPARATOR).map(rawPlatform => rawPlatform.trim());
|
|
21665
|
+
let result = 0;
|
|
21666
|
+
for (let rawPlatform of rawPlatformList) {
|
|
21667
|
+
// negation, e.g. 'adg_any|~adg_safari_any' means any AdGuard product except Safari content blockers
|
|
21668
|
+
let negated = false;
|
|
21669
|
+
if (rawPlatform.startsWith(PLATFORM_NEGATION)) {
|
|
21670
|
+
negated = true;
|
|
21671
|
+
rawPlatform = rawPlatform.slice(1).trim();
|
|
21755
21672
|
}
|
|
21756
|
-
|
|
21757
|
-
|
|
21758
|
-
|
|
21759
|
-
}
|
|
21760
|
-
|
|
21761
|
-
|
|
21762
|
-
|
|
21673
|
+
const platform = SPECIFIC_PLATFORM_MAP.get(rawPlatform) ?? GENERIC_PLATFORM_MAP.get(rawPlatform);
|
|
21674
|
+
if (isUndefined(platform)) {
|
|
21675
|
+
throw new Error(`Unknown platform: ${rawPlatform}`);
|
|
21676
|
+
}
|
|
21677
|
+
if (negated) {
|
|
21678
|
+
result &= ~platform;
|
|
21679
|
+
} else {
|
|
21680
|
+
result |= platform;
|
|
21681
|
+
}
|
|
21682
|
+
}
|
|
21683
|
+
if (result === 0) {
|
|
21684
|
+
throw new Error('No platforms specified');
|
|
21685
|
+
}
|
|
21686
|
+
return result;
|
|
21763
21687
|
};
|
|
21764
|
-
var PseudoClasses;
|
|
21765
|
-
(function (PseudoClasses) {
|
|
21766
|
-
PseudoClasses["AbpContains"] = "-abp-contains";
|
|
21767
|
-
PseudoClasses["AbpHas"] = "-abp-has";
|
|
21768
|
-
PseudoClasses["Contains"] = "contains";
|
|
21769
|
-
PseudoClasses["Has"] = "has";
|
|
21770
|
-
PseudoClasses["HasText"] = "has-text";
|
|
21771
|
-
PseudoClasses["MatchesCss"] = "matches-css";
|
|
21772
|
-
PseudoClasses["MatchesCssAfter"] = "matches-css-after";
|
|
21773
|
-
PseudoClasses["MatchesCssBefore"] = "matches-css-before";
|
|
21774
|
-
PseudoClasses["Not"] = "not";
|
|
21775
|
-
})(PseudoClasses || (PseudoClasses = {}));
|
|
21776
|
-
var PseudoElements;
|
|
21777
|
-
(function (PseudoElements) {
|
|
21778
|
-
PseudoElements["After"] = "after";
|
|
21779
|
-
PseudoElements["Before"] = "before";
|
|
21780
|
-
})(PseudoElements || (PseudoElements = {}));
|
|
21781
|
-
const PSEUDO_ELEMENT_NAMES = new Set([PseudoElements.After, PseudoElements.Before]);
|
|
21782
21688
|
/**
|
|
21783
|
-
*
|
|
21784
|
-
*
|
|
21785
|
-
* @todo Implement `convertToUbo` and `convertToAbp`
|
|
21689
|
+
* Platform schema.
|
|
21786
21690
|
*/
|
|
21787
|
-
|
|
21788
|
-
|
|
21789
|
-
|
|
21790
|
-
*
|
|
21791
|
-
* @param selectorList Selector list to convert
|
|
21792
|
-
* @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
|
|
21793
|
-
* the converted node, and its `isConverted` flag indicates whether the original node was converted.
|
|
21794
|
-
* If the node was not converted, the result will contain the original node with the same object reference
|
|
21795
|
-
* @throws If the rule is invalid or incompatible
|
|
21796
|
-
*/
|
|
21797
|
-
static convertToAdg(selectorList) {
|
|
21798
|
-
const stream = selectorList instanceof CssTokenStream ? selectorList : new CssTokenStream(selectorList);
|
|
21799
|
-
const converted = [];
|
|
21800
|
-
const convertAndPushPseudo = pseudo => {
|
|
21801
|
-
switch (pseudo) {
|
|
21802
|
-
case PseudoClasses.AbpContains:
|
|
21803
|
-
case PseudoClasses.HasText:
|
|
21804
|
-
converted.push(PseudoClasses.Contains);
|
|
21805
|
-
converted.push(OPEN_PARENTHESIS);
|
|
21806
|
-
break;
|
|
21807
|
-
case PseudoClasses.AbpHas:
|
|
21808
|
-
converted.push(PseudoClasses.Has);
|
|
21809
|
-
converted.push(OPEN_PARENTHESIS);
|
|
21810
|
-
break;
|
|
21811
|
-
// a bit special case:
|
|
21812
|
-
// - `:matches-css-before(...)` → `:matches-css(before, ...)`
|
|
21813
|
-
// - `:matches-css-after(...)` → `:matches-css(after, ...)`
|
|
21814
|
-
case PseudoClasses.MatchesCssBefore:
|
|
21815
|
-
case PseudoClasses.MatchesCssAfter:
|
|
21816
|
-
converted.push(PseudoClasses.MatchesCss);
|
|
21817
|
-
converted.push(OPEN_PARENTHESIS);
|
|
21818
|
-
converted.push(pseudo.substring(PseudoClasses.MatchesCss.length + 1));
|
|
21819
|
-
converted.push(COMMA);
|
|
21820
|
-
break;
|
|
21821
|
-
default:
|
|
21822
|
-
converted.push(pseudo);
|
|
21823
|
-
converted.push(OPEN_PARENTHESIS);
|
|
21824
|
-
break;
|
|
21825
|
-
}
|
|
21826
|
-
};
|
|
21827
|
-
while (!stream.isEof()) {
|
|
21828
|
-
const token = stream.getOrFail();
|
|
21829
|
-
if (token.type === cssTokenizer.TokenType.Colon) {
|
|
21830
|
-
// Advance colon
|
|
21831
|
-
stream.advance();
|
|
21832
|
-
converted.push(COLON);
|
|
21833
|
-
const tempToken = stream.getOrFail();
|
|
21834
|
-
// Double colon is a pseudo-element
|
|
21835
|
-
if (tempToken.type === cssTokenizer.TokenType.Colon) {
|
|
21836
|
-
stream.advance();
|
|
21837
|
-
converted.push(COLON);
|
|
21838
|
-
continue;
|
|
21839
|
-
}
|
|
21840
|
-
if (tempToken.type === cssTokenizer.TokenType.Ident) {
|
|
21841
|
-
const name = stream.source.slice(tempToken.start, tempToken.end);
|
|
21842
|
-
if (PSEUDO_ELEMENT_NAMES.has(name)) {
|
|
21843
|
-
// Add an extra colon to the name
|
|
21844
|
-
converted.push(COLON);
|
|
21845
|
-
converted.push(name);
|
|
21846
|
-
} else {
|
|
21847
|
-
// Add the name as is
|
|
21848
|
-
converted.push(name);
|
|
21849
|
-
}
|
|
21850
|
-
// Advance the names
|
|
21851
|
-
stream.advance();
|
|
21852
|
-
} else if (tempToken.type === cssTokenizer.TokenType.Function) {
|
|
21853
|
-
const name = stream.source.slice(tempToken.start, tempToken.end - 1); // omit the last parenthesis
|
|
21854
|
-
// :-abp-contains(...) → :contains(...)
|
|
21855
|
-
// :has-text(...) → :contains(...)
|
|
21856
|
-
// :-abp-has(...) → :has(...)
|
|
21857
|
-
convertAndPushPseudo(name);
|
|
21858
|
-
// Advance the function name
|
|
21859
|
-
stream.advance();
|
|
21860
|
-
}
|
|
21861
|
-
} else if (token.type === cssTokenizer.TokenType.OpenSquareBracket) {
|
|
21862
|
-
let tempToken;
|
|
21863
|
-
const {
|
|
21864
|
-
start
|
|
21865
|
-
} = token;
|
|
21866
|
-
stream.advance();
|
|
21867
|
-
// Converts legacy Extended CSS selectors to the modern Extended CSS syntax.
|
|
21868
|
-
// For example:
|
|
21869
|
-
// - `[-ext-has=...]` → `:has(...)`
|
|
21870
|
-
// - `[-ext-contains=...]` → `:contains(...)`
|
|
21871
|
-
// - `[-ext-matches-css-before=...]` → `:matches-css(before, ...)`
|
|
21872
|
-
stream.skipWhitespace();
|
|
21873
|
-
stream.expect(cssTokenizer.TokenType.Ident);
|
|
21874
|
-
tempToken = stream.getOrFail();
|
|
21875
|
-
let attr = stream.source.slice(tempToken.start, tempToken.end);
|
|
21876
|
-
// Skip if the attribute name is not a legacy Extended CSS one
|
|
21877
|
-
if (!(attr.startsWith(LEGACY_EXT_CSS_ATTRIBUTE_PREFIX) || attr.startsWith(ABP_EXT_CSS_PREFIX))) {
|
|
21878
|
-
converted.push(stream.source.slice(start, tempToken.end));
|
|
21879
|
-
stream.advance();
|
|
21880
|
-
continue;
|
|
21881
|
-
}
|
|
21882
|
-
if (attr.startsWith(LEGACY_EXT_CSS_ATTRIBUTE_PREFIX)) {
|
|
21883
|
-
attr = attr.slice(LEGACY_EXT_CSS_ATTRIBUTE_PREFIX.length);
|
|
21884
|
-
}
|
|
21885
|
-
stream.advance();
|
|
21886
|
-
stream.skipWhitespace();
|
|
21887
|
-
// Next token should be an equality operator (=), because Extended CSS attribute selectors
|
|
21888
|
-
// do not support other operators
|
|
21889
|
-
stream.expect(cssTokenizer.TokenType.Delim, {
|
|
21890
|
-
value: EQUALS
|
|
21891
|
-
});
|
|
21892
|
-
stream.advance();
|
|
21893
|
-
// Skip optional whitespace after the operator
|
|
21894
|
-
stream.skipWhitespace();
|
|
21895
|
-
// Parse attribute value
|
|
21896
|
-
tempToken = stream.getOrFail();
|
|
21897
|
-
// According to the spec, attribute value should be an identifier or a string
|
|
21898
|
-
if (tempToken.type !== cssTokenizer.TokenType.Ident && tempToken.type !== cssTokenizer.TokenType.String) {
|
|
21899
|
-
throw new Error(sprintfJs.sprintf(ERROR_MESSAGES$1.INVALID_ATTRIBUTE_VALUE, cssTokenizer.getFormattedTokenName(tempToken.type), stream.source.slice(tempToken.start, tempToken.end)));
|
|
21900
|
-
}
|
|
21901
|
-
const value = stream.source.slice(tempToken.start, tempToken.end);
|
|
21902
|
-
// Advance the attribute value
|
|
21903
|
-
stream.advance();
|
|
21904
|
-
// Skip optional whitespace after the attribute value
|
|
21905
|
-
stream.skipWhitespace();
|
|
21906
|
-
// Next character should be a closing square bracket
|
|
21907
|
-
// We don't allow flags for Extended CSS attribute selectors
|
|
21908
|
-
stream.expect(cssTokenizer.TokenType.CloseSquareBracket);
|
|
21909
|
-
stream.advance();
|
|
21910
|
-
converted.push(COLON);
|
|
21911
|
-
convertAndPushPseudo(attr);
|
|
21912
|
-
let processedValue = value.slice(1, -1); // omit the quotes
|
|
21913
|
-
if (attr === PseudoClasses.Has) {
|
|
21914
|
-
// TODO: Optimize this to avoid double tokenization
|
|
21915
|
-
processedValue = CssSelectorConverter.convertToAdg(processedValue).result;
|
|
21916
|
-
}
|
|
21917
|
-
converted.push(processedValue);
|
|
21918
|
-
converted.push(CLOSE_PARENTHESIS);
|
|
21919
|
-
} else {
|
|
21920
|
-
converted.push(stream.source.slice(token.start, token.end));
|
|
21921
|
-
// Advance the token
|
|
21922
|
-
stream.advance();
|
|
21923
|
-
}
|
|
21924
|
-
}
|
|
21925
|
-
const convertedSelectorList = converted.join(EMPTY);
|
|
21926
|
-
return createConversionResult(convertedSelectorList, stream.source !== convertedSelectorList);
|
|
21927
|
-
}
|
|
21691
|
+
zod.string().min(1).transform(value => parseRawPlatforms(value));
|
|
21692
|
+
function getDefaultExportFromCjs(x) {
|
|
21693
|
+
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
|
|
21928
21694
|
}
|
|
21695
|
+
var mapObj$1 = {
|
|
21696
|
+
exports: {}
|
|
21697
|
+
};
|
|
21698
|
+
const isObject$1 = value => typeof value === 'object' && value !== null;
|
|
21699
|
+
const mapObjectSkip = Symbol('skip');
|
|
21929
21700
|
|
|
21930
|
-
|
|
21931
|
-
|
|
21932
|
-
|
|
21933
|
-
|
|
21934
|
-
|
|
21935
|
-
|
|
21936
|
-
|
|
21937
|
-
|
|
21938
|
-
|
|
21939
|
-
|
|
21940
|
-
* Converts a CSS injection rule to AdGuard format, if possible.
|
|
21941
|
-
*
|
|
21942
|
-
* @param rule Rule node to convert
|
|
21943
|
-
* @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
|
|
21944
|
-
* the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
|
|
21945
|
-
* If the rule was not converted, the result array will contain the original node with the same object reference
|
|
21946
|
-
* @throws If the rule is invalid or cannot be converted
|
|
21947
|
-
*/
|
|
21948
|
-
static convertToAdg(rule) {
|
|
21949
|
-
const separator = rule.separator.value;
|
|
21950
|
-
let convertedSeparator = separator;
|
|
21951
|
-
const stream = new CssTokenStream(rule.body.selectorList.value);
|
|
21952
|
-
const convertedSelectorList = CssSelectorConverter.convertToAdg(stream);
|
|
21953
|
-
// Change the separator if the rule contains ExtendedCSS elements,
|
|
21954
|
-
// but do not force non-extended CSS separator if the rule does not contain any ExtendedCSS selectors,
|
|
21955
|
-
// because sometimes we use it to force executing ExtendedCSS library.
|
|
21956
|
-
if (stream.hasAnySelectorExtendedCssNodeStrict() || rule.body.remove) {
|
|
21957
|
-
convertedSeparator = rule.exception ? exports.CosmeticRuleSeparator.AdgExtendedCssInjectionException : exports.CosmeticRuleSeparator.AdgExtendedCssInjection;
|
|
21958
|
-
} else if (rule.syntax !== exports.AdblockSyntax.Adg) {
|
|
21959
|
-
// If the original rule syntax is not AdGuard, use the default separator
|
|
21960
|
-
// e.g. if the input rule is from uBO, we need to convert ## to #$#.
|
|
21961
|
-
convertedSeparator = rule.exception ? exports.CosmeticRuleSeparator.AdgCssInjectionException : exports.CosmeticRuleSeparator.AdgCssInjection;
|
|
21962
|
-
}
|
|
21963
|
-
// Check if the rule needs to be converted
|
|
21964
|
-
if (!(rule.syntax === exports.AdblockSyntax.Common || rule.syntax === exports.AdblockSyntax.Adg) || separator !== convertedSeparator || convertedSelectorList.isConverted) {
|
|
21965
|
-
// TODO: Replace with custom clone method
|
|
21966
|
-
const ruleClone = clone(rule);
|
|
21967
|
-
ruleClone.syntax = exports.AdblockSyntax.Adg;
|
|
21968
|
-
ruleClone.separator.value = convertedSeparator;
|
|
21969
|
-
ruleClone.body.selectorList.value = convertedSelectorList.result;
|
|
21970
|
-
return createNodeConversionResult([ruleClone], true);
|
|
21971
|
-
}
|
|
21972
|
-
// Otherwise, return the original rule
|
|
21973
|
-
return createNodeConversionResult([rule], false);
|
|
21701
|
+
// Customized for this use-case
|
|
21702
|
+
const isObjectCustom = value => isObject$1(value) && !(value instanceof RegExp) && !(value instanceof Error) && !(value instanceof Date);
|
|
21703
|
+
const mapObject = (object, mapper, options, isSeen = new WeakMap()) => {
|
|
21704
|
+
options = {
|
|
21705
|
+
deep: false,
|
|
21706
|
+
target: {},
|
|
21707
|
+
...options
|
|
21708
|
+
};
|
|
21709
|
+
if (isSeen.has(object)) {
|
|
21710
|
+
return isSeen.get(object);
|
|
21974
21711
|
}
|
|
21975
|
-
|
|
21712
|
+
isSeen.set(object, options.target);
|
|
21713
|
+
const {
|
|
21714
|
+
target
|
|
21715
|
+
} = options;
|
|
21716
|
+
delete options.target;
|
|
21717
|
+
const mapArray = array => array.map(element => isObjectCustom(element) ? mapObject(element, mapper, options, isSeen) : element);
|
|
21718
|
+
if (Array.isArray(object)) {
|
|
21719
|
+
return mapArray(object);
|
|
21720
|
+
}
|
|
21721
|
+
for (const [key, value] of Object.entries(object)) {
|
|
21722
|
+
const mapResult = mapper(key, value, object);
|
|
21723
|
+
if (mapResult === mapObjectSkip) {
|
|
21724
|
+
continue;
|
|
21725
|
+
}
|
|
21726
|
+
let [newKey, newValue, {
|
|
21727
|
+
shouldRecurse = true
|
|
21728
|
+
} = {}] = mapResult;
|
|
21976
21729
|
|
|
21977
|
-
|
|
21978
|
-
|
|
21979
|
-
|
|
21980
|
-
/**
|
|
21981
|
-
* Element hiding rule converter class
|
|
21982
|
-
*
|
|
21983
|
-
* @todo Implement `convertToUbo` and `convertToAbp`
|
|
21984
|
-
*/
|
|
21985
|
-
class ElementHidingRuleConverter extends RuleConverterBase {
|
|
21986
|
-
/**
|
|
21987
|
-
* Converts an element hiding rule to AdGuard format, if possible.
|
|
21988
|
-
*
|
|
21989
|
-
* @param rule Rule node to convert
|
|
21990
|
-
* @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
|
|
21991
|
-
* the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
|
|
21992
|
-
* If the rule was not converted, the result array will contain the original node with the same object reference
|
|
21993
|
-
* @throws If the rule is invalid or cannot be converted
|
|
21994
|
-
*/
|
|
21995
|
-
static convertToAdg(rule) {
|
|
21996
|
-
const separator = rule.separator.value;
|
|
21997
|
-
let convertedSeparator = separator;
|
|
21998
|
-
const stream = new CssTokenStream(rule.body.selectorList.value);
|
|
21999
|
-
const convertedSelectorList = CssSelectorConverter.convertToAdg(stream);
|
|
22000
|
-
// Change the separator if the rule contains ExtendedCSS elements,
|
|
22001
|
-
// but do not force non-extended CSS separator if the rule does not contain any ExtendedCSS selectors,
|
|
22002
|
-
// because sometimes we use it to force executing ExtendedCSS library.
|
|
22003
|
-
if (stream.hasAnySelectorExtendedCssNodeStrict()) {
|
|
22004
|
-
convertedSeparator = rule.exception ? exports.CosmeticRuleSeparator.ExtendedElementHidingException : exports.CosmeticRuleSeparator.ExtendedElementHiding;
|
|
21730
|
+
// Drop `__proto__` keys.
|
|
21731
|
+
if (newKey === '__proto__') {
|
|
21732
|
+
continue;
|
|
22005
21733
|
}
|
|
22006
|
-
|
|
22007
|
-
|
|
22008
|
-
// TODO: Replace with custom clone method
|
|
22009
|
-
const ruleClone = clone(rule);
|
|
22010
|
-
ruleClone.syntax = exports.AdblockSyntax.Adg;
|
|
22011
|
-
ruleClone.separator.value = convertedSeparator;
|
|
22012
|
-
ruleClone.body.selectorList.value = convertedSelectorList.result;
|
|
22013
|
-
return createNodeConversionResult([ruleClone], true);
|
|
21734
|
+
if (options.deep && shouldRecurse && isObjectCustom(newValue)) {
|
|
21735
|
+
newValue = Array.isArray(newValue) ? mapArray(newValue) : mapObject(newValue, mapper, options, isSeen);
|
|
22014
21736
|
}
|
|
22015
|
-
|
|
22016
|
-
return createNodeConversionResult([rule], false);
|
|
21737
|
+
target[newKey] = newValue;
|
|
22017
21738
|
}
|
|
22018
|
-
|
|
22019
|
-
|
|
22020
|
-
|
|
22021
|
-
|
|
22022
|
-
|
|
22023
|
-
|
|
22024
|
-
|
|
22025
|
-
|
|
22026
|
-
|
|
22027
|
-
|
|
22028
|
-
|
|
22029
|
-
|
|
22030
|
-
|
|
22031
|
-
|
|
22032
|
-
|
|
22033
|
-
|
|
22034
|
-
|
|
22035
|
-
|
|
22036
|
-
|
|
22037
|
-
|
|
22038
|
-
|
|
22039
|
-
|
|
22040
|
-
|
|
21739
|
+
return target;
|
|
21740
|
+
};
|
|
21741
|
+
mapObj$1.exports = (object, mapper, options) => {
|
|
21742
|
+
if (!isObject$1(object)) {
|
|
21743
|
+
throw new TypeError(`Expected an object, got \`${object}\` (${typeof object})`);
|
|
21744
|
+
}
|
|
21745
|
+
return mapObject(object, mapper, options);
|
|
21746
|
+
};
|
|
21747
|
+
mapObj$1.exports.mapObjectSkip = mapObjectSkip;
|
|
21748
|
+
var mapObjExports = mapObj$1.exports;
|
|
21749
|
+
var camelcase = {
|
|
21750
|
+
exports: {}
|
|
21751
|
+
};
|
|
21752
|
+
const UPPERCASE = /[\p{Lu}]/u;
|
|
21753
|
+
const LOWERCASE = /[\p{Ll}]/u;
|
|
21754
|
+
const LEADING_CAPITAL = /^[\p{Lu}](?![\p{Lu}])/gu;
|
|
21755
|
+
const IDENTIFIER = /([\p{Alpha}\p{N}_]|$)/u;
|
|
21756
|
+
const SEPARATORS = /[_.\- ]+/;
|
|
21757
|
+
const LEADING_SEPARATORS = new RegExp('^' + SEPARATORS.source);
|
|
21758
|
+
const SEPARATORS_AND_IDENTIFIER = new RegExp(SEPARATORS.source + IDENTIFIER.source, 'gu');
|
|
21759
|
+
const NUMBERS_AND_IDENTIFIER = new RegExp('\\d+' + IDENTIFIER.source, 'gu');
|
|
21760
|
+
const preserveCamelCase = (string, toLowerCase, toUpperCase) => {
|
|
21761
|
+
let isLastCharLower = false;
|
|
21762
|
+
let isLastCharUpper = false;
|
|
21763
|
+
let isLastLastCharUpper = false;
|
|
21764
|
+
for (let i = 0; i < string.length; i++) {
|
|
21765
|
+
const character = string[i];
|
|
21766
|
+
if (isLastCharLower && UPPERCASE.test(character)) {
|
|
21767
|
+
string = string.slice(0, i) + '-' + string.slice(i);
|
|
21768
|
+
isLastCharLower = false;
|
|
21769
|
+
isLastLastCharUpper = isLastCharUpper;
|
|
21770
|
+
isLastCharUpper = true;
|
|
21771
|
+
i++;
|
|
21772
|
+
} else if (isLastCharUpper && isLastLastCharUpper && LOWERCASE.test(character)) {
|
|
21773
|
+
string = string.slice(0, i - 1) + '-' + string.slice(i - 1);
|
|
21774
|
+
isLastLastCharUpper = isLastCharUpper;
|
|
21775
|
+
isLastCharUpper = false;
|
|
21776
|
+
isLastCharLower = true;
|
|
21777
|
+
} else {
|
|
21778
|
+
isLastCharLower = toLowerCase(character) === character && toUpperCase(character) !== character;
|
|
21779
|
+
isLastLastCharUpper = isLastCharUpper;
|
|
21780
|
+
isLastCharUpper = toUpperCase(character) === character && toLowerCase(character) !== character;
|
|
22041
21781
|
}
|
|
21782
|
+
}
|
|
21783
|
+
return string;
|
|
21784
|
+
};
|
|
21785
|
+
const preserveConsecutiveUppercase = (input, toLowerCase) => {
|
|
21786
|
+
LEADING_CAPITAL.lastIndex = 0;
|
|
21787
|
+
return input.replace(LEADING_CAPITAL, m1 => toLowerCase(m1));
|
|
21788
|
+
};
|
|
21789
|
+
const postProcess = (input, toUpperCase) => {
|
|
21790
|
+
SEPARATORS_AND_IDENTIFIER.lastIndex = 0;
|
|
21791
|
+
NUMBERS_AND_IDENTIFIER.lastIndex = 0;
|
|
21792
|
+
return input.replace(SEPARATORS_AND_IDENTIFIER, (_, identifier) => toUpperCase(identifier)).replace(NUMBERS_AND_IDENTIFIER, m => toUpperCase(m));
|
|
21793
|
+
};
|
|
21794
|
+
const camelCase$1 = (input, options) => {
|
|
21795
|
+
if (!(typeof input === 'string' || Array.isArray(input))) {
|
|
21796
|
+
throw new TypeError('Expected the input to be `string | string[]`');
|
|
21797
|
+
}
|
|
21798
|
+
options = {
|
|
21799
|
+
pascalCase: false,
|
|
21800
|
+
preserveConsecutiveUppercase: false,
|
|
21801
|
+
...options
|
|
22042
21802
|
};
|
|
22043
|
-
if (
|
|
22044
|
-
|
|
21803
|
+
if (Array.isArray(input)) {
|
|
21804
|
+
input = input.map(x => x.trim()).filter(x => x.length).join('-');
|
|
21805
|
+
} else {
|
|
21806
|
+
input = input.trim();
|
|
22045
21807
|
}
|
|
22046
|
-
|
|
22047
|
-
|
|
22048
|
-
|
|
22049
|
-
|
|
22050
|
-
|
|
22051
|
-
|
|
22052
|
-
|
|
22053
|
-
|
|
22054
|
-
const
|
|
22055
|
-
|
|
22056
|
-
|
|
22057
|
-
|
|
21808
|
+
if (input.length === 0) {
|
|
21809
|
+
return '';
|
|
21810
|
+
}
|
|
21811
|
+
const toLowerCase = options.locale === false ? string => string.toLowerCase() : string => string.toLocaleLowerCase(options.locale);
|
|
21812
|
+
const toUpperCase = options.locale === false ? string => string.toUpperCase() : string => string.toLocaleUpperCase(options.locale);
|
|
21813
|
+
if (input.length === 1) {
|
|
21814
|
+
return options.pascalCase ? toUpperCase(input) : toLowerCase(input);
|
|
21815
|
+
}
|
|
21816
|
+
const hasUpperCase = input !== toLowerCase(input);
|
|
21817
|
+
if (hasUpperCase) {
|
|
21818
|
+
input = preserveCamelCase(input, toLowerCase, toUpperCase);
|
|
21819
|
+
}
|
|
21820
|
+
input = input.replace(LEADING_SEPARATORS, '');
|
|
21821
|
+
if (options.preserveConsecutiveUppercase) {
|
|
21822
|
+
input = preserveConsecutiveUppercase(input, toLowerCase);
|
|
21823
|
+
} else {
|
|
21824
|
+
input = toLowerCase(input);
|
|
21825
|
+
}
|
|
21826
|
+
if (options.pascalCase) {
|
|
21827
|
+
input = toUpperCase(input.charAt(0)) + input.slice(1);
|
|
21828
|
+
}
|
|
21829
|
+
return postProcess(input, toUpperCase);
|
|
22058
21830
|
};
|
|
22059
|
-
|
|
22060
|
-
|
|
22061
|
-
|
|
22062
|
-
|
|
22063
|
-
|
|
22064
|
-
|
|
22065
|
-
|
|
22066
|
-
|
|
22067
|
-
*
|
|
22068
|
-
* @param rule Rule node to convert
|
|
22069
|
-
* @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
|
|
22070
|
-
* the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
|
|
22071
|
-
* If the rule was not converted, the result array will contain the original node with the same object reference
|
|
22072
|
-
* @throws If the rule is invalid or cannot be converted
|
|
22073
|
-
* @example
|
|
22074
|
-
* If the input rule is:
|
|
22075
|
-
* ```adblock
|
|
22076
|
-
* example.com##^responseheader(header-name)
|
|
22077
|
-
* ```
|
|
22078
|
-
* The output will be:
|
|
22079
|
-
* ```adblock
|
|
22080
|
-
* ||example.com^$removeheader=header-name
|
|
22081
|
-
* ```
|
|
22082
|
-
*/
|
|
22083
|
-
static convertToAdg(rule) {
|
|
22084
|
-
// TODO: Add support for ABP syntax once it starts supporting header removal rules
|
|
22085
|
-
// Leave the rule as is if it's not a header removal rule
|
|
22086
|
-
if (rule.category !== exports.RuleCategory.Cosmetic || rule.type !== exports.CosmeticRuleType.HtmlFilteringRule) {
|
|
22087
|
-
return createNodeConversionResult([rule], false);
|
|
21831
|
+
camelcase.exports = camelCase$1;
|
|
21832
|
+
// TODO: Remove this for the next major release
|
|
21833
|
+
camelcase.exports.default = camelCase$1;
|
|
21834
|
+
var camelcaseExports = camelcase.exports;
|
|
21835
|
+
class QuickLRU {
|
|
21836
|
+
constructor(options = {}) {
|
|
21837
|
+
if (!(options.maxSize && options.maxSize > 0)) {
|
|
21838
|
+
throw new TypeError('`maxSize` must be a number greater than 0');
|
|
22088
21839
|
}
|
|
22089
|
-
|
|
22090
|
-
|
|
22091
|
-
|
|
22092
|
-
|
|
22093
|
-
|
|
22094
|
-
|
|
22095
|
-
|
|
22096
|
-
|
|
21840
|
+
this.maxSize = options.maxSize;
|
|
21841
|
+
this.onEviction = options.onEviction;
|
|
21842
|
+
this.cache = new Map();
|
|
21843
|
+
this.oldCache = new Map();
|
|
21844
|
+
this._size = 0;
|
|
21845
|
+
}
|
|
21846
|
+
_set(key, value) {
|
|
21847
|
+
this.cache.set(key, value);
|
|
21848
|
+
this._size++;
|
|
21849
|
+
if (this._size >= this.maxSize) {
|
|
21850
|
+
this._size = 0;
|
|
21851
|
+
if (typeof this.onEviction === 'function') {
|
|
21852
|
+
for (const [key, value] of this.oldCache.entries()) {
|
|
21853
|
+
this.onEviction(key, value);
|
|
21854
|
+
}
|
|
21855
|
+
}
|
|
21856
|
+
this.oldCache = this.cache;
|
|
21857
|
+
this.cache = new Map();
|
|
22097
21858
|
}
|
|
22098
|
-
|
|
22099
|
-
|
|
22100
|
-
if (
|
|
22101
|
-
return
|
|
21859
|
+
}
|
|
21860
|
+
get(key) {
|
|
21861
|
+
if (this.cache.has(key)) {
|
|
21862
|
+
return this.cache.get(key);
|
|
21863
|
+
}
|
|
21864
|
+
if (this.oldCache.has(key)) {
|
|
21865
|
+
const value = this.oldCache.get(key);
|
|
21866
|
+
this.oldCache.delete(key);
|
|
21867
|
+
this._set(key, value);
|
|
21868
|
+
return value;
|
|
21869
|
+
}
|
|
21870
|
+
}
|
|
21871
|
+
set(key, value) {
|
|
21872
|
+
if (this.cache.has(key)) {
|
|
21873
|
+
this.cache.set(key, value);
|
|
21874
|
+
} else {
|
|
21875
|
+
this._set(key, value);
|
|
21876
|
+
}
|
|
21877
|
+
return this;
|
|
21878
|
+
}
|
|
21879
|
+
has(key) {
|
|
21880
|
+
return this.cache.has(key) || this.oldCache.has(key);
|
|
21881
|
+
}
|
|
21882
|
+
peek(key) {
|
|
21883
|
+
if (this.cache.has(key)) {
|
|
21884
|
+
return this.cache.get(key);
|
|
22102
21885
|
}
|
|
22103
|
-
|
|
22104
|
-
|
|
22105
|
-
return createNodeConversionResult([rule], false);
|
|
21886
|
+
if (this.oldCache.has(key)) {
|
|
21887
|
+
return this.oldCache.get(key);
|
|
22106
21888
|
}
|
|
22107
|
-
|
|
22108
|
-
|
|
22109
|
-
|
|
22110
|
-
|
|
22111
|
-
|
|
22112
|
-
// Do not allow empty parameter
|
|
22113
|
-
if (param.length === 0) {
|
|
22114
|
-
throw new RuleConversionError(ERROR_MESSAGES.EMPTY_PARAMETER);
|
|
21889
|
+
}
|
|
21890
|
+
delete(key) {
|
|
21891
|
+
const deleted = this.cache.delete(key);
|
|
21892
|
+
if (deleted) {
|
|
21893
|
+
this._size--;
|
|
22115
21894
|
}
|
|
22116
|
-
|
|
22117
|
-
|
|
22118
|
-
|
|
22119
|
-
|
|
22120
|
-
|
|
22121
|
-
|
|
22122
|
-
|
|
22123
|
-
|
|
21895
|
+
return this.oldCache.delete(key) || deleted;
|
|
21896
|
+
}
|
|
21897
|
+
clear() {
|
|
21898
|
+
this.cache.clear();
|
|
21899
|
+
this.oldCache.clear();
|
|
21900
|
+
this._size = 0;
|
|
21901
|
+
}
|
|
21902
|
+
*keys() {
|
|
21903
|
+
for (const [key] of this) {
|
|
21904
|
+
yield key;
|
|
22124
21905
|
}
|
|
22125
|
-
|
|
22126
|
-
|
|
22127
|
-
|
|
22128
|
-
|
|
22129
|
-
// ||single-domain-from-the-rule^
|
|
22130
|
-
pattern.push(ADBLOCK_URL_START, rule.domains.children[0].value, ADBLOCK_URL_SEPARATOR);
|
|
22131
|
-
} else if (rule.domains.children.length > 1) {
|
|
22132
|
-
// TODO: Add support for multiple domains, for example:
|
|
22133
|
-
// example.com,example.org,example.net##^responseheader(header-name)
|
|
22134
|
-
// We should consider allowing $domain with $removeheader modifier,
|
|
22135
|
-
// for example:
|
|
22136
|
-
// $removeheader=header-name,domain=example.com|example.org|example.net
|
|
22137
|
-
throw new RuleConversionError(ERROR_MESSAGES.MULTIPLE_DOMAINS_NOT_SUPPORTED);
|
|
21906
|
+
}
|
|
21907
|
+
*values() {
|
|
21908
|
+
for (const [, value] of this) {
|
|
21909
|
+
yield value;
|
|
22138
21910
|
}
|
|
22139
|
-
// Prepare network rule modifiers
|
|
22140
|
-
const modifiers = createModifierListNode();
|
|
22141
|
-
modifiers.children.push(createModifierNode(ADG_REMOVEHEADER_MODIFIER, param));
|
|
22142
|
-
// Construct the network rule
|
|
22143
|
-
return createNodeConversionResult([createNetworkRuleNode(pattern.join(EMPTY), modifiers,
|
|
22144
|
-
// Copy the exception flag
|
|
22145
|
-
rule.exception, exports.AdblockSyntax.Adg)], true);
|
|
22146
21911
|
}
|
|
22147
|
-
|
|
22148
|
-
|
|
22149
|
-
|
|
22150
|
-
* @file Cosmetic rule converter
|
|
22151
|
-
*/
|
|
22152
|
-
/**
|
|
22153
|
-
* Cosmetic rule converter class (also known as "non-basic rule converter")
|
|
22154
|
-
*
|
|
22155
|
-
* @todo Implement `convertToUbo` and `convertToAbp`
|
|
22156
|
-
*/
|
|
22157
|
-
class CosmeticRuleConverter extends RuleConverterBase {
|
|
22158
|
-
/**
|
|
22159
|
-
* Converts a cosmetic rule to AdGuard syntax, if possible.
|
|
22160
|
-
*
|
|
22161
|
-
* @param rule Rule node to convert
|
|
22162
|
-
* @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
|
|
22163
|
-
* the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
|
|
22164
|
-
* If the rule was not converted, the result array will contain the original node with the same object reference
|
|
22165
|
-
* @throws If the rule is invalid or cannot be converted
|
|
22166
|
-
*/
|
|
22167
|
-
static convertToAdg(rule) {
|
|
22168
|
-
let subconverterResult;
|
|
22169
|
-
// Convert cosmetic rule based on its type
|
|
22170
|
-
switch (rule.type) {
|
|
22171
|
-
case exports.CosmeticRuleType.ElementHidingRule:
|
|
22172
|
-
subconverterResult = ElementHidingRuleConverter.convertToAdg(rule);
|
|
22173
|
-
break;
|
|
22174
|
-
case exports.CosmeticRuleType.ScriptletInjectionRule:
|
|
22175
|
-
subconverterResult = ScriptletRuleConverter.convertToAdg(rule);
|
|
22176
|
-
break;
|
|
22177
|
-
case exports.CosmeticRuleType.CssInjectionRule:
|
|
22178
|
-
subconverterResult = CssInjectionRuleConverter.convertToAdg(rule);
|
|
22179
|
-
break;
|
|
22180
|
-
case exports.CosmeticRuleType.HtmlFilteringRule:
|
|
22181
|
-
// Handle special case: uBO response header filtering rule
|
|
22182
|
-
// TODO: Optimize double CSS tokenization here
|
|
22183
|
-
subconverterResult = HeaderRemovalRuleConverter.convertToAdg(rule);
|
|
22184
|
-
if (subconverterResult.isConverted) {
|
|
22185
|
-
break;
|
|
22186
|
-
}
|
|
22187
|
-
subconverterResult = HtmlRuleConverter.convertToAdg(rule);
|
|
22188
|
-
break;
|
|
22189
|
-
// Note: Currently, only ADG supports JS injection rules, so we don't need to convert them
|
|
22190
|
-
case exports.CosmeticRuleType.JsInjectionRule:
|
|
22191
|
-
subconverterResult = createNodeConversionResult([rule], false);
|
|
22192
|
-
break;
|
|
22193
|
-
default:
|
|
22194
|
-
throw new RuleConversionError('Unsupported cosmetic rule type');
|
|
21912
|
+
*[Symbol.iterator]() {
|
|
21913
|
+
for (const item of this.cache) {
|
|
21914
|
+
yield item;
|
|
22195
21915
|
}
|
|
22196
|
-
|
|
22197
|
-
|
|
22198
|
-
|
|
22199
|
-
|
|
22200
|
-
// uBO doesn't support this rule:
|
|
22201
|
-
// example.com##+js(set-constant.js, foo, bar):matches-path(/baz)
|
|
22202
|
-
if (rule.type === exports.CosmeticRuleType.ScriptletInjectionRule) {
|
|
22203
|
-
throw new RuleConversionError('uBO scriptlet injection rules don\'t support cosmetic rule modifiers');
|
|
22204
|
-
}
|
|
22205
|
-
convertedModifiers = AdgCosmeticRuleModifierConverter.convertFromUbo(rule.modifiers);
|
|
22206
|
-
} else if (rule.syntax === exports.AdblockSyntax.Abp) {
|
|
22207
|
-
// TODO: Implement once ABP starts supporting cosmetic rule modifiers
|
|
22208
|
-
throw new RuleConversionError('ABP don\'t support cosmetic rule modifiers');
|
|
21916
|
+
for (const item of this.oldCache) {
|
|
21917
|
+
const [key] = item;
|
|
21918
|
+
if (!this.cache.has(key)) {
|
|
21919
|
+
yield item;
|
|
22209
21920
|
}
|
|
22210
21921
|
}
|
|
22211
|
-
|
|
22212
|
-
|
|
22213
|
-
|
|
22214
|
-
|
|
22215
|
-
|
|
22216
|
-
|
|
22217
|
-
|
|
22218
|
-
});
|
|
22219
|
-
return subconverterResult;
|
|
21922
|
+
}
|
|
21923
|
+
get size() {
|
|
21924
|
+
let oldCacheSize = 0;
|
|
21925
|
+
for (const key of this.oldCache.keys()) {
|
|
21926
|
+
if (!this.cache.has(key)) {
|
|
21927
|
+
oldCacheSize++;
|
|
21928
|
+
}
|
|
22220
21929
|
}
|
|
22221
|
-
return
|
|
21930
|
+
return Math.min(this._size + oldCacheSize, this.maxSize);
|
|
22222
21931
|
}
|
|
22223
21932
|
}
|
|
21933
|
+
var quickLru = QuickLRU;
|
|
21934
|
+
const mapObj = mapObjExports;
|
|
21935
|
+
const camelCase = camelcaseExports;
|
|
21936
|
+
const QuickLru = quickLru;
|
|
21937
|
+
const has = (array, key) => array.some(x => {
|
|
21938
|
+
if (typeof x === 'string') {
|
|
21939
|
+
return x === key;
|
|
21940
|
+
}
|
|
21941
|
+
x.lastIndex = 0;
|
|
21942
|
+
return x.test(key);
|
|
21943
|
+
});
|
|
21944
|
+
const cache = new QuickLru({
|
|
21945
|
+
maxSize: 100000
|
|
21946
|
+
});
|
|
22224
21947
|
|
|
22225
|
-
|
|
22226
|
-
|
|
22227
|
-
|
|
22228
|
-
|
|
22229
|
-
|
|
22230
|
-
*/
|
|
22231
|
-
const ABP_RESOURCE_PREFIX = 'abp-resource:';
|
|
22232
|
-
const ABP_RESOURCE_PREFIX_LENGTH = ABP_RESOURCE_PREFIX.length;
|
|
22233
|
-
/**
|
|
22234
|
-
* Transforms the name of an ABP redirect to a normalized form.
|
|
22235
|
-
*
|
|
22236
|
-
* @param name Redirect name to normalize.
|
|
22237
|
-
*
|
|
22238
|
-
* @returns Normalized redirect name.
|
|
22239
|
-
*
|
|
22240
|
-
* @example
|
|
22241
|
-
* abpRedirectNameNormalizer('abp-resource:my-resource') // => 'my-resource'
|
|
22242
|
-
*/
|
|
22243
|
-
const abpRedirectNameNormalizer = name => {
|
|
22244
|
-
if (name.startsWith(ABP_RESOURCE_PREFIX)) {
|
|
22245
|
-
return name.slice(ABP_RESOURCE_PREFIX_LENGTH);
|
|
21948
|
+
// Reproduces behavior from `map-obj`
|
|
21949
|
+
const isObject = value => typeof value === 'object' && value !== null && !(value instanceof RegExp) && !(value instanceof Error) && !(value instanceof Date);
|
|
21950
|
+
const camelCaseConvert = (input, options) => {
|
|
21951
|
+
if (!isObject(input)) {
|
|
21952
|
+
return input;
|
|
22246
21953
|
}
|
|
22247
|
-
|
|
21954
|
+
options = {
|
|
21955
|
+
deep: false,
|
|
21956
|
+
pascalCase: false,
|
|
21957
|
+
...options
|
|
21958
|
+
};
|
|
21959
|
+
const {
|
|
21960
|
+
exclude,
|
|
21961
|
+
pascalCase,
|
|
21962
|
+
stopPaths,
|
|
21963
|
+
deep
|
|
21964
|
+
} = options;
|
|
21965
|
+
const stopPathsSet = new Set(stopPaths);
|
|
21966
|
+
const makeMapper = parentPath => (key, value) => {
|
|
21967
|
+
if (deep && isObject(value)) {
|
|
21968
|
+
const path = parentPath === undefined ? key : `${parentPath}.${key}`;
|
|
21969
|
+
if (!stopPathsSet.has(path)) {
|
|
21970
|
+
value = mapObj(value, makeMapper(path));
|
|
21971
|
+
}
|
|
21972
|
+
}
|
|
21973
|
+
if (!(exclude && has(exclude, key))) {
|
|
21974
|
+
const cacheKey = pascalCase ? `${key}_` : key;
|
|
21975
|
+
if (cache.has(cacheKey)) {
|
|
21976
|
+
key = cache.get(cacheKey);
|
|
21977
|
+
} else {
|
|
21978
|
+
const returnValue = camelCase(key, {
|
|
21979
|
+
pascalCase,
|
|
21980
|
+
locale: false
|
|
21981
|
+
});
|
|
21982
|
+
if (key.length < 100) {
|
|
21983
|
+
// Prevent abuse
|
|
21984
|
+
cache.set(cacheKey, returnValue);
|
|
21985
|
+
}
|
|
21986
|
+
key = returnValue;
|
|
21987
|
+
}
|
|
21988
|
+
}
|
|
21989
|
+
return [key, value];
|
|
21990
|
+
};
|
|
21991
|
+
return mapObj(input, makeMapper(undefined));
|
|
22248
21992
|
};
|
|
22249
|
-
|
|
22250
|
-
|
|
22251
|
-
|
|
22252
|
-
class RedirectsCompatibilityTable extends CompatibilityTableBase {
|
|
22253
|
-
/**
|
|
22254
|
-
* Creates a new instance of the compatibility table for redirects.
|
|
22255
|
-
*
|
|
22256
|
-
* @param data Compatibility table data.
|
|
22257
|
-
*/
|
|
22258
|
-
constructor(data) {
|
|
22259
|
-
super(data, abpRedirectNameNormalizer);
|
|
21993
|
+
var camelcaseKeys = (input, options) => {
|
|
21994
|
+
if (Array.isArray(input)) {
|
|
21995
|
+
return Object.keys(input).map(key => camelCaseConvert(input[key], options));
|
|
22260
21996
|
}
|
|
22261
|
-
|
|
22262
|
-
|
|
22263
|
-
|
|
22264
|
-
*/
|
|
22265
|
-
deepFreeze(redirectsCompatibilityTableData);
|
|
22266
|
-
/**
|
|
22267
|
-
* Compatibility table instance for redirects.
|
|
22268
|
-
*/
|
|
22269
|
-
const redirectsCompatibilityTable = new RedirectsCompatibilityTable(redirectsCompatibilityTableData);
|
|
21997
|
+
return camelCaseConvert(input, options);
|
|
21998
|
+
};
|
|
21999
|
+
var camelCaseKeys = /*@__PURE__*/getDefaultExportFromCjs(camelcaseKeys);
|
|
22270
22000
|
|
|
22271
22001
|
/**
|
|
22272
|
-
* @file
|
|
22002
|
+
* @file Zod camelCase utility.
|
|
22273
22003
|
*/
|
|
22004
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
22274
22005
|
/**
|
|
22275
|
-
*
|
|
22006
|
+
* Transforms Zod schema to camelCase.
|
|
22007
|
+
*
|
|
22008
|
+
* @param zod Zod schema.
|
|
22009
|
+
*
|
|
22010
|
+
* @returns Zod schema with camelCase properties.
|
|
22011
|
+
*
|
|
22012
|
+
* @see {@link https://github.com/colinhacks/zod/issues/486#issuecomment-1501097361}
|
|
22276
22013
|
*/
|
|
22277
|
-
|
|
22014
|
+
const zodToCamelCase = zod => {
|
|
22015
|
+
return zod.transform(val => camelCaseKeys(val));
|
|
22016
|
+
};
|
|
22017
|
+
|
|
22278
22018
|
/**
|
|
22279
|
-
*
|
|
22019
|
+
* @file Base compatibility data schema, which is commonly used in compatibility tables.
|
|
22280
22020
|
*/
|
|
22281
|
-
deepFreeze(scriptletsCompatibilityTableData);
|
|
22282
22021
|
/**
|
|
22283
|
-
*
|
|
22022
|
+
* Zod schema for boolean values. Accepts both boolean and string values.
|
|
22284
22023
|
*/
|
|
22285
|
-
const
|
|
22286
|
-
|
|
22287
|
-
/* eslint-disable no-bitwise */
|
|
22024
|
+
const booleanSchema = zod.union([zod.string().transform(val => val.trim().toLowerCase() === 'true'), zod.boolean()]);
|
|
22288
22025
|
/**
|
|
22289
|
-
*
|
|
22026
|
+
* Zod schema for non-empty string values.
|
|
22290
22027
|
*/
|
|
22028
|
+
const nonEmptyStringSchema = zod.string().transform(val => val.trim()).pipe(zod.string().min(1));
|
|
22291
22029
|
/**
|
|
22292
|
-
*
|
|
22293
|
-
*
|
|
22030
|
+
* Zod schema for base compatibility data.
|
|
22031
|
+
* Here we use snake_case properties because the compatibility data is stored in YAML files.
|
|
22294
22032
|
*/
|
|
22295
|
-
const
|
|
22033
|
+
const baseCompatibilityDataSchema = zod.object({
|
|
22034
|
+
/**
|
|
22035
|
+
* Name of the actual entity.
|
|
22036
|
+
*/
|
|
22037
|
+
name: nonEmptyStringSchema,
|
|
22038
|
+
/**
|
|
22039
|
+
* List of aliases for the entity (if any).
|
|
22040
|
+
*/
|
|
22041
|
+
aliases: zod.array(nonEmptyStringSchema).nullable().default(null),
|
|
22042
|
+
/**
|
|
22043
|
+
* Short description of the actual entity.
|
|
22044
|
+
* If not specified or it's value is `null`, then the description is not available.
|
|
22045
|
+
*/
|
|
22046
|
+
description: nonEmptyStringSchema.nullable().default(null),
|
|
22047
|
+
/**
|
|
22048
|
+
* Link to the documentation. If not specified or it's value is `null`, then the documentation is not available.
|
|
22049
|
+
*/
|
|
22050
|
+
docs: nonEmptyStringSchema.nullable().default(null),
|
|
22051
|
+
/**
|
|
22052
|
+
* The version of the adblocker in which the entity was added.
|
|
22053
|
+
* For AdGuard resources, the version of the library is specified.
|
|
22054
|
+
*/
|
|
22055
|
+
version_added: nonEmptyStringSchema.nullable().default(null),
|
|
22056
|
+
/**
|
|
22057
|
+
* The version of the adblocker when the entity was removed.
|
|
22058
|
+
*/
|
|
22059
|
+
version_removed: nonEmptyStringSchema.nullable().default(null),
|
|
22060
|
+
/**
|
|
22061
|
+
* Describes whether the entity is deprecated.
|
|
22062
|
+
*/
|
|
22063
|
+
deprecated: booleanSchema.default(false),
|
|
22064
|
+
/**
|
|
22065
|
+
* Message that describes why the entity is deprecated.
|
|
22066
|
+
* If not specified or it's value is `null`, then the message is not available.
|
|
22067
|
+
* It's value is omitted if the entity is not marked as deprecated.
|
|
22068
|
+
*/
|
|
22069
|
+
deprecation_message: nonEmptyStringSchema.nullable().default(null),
|
|
22070
|
+
/**
|
|
22071
|
+
* Describes whether the entity is removed; for *already removed* features.
|
|
22072
|
+
*/
|
|
22073
|
+
removed: booleanSchema.default(false),
|
|
22074
|
+
/**
|
|
22075
|
+
* Message that describes why the entity is removed.
|
|
22076
|
+
* If not specified or it's value is `null`, then the message is not available.
|
|
22077
|
+
* It's value is omitted if the entity is not marked as deprecated.
|
|
22078
|
+
*/
|
|
22079
|
+
removal_message: nonEmptyStringSchema.nullable().default(null)
|
|
22080
|
+
});
|
|
22296
22081
|
/**
|
|
22297
|
-
*
|
|
22298
|
-
* Safari content blockers.
|
|
22082
|
+
* Zod schema for base compatibility data with camelCase properties.
|
|
22299
22083
|
*/
|
|
22300
|
-
|
|
22084
|
+
zodToCamelCase(baseCompatibilityDataSchema);
|
|
22301
22085
|
/**
|
|
22302
|
-
*
|
|
22303
|
-
*
|
|
22304
|
-
* @param rawPlatforms Raw platform string, e.g. 'adg_safari_any|adg_os_any'.
|
|
22086
|
+
* Refinement logic for base compatibility data.
|
|
22305
22087
|
*
|
|
22306
|
-
* @
|
|
22307
|
-
|
|
22308
|
-
const parseRawPlatforms = rawPlatforms => {
|
|
22309
|
-
// e.g. 'adg_safari_any|adg_os_any'
|
|
22310
|
-
const rawPlatformList = rawPlatforms.split(PLATFORM_SEPARATOR).map(rawPlatform => rawPlatform.trim());
|
|
22311
|
-
let result = 0;
|
|
22312
|
-
for (let rawPlatform of rawPlatformList) {
|
|
22313
|
-
// negation, e.g. 'adg_any|~adg_safari_any' means any AdGuard product except Safari content blockers
|
|
22314
|
-
let negated = false;
|
|
22315
|
-
if (rawPlatform.startsWith(PLATFORM_NEGATION)) {
|
|
22316
|
-
negated = true;
|
|
22317
|
-
rawPlatform = rawPlatform.slice(1).trim();
|
|
22318
|
-
}
|
|
22319
|
-
const platform = SPECIFIC_PLATFORM_MAP.get(rawPlatform) ?? GENERIC_PLATFORM_MAP.get(rawPlatform);
|
|
22320
|
-
if (isUndefined(platform)) {
|
|
22321
|
-
throw new Error(`Unknown platform: ${rawPlatform}`);
|
|
22322
|
-
}
|
|
22323
|
-
if (negated) {
|
|
22324
|
-
result &= ~platform;
|
|
22325
|
-
} else {
|
|
22326
|
-
result |= platform;
|
|
22327
|
-
}
|
|
22328
|
-
}
|
|
22329
|
-
if (result === 0) {
|
|
22330
|
-
throw new Error('No platforms specified');
|
|
22331
|
-
}
|
|
22332
|
-
return result;
|
|
22333
|
-
};
|
|
22334
|
-
/**
|
|
22335
|
-
* Platform schema.
|
|
22088
|
+
* @param data Base compatibility data.
|
|
22089
|
+
* @param ctx Refinement context.
|
|
22336
22090
|
*/
|
|
22337
|
-
|
|
22338
|
-
|
|
22339
|
-
|
|
22340
|
-
|
|
22341
|
-
|
|
22342
|
-
|
|
22343
|
-
};
|
|
22344
|
-
const isObject$1 = value => typeof value === 'object' && value !== null;
|
|
22345
|
-
const mapObjectSkip = Symbol('skip');
|
|
22346
|
-
|
|
22347
|
-
// Customized for this use-case
|
|
22348
|
-
const isObjectCustom = value => isObject$1(value) && !(value instanceof RegExp) && !(value instanceof Error) && !(value instanceof Date);
|
|
22349
|
-
const mapObject = (object, mapper, options, isSeen = new WeakMap()) => {
|
|
22350
|
-
options = {
|
|
22351
|
-
deep: false,
|
|
22352
|
-
target: {},
|
|
22353
|
-
...options
|
|
22354
|
-
};
|
|
22355
|
-
if (isSeen.has(object)) {
|
|
22356
|
-
return isSeen.get(object);
|
|
22357
|
-
}
|
|
22358
|
-
isSeen.set(object, options.target);
|
|
22359
|
-
const {
|
|
22360
|
-
target
|
|
22361
|
-
} = options;
|
|
22362
|
-
delete options.target;
|
|
22363
|
-
const mapArray = array => array.map(element => isObjectCustom(element) ? mapObject(element, mapper, options, isSeen) : element);
|
|
22364
|
-
if (Array.isArray(object)) {
|
|
22365
|
-
return mapArray(object);
|
|
22366
|
-
}
|
|
22367
|
-
for (const [key, value] of Object.entries(object)) {
|
|
22368
|
-
const mapResult = mapper(key, value, object);
|
|
22369
|
-
if (mapResult === mapObjectSkip) {
|
|
22370
|
-
continue;
|
|
22371
|
-
}
|
|
22372
|
-
let [newKey, newValue, {
|
|
22373
|
-
shouldRecurse = true
|
|
22374
|
-
} = {}] = mapResult;
|
|
22375
|
-
|
|
22376
|
-
// Drop `__proto__` keys.
|
|
22377
|
-
if (newKey === '__proto__') {
|
|
22378
|
-
continue;
|
|
22379
|
-
}
|
|
22380
|
-
if (options.deep && shouldRecurse && isObjectCustom(newValue)) {
|
|
22381
|
-
newValue = Array.isArray(newValue) ? mapArray(newValue) : mapObject(newValue, mapper, options, isSeen);
|
|
22382
|
-
}
|
|
22383
|
-
target[newKey] = newValue;
|
|
22384
|
-
}
|
|
22385
|
-
return target;
|
|
22386
|
-
};
|
|
22387
|
-
mapObj$1.exports = (object, mapper, options) => {
|
|
22388
|
-
if (!isObject$1(object)) {
|
|
22389
|
-
throw new TypeError(`Expected an object, got \`${object}\` (${typeof object})`);
|
|
22390
|
-
}
|
|
22391
|
-
return mapObject(object, mapper, options);
|
|
22392
|
-
};
|
|
22393
|
-
mapObj$1.exports.mapObjectSkip = mapObjectSkip;
|
|
22394
|
-
var mapObjExports = mapObj$1.exports;
|
|
22395
|
-
var camelcase = {
|
|
22396
|
-
exports: {}
|
|
22397
|
-
};
|
|
22398
|
-
const UPPERCASE = /[\p{Lu}]/u;
|
|
22399
|
-
const LOWERCASE = /[\p{Ll}]/u;
|
|
22400
|
-
const LEADING_CAPITAL = /^[\p{Lu}](?![\p{Lu}])/gu;
|
|
22401
|
-
const IDENTIFIER = /([\p{Alpha}\p{N}_]|$)/u;
|
|
22402
|
-
const SEPARATORS = /[_.\- ]+/;
|
|
22403
|
-
const LEADING_SEPARATORS = new RegExp('^' + SEPARATORS.source);
|
|
22404
|
-
const SEPARATORS_AND_IDENTIFIER = new RegExp(SEPARATORS.source + IDENTIFIER.source, 'gu');
|
|
22405
|
-
const NUMBERS_AND_IDENTIFIER = new RegExp('\\d+' + IDENTIFIER.source, 'gu');
|
|
22406
|
-
const preserveCamelCase = (string, toLowerCase, toUpperCase) => {
|
|
22407
|
-
let isLastCharLower = false;
|
|
22408
|
-
let isLastCharUpper = false;
|
|
22409
|
-
let isLastLastCharUpper = false;
|
|
22410
|
-
for (let i = 0; i < string.length; i++) {
|
|
22411
|
-
const character = string[i];
|
|
22412
|
-
if (isLastCharLower && UPPERCASE.test(character)) {
|
|
22413
|
-
string = string.slice(0, i) + '-' + string.slice(i);
|
|
22414
|
-
isLastCharLower = false;
|
|
22415
|
-
isLastLastCharUpper = isLastCharUpper;
|
|
22416
|
-
isLastCharUpper = true;
|
|
22417
|
-
i++;
|
|
22418
|
-
} else if (isLastCharUpper && isLastLastCharUpper && LOWERCASE.test(character)) {
|
|
22419
|
-
string = string.slice(0, i - 1) + '-' + string.slice(i - 1);
|
|
22420
|
-
isLastLastCharUpper = isLastCharUpper;
|
|
22421
|
-
isLastCharUpper = false;
|
|
22422
|
-
isLastCharLower = true;
|
|
22423
|
-
} else {
|
|
22424
|
-
isLastCharLower = toLowerCase(character) === character && toUpperCase(character) !== character;
|
|
22425
|
-
isLastLastCharUpper = isLastCharUpper;
|
|
22426
|
-
isLastCharUpper = toUpperCase(character) === character && toLowerCase(character) !== character;
|
|
22427
|
-
}
|
|
22428
|
-
}
|
|
22429
|
-
return string;
|
|
22430
|
-
};
|
|
22431
|
-
const preserveConsecutiveUppercase = (input, toLowerCase) => {
|
|
22432
|
-
LEADING_CAPITAL.lastIndex = 0;
|
|
22433
|
-
return input.replace(LEADING_CAPITAL, m1 => toLowerCase(m1));
|
|
22434
|
-
};
|
|
22435
|
-
const postProcess = (input, toUpperCase) => {
|
|
22436
|
-
SEPARATORS_AND_IDENTIFIER.lastIndex = 0;
|
|
22437
|
-
NUMBERS_AND_IDENTIFIER.lastIndex = 0;
|
|
22438
|
-
return input.replace(SEPARATORS_AND_IDENTIFIER, (_, identifier) => toUpperCase(identifier)).replace(NUMBERS_AND_IDENTIFIER, m => toUpperCase(m));
|
|
22439
|
-
};
|
|
22440
|
-
const camelCase$1 = (input, options) => {
|
|
22441
|
-
if (!(typeof input === 'string' || Array.isArray(input))) {
|
|
22442
|
-
throw new TypeError('Expected the input to be `string | string[]`');
|
|
22443
|
-
}
|
|
22444
|
-
options = {
|
|
22445
|
-
pascalCase: false,
|
|
22446
|
-
preserveConsecutiveUppercase: false,
|
|
22447
|
-
...options
|
|
22448
|
-
};
|
|
22449
|
-
if (Array.isArray(input)) {
|
|
22450
|
-
input = input.map(x => x.trim()).filter(x => x.length).join('-');
|
|
22451
|
-
} else {
|
|
22452
|
-
input = input.trim();
|
|
22453
|
-
}
|
|
22454
|
-
if (input.length === 0) {
|
|
22455
|
-
return '';
|
|
22456
|
-
}
|
|
22457
|
-
const toLowerCase = options.locale === false ? string => string.toLowerCase() : string => string.toLocaleLowerCase(options.locale);
|
|
22458
|
-
const toUpperCase = options.locale === false ? string => string.toUpperCase() : string => string.toLocaleUpperCase(options.locale);
|
|
22459
|
-
if (input.length === 1) {
|
|
22460
|
-
return options.pascalCase ? toUpperCase(input) : toLowerCase(input);
|
|
22461
|
-
}
|
|
22462
|
-
const hasUpperCase = input !== toLowerCase(input);
|
|
22463
|
-
if (hasUpperCase) {
|
|
22464
|
-
input = preserveCamelCase(input, toLowerCase, toUpperCase);
|
|
22091
|
+
const baseRefineLogic = (data, ctx) => {
|
|
22092
|
+
if (data.deprecated && !data.deprecation_message) {
|
|
22093
|
+
ctx.addIssue({
|
|
22094
|
+
code: zod.ZodIssueCode.custom,
|
|
22095
|
+
message: 'deprecation_message is required for deprecated modifiers'
|
|
22096
|
+
});
|
|
22465
22097
|
}
|
|
22466
|
-
|
|
22467
|
-
|
|
22468
|
-
|
|
22469
|
-
|
|
22470
|
-
|
|
22098
|
+
if (!data.deprecated && data.deprecation_message) {
|
|
22099
|
+
ctx.addIssue({
|
|
22100
|
+
code: zod.ZodIssueCode.custom,
|
|
22101
|
+
message: 'deprecation_message is only allowed for deprecated modifiers'
|
|
22102
|
+
});
|
|
22471
22103
|
}
|
|
22472
|
-
if (
|
|
22473
|
-
|
|
22104
|
+
if (data.aliases && data.aliases.length !== new Set(data.aliases).size) {
|
|
22105
|
+
ctx.addIssue({
|
|
22106
|
+
code: zod.ZodIssueCode.custom,
|
|
22107
|
+
message: 'Aliases must be unique'
|
|
22108
|
+
});
|
|
22474
22109
|
}
|
|
22475
|
-
return postProcess(input, toUpperCase);
|
|
22476
22110
|
};
|
|
22477
|
-
|
|
22478
|
-
|
|
22479
|
-
|
|
22480
|
-
|
|
22481
|
-
|
|
22482
|
-
|
|
22483
|
-
|
|
22484
|
-
|
|
22485
|
-
|
|
22486
|
-
|
|
22487
|
-
|
|
22488
|
-
|
|
22489
|
-
|
|
22490
|
-
|
|
22491
|
-
|
|
22492
|
-
|
|
22493
|
-
|
|
22494
|
-
|
|
22495
|
-
|
|
22496
|
-
this._size = 0;
|
|
22497
|
-
if (typeof this.onEviction === 'function') {
|
|
22498
|
-
for (const [key, value] of this.oldCache.entries()) {
|
|
22499
|
-
this.onEviction(key, value);
|
|
22500
|
-
}
|
|
22501
|
-
}
|
|
22502
|
-
this.oldCache = this.cache;
|
|
22503
|
-
this.cache = new Map();
|
|
22504
|
-
}
|
|
22111
|
+
|
|
22112
|
+
/**
|
|
22113
|
+
* Checks if error has message.
|
|
22114
|
+
*
|
|
22115
|
+
* @param error Error object.
|
|
22116
|
+
* @returns If param is error.
|
|
22117
|
+
*/
|
|
22118
|
+
function isErrorWithMessage(error) {
|
|
22119
|
+
return typeof error === 'object' && error !== null && 'message' in error && typeof error.message === 'string';
|
|
22120
|
+
}
|
|
22121
|
+
/**
|
|
22122
|
+
* Converts error to the error with message.
|
|
22123
|
+
*
|
|
22124
|
+
* @param maybeError Possible error.
|
|
22125
|
+
* @returns Error with message.
|
|
22126
|
+
*/
|
|
22127
|
+
function toErrorWithMessage(maybeError) {
|
|
22128
|
+
if (isErrorWithMessage(maybeError)) {
|
|
22129
|
+
return maybeError;
|
|
22505
22130
|
}
|
|
22506
|
-
|
|
22507
|
-
|
|
22508
|
-
|
|
22509
|
-
|
|
22510
|
-
|
|
22511
|
-
|
|
22512
|
-
this.oldCache.delete(key);
|
|
22513
|
-
this._set(key, value);
|
|
22514
|
-
return value;
|
|
22515
|
-
}
|
|
22131
|
+
try {
|
|
22132
|
+
return new Error(JSON.stringify(maybeError));
|
|
22133
|
+
} catch {
|
|
22134
|
+
// fallback in case there's an error stringifying the maybeError
|
|
22135
|
+
// like with circular references for example.
|
|
22136
|
+
return new Error(String(maybeError));
|
|
22516
22137
|
}
|
|
22517
|
-
|
|
22518
|
-
|
|
22519
|
-
|
|
22520
|
-
|
|
22521
|
-
|
|
22522
|
-
|
|
22523
|
-
|
|
22138
|
+
}
|
|
22139
|
+
/**
|
|
22140
|
+
* Converts error object to error with message. This method might be helpful to handle thrown errors.
|
|
22141
|
+
*
|
|
22142
|
+
* @param error Error object.
|
|
22143
|
+
*
|
|
22144
|
+
* @returns Message of the error.
|
|
22145
|
+
*/
|
|
22146
|
+
function getErrorMessage(error) {
|
|
22147
|
+
return toErrorWithMessage(error).message;
|
|
22148
|
+
}
|
|
22149
|
+
|
|
22150
|
+
/**
|
|
22151
|
+
* @file Schema for modifier data.
|
|
22152
|
+
*/
|
|
22153
|
+
/**
|
|
22154
|
+
* Known validators that don't need to be validated as regex.
|
|
22155
|
+
*/
|
|
22156
|
+
const KNOWN_VALIDATORS = new Set(['domain', 'pipe_separated_domains', 'regexp', 'url']);
|
|
22157
|
+
/**
|
|
22158
|
+
* Zod schema for modifier data.
|
|
22159
|
+
*/
|
|
22160
|
+
zodToCamelCase(baseCompatibilityDataSchema.extend({
|
|
22161
|
+
/**
|
|
22162
|
+
* List of modifiers that are incompatible with the actual one.
|
|
22163
|
+
*/
|
|
22164
|
+
conflicts: zod.array(nonEmptyStringSchema).nullable().default(null),
|
|
22165
|
+
/**
|
|
22166
|
+
* The actual modifier is incompatible with all other modifiers, except the ones listed in `conflicts`.
|
|
22167
|
+
*/
|
|
22168
|
+
inverse_conflicts: booleanSchema.default(false),
|
|
22169
|
+
/**
|
|
22170
|
+
* Describes whether the actual modifier supports value assignment. For example, `$domain` is assignable,
|
|
22171
|
+
* so it can be used like this: `$domain=domain.com\|~subdomain.domain.com`, where `=` is the assignment operator
|
|
22172
|
+
* and `domain.com\|~subdomain.domain.com` is the value.
|
|
22173
|
+
*/
|
|
22174
|
+
assignable: booleanSchema.default(false),
|
|
22175
|
+
/**
|
|
22176
|
+
* Describes whether the actual modifier can be negated. For example, `$third-party` is negatable,
|
|
22177
|
+
* so it can be used like this: `$~third-party`.
|
|
22178
|
+
*/
|
|
22179
|
+
negatable: booleanSchema.default(true),
|
|
22180
|
+
/**
|
|
22181
|
+
* The actual modifier can only be used in blocking rules, it cannot be used in exceptions.
|
|
22182
|
+
* If it's value is `true`, then the modifier can be used only in blocking rules.
|
|
22183
|
+
* `exception_only` and `block_only` cannot be used together (they are mutually exclusive).
|
|
22184
|
+
*/
|
|
22185
|
+
block_only: booleanSchema.default(false),
|
|
22186
|
+
/**
|
|
22187
|
+
* The actual modifier can only be used in exceptions, it cannot be used in blocking rules.
|
|
22188
|
+
* If it's value is `true`, then the modifier can be used only in exceptions.
|
|
22189
|
+
* `exception_only` and `block_only` cannot be used together (they are mutually exclusive).
|
|
22190
|
+
*/
|
|
22191
|
+
exception_only: booleanSchema.default(false),
|
|
22192
|
+
/**
|
|
22193
|
+
* Describes whether the *assignable* modifier value is required.
|
|
22194
|
+
* For example, `$cookie` is assignable but it can be used without a value in exception rules:
|
|
22195
|
+
* `@@\|\|example.com^$cookie`.
|
|
22196
|
+
* If `false`, the `value_format` is required, e.g. the value of `$app` should always be specified
|
|
22197
|
+
*/
|
|
22198
|
+
value_optional: booleanSchema.default(false),
|
|
22199
|
+
/**
|
|
22200
|
+
* Describes the format of the value for the *assignable* modifier.
|
|
22201
|
+
* Its value can be a regex pattern or a known validator name (e.g. `domain`, `pipe_separated_domains`, etc.).
|
|
22202
|
+
*/
|
|
22203
|
+
value_format: nonEmptyStringSchema.nullable().default(null)
|
|
22204
|
+
}).superRefine((data, ctx) => {
|
|
22205
|
+
// TODO: find something better, for now we can't add refine logic to the base schema:
|
|
22206
|
+
// https://github.com/colinhacks/zod/issues/454#issuecomment-848370721
|
|
22207
|
+
baseRefineLogic(data, ctx);
|
|
22208
|
+
if (data.block_only && data.exception_only) {
|
|
22209
|
+
ctx.addIssue({
|
|
22210
|
+
code: zod.ZodIssueCode.custom,
|
|
22211
|
+
message: 'block_only and exception_only are mutually exclusive'
|
|
22212
|
+
});
|
|
22524
22213
|
}
|
|
22525
|
-
|
|
22526
|
-
|
|
22214
|
+
if (data.assignable && !data.value_format) {
|
|
22215
|
+
ctx.addIssue({
|
|
22216
|
+
code: zod.ZodIssueCode.custom,
|
|
22217
|
+
message: 'value_format is required for assignable modifiers'
|
|
22218
|
+
});
|
|
22527
22219
|
}
|
|
22528
|
-
|
|
22529
|
-
|
|
22530
|
-
|
|
22531
|
-
|
|
22532
|
-
|
|
22533
|
-
return this.oldCache.get(key);
|
|
22220
|
+
if (data.value_format) {
|
|
22221
|
+
const valueFormat = data.value_format.trim();
|
|
22222
|
+
// if it is a known validator, we don't need to validate it further
|
|
22223
|
+
if (KNOWN_VALIDATORS.has(valueFormat)) {
|
|
22224
|
+
return;
|
|
22534
22225
|
}
|
|
22535
|
-
|
|
22536
|
-
|
|
22537
|
-
|
|
22538
|
-
|
|
22539
|
-
|
|
22226
|
+
// otherwise, we need to validate it as a regex
|
|
22227
|
+
try {
|
|
22228
|
+
XRegExp(valueFormat);
|
|
22229
|
+
} catch (error) {
|
|
22230
|
+
ctx.addIssue({
|
|
22231
|
+
code: zod.ZodIssueCode.custom,
|
|
22232
|
+
message: getErrorMessage(error)
|
|
22233
|
+
});
|
|
22540
22234
|
}
|
|
22541
|
-
return this.oldCache.delete(key) || deleted;
|
|
22542
22235
|
}
|
|
22543
|
-
|
|
22544
|
-
|
|
22545
|
-
|
|
22546
|
-
|
|
22236
|
+
}));
|
|
22237
|
+
|
|
22238
|
+
/**
|
|
22239
|
+
* @file Schema for redirect data.
|
|
22240
|
+
*/
|
|
22241
|
+
/**
|
|
22242
|
+
* Zod schema for redirect data.
|
|
22243
|
+
*/
|
|
22244
|
+
zodToCamelCase(baseCompatibilityDataSchema.extend({
|
|
22245
|
+
/**
|
|
22246
|
+
* Whether the redirect is blocking.
|
|
22247
|
+
*/
|
|
22248
|
+
is_blocking: booleanSchema.default(false)
|
|
22249
|
+
}).superRefine(baseRefineLogic));
|
|
22250
|
+
|
|
22251
|
+
/**
|
|
22252
|
+
* @file Schema for scriptlet data.
|
|
22253
|
+
*/
|
|
22254
|
+
/**
|
|
22255
|
+
* Zod schema for scriptlet parameter data.
|
|
22256
|
+
*/
|
|
22257
|
+
const scriptletParameterSchema = zod.object({
|
|
22258
|
+
/**
|
|
22259
|
+
* Name of the actual parameter.
|
|
22260
|
+
*/
|
|
22261
|
+
name: nonEmptyStringSchema,
|
|
22262
|
+
/**
|
|
22263
|
+
* Describes whether the parameter is required. Empty parameters are not allowed.
|
|
22264
|
+
*/
|
|
22265
|
+
required: booleanSchema,
|
|
22266
|
+
/**
|
|
22267
|
+
* Short description of the parameter.
|
|
22268
|
+
* If not specified or it's value is `null`,then the description is not available.
|
|
22269
|
+
*/
|
|
22270
|
+
description: nonEmptyStringSchema.nullable().default(null),
|
|
22271
|
+
/**
|
|
22272
|
+
* Regular expression that matches the value of the parameter.
|
|
22273
|
+
* If it's value is `null`, then the parameter value is not checked.
|
|
22274
|
+
*/
|
|
22275
|
+
pattern: nonEmptyStringSchema.nullable().default(null),
|
|
22276
|
+
/**
|
|
22277
|
+
* Default value of the parameter (if any).
|
|
22278
|
+
*/
|
|
22279
|
+
default: nonEmptyStringSchema.nullable().default(null),
|
|
22280
|
+
/**
|
|
22281
|
+
* Describes whether the parameter is used only for debugging purposes.
|
|
22282
|
+
*/
|
|
22283
|
+
debug: booleanSchema.default(false)
|
|
22284
|
+
});
|
|
22285
|
+
/**
|
|
22286
|
+
* Zod schema for scriptlet parameters.
|
|
22287
|
+
*/
|
|
22288
|
+
const scriptletParametersSchema = zod.array(scriptletParameterSchema);
|
|
22289
|
+
/**
|
|
22290
|
+
* Zod schema for scriptlet data.
|
|
22291
|
+
*/
|
|
22292
|
+
zodToCamelCase(baseCompatibilityDataSchema.extend({
|
|
22293
|
+
/**
|
|
22294
|
+
* List of parameters that the scriptlet accepts.
|
|
22295
|
+
* **Every** parameter should be listed here, because we check that the scriptlet is used correctly
|
|
22296
|
+
* (e.g. that the number of parameters is correct).
|
|
22297
|
+
*/
|
|
22298
|
+
parameters: scriptletParametersSchema.optional()
|
|
22299
|
+
}).superRefine((data, ctx) => {
|
|
22300
|
+
// TODO: find something better, for now we can't add refine logic to the base schema:
|
|
22301
|
+
// https://github.com/colinhacks/zod/issues/454#issuecomment-848370721
|
|
22302
|
+
baseRefineLogic(data, ctx);
|
|
22303
|
+
// we don't allow required parameters after optional ones
|
|
22304
|
+
if (!data.parameters) {
|
|
22305
|
+
return;
|
|
22547
22306
|
}
|
|
22548
|
-
|
|
22549
|
-
|
|
22550
|
-
|
|
22307
|
+
let optionalFound = false;
|
|
22308
|
+
for (const parameter of data.parameters) {
|
|
22309
|
+
if (optionalFound && parameter.required) {
|
|
22310
|
+
ctx.addIssue({
|
|
22311
|
+
code: zod.ZodIssueCode.custom,
|
|
22312
|
+
message: 'Required parameters must be before optional ones'
|
|
22313
|
+
});
|
|
22551
22314
|
}
|
|
22552
|
-
|
|
22553
|
-
|
|
22554
|
-
for (const [, value] of this) {
|
|
22555
|
-
yield value;
|
|
22315
|
+
if (!parameter.required) {
|
|
22316
|
+
optionalFound = true;
|
|
22556
22317
|
}
|
|
22557
22318
|
}
|
|
22558
|
-
|
|
22559
|
-
|
|
22560
|
-
|
|
22319
|
+
}));
|
|
22320
|
+
|
|
22321
|
+
/**
|
|
22322
|
+
* @file Scriptlet injection rule converter
|
|
22323
|
+
*/
|
|
22324
|
+
const ABP_SCRIPTLET_PREFIX = 'abp-';
|
|
22325
|
+
const UBO_SCRIPTLET_PREFIX = 'ubo-';
|
|
22326
|
+
const UBO_SCRIPTLET_PREFIX_LENGTH = UBO_SCRIPTLET_PREFIX.length;
|
|
22327
|
+
const UBO_SCRIPTLET_JS_SUFFIX = '.js';
|
|
22328
|
+
const UBO_SCRIPTLET_JS_SUFFIX_LENGTH = UBO_SCRIPTLET_JS_SUFFIX.length;
|
|
22329
|
+
const COMMA_SEPARATOR = ',';
|
|
22330
|
+
const ADG_SET_CONSTANT_NAME = 'set-constant';
|
|
22331
|
+
const ADG_SET_CONSTANT_EMPTY_STRING = '';
|
|
22332
|
+
const ADG_SET_CONSTANT_EMPTY_ARRAY = 'emptyArr';
|
|
22333
|
+
const ADG_SET_CONSTANT_EMPTY_OBJECT = 'emptyObj';
|
|
22334
|
+
const UBO_SET_CONSTANT_EMPTY_STRING = '\'\'';
|
|
22335
|
+
const UBO_SET_CONSTANT_EMPTY_ARRAY = '[]';
|
|
22336
|
+
const UBO_SET_CONSTANT_EMPTY_OBJECT = '{}';
|
|
22337
|
+
const ADG_PREVENT_FETCH_NAME = 'prevent-fetch';
|
|
22338
|
+
const ADG_PREVENT_FETCH_EMPTY_STRING = '';
|
|
22339
|
+
const ADG_PREVENT_FETCH_WILDCARD = '*';
|
|
22340
|
+
const UBO_NO_FETCH_IF_WILDCARD = '/^/';
|
|
22341
|
+
const setConstantAdgToUboMap = {
|
|
22342
|
+
[ADG_SET_CONSTANT_EMPTY_STRING]: UBO_SET_CONSTANT_EMPTY_STRING,
|
|
22343
|
+
[ADG_SET_CONSTANT_EMPTY_ARRAY]: UBO_SET_CONSTANT_EMPTY_ARRAY,
|
|
22344
|
+
[ADG_SET_CONSTANT_EMPTY_OBJECT]: UBO_SET_CONSTANT_EMPTY_OBJECT
|
|
22345
|
+
};
|
|
22346
|
+
/**
|
|
22347
|
+
* Scriptlet injection rule converter class
|
|
22348
|
+
*
|
|
22349
|
+
* @todo Implement `convertToUbo` and `convertToAbp`
|
|
22350
|
+
*/
|
|
22351
|
+
class ScriptletRuleConverter extends RuleConverterBase {
|
|
22352
|
+
/**
|
|
22353
|
+
* Converts a scriptlet injection rule to AdGuard format, if possible.
|
|
22354
|
+
*
|
|
22355
|
+
* @param rule Rule node to convert
|
|
22356
|
+
* @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
|
|
22357
|
+
* the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
|
|
22358
|
+
* If the rule was not converted, the result array will contain the original node with the same object reference
|
|
22359
|
+
* @throws If the rule is invalid or cannot be converted
|
|
22360
|
+
*/
|
|
22361
|
+
static convertToAdg(rule) {
|
|
22362
|
+
// Ignore AdGuard rules
|
|
22363
|
+
if (rule.syntax === exports.AdblockSyntax.Adg) {
|
|
22364
|
+
return createNodeConversionResult([rule], false);
|
|
22561
22365
|
}
|
|
22562
|
-
|
|
22563
|
-
|
|
22564
|
-
|
|
22565
|
-
|
|
22366
|
+
const separator = rule.separator.value;
|
|
22367
|
+
let convertedSeparator = separator;
|
|
22368
|
+
convertedSeparator = rule.exception ? exports.CosmeticRuleSeparator.AdgJsInjectionException : exports.CosmeticRuleSeparator.AdgJsInjection;
|
|
22369
|
+
const convertedScriptlets = [];
|
|
22370
|
+
// Special case: empty uBO exception scriptlet, e.g. `example.com#@#+js()`
|
|
22371
|
+
if (rule.syntax === exports.AdblockSyntax.Ubo && rule.body.children.length === 1 && rule.body.children[0].children.length === 0) {
|
|
22372
|
+
convertedScriptlets.push(rule.body.children[0]);
|
|
22373
|
+
} else {
|
|
22374
|
+
for (const scriptlet of rule.body.children) {
|
|
22375
|
+
// Clone the node to avoid any side effects
|
|
22376
|
+
const scriptletClone = cloneScriptletRuleNode(scriptlet);
|
|
22377
|
+
// Remove possible quotes just to make it easier to work with the scriptlet name
|
|
22378
|
+
const scriptletName = QuoteUtils.setStringQuoteType(getScriptletName(scriptletClone), exports.QuoteType.None);
|
|
22379
|
+
// Add prefix if it's not already there
|
|
22380
|
+
let prefix;
|
|
22381
|
+
switch (rule.syntax) {
|
|
22382
|
+
case exports.AdblockSyntax.Abp:
|
|
22383
|
+
prefix = ABP_SCRIPTLET_PREFIX;
|
|
22384
|
+
break;
|
|
22385
|
+
case exports.AdblockSyntax.Ubo:
|
|
22386
|
+
prefix = UBO_SCRIPTLET_PREFIX;
|
|
22387
|
+
break;
|
|
22388
|
+
default:
|
|
22389
|
+
prefix = EMPTY;
|
|
22390
|
+
}
|
|
22391
|
+
if (!scriptletName.startsWith(prefix)) {
|
|
22392
|
+
setScriptletName(scriptletClone, `${prefix}${scriptletName}`);
|
|
22393
|
+
}
|
|
22394
|
+
// ADG scriptlet parameters should be quoted, and single quoted are preferred
|
|
22395
|
+
setScriptletQuoteType(scriptletClone, exports.QuoteType.Single);
|
|
22396
|
+
convertedScriptlets.push(scriptletClone);
|
|
22566
22397
|
}
|
|
22567
22398
|
}
|
|
22568
|
-
|
|
22569
|
-
|
|
22570
|
-
|
|
22571
|
-
|
|
22572
|
-
|
|
22573
|
-
|
|
22399
|
+
return createNodeConversionResult(convertedScriptlets.map(scriptlet => {
|
|
22400
|
+
const res = {
|
|
22401
|
+
category: rule.category,
|
|
22402
|
+
type: rule.type,
|
|
22403
|
+
syntax: exports.AdblockSyntax.Adg,
|
|
22404
|
+
exception: rule.exception,
|
|
22405
|
+
domains: cloneDomainListNode(rule.domains),
|
|
22406
|
+
separator: {
|
|
22407
|
+
type: 'Value',
|
|
22408
|
+
value: convertedSeparator
|
|
22409
|
+
},
|
|
22410
|
+
body: {
|
|
22411
|
+
type: rule.body.type,
|
|
22412
|
+
children: [scriptlet]
|
|
22413
|
+
}
|
|
22414
|
+
};
|
|
22415
|
+
if (rule.modifiers) {
|
|
22416
|
+
res.modifiers = cloneModifierListNode(rule.modifiers);
|
|
22574
22417
|
}
|
|
22575
|
-
|
|
22576
|
-
|
|
22577
|
-
}
|
|
22578
|
-
}
|
|
22579
|
-
var quickLru = QuickLRU;
|
|
22580
|
-
const mapObj = mapObjExports;
|
|
22581
|
-
const camelCase = camelcaseExports;
|
|
22582
|
-
const QuickLru = quickLru;
|
|
22583
|
-
const has = (array, key) => array.some(x => {
|
|
22584
|
-
if (typeof x === 'string') {
|
|
22585
|
-
return x === key;
|
|
22586
|
-
}
|
|
22587
|
-
x.lastIndex = 0;
|
|
22588
|
-
return x.test(key);
|
|
22589
|
-
});
|
|
22590
|
-
const cache = new QuickLru({
|
|
22591
|
-
maxSize: 100000
|
|
22592
|
-
});
|
|
22593
|
-
|
|
22594
|
-
// Reproduces behavior from `map-obj`
|
|
22595
|
-
const isObject = value => typeof value === 'object' && value !== null && !(value instanceof RegExp) && !(value instanceof Error) && !(value instanceof Date);
|
|
22596
|
-
const camelCaseConvert = (input, options) => {
|
|
22597
|
-
if (!isObject(input)) {
|
|
22598
|
-
return input;
|
|
22418
|
+
return res;
|
|
22419
|
+
}), true);
|
|
22599
22420
|
}
|
|
22600
|
-
|
|
22601
|
-
|
|
22602
|
-
|
|
22603
|
-
|
|
22604
|
-
|
|
22605
|
-
|
|
22606
|
-
|
|
22607
|
-
|
|
22608
|
-
|
|
22609
|
-
|
|
22610
|
-
|
|
22611
|
-
|
|
22612
|
-
|
|
22613
|
-
if (deep && isObject(value)) {
|
|
22614
|
-
const path = parentPath === undefined ? key : `${parentPath}.${key}`;
|
|
22615
|
-
if (!stopPathsSet.has(path)) {
|
|
22616
|
-
value = mapObj(value, makeMapper(path));
|
|
22617
|
-
}
|
|
22421
|
+
/**
|
|
22422
|
+
* Converts a scriptlet injection rule to uBlock format, if possible.
|
|
22423
|
+
*
|
|
22424
|
+
* @param rule Rule node to convert
|
|
22425
|
+
* @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
|
|
22426
|
+
* the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
|
|
22427
|
+
* If the rule was not converted, the result array will contain the original node with the same object reference
|
|
22428
|
+
* @throws If the rule is invalid or cannot be converted
|
|
22429
|
+
*/
|
|
22430
|
+
static convertToUbo(rule) {
|
|
22431
|
+
// Ignore uBlock rules
|
|
22432
|
+
if (rule.syntax === exports.AdblockSyntax.Ubo) {
|
|
22433
|
+
return createNodeConversionResult([rule], false);
|
|
22618
22434
|
}
|
|
22619
|
-
|
|
22620
|
-
|
|
22621
|
-
|
|
22622
|
-
|
|
22623
|
-
|
|
22624
|
-
|
|
22625
|
-
|
|
22626
|
-
|
|
22435
|
+
const separator = rule.separator.value;
|
|
22436
|
+
let convertedSeparator = separator;
|
|
22437
|
+
convertedSeparator = rule.exception ? exports.CosmeticRuleSeparator.ElementHidingException : exports.CosmeticRuleSeparator.ElementHiding;
|
|
22438
|
+
const convertedScriptlets = [];
|
|
22439
|
+
// Special case: empty AdGuard exception scriptlet, e.g. `example.com#%#//scriptlet()`
|
|
22440
|
+
if (rule.syntax === exports.AdblockSyntax.Adg && rule.body.children.length === 1 && rule.body.children[0].children.length === 0) {
|
|
22441
|
+
convertedScriptlets.push(rule.body.children[0]);
|
|
22442
|
+
} else {
|
|
22443
|
+
for (const scriptlet of rule.body.children) {
|
|
22444
|
+
// Clone the node to avoid any side effects
|
|
22445
|
+
const scriptletClone = cloneScriptletRuleNode(scriptlet);
|
|
22446
|
+
// Remove possible quotes just to make it easier to work with the scriptlet name
|
|
22447
|
+
const scriptletName = QuoteUtils.setStringQuoteType(getScriptletName(scriptletClone), exports.QuoteType.None);
|
|
22448
|
+
let uboScriptletName;
|
|
22449
|
+
if (rule.syntax === exports.AdblockSyntax.Adg && scriptletName.startsWith(UBO_SCRIPTLET_PREFIX)) {
|
|
22450
|
+
// Special case: AdGuard syntax 'preserves' the original scriptlet name,
|
|
22451
|
+
// so we need to convert it back by removing the uBO prefix
|
|
22452
|
+
uboScriptletName = scriptletName.slice(UBO_SCRIPTLET_PREFIX_LENGTH);
|
|
22453
|
+
} else {
|
|
22454
|
+
// Otherwise, try to find the corresponding uBO scriptlet name, or use the original one if not found
|
|
22455
|
+
const uboScriptlet = scriptletsCompatibilityTable.getFirst(scriptletName, exports.GenericPlatform.UboAny);
|
|
22456
|
+
uboScriptletName = uboScriptlet?.name ?? scriptletName;
|
|
22457
|
+
}
|
|
22458
|
+
// Remove the '.js' suffix if it's there - its presence is not mandatory
|
|
22459
|
+
if (uboScriptletName.endsWith(UBO_SCRIPTLET_JS_SUFFIX)) {
|
|
22460
|
+
uboScriptletName = uboScriptletName.slice(0, -UBO_SCRIPTLET_JS_SUFFIX_LENGTH);
|
|
22461
|
+
}
|
|
22462
|
+
setScriptletName(scriptletClone, uboScriptletName);
|
|
22463
|
+
setScriptletQuoteType(scriptletClone, exports.QuoteType.None);
|
|
22464
|
+
// Escape unescaped commas in parameters, because uBlock Origin uses them as separators.
|
|
22465
|
+
// For example, the following AdGuard rule:
|
|
22466
|
+
//
|
|
22467
|
+
// example.com#%#//scriptlet('spoof-css', '.adsbygoogle, #ads', 'visibility', 'visible')
|
|
22468
|
+
//
|
|
22469
|
+
// ↓↓ should be converted to ↓↓
|
|
22470
|
+
//
|
|
22471
|
+
// example.com##+js(spoof-css.js, .adsbygoogle\, #ads, visibility, visible)
|
|
22472
|
+
// ------------ ------------------- ---------- -------
|
|
22473
|
+
// arg 0 arg 1 arg 2 arg 3
|
|
22474
|
+
//
|
|
22475
|
+
// and we need to escape the comma in the second argument to prevent it from being treated
|
|
22476
|
+
// as two separate arguments.
|
|
22477
|
+
transformAllScriptletArguments(scriptletClone, value => {
|
|
22478
|
+
return QuoteUtils.escapeUnescapedOccurrences(value, COMMA_SEPARATOR);
|
|
22627
22479
|
});
|
|
22628
|
-
|
|
22629
|
-
|
|
22630
|
-
|
|
22480
|
+
// Some scriptlets have special values that need to be converted
|
|
22481
|
+
switch (scriptletName) {
|
|
22482
|
+
case ADG_SET_CONSTANT_NAME:
|
|
22483
|
+
transformNthScriptletArgument(scriptletClone, 2, value => {
|
|
22484
|
+
return setConstantAdgToUboMap[value] ?? value;
|
|
22485
|
+
});
|
|
22486
|
+
break;
|
|
22487
|
+
case ADG_PREVENT_FETCH_NAME:
|
|
22488
|
+
transformNthScriptletArgument(scriptletClone, 1, value => {
|
|
22489
|
+
if (value === ADG_PREVENT_FETCH_EMPTY_STRING || value === ADG_PREVENT_FETCH_WILDCARD) {
|
|
22490
|
+
return UBO_NO_FETCH_IF_WILDCARD;
|
|
22491
|
+
}
|
|
22492
|
+
return value;
|
|
22493
|
+
});
|
|
22494
|
+
break;
|
|
22631
22495
|
}
|
|
22632
|
-
|
|
22496
|
+
convertedScriptlets.push(scriptletClone);
|
|
22633
22497
|
}
|
|
22634
22498
|
}
|
|
22635
|
-
return
|
|
22636
|
-
|
|
22637
|
-
|
|
22638
|
-
|
|
22639
|
-
|
|
22640
|
-
|
|
22641
|
-
|
|
22499
|
+
return createNodeConversionResult(convertedScriptlets.map(scriptlet => {
|
|
22500
|
+
const res = {
|
|
22501
|
+
category: rule.category,
|
|
22502
|
+
type: rule.type,
|
|
22503
|
+
syntax: exports.AdblockSyntax.Ubo,
|
|
22504
|
+
exception: rule.exception,
|
|
22505
|
+
domains: cloneDomainListNode(rule.domains),
|
|
22506
|
+
separator: {
|
|
22507
|
+
type: 'Value',
|
|
22508
|
+
value: convertedSeparator
|
|
22509
|
+
},
|
|
22510
|
+
body: {
|
|
22511
|
+
type: rule.body.type,
|
|
22512
|
+
children: [scriptlet]
|
|
22513
|
+
}
|
|
22514
|
+
};
|
|
22515
|
+
if (rule.modifiers) {
|
|
22516
|
+
res.modifiers = cloneModifierListNode(rule.modifiers);
|
|
22517
|
+
}
|
|
22518
|
+
return res;
|
|
22519
|
+
}), true);
|
|
22642
22520
|
}
|
|
22643
|
-
|
|
22644
|
-
};
|
|
22645
|
-
var camelCaseKeys = /*@__PURE__*/getDefaultExportFromCjs(camelcaseKeys);
|
|
22521
|
+
}
|
|
22646
22522
|
|
|
22647
22523
|
/**
|
|
22648
|
-
* @file
|
|
22524
|
+
* @file Utility functions for working with modifier nodes
|
|
22649
22525
|
*/
|
|
22650
|
-
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
22651
22526
|
/**
|
|
22652
|
-
*
|
|
22653
|
-
*
|
|
22654
|
-
* @param zod Zod schema.
|
|
22527
|
+
* Creates a modifier node
|
|
22655
22528
|
*
|
|
22656
|
-
* @
|
|
22529
|
+
* @param name Name of the modifier
|
|
22530
|
+
* @param value Value of the modifier
|
|
22531
|
+
* @param exception Whether the modifier is an exception
|
|
22532
|
+
* @returns Modifier node
|
|
22533
|
+
*/
|
|
22534
|
+
function createModifierNode(name, value = undefined, exception = false) {
|
|
22535
|
+
const result = {
|
|
22536
|
+
type: 'Modifier',
|
|
22537
|
+
exception,
|
|
22538
|
+
name: {
|
|
22539
|
+
type: 'Value',
|
|
22540
|
+
value: name
|
|
22541
|
+
}
|
|
22542
|
+
};
|
|
22543
|
+
if (!isUndefined(value)) {
|
|
22544
|
+
result.value = {
|
|
22545
|
+
type: 'Value',
|
|
22546
|
+
value
|
|
22547
|
+
};
|
|
22548
|
+
}
|
|
22549
|
+
return result;
|
|
22550
|
+
}
|
|
22551
|
+
/**
|
|
22552
|
+
* Creates a modifier list node
|
|
22657
22553
|
*
|
|
22658
|
-
* @
|
|
22554
|
+
* @param modifiers Modifiers to put in the list (optional, defaults to an empty list)
|
|
22555
|
+
* @returns Modifier list node
|
|
22659
22556
|
*/
|
|
22660
|
-
|
|
22661
|
-
|
|
22662
|
-
|
|
22557
|
+
function createModifierListNode(modifiers = []) {
|
|
22558
|
+
const result = {
|
|
22559
|
+
type: 'ModifierList',
|
|
22560
|
+
// We need to clone the modifiers to avoid side effects
|
|
22561
|
+
children: modifiers.length ? clone(modifiers) : []
|
|
22562
|
+
};
|
|
22563
|
+
return result;
|
|
22564
|
+
}
|
|
22663
22565
|
|
|
22664
22566
|
/**
|
|
22665
|
-
*
|
|
22567
|
+
* A very simple map extension that allows to store multiple values for the same key
|
|
22568
|
+
* by storing them in an array.
|
|
22569
|
+
*
|
|
22570
|
+
* @todo Add more methods if needed
|
|
22666
22571
|
*/
|
|
22572
|
+
class MultiValueMap extends Map {
|
|
22573
|
+
/**
|
|
22574
|
+
* Adds a value to the map. If the key already exists, the value will be appended to the existing array,
|
|
22575
|
+
* otherwise a new array will be created for the key.
|
|
22576
|
+
*
|
|
22577
|
+
* @param key Key to add
|
|
22578
|
+
* @param values Value(s) to add
|
|
22579
|
+
*/
|
|
22580
|
+
add(key, ...values) {
|
|
22581
|
+
let currentValues = super.get(key);
|
|
22582
|
+
if (isUndefined(currentValues)) {
|
|
22583
|
+
currentValues = [];
|
|
22584
|
+
super.set(key, values);
|
|
22585
|
+
}
|
|
22586
|
+
currentValues.push(...values);
|
|
22587
|
+
}
|
|
22588
|
+
}
|
|
22589
|
+
|
|
22667
22590
|
/**
|
|
22668
|
-
*
|
|
22591
|
+
* @file Cosmetic rule modifier converter from uBO to ADG
|
|
22669
22592
|
*/
|
|
22670
|
-
const
|
|
22593
|
+
const UBO_MATCHES_PATH_OPERATOR = 'matches-path';
|
|
22594
|
+
const ADG_PATH_MODIFIER = 'path';
|
|
22671
22595
|
/**
|
|
22672
|
-
*
|
|
22596
|
+
* Special characters in modifier regexps that should be escaped
|
|
22673
22597
|
*/
|
|
22674
|
-
const
|
|
22598
|
+
const SPECIAL_MODIFIER_REGEX_CHARS = new Set([OPEN_SQUARE_BRACKET, CLOSE_SQUARE_BRACKET, COMMA, ESCAPE_CHARACTER]);
|
|
22675
22599
|
/**
|
|
22676
|
-
*
|
|
22677
|
-
* Here we use snake_case properties because the compatibility data is stored in YAML files.
|
|
22600
|
+
* Helper class for converting cosmetic rule modifiers from uBO to ADG
|
|
22678
22601
|
*/
|
|
22679
|
-
|
|
22680
|
-
/**
|
|
22681
|
-
* Name of the actual entity.
|
|
22682
|
-
*/
|
|
22683
|
-
name: nonEmptyStringSchema,
|
|
22684
|
-
/**
|
|
22685
|
-
* List of aliases for the entity (if any).
|
|
22686
|
-
*/
|
|
22687
|
-
aliases: zod.array(nonEmptyStringSchema).nullable().default(null),
|
|
22688
|
-
/**
|
|
22689
|
-
* Short description of the actual entity.
|
|
22690
|
-
* If not specified or it's value is `null`, then the description is not available.
|
|
22691
|
-
*/
|
|
22692
|
-
description: nonEmptyStringSchema.nullable().default(null),
|
|
22693
|
-
/**
|
|
22694
|
-
* Link to the documentation. If not specified or it's value is `null`, then the documentation is not available.
|
|
22695
|
-
*/
|
|
22696
|
-
docs: nonEmptyStringSchema.nullable().default(null),
|
|
22697
|
-
/**
|
|
22698
|
-
* The version of the adblocker in which the entity was added.
|
|
22699
|
-
* For AdGuard resources, the version of the library is specified.
|
|
22700
|
-
*/
|
|
22701
|
-
version_added: nonEmptyStringSchema.nullable().default(null),
|
|
22702
|
-
/**
|
|
22703
|
-
* The version of the adblocker when the entity was removed.
|
|
22704
|
-
*/
|
|
22705
|
-
version_removed: nonEmptyStringSchema.nullable().default(null),
|
|
22706
|
-
/**
|
|
22707
|
-
* Describes whether the entity is deprecated.
|
|
22708
|
-
*/
|
|
22709
|
-
deprecated: booleanSchema.default(false),
|
|
22710
|
-
/**
|
|
22711
|
-
* Message that describes why the entity is deprecated.
|
|
22712
|
-
* If not specified or it's value is `null`, then the message is not available.
|
|
22713
|
-
* It's value is omitted if the entity is not marked as deprecated.
|
|
22714
|
-
*/
|
|
22715
|
-
deprecation_message: nonEmptyStringSchema.nullable().default(null),
|
|
22602
|
+
class AdgCosmeticRuleModifierConverter {
|
|
22716
22603
|
/**
|
|
22717
|
-
*
|
|
22604
|
+
* Converts a uBO cosmetic rule modifier list to ADG, if possible.
|
|
22605
|
+
*
|
|
22606
|
+
* @param modifierList Cosmetic rule modifier list node to convert
|
|
22607
|
+
* @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
|
|
22608
|
+
* the converted node, and its `isConverted` flag indicates whether the original node was converted.
|
|
22609
|
+
* If the node was not converted, the result will contain the original node with the same object reference
|
|
22610
|
+
* @throws If the modifier list cannot be converted
|
|
22611
|
+
* @see {@link https://github.com/gorhill/uBlock/wiki/Procedural-cosmetic-filters#cosmetic-filter-operators}
|
|
22718
22612
|
*/
|
|
22719
|
-
|
|
22613
|
+
static convertFromUbo(modifierList) {
|
|
22614
|
+
const conversionMap = new MultiValueMap();
|
|
22615
|
+
modifierList.children.forEach((modifier, index) => {
|
|
22616
|
+
// :matches-path
|
|
22617
|
+
if (modifier.name.value === UBO_MATCHES_PATH_OPERATOR) {
|
|
22618
|
+
if (!modifier.value) {
|
|
22619
|
+
throw new RuleConversionError(`'${UBO_MATCHES_PATH_OPERATOR}' operator requires a value`);
|
|
22620
|
+
}
|
|
22621
|
+
const value = RegExpUtils.isRegexPattern(modifier.value.value) ? StringUtils.escapeCharacters(modifier.value.value, SPECIAL_MODIFIER_REGEX_CHARS) : modifier.value.value;
|
|
22622
|
+
// Convert uBO's `:matches-path(...)` operator to ADG's `$path=...` modifier
|
|
22623
|
+
conversionMap.add(index, createModifierNode(ADG_PATH_MODIFIER,
|
|
22624
|
+
// We should negate the regexp if the modifier is an exception
|
|
22625
|
+
modifier.exception
|
|
22626
|
+
// eslint-disable-next-line max-len
|
|
22627
|
+
? `${REGEX_MARKER}${RegExpUtils.negateRegexPattern(RegExpUtils.patternToRegexp(value))}${REGEX_MARKER}` : value));
|
|
22628
|
+
}
|
|
22629
|
+
});
|
|
22630
|
+
// Check if we have any converted modifiers
|
|
22631
|
+
if (conversionMap.size) {
|
|
22632
|
+
const modifierListClone = clone(modifierList);
|
|
22633
|
+
// Replace the original modifiers with the converted ones
|
|
22634
|
+
modifierListClone.children = modifierListClone.children.map((modifier, index) => {
|
|
22635
|
+
const convertedModifier = conversionMap.get(index);
|
|
22636
|
+
return convertedModifier ?? modifier;
|
|
22637
|
+
}).flat();
|
|
22638
|
+
return createConversionResult(modifierListClone, true);
|
|
22639
|
+
}
|
|
22640
|
+
// Otherwise, just return the original modifier list
|
|
22641
|
+
return createConversionResult(modifierList, false);
|
|
22642
|
+
}
|
|
22643
|
+
}
|
|
22644
|
+
const ERROR_MESSAGES$1 = {
|
|
22645
|
+
// eslint-disable-next-line max-len
|
|
22646
|
+
INVALID_ATTRIBUTE_VALUE: `Expected '${cssTokenizer.getFormattedTokenName(cssTokenizer.TokenType.Ident)}' or '${cssTokenizer.getFormattedTokenName(cssTokenizer.TokenType.String)}' as attribute value, but got '%s' with value '%s`
|
|
22647
|
+
};
|
|
22648
|
+
var PseudoClasses;
|
|
22649
|
+
(function (PseudoClasses) {
|
|
22650
|
+
PseudoClasses["AbpContains"] = "-abp-contains";
|
|
22651
|
+
PseudoClasses["AbpHas"] = "-abp-has";
|
|
22652
|
+
PseudoClasses["Contains"] = "contains";
|
|
22653
|
+
PseudoClasses["Has"] = "has";
|
|
22654
|
+
PseudoClasses["HasText"] = "has-text";
|
|
22655
|
+
PseudoClasses["MatchesCss"] = "matches-css";
|
|
22656
|
+
PseudoClasses["MatchesCssAfter"] = "matches-css-after";
|
|
22657
|
+
PseudoClasses["MatchesCssBefore"] = "matches-css-before";
|
|
22658
|
+
PseudoClasses["Not"] = "not";
|
|
22659
|
+
})(PseudoClasses || (PseudoClasses = {}));
|
|
22660
|
+
var PseudoElements;
|
|
22661
|
+
(function (PseudoElements) {
|
|
22662
|
+
PseudoElements["After"] = "after";
|
|
22663
|
+
PseudoElements["Before"] = "before";
|
|
22664
|
+
})(PseudoElements || (PseudoElements = {}));
|
|
22665
|
+
const PSEUDO_ELEMENT_NAMES = new Set([PseudoElements.After, PseudoElements.Before]);
|
|
22666
|
+
/**
|
|
22667
|
+
* CSS selector converter
|
|
22668
|
+
*
|
|
22669
|
+
* @todo Implement `convertToUbo` and `convertToAbp`
|
|
22670
|
+
*/
|
|
22671
|
+
class CssSelectorConverter extends ConverterBase {
|
|
22720
22672
|
/**
|
|
22721
|
-
*
|
|
22722
|
-
*
|
|
22723
|
-
*
|
|
22673
|
+
* Converts Extended CSS elements to AdGuard-compatible ones
|
|
22674
|
+
*
|
|
22675
|
+
* @param selectorList Selector list to convert
|
|
22676
|
+
* @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
|
|
22677
|
+
* the converted node, and its `isConverted` flag indicates whether the original node was converted.
|
|
22678
|
+
* If the node was not converted, the result will contain the original node with the same object reference
|
|
22679
|
+
* @throws If the rule is invalid or incompatible
|
|
22724
22680
|
*/
|
|
22725
|
-
|
|
22726
|
-
|
|
22681
|
+
static convertToAdg(selectorList) {
|
|
22682
|
+
const stream = selectorList instanceof CssTokenStream ? selectorList : new CssTokenStream(selectorList);
|
|
22683
|
+
const converted = [];
|
|
22684
|
+
const convertAndPushPseudo = pseudo => {
|
|
22685
|
+
switch (pseudo) {
|
|
22686
|
+
case PseudoClasses.AbpContains:
|
|
22687
|
+
case PseudoClasses.HasText:
|
|
22688
|
+
converted.push(PseudoClasses.Contains);
|
|
22689
|
+
converted.push(OPEN_PARENTHESIS);
|
|
22690
|
+
break;
|
|
22691
|
+
case PseudoClasses.AbpHas:
|
|
22692
|
+
converted.push(PseudoClasses.Has);
|
|
22693
|
+
converted.push(OPEN_PARENTHESIS);
|
|
22694
|
+
break;
|
|
22695
|
+
// a bit special case:
|
|
22696
|
+
// - `:matches-css-before(...)` → `:matches-css(before, ...)`
|
|
22697
|
+
// - `:matches-css-after(...)` → `:matches-css(after, ...)`
|
|
22698
|
+
case PseudoClasses.MatchesCssBefore:
|
|
22699
|
+
case PseudoClasses.MatchesCssAfter:
|
|
22700
|
+
converted.push(PseudoClasses.MatchesCss);
|
|
22701
|
+
converted.push(OPEN_PARENTHESIS);
|
|
22702
|
+
converted.push(pseudo.substring(PseudoClasses.MatchesCss.length + 1));
|
|
22703
|
+
converted.push(COMMA);
|
|
22704
|
+
break;
|
|
22705
|
+
default:
|
|
22706
|
+
converted.push(pseudo);
|
|
22707
|
+
converted.push(OPEN_PARENTHESIS);
|
|
22708
|
+
break;
|
|
22709
|
+
}
|
|
22710
|
+
};
|
|
22711
|
+
while (!stream.isEof()) {
|
|
22712
|
+
const token = stream.getOrFail();
|
|
22713
|
+
if (token.type === cssTokenizer.TokenType.Colon) {
|
|
22714
|
+
// Advance colon
|
|
22715
|
+
stream.advance();
|
|
22716
|
+
converted.push(COLON);
|
|
22717
|
+
const tempToken = stream.getOrFail();
|
|
22718
|
+
// Double colon is a pseudo-element
|
|
22719
|
+
if (tempToken.type === cssTokenizer.TokenType.Colon) {
|
|
22720
|
+
stream.advance();
|
|
22721
|
+
converted.push(COLON);
|
|
22722
|
+
continue;
|
|
22723
|
+
}
|
|
22724
|
+
if (tempToken.type === cssTokenizer.TokenType.Ident) {
|
|
22725
|
+
const name = stream.source.slice(tempToken.start, tempToken.end);
|
|
22726
|
+
if (PSEUDO_ELEMENT_NAMES.has(name)) {
|
|
22727
|
+
// Add an extra colon to the name
|
|
22728
|
+
converted.push(COLON);
|
|
22729
|
+
converted.push(name);
|
|
22730
|
+
} else {
|
|
22731
|
+
// Add the name as is
|
|
22732
|
+
converted.push(name);
|
|
22733
|
+
}
|
|
22734
|
+
// Advance the names
|
|
22735
|
+
stream.advance();
|
|
22736
|
+
} else if (tempToken.type === cssTokenizer.TokenType.Function) {
|
|
22737
|
+
const name = stream.source.slice(tempToken.start, tempToken.end - 1); // omit the last parenthesis
|
|
22738
|
+
// :-abp-contains(...) → :contains(...)
|
|
22739
|
+
// :has-text(...) → :contains(...)
|
|
22740
|
+
// :-abp-has(...) → :has(...)
|
|
22741
|
+
convertAndPushPseudo(name);
|
|
22742
|
+
// Advance the function name
|
|
22743
|
+
stream.advance();
|
|
22744
|
+
}
|
|
22745
|
+
} else if (token.type === cssTokenizer.TokenType.OpenSquareBracket) {
|
|
22746
|
+
let tempToken;
|
|
22747
|
+
const {
|
|
22748
|
+
start
|
|
22749
|
+
} = token;
|
|
22750
|
+
stream.advance();
|
|
22751
|
+
// Converts legacy Extended CSS selectors to the modern Extended CSS syntax.
|
|
22752
|
+
// For example:
|
|
22753
|
+
// - `[-ext-has=...]` → `:has(...)`
|
|
22754
|
+
// - `[-ext-contains=...]` → `:contains(...)`
|
|
22755
|
+
// - `[-ext-matches-css-before=...]` → `:matches-css(before, ...)`
|
|
22756
|
+
stream.skipWhitespace();
|
|
22757
|
+
stream.expect(cssTokenizer.TokenType.Ident);
|
|
22758
|
+
tempToken = stream.getOrFail();
|
|
22759
|
+
let attr = stream.source.slice(tempToken.start, tempToken.end);
|
|
22760
|
+
// Skip if the attribute name is not a legacy Extended CSS one
|
|
22761
|
+
if (!(attr.startsWith(LEGACY_EXT_CSS_ATTRIBUTE_PREFIX) || attr.startsWith(ABP_EXT_CSS_PREFIX))) {
|
|
22762
|
+
converted.push(stream.source.slice(start, tempToken.end));
|
|
22763
|
+
stream.advance();
|
|
22764
|
+
continue;
|
|
22765
|
+
}
|
|
22766
|
+
if (attr.startsWith(LEGACY_EXT_CSS_ATTRIBUTE_PREFIX)) {
|
|
22767
|
+
attr = attr.slice(LEGACY_EXT_CSS_ATTRIBUTE_PREFIX.length);
|
|
22768
|
+
}
|
|
22769
|
+
stream.advance();
|
|
22770
|
+
stream.skipWhitespace();
|
|
22771
|
+
// Next token should be an equality operator (=), because Extended CSS attribute selectors
|
|
22772
|
+
// do not support other operators
|
|
22773
|
+
stream.expect(cssTokenizer.TokenType.Delim, {
|
|
22774
|
+
value: EQUALS
|
|
22775
|
+
});
|
|
22776
|
+
stream.advance();
|
|
22777
|
+
// Skip optional whitespace after the operator
|
|
22778
|
+
stream.skipWhitespace();
|
|
22779
|
+
// Parse attribute value
|
|
22780
|
+
tempToken = stream.getOrFail();
|
|
22781
|
+
// According to the spec, attribute value should be an identifier or a string
|
|
22782
|
+
if (tempToken.type !== cssTokenizer.TokenType.Ident && tempToken.type !== cssTokenizer.TokenType.String) {
|
|
22783
|
+
throw new Error(sprintfJs.sprintf(ERROR_MESSAGES$1.INVALID_ATTRIBUTE_VALUE, cssTokenizer.getFormattedTokenName(tempToken.type), stream.source.slice(tempToken.start, tempToken.end)));
|
|
22784
|
+
}
|
|
22785
|
+
const value = stream.source.slice(tempToken.start, tempToken.end);
|
|
22786
|
+
// Advance the attribute value
|
|
22787
|
+
stream.advance();
|
|
22788
|
+
// Skip optional whitespace after the attribute value
|
|
22789
|
+
stream.skipWhitespace();
|
|
22790
|
+
// Next character should be a closing square bracket
|
|
22791
|
+
// We don't allow flags for Extended CSS attribute selectors
|
|
22792
|
+
stream.expect(cssTokenizer.TokenType.CloseSquareBracket);
|
|
22793
|
+
stream.advance();
|
|
22794
|
+
converted.push(COLON);
|
|
22795
|
+
convertAndPushPseudo(attr);
|
|
22796
|
+
let processedValue = value.slice(1, -1); // omit the quotes
|
|
22797
|
+
if (attr === PseudoClasses.Has) {
|
|
22798
|
+
// TODO: Optimize this to avoid double tokenization
|
|
22799
|
+
processedValue = CssSelectorConverter.convertToAdg(processedValue).result;
|
|
22800
|
+
}
|
|
22801
|
+
converted.push(processedValue);
|
|
22802
|
+
converted.push(CLOSE_PARENTHESIS);
|
|
22803
|
+
} else {
|
|
22804
|
+
converted.push(stream.source.slice(token.start, token.end));
|
|
22805
|
+
// Advance the token
|
|
22806
|
+
stream.advance();
|
|
22807
|
+
}
|
|
22808
|
+
}
|
|
22809
|
+
const convertedSelectorList = converted.join(EMPTY);
|
|
22810
|
+
return createConversionResult(convertedSelectorList, stream.source !== convertedSelectorList);
|
|
22811
|
+
}
|
|
22812
|
+
}
|
|
22813
|
+
|
|
22727
22814
|
/**
|
|
22728
|
-
*
|
|
22815
|
+
* @file CSS injection rule converter
|
|
22729
22816
|
*/
|
|
22730
|
-
zodToCamelCase(baseCompatibilityDataSchema);
|
|
22731
22817
|
/**
|
|
22732
|
-
*
|
|
22818
|
+
* CSS injection rule converter class
|
|
22733
22819
|
*
|
|
22734
|
-
* @
|
|
22735
|
-
* @param ctx Refinement context.
|
|
22820
|
+
* @todo Implement `convertToUbo` and `convertToAbp`
|
|
22736
22821
|
*/
|
|
22737
|
-
|
|
22738
|
-
|
|
22739
|
-
|
|
22740
|
-
|
|
22741
|
-
|
|
22742
|
-
|
|
22743
|
-
|
|
22744
|
-
|
|
22745
|
-
|
|
22746
|
-
|
|
22747
|
-
|
|
22748
|
-
|
|
22749
|
-
|
|
22750
|
-
|
|
22751
|
-
|
|
22752
|
-
|
|
22753
|
-
|
|
22754
|
-
|
|
22822
|
+
class CssInjectionRuleConverter extends RuleConverterBase {
|
|
22823
|
+
/**
|
|
22824
|
+
* Converts a CSS injection rule to AdGuard format, if possible.
|
|
22825
|
+
*
|
|
22826
|
+
* @param rule Rule node to convert
|
|
22827
|
+
* @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
|
|
22828
|
+
* the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
|
|
22829
|
+
* If the rule was not converted, the result array will contain the original node with the same object reference
|
|
22830
|
+
* @throws If the rule is invalid or cannot be converted
|
|
22831
|
+
*/
|
|
22832
|
+
static convertToAdg(rule) {
|
|
22833
|
+
const separator = rule.separator.value;
|
|
22834
|
+
let convertedSeparator = separator;
|
|
22835
|
+
const stream = new CssTokenStream(rule.body.selectorList.value);
|
|
22836
|
+
const convertedSelectorList = CssSelectorConverter.convertToAdg(stream);
|
|
22837
|
+
// Change the separator if the rule contains ExtendedCSS elements,
|
|
22838
|
+
// but do not force non-extended CSS separator if the rule does not contain any ExtendedCSS selectors,
|
|
22839
|
+
// because sometimes we use it to force executing ExtendedCSS library.
|
|
22840
|
+
if (stream.hasAnySelectorExtendedCssNodeStrict() || rule.body.remove) {
|
|
22841
|
+
convertedSeparator = rule.exception ? exports.CosmeticRuleSeparator.AdgExtendedCssInjectionException : exports.CosmeticRuleSeparator.AdgExtendedCssInjection;
|
|
22842
|
+
} else if (rule.syntax !== exports.AdblockSyntax.Adg) {
|
|
22843
|
+
// If the original rule syntax is not AdGuard, use the default separator
|
|
22844
|
+
// e.g. if the input rule is from uBO, we need to convert ## to #$#.
|
|
22845
|
+
convertedSeparator = rule.exception ? exports.CosmeticRuleSeparator.AdgCssInjectionException : exports.CosmeticRuleSeparator.AdgCssInjection;
|
|
22846
|
+
}
|
|
22847
|
+
// Check if the rule needs to be converted
|
|
22848
|
+
if (!(rule.syntax === exports.AdblockSyntax.Common || rule.syntax === exports.AdblockSyntax.Adg) || separator !== convertedSeparator || convertedSelectorList.isConverted) {
|
|
22849
|
+
// TODO: Replace with custom clone method
|
|
22850
|
+
const ruleClone = clone(rule);
|
|
22851
|
+
ruleClone.syntax = exports.AdblockSyntax.Adg;
|
|
22852
|
+
ruleClone.separator.value = convertedSeparator;
|
|
22853
|
+
ruleClone.body.selectorList.value = convertedSelectorList.result;
|
|
22854
|
+
return createNodeConversionResult([ruleClone], true);
|
|
22855
|
+
}
|
|
22856
|
+
// Otherwise, return the original rule
|
|
22857
|
+
return createNodeConversionResult([rule], false);
|
|
22755
22858
|
}
|
|
22756
|
-
}
|
|
22859
|
+
}
|
|
22757
22860
|
|
|
22758
22861
|
/**
|
|
22759
|
-
*
|
|
22760
|
-
*
|
|
22761
|
-
* @param error Error object.
|
|
22762
|
-
* @returns If param is error.
|
|
22862
|
+
* @file Element hiding rule converter
|
|
22763
22863
|
*/
|
|
22764
|
-
function isErrorWithMessage(error) {
|
|
22765
|
-
return typeof error === 'object' && error !== null && 'message' in error && typeof error.message === 'string';
|
|
22766
|
-
}
|
|
22767
22864
|
/**
|
|
22768
|
-
*
|
|
22865
|
+
* Element hiding rule converter class
|
|
22769
22866
|
*
|
|
22770
|
-
* @
|
|
22771
|
-
* @returns Error with message.
|
|
22867
|
+
* @todo Implement `convertToUbo` and `convertToAbp`
|
|
22772
22868
|
*/
|
|
22773
|
-
|
|
22774
|
-
|
|
22775
|
-
|
|
22776
|
-
|
|
22777
|
-
|
|
22778
|
-
|
|
22779
|
-
|
|
22780
|
-
|
|
22781
|
-
|
|
22782
|
-
|
|
22869
|
+
class ElementHidingRuleConverter extends RuleConverterBase {
|
|
22870
|
+
/**
|
|
22871
|
+
* Converts an element hiding rule to AdGuard format, if possible.
|
|
22872
|
+
*
|
|
22873
|
+
* @param rule Rule node to convert
|
|
22874
|
+
* @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
|
|
22875
|
+
* the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
|
|
22876
|
+
* If the rule was not converted, the result array will contain the original node with the same object reference
|
|
22877
|
+
* @throws If the rule is invalid or cannot be converted
|
|
22878
|
+
*/
|
|
22879
|
+
static convertToAdg(rule) {
|
|
22880
|
+
const separator = rule.separator.value;
|
|
22881
|
+
let convertedSeparator = separator;
|
|
22882
|
+
const stream = new CssTokenStream(rule.body.selectorList.value);
|
|
22883
|
+
const convertedSelectorList = CssSelectorConverter.convertToAdg(stream);
|
|
22884
|
+
// Change the separator if the rule contains ExtendedCSS elements,
|
|
22885
|
+
// but do not force non-extended CSS separator if the rule does not contain any ExtendedCSS selectors,
|
|
22886
|
+
// because sometimes we use it to force executing ExtendedCSS library.
|
|
22887
|
+
if (stream.hasAnySelectorExtendedCssNodeStrict()) {
|
|
22888
|
+
convertedSeparator = rule.exception ? exports.CosmeticRuleSeparator.ExtendedElementHidingException : exports.CosmeticRuleSeparator.ExtendedElementHiding;
|
|
22889
|
+
}
|
|
22890
|
+
// Check if the rule needs to be converted
|
|
22891
|
+
if (!(rule.syntax === exports.AdblockSyntax.Common || rule.syntax === exports.AdblockSyntax.Adg) || separator !== convertedSeparator || convertedSelectorList.isConverted) {
|
|
22892
|
+
// TODO: Replace with custom clone method
|
|
22893
|
+
const ruleClone = clone(rule);
|
|
22894
|
+
ruleClone.syntax = exports.AdblockSyntax.Adg;
|
|
22895
|
+
ruleClone.separator.value = convertedSeparator;
|
|
22896
|
+
ruleClone.body.selectorList.value = convertedSelectorList.result;
|
|
22897
|
+
return createNodeConversionResult([ruleClone], true);
|
|
22898
|
+
}
|
|
22899
|
+
// Otherwise, return the original rule
|
|
22900
|
+
return createNodeConversionResult([rule], false);
|
|
22783
22901
|
}
|
|
22784
22902
|
}
|
|
22785
|
-
/**
|
|
22786
|
-
* Converts error object to error with message. This method might be helpful to handle thrown errors.
|
|
22787
|
-
*
|
|
22788
|
-
* @param error Error object.
|
|
22789
|
-
*
|
|
22790
|
-
* @returns Message of the error.
|
|
22791
|
-
*/
|
|
22792
|
-
function getErrorMessage(error) {
|
|
22793
|
-
return toErrorWithMessage(error).message;
|
|
22794
|
-
}
|
|
22795
22903
|
|
|
22796
22904
|
/**
|
|
22797
|
-
* @file
|
|
22798
|
-
*/
|
|
22799
|
-
/**
|
|
22800
|
-
* Known validators that don't need to be validated as regex.
|
|
22905
|
+
* @file Utility functions for working with network rule nodes
|
|
22801
22906
|
*/
|
|
22802
|
-
const KNOWN_VALIDATORS = new Set(['domain', 'pipe_separated_domains', 'regexp', 'url']);
|
|
22803
22907
|
/**
|
|
22804
|
-
*
|
|
22908
|
+
* Creates a network rule node
|
|
22909
|
+
*
|
|
22910
|
+
* @param pattern Rule pattern
|
|
22911
|
+
* @param modifiers Rule modifiers (optional, default: undefined)
|
|
22912
|
+
* @param exception Exception rule flag (optional, default: false)
|
|
22913
|
+
* @param syntax Adblock syntax (optional, default: Common)
|
|
22914
|
+
* @returns Network rule node
|
|
22805
22915
|
*/
|
|
22806
|
-
|
|
22807
|
-
|
|
22808
|
-
|
|
22809
|
-
|
|
22810
|
-
|
|
22811
|
-
|
|
22812
|
-
|
|
22813
|
-
|
|
22814
|
-
|
|
22815
|
-
/**
|
|
22816
|
-
* Describes whether the actual modifier supports value assignment. For example, `$domain` is assignable,
|
|
22817
|
-
* so it can be used like this: `$domain=domain.com\|~subdomain.domain.com`, where `=` is the assignment operator
|
|
22818
|
-
* and `domain.com\|~subdomain.domain.com` is the value.
|
|
22819
|
-
*/
|
|
22820
|
-
assignable: booleanSchema.default(false),
|
|
22821
|
-
/**
|
|
22822
|
-
* Describes whether the actual modifier can be negated. For example, `$third-party` is negatable,
|
|
22823
|
-
* so it can be used like this: `$~third-party`.
|
|
22824
|
-
*/
|
|
22825
|
-
negatable: booleanSchema.default(true),
|
|
22826
|
-
/**
|
|
22827
|
-
* The actual modifier can only be used in blocking rules, it cannot be used in exceptions.
|
|
22828
|
-
* If it's value is `true`, then the modifier can be used only in blocking rules.
|
|
22829
|
-
* `exception_only` and `block_only` cannot be used together (they are mutually exclusive).
|
|
22830
|
-
*/
|
|
22831
|
-
block_only: booleanSchema.default(false),
|
|
22832
|
-
/**
|
|
22833
|
-
* The actual modifier can only be used in exceptions, it cannot be used in blocking rules.
|
|
22834
|
-
* If it's value is `true`, then the modifier can be used only in exceptions.
|
|
22835
|
-
* `exception_only` and `block_only` cannot be used together (they are mutually exclusive).
|
|
22836
|
-
*/
|
|
22837
|
-
exception_only: booleanSchema.default(false),
|
|
22838
|
-
/**
|
|
22839
|
-
* Describes whether the *assignable* modifier value is required.
|
|
22840
|
-
* For example, `$cookie` is assignable but it can be used without a value in exception rules:
|
|
22841
|
-
* `@@\|\|example.com^$cookie`.
|
|
22842
|
-
* If `false`, the `value_format` is required, e.g. the value of `$app` should always be specified
|
|
22843
|
-
*/
|
|
22844
|
-
value_optional: booleanSchema.default(false),
|
|
22845
|
-
/**
|
|
22846
|
-
* Describes the format of the value for the *assignable* modifier.
|
|
22847
|
-
* Its value can be a regex pattern or a known validator name (e.g. `domain`, `pipe_separated_domains`, etc.).
|
|
22848
|
-
*/
|
|
22849
|
-
value_format: nonEmptyStringSchema.nullable().default(null)
|
|
22850
|
-
}).superRefine((data, ctx) => {
|
|
22851
|
-
// TODO: find something better, for now we can't add refine logic to the base schema:
|
|
22852
|
-
// https://github.com/colinhacks/zod/issues/454#issuecomment-848370721
|
|
22853
|
-
baseRefineLogic(data, ctx);
|
|
22854
|
-
if (data.block_only && data.exception_only) {
|
|
22855
|
-
ctx.addIssue({
|
|
22856
|
-
code: zod.ZodIssueCode.custom,
|
|
22857
|
-
message: 'block_only and exception_only are mutually exclusive'
|
|
22858
|
-
});
|
|
22859
|
-
}
|
|
22860
|
-
if (data.assignable && !data.value_format) {
|
|
22861
|
-
ctx.addIssue({
|
|
22862
|
-
code: zod.ZodIssueCode.custom,
|
|
22863
|
-
message: 'value_format is required for assignable modifiers'
|
|
22864
|
-
});
|
|
22865
|
-
}
|
|
22866
|
-
if (data.value_format) {
|
|
22867
|
-
const valueFormat = data.value_format.trim();
|
|
22868
|
-
// if it is a known validator, we don't need to validate it further
|
|
22869
|
-
if (KNOWN_VALIDATORS.has(valueFormat)) {
|
|
22870
|
-
return;
|
|
22871
|
-
}
|
|
22872
|
-
// otherwise, we need to validate it as a regex
|
|
22873
|
-
try {
|
|
22874
|
-
XRegExp(valueFormat);
|
|
22875
|
-
} catch (error) {
|
|
22876
|
-
ctx.addIssue({
|
|
22877
|
-
code: zod.ZodIssueCode.custom,
|
|
22878
|
-
message: getErrorMessage(error)
|
|
22879
|
-
});
|
|
22916
|
+
function createNetworkRuleNode(pattern, modifiers = undefined, exception = false, syntax = exports.AdblockSyntax.Common) {
|
|
22917
|
+
const result = {
|
|
22918
|
+
category: exports.RuleCategory.Network,
|
|
22919
|
+
type: exports.NetworkRuleType.NetworkRule,
|
|
22920
|
+
syntax,
|
|
22921
|
+
exception,
|
|
22922
|
+
pattern: {
|
|
22923
|
+
type: 'Value',
|
|
22924
|
+
value: pattern
|
|
22880
22925
|
}
|
|
22926
|
+
};
|
|
22927
|
+
if (!isUndefined(modifiers)) {
|
|
22928
|
+
result.modifiers = clone(modifiers);
|
|
22881
22929
|
}
|
|
22882
|
-
|
|
22930
|
+
return result;
|
|
22931
|
+
}
|
|
22883
22932
|
|
|
22884
22933
|
/**
|
|
22885
|
-
* @file
|
|
22934
|
+
* @file Converter for request header removal rules
|
|
22886
22935
|
*/
|
|
22936
|
+
const UBO_RESPONSEHEADER_FN = 'responseheader';
|
|
22937
|
+
const ADG_REMOVEHEADER_MODIFIER = 'removeheader';
|
|
22938
|
+
const ERROR_MESSAGES = {
|
|
22939
|
+
EMPTY_PARAMETER: `Empty parameter for '${UBO_RESPONSEHEADER_FN}' function`,
|
|
22940
|
+
EXPECTED_END_OF_RULE: "Expected end of rule, but got '%s'",
|
|
22941
|
+
MULTIPLE_DOMAINS_NOT_SUPPORTED: 'Multiple domains are not supported yet'
|
|
22942
|
+
};
|
|
22887
22943
|
/**
|
|
22888
|
-
*
|
|
22944
|
+
* Converter for request header removal rules
|
|
22945
|
+
*
|
|
22946
|
+
* @todo Implement `convertToUbo` (ABP currently doesn't support header removal rules)
|
|
22889
22947
|
*/
|
|
22890
|
-
|
|
22948
|
+
class HeaderRemovalRuleConverter extends RuleConverterBase {
|
|
22891
22949
|
/**
|
|
22892
|
-
*
|
|
22950
|
+
* Converts a header removal rule to AdGuard syntax, if possible.
|
|
22951
|
+
*
|
|
22952
|
+
* @param rule Rule node to convert
|
|
22953
|
+
* @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
|
|
22954
|
+
* the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
|
|
22955
|
+
* If the rule was not converted, the result array will contain the original node with the same object reference
|
|
22956
|
+
* @throws If the rule is invalid or cannot be converted
|
|
22957
|
+
* @example
|
|
22958
|
+
* If the input rule is:
|
|
22959
|
+
* ```adblock
|
|
22960
|
+
* example.com##^responseheader(header-name)
|
|
22961
|
+
* ```
|
|
22962
|
+
* The output will be:
|
|
22963
|
+
* ```adblock
|
|
22964
|
+
* ||example.com^$removeheader=header-name
|
|
22965
|
+
* ```
|
|
22893
22966
|
*/
|
|
22894
|
-
|
|
22895
|
-
|
|
22967
|
+
static convertToAdg(rule) {
|
|
22968
|
+
// TODO: Add support for ABP syntax once it starts supporting header removal rules
|
|
22969
|
+
// Leave the rule as is if it's not a header removal rule
|
|
22970
|
+
if (rule.category !== exports.RuleCategory.Cosmetic || rule.type !== exports.CosmeticRuleType.HtmlFilteringRule) {
|
|
22971
|
+
return createNodeConversionResult([rule], false);
|
|
22972
|
+
}
|
|
22973
|
+
const stream = new CssTokenStream(rule.body.value);
|
|
22974
|
+
let token;
|
|
22975
|
+
// Skip leading whitespace
|
|
22976
|
+
stream.skipWhitespace();
|
|
22977
|
+
// Next token should be the `^` followed by a `responseheader` function
|
|
22978
|
+
token = stream.get();
|
|
22979
|
+
if (!token || token.type !== cssTokenizer.TokenType.Delim || rule.body.value[token.start] !== UBO_HTML_MASK) {
|
|
22980
|
+
return createNodeConversionResult([rule], false);
|
|
22981
|
+
}
|
|
22982
|
+
stream.advance();
|
|
22983
|
+
token = stream.get();
|
|
22984
|
+
if (!token) {
|
|
22985
|
+
return createNodeConversionResult([rule], false);
|
|
22986
|
+
}
|
|
22987
|
+
const functionName = rule.body.value.slice(token.start, token.end - 1);
|
|
22988
|
+
if (functionName !== UBO_RESPONSEHEADER_FN) {
|
|
22989
|
+
return createNodeConversionResult([rule], false);
|
|
22990
|
+
}
|
|
22991
|
+
// Parse the parameter
|
|
22992
|
+
const paramStart = token.end;
|
|
22993
|
+
stream.skipUntilBalanced();
|
|
22994
|
+
const paramEnd = stream.getOrFail().end;
|
|
22995
|
+
const param = rule.body.value.slice(paramStart, paramEnd - 1).trim();
|
|
22996
|
+
// Do not allow empty parameter
|
|
22997
|
+
if (param.length === 0) {
|
|
22998
|
+
throw new RuleConversionError(ERROR_MESSAGES.EMPTY_PARAMETER);
|
|
22999
|
+
}
|
|
23000
|
+
stream.expect(cssTokenizer.TokenType.CloseParenthesis);
|
|
23001
|
+
stream.advance();
|
|
23002
|
+
// Skip trailing whitespace after the function call
|
|
23003
|
+
stream.skipWhitespace();
|
|
23004
|
+
// Expect the end of the rule - so nothing should be left in the stream
|
|
23005
|
+
if (!stream.isEof()) {
|
|
23006
|
+
token = stream.getOrFail();
|
|
23007
|
+
throw new RuleConversionError(sprintfJs.sprintf(ERROR_MESSAGES.EXPECTED_END_OF_RULE, cssTokenizer.getFormattedTokenName(token.type)));
|
|
23008
|
+
}
|
|
23009
|
+
// Prepare network rule pattern
|
|
23010
|
+
const pattern = [];
|
|
23011
|
+
if (rule.domains.children.length === 1) {
|
|
23012
|
+
// If the rule has only one domain, we can use a simple network rule pattern:
|
|
23013
|
+
// ||single-domain-from-the-rule^
|
|
23014
|
+
pattern.push(ADBLOCK_URL_START, rule.domains.children[0].value, ADBLOCK_URL_SEPARATOR);
|
|
23015
|
+
} else if (rule.domains.children.length > 1) {
|
|
23016
|
+
// TODO: Add support for multiple domains, for example:
|
|
23017
|
+
// example.com,example.org,example.net##^responseheader(header-name)
|
|
23018
|
+
// We should consider allowing $domain with $removeheader modifier,
|
|
23019
|
+
// for example:
|
|
23020
|
+
// $removeheader=header-name,domain=example.com|example.org|example.net
|
|
23021
|
+
throw new RuleConversionError(ERROR_MESSAGES.MULTIPLE_DOMAINS_NOT_SUPPORTED);
|
|
23022
|
+
}
|
|
23023
|
+
// Prepare network rule modifiers
|
|
23024
|
+
const modifiers = createModifierListNode();
|
|
23025
|
+
modifiers.children.push(createModifierNode(ADG_REMOVEHEADER_MODIFIER, param));
|
|
23026
|
+
// Construct the network rule
|
|
23027
|
+
return createNodeConversionResult([createNetworkRuleNode(pattern.join(EMPTY), modifiers,
|
|
23028
|
+
// Copy the exception flag
|
|
23029
|
+
rule.exception, exports.AdblockSyntax.Adg)], true);
|
|
23030
|
+
}
|
|
23031
|
+
}
|
|
22896
23032
|
|
|
22897
23033
|
/**
|
|
22898
|
-
* @file
|
|
22899
|
-
*/
|
|
22900
|
-
/**
|
|
22901
|
-
* Zod schema for scriptlet parameter data.
|
|
22902
|
-
*/
|
|
22903
|
-
const scriptletParameterSchema = zod.object({
|
|
22904
|
-
/**
|
|
22905
|
-
* Name of the actual parameter.
|
|
22906
|
-
*/
|
|
22907
|
-
name: nonEmptyStringSchema,
|
|
22908
|
-
/**
|
|
22909
|
-
* Describes whether the parameter is required. Empty parameters are not allowed.
|
|
22910
|
-
*/
|
|
22911
|
-
required: booleanSchema,
|
|
22912
|
-
/**
|
|
22913
|
-
* Short description of the parameter.
|
|
22914
|
-
* If not specified or it's value is `null`,then the description is not available.
|
|
22915
|
-
*/
|
|
22916
|
-
description: nonEmptyStringSchema.nullable().default(null),
|
|
22917
|
-
/**
|
|
22918
|
-
* Regular expression that matches the value of the parameter.
|
|
22919
|
-
* If it's value is `null`, then the parameter value is not checked.
|
|
22920
|
-
*/
|
|
22921
|
-
pattern: nonEmptyStringSchema.nullable().default(null),
|
|
22922
|
-
/**
|
|
22923
|
-
* Default value of the parameter (if any).
|
|
22924
|
-
*/
|
|
22925
|
-
default: nonEmptyStringSchema.nullable().default(null),
|
|
22926
|
-
/**
|
|
22927
|
-
* Describes whether the parameter is used only for debugging purposes.
|
|
22928
|
-
*/
|
|
22929
|
-
debug: booleanSchema.default(false)
|
|
22930
|
-
});
|
|
22931
|
-
/**
|
|
22932
|
-
* Zod schema for scriptlet parameters.
|
|
23034
|
+
* @file Cosmetic rule converter
|
|
22933
23035
|
*/
|
|
22934
|
-
const scriptletParametersSchema = zod.array(scriptletParameterSchema);
|
|
22935
23036
|
/**
|
|
22936
|
-
*
|
|
23037
|
+
* Cosmetic rule converter class (also known as "non-basic rule converter")
|
|
23038
|
+
*
|
|
23039
|
+
* @todo Implement `convertToUbo` and `convertToAbp`
|
|
22937
23040
|
*/
|
|
22938
|
-
|
|
23041
|
+
class CosmeticRuleConverter extends RuleConverterBase {
|
|
22939
23042
|
/**
|
|
22940
|
-
*
|
|
22941
|
-
*
|
|
22942
|
-
*
|
|
23043
|
+
* Converts a cosmetic rule to AdGuard syntax, if possible.
|
|
23044
|
+
*
|
|
23045
|
+
* @param rule Rule node to convert
|
|
23046
|
+
* @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
|
|
23047
|
+
* the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
|
|
23048
|
+
* If the rule was not converted, the result array will contain the original node with the same object reference
|
|
23049
|
+
* @throws If the rule is invalid or cannot be converted
|
|
22943
23050
|
*/
|
|
22944
|
-
|
|
22945
|
-
|
|
22946
|
-
|
|
22947
|
-
|
|
22948
|
-
|
|
22949
|
-
|
|
22950
|
-
|
|
22951
|
-
|
|
22952
|
-
|
|
22953
|
-
|
|
22954
|
-
|
|
22955
|
-
|
|
22956
|
-
|
|
22957
|
-
|
|
22958
|
-
|
|
23051
|
+
static convertToAdg(rule) {
|
|
23052
|
+
let subconverterResult;
|
|
23053
|
+
// Convert cosmetic rule based on its type
|
|
23054
|
+
switch (rule.type) {
|
|
23055
|
+
case exports.CosmeticRuleType.ElementHidingRule:
|
|
23056
|
+
subconverterResult = ElementHidingRuleConverter.convertToAdg(rule);
|
|
23057
|
+
break;
|
|
23058
|
+
case exports.CosmeticRuleType.ScriptletInjectionRule:
|
|
23059
|
+
subconverterResult = ScriptletRuleConverter.convertToAdg(rule);
|
|
23060
|
+
break;
|
|
23061
|
+
case exports.CosmeticRuleType.CssInjectionRule:
|
|
23062
|
+
subconverterResult = CssInjectionRuleConverter.convertToAdg(rule);
|
|
23063
|
+
break;
|
|
23064
|
+
case exports.CosmeticRuleType.HtmlFilteringRule:
|
|
23065
|
+
// Handle special case: uBO response header filtering rule
|
|
23066
|
+
// TODO: Optimize double CSS tokenization here
|
|
23067
|
+
subconverterResult = HeaderRemovalRuleConverter.convertToAdg(rule);
|
|
23068
|
+
if (subconverterResult.isConverted) {
|
|
23069
|
+
break;
|
|
23070
|
+
}
|
|
23071
|
+
subconverterResult = HtmlRuleConverter.convertToAdg(rule);
|
|
23072
|
+
break;
|
|
23073
|
+
// Note: Currently, only ADG supports JS injection rules, so we don't need to convert them
|
|
23074
|
+
case exports.CosmeticRuleType.JsInjectionRule:
|
|
23075
|
+
subconverterResult = createNodeConversionResult([rule], false);
|
|
23076
|
+
break;
|
|
23077
|
+
default:
|
|
23078
|
+
throw new RuleConversionError('Unsupported cosmetic rule type');
|
|
23079
|
+
}
|
|
23080
|
+
let convertedModifiers;
|
|
23081
|
+
// Convert cosmetic rule modifiers, if any
|
|
23082
|
+
if (rule.modifiers) {
|
|
23083
|
+
if (rule.syntax === exports.AdblockSyntax.Ubo) {
|
|
23084
|
+
// uBO doesn't support this rule:
|
|
23085
|
+
// example.com##+js(set-constant.js, foo, bar):matches-path(/baz)
|
|
23086
|
+
if (rule.type === exports.CosmeticRuleType.ScriptletInjectionRule) {
|
|
23087
|
+
throw new RuleConversionError('uBO scriptlet injection rules don\'t support cosmetic rule modifiers');
|
|
23088
|
+
}
|
|
23089
|
+
convertedModifiers = AdgCosmeticRuleModifierConverter.convertFromUbo(rule.modifiers);
|
|
23090
|
+
} else if (rule.syntax === exports.AdblockSyntax.Abp) {
|
|
23091
|
+
// TODO: Implement once ABP starts supporting cosmetic rule modifiers
|
|
23092
|
+
throw new RuleConversionError('ABP don\'t support cosmetic rule modifiers');
|
|
23093
|
+
}
|
|
23094
|
+
}
|
|
23095
|
+
if (subconverterResult.result.length > 1 || subconverterResult.isConverted || convertedModifiers && convertedModifiers.isConverted) {
|
|
23096
|
+
// Add modifier list to the subconverter result rules
|
|
23097
|
+
subconverterResult.result.forEach(subconverterRule => {
|
|
23098
|
+
if (convertedModifiers && subconverterRule.category === exports.RuleCategory.Cosmetic) {
|
|
23099
|
+
// eslint-disable-next-line no-param-reassign
|
|
23100
|
+
subconverterRule.modifiers = convertedModifiers.result;
|
|
23101
|
+
}
|
|
22959
23102
|
});
|
|
23103
|
+
return subconverterResult;
|
|
22960
23104
|
}
|
|
22961
|
-
|
|
22962
|
-
|
|
23105
|
+
return createNodeConversionResult([rule], false);
|
|
23106
|
+
}
|
|
23107
|
+
/**
|
|
23108
|
+
* Converts a cosmetic rule to uBlock Origin syntax, if possible.
|
|
23109
|
+
*
|
|
23110
|
+
* @param rule Rule node to convert
|
|
23111
|
+
* @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
|
|
23112
|
+
* the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
|
|
23113
|
+
* If the rule was not converted, the result array will contain the original node with the same object reference
|
|
23114
|
+
* @throws If the rule is invalid or cannot be converted
|
|
23115
|
+
*/
|
|
23116
|
+
// TODO: Add support for other cosmetic rule types
|
|
23117
|
+
static convertToUbo(rule) {
|
|
23118
|
+
// Convert cosmetic rule based on its type
|
|
23119
|
+
if (rule.type === exports.CosmeticRuleType.ScriptletInjectionRule) {
|
|
23120
|
+
if (rule.syntax === exports.AdblockSyntax.Adg && rule.modifiers?.children.length) {
|
|
23121
|
+
// e.g. example.com##+js(set-constant.js, foo, bar):matches-path(/baz)
|
|
23122
|
+
throw new RuleConversionError('uBO scriptlet injection rules do not support cosmetic rule modifiers');
|
|
23123
|
+
}
|
|
23124
|
+
return ScriptletRuleConverter.convertToUbo(rule);
|
|
22963
23125
|
}
|
|
23126
|
+
return createNodeConversionResult([rule], false);
|
|
22964
23127
|
}
|
|
22965
|
-
}
|
|
23128
|
+
}
|
|
22966
23129
|
|
|
22967
23130
|
/**
|
|
22968
23131
|
* @file Network rule modifier list converter.
|
|
@@ -23234,6 +23397,22 @@ class RuleConverter extends RuleConverterBase {
|
|
|
23234
23397
|
throw new RuleConversionError('Unknown rule category');
|
|
23235
23398
|
}
|
|
23236
23399
|
}
|
|
23400
|
+
/**
|
|
23401
|
+
* Converts an adblock filtering rule to uBlock Origin format, if possible.
|
|
23402
|
+
*
|
|
23403
|
+
* @param rule Rule node to convert
|
|
23404
|
+
* @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
|
|
23405
|
+
* the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
|
|
23406
|
+
* If the rule was not converted, the result array will contain the original node with the same object reference
|
|
23407
|
+
* @throws If the rule is invalid or cannot be converted
|
|
23408
|
+
*/
|
|
23409
|
+
// TODO: Add support for other rule types
|
|
23410
|
+
static convertToUbo(rule) {
|
|
23411
|
+
if (rule.category === exports.RuleCategory.Cosmetic) {
|
|
23412
|
+
return CosmeticRuleConverter.convertToUbo(rule);
|
|
23413
|
+
}
|
|
23414
|
+
return createConversionResult([rule], false);
|
|
23415
|
+
}
|
|
23237
23416
|
}
|
|
23238
23417
|
|
|
23239
23418
|
/**
|
|
@@ -23627,7 +23806,7 @@ class ByteBuffer {
|
|
|
23627
23806
|
* @see {@link https://stackoverflow.com/a/62797156}
|
|
23628
23807
|
*/
|
|
23629
23808
|
const isChromium = () => {
|
|
23630
|
-
return typeof window !== 'undefined' && (Object.prototype.hasOwnProperty.call(window, 'chrome') || typeof window.navigator !== 'undefined' && /chrome/i.test(window.navigator.userAgent
|
|
23809
|
+
return typeof window !== 'undefined' && (Object.prototype.hasOwnProperty.call(window, 'chrome') || typeof window.navigator !== 'undefined' && /chrome/i.test(window.navigator.userAgent));
|
|
23631
23810
|
};
|
|
23632
23811
|
|
|
23633
23812
|
/* eslint-disable no-param-reassign */
|
|
@@ -23718,16 +23897,23 @@ class OutputByteBuffer extends ByteBuffer {
|
|
|
23718
23897
|
*/
|
|
23719
23898
|
offset;
|
|
23720
23899
|
/**
|
|
23721
|
-
* Size of the shared buffer for encoding strings.
|
|
23900
|
+
* Size of the shared buffer for encoding strings in bytes.
|
|
23901
|
+
* This is a divisor of ByteBuffer.CHUNK_SIZE and experience shows that this value works optimally.
|
|
23902
|
+
* This is sufficient for most strings that occur in filter lists (we checked average string length in popular
|
|
23903
|
+
* filter lists).
|
|
23722
23904
|
*/
|
|
23723
23905
|
static ENCODER_BUFFER_SIZE = 8192;
|
|
23724
23906
|
/**
|
|
23725
|
-
*
|
|
23907
|
+
* Length threshold for using a shared buffer for encoding strings.
|
|
23908
|
+
* This temp buffer is needed because we write the short strings in it
|
|
23909
|
+
* (so there is no need to constantly allocate a new buffer).
|
|
23910
|
+
* The reason for dividing ENCODER_BUFFER_SIZE by 4 is to ensure that the encoded string fits in the buffer,
|
|
23911
|
+
* if we also take into account the worst possible case (each character is encoded with 4 bytes).
|
|
23726
23912
|
*/
|
|
23727
23913
|
static SHORT_STRING_THRESHOLD = 2048; // 8192 / 4
|
|
23728
23914
|
/**
|
|
23729
23915
|
* Represents the maximum value that can be written as a 'storage optimized' unsigned integer.
|
|
23730
|
-
* 0x1FFFFFFF means 32 bits minus 3 bits
|
|
23916
|
+
* 0x1FFFFFFF means 29 bits — 32 bits minus 3 bits — because the last bit in each byte is a flag indicating
|
|
23731
23917
|
* if there are more bytes (except for the last byte).
|
|
23732
23918
|
*/
|
|
23733
23919
|
static MAX_OPTIMIZED_UINT = 0x1FFFFFFF;
|
|
@@ -24274,7 +24460,7 @@ class RuleCategorizer {
|
|
|
24274
24460
|
}
|
|
24275
24461
|
}
|
|
24276
24462
|
}
|
|
24277
|
-
const version = "2.0.0
|
|
24463
|
+
const version = "2.0.0";
|
|
24278
24464
|
|
|
24279
24465
|
/**
|
|
24280
24466
|
* @file AGTree version
|