diff --git a/CHANGELOG.md b/CHANGELOG.md index eb7c7809d..45d5ef5eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,12 +11,13 @@ The format is based on [Keep a Changelog], and this project adheres to [Semantic ### Added +- `trusted-suppress-native-method` scriptlet [#383] - `json-prune-fetch-response` scriptlet [#361] - `json-prune-xhr-response` scriptlet [#360] - `href-sanitizer` scriptlet [#327] - `no-protected-audience` scriptlet [#395] - Domain value for setting cookie scriptlets [#389] -- Multiple redirects can be used as scriptlets [#300]: +- Multiple redirects can now be used as scriptlets [#300]: - `amazon-apstag` - `didomi-loader` - `fingerprintjs2` @@ -53,6 +54,7 @@ The format is based on [Keep a Changelog], and this project adheres to [Semantic [#395]: https://github.com/AdguardTeam/Scriptlets/issues/395 [#389]: https://github.com/AdguardTeam/Scriptlets/issues/389 [#388]: https://github.com/AdguardTeam/Scriptlets/issues/388 +[#383]: https://github.com/AdguardTeam/Scriptlets/issues/383 [#377]: https://github.com/AdguardTeam/Scriptlets/issues/377 [#361]: https://github.com/AdguardTeam/Scriptlets/issues/361 [#360]: https://github.com/AdguardTeam/Scriptlets/issues/360 diff --git a/package.json b/package.json index 6cba48b5c..3d978d297 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "@rollup/plugin-json": "^6.0.1", "@rollup/plugin-node-resolve": "^15.0.1", "@rollup/plugin-replace": "^5.0.2", + "@types/jest": "^29.5.12", "@typescript-eslint/eslint-plugin": "^5.52.0", "@typescript-eslint/parser": "^5.52.0", "axios": "^1.2.0", diff --git a/src/helpers/create-on-error-handler.ts b/src/helpers/create-on-error-handler.ts index b027b3d02..47cc9defe 100644 --- a/src/helpers/create-on-error-handler.ts +++ b/src/helpers/create-on-error-handler.ts @@ -1,3 +1,5 @@ +import { randomId } from './random-id'; + /** * Generates function which silents global errors on page generated by scriptlet * If error doesn't belong to our error we transfer it to the native onError handler @@ -5,7 +7,7 @@ * @param rid - unique identifier of scriptlet * @returns window.onerror handler */ -export function createOnErrorHandler(rid: string): OnErrorEventHandler { +export function createOnErrorHandler(rid: string): OnErrorEventHandlerNonNull { // eslint-disable-next-line consistent-return const nativeOnError = window.onerror; return function onError(error, ...args) { @@ -18,3 +20,21 @@ export function createOnErrorHandler(rid: string): OnErrorEventHandler { return false; }; } + +/** + * Silently aborts currently running script + * TODO use this for other abort scriptlets + * + * @returns abort function + */ +export function getAbortFunc() { + const rid = randomId(); + let isErrorHandlerSet = false; + return function abort() { + if (!isErrorHandlerSet) { + window.onerror = createOnErrorHandler(rid); + isErrorHandlerSet = true; + } + throw new ReferenceError(rid); + }; +} diff --git a/src/helpers/index.ts b/src/helpers/index.ts index 91bb2c4f2..4380ed0c7 100644 --- a/src/helpers/index.ts +++ b/src/helpers/index.ts @@ -37,3 +37,4 @@ export * from './random-id'; export * from './throttle'; export * from './shadow-dom-utils'; export * from './node-text-utils'; +export * from './value-matchers'; diff --git a/src/helpers/object-utils.ts b/src/helpers/object-utils.ts index 4734c595c..fdd187588 100644 --- a/src/helpers/object-utils.ts +++ b/src/helpers/object-utils.ts @@ -43,3 +43,13 @@ export function setPropertyAccess( Object.defineProperty(object, property, descriptor); return true; } + +/** + * Checks whether the value is an arbitrary object + * + * @param value arbitrary value + * @returns true, if value is an arbitrary object + */ +export function isArbitraryObject(value: unknown): value is ArbitraryObject { + return value !== null && typeof value === 'object' && !Array.isArray(value) && !(value instanceof RegExp); +} diff --git a/src/helpers/string-utils.ts b/src/helpers/string-utils.ts index 7db6a79dd..8ba9df4b8 100644 --- a/src/helpers/string-utils.ts +++ b/src/helpers/string-utils.ts @@ -436,6 +436,10 @@ export function inferValue(value: string): unknown { return NaN; } + if (value.startsWith('/') && value.endsWith('/')) { + return toRegExp(value); + } + // Number class constructor works 2 times faster than JSON.parse // and wont interpret mixed inputs like '123asd' as parseFloat would const MAX_ALLOWED_NUM = 32767; diff --git a/src/helpers/value-matchers.ts b/src/helpers/value-matchers.ts new file mode 100644 index 000000000..6b61cf1bd --- /dev/null +++ b/src/helpers/value-matchers.ts @@ -0,0 +1,133 @@ +import { isArbitraryObject } from './object-utils'; +import { nativeIsNaN } from './number-utils'; + +/** + * Matches an arbitrary value by matcher value. + * Supported value types and corresponding matchers: + * - string – exact string, part of the string or regexp pattern. Empty string `""` to match an empty string. + * - number, boolean, null, undefined – exact value, + * - object – partial of the object with the values as mentioned above, + * i.e by another object, that includes property names and values to be matched, + * - array – partial of the array with the values to be included in the incoming array, + * without considering the order of values, + * - function – not supported. + * + * @param value arbitrary value + * @param matcher value matcher + * @returns true, if incoming value matches the matcher value + */ +export function isValueMatched(value: unknown, matcher: unknown): boolean { + if (typeof value === 'function') { + return false; + } + + if (nativeIsNaN(value)) { + return nativeIsNaN(matcher); + } + + if ( + value === null + || typeof value === 'undefined' + || typeof value === 'number' + || typeof value === 'boolean' + ) { + return value === matcher; + } + + if (typeof value === 'string') { + if (typeof matcher === 'string' || matcher instanceof RegExp) { + // eslint-disable-next-line @typescript-eslint/no-use-before-define + return isStringMatched(value, matcher); + } + return false; + } + + if (Array.isArray(value) && Array.isArray(matcher)) { + // eslint-disable-next-line @typescript-eslint/no-use-before-define + return isArrayMatched(value, matcher); + } + + if (isArbitraryObject(value) && isArbitraryObject(matcher)) { + // eslint-disable-next-line @typescript-eslint/no-use-before-define + return isObjectMatched(value, matcher); + } + + return false; +} + +/** + * Matches string by substring or regexp pattern. + * + * @param str incoming string + * @param matcher string matcher + * @returns true, if incoming string includes the matcher or matches the regexp pattern + */ +export function isStringMatched(str: string, matcher: string | RegExp): boolean { + if (typeof matcher === 'string') { + if (matcher === '') { + return str === matcher; + } + return str.includes(matcher); + } + + if (matcher instanceof RegExp) { + return matcher.test(str); + } + + return false; +} + +/** + * Matches incoming object by partial of the object, i.e by another object, + * that includes property names and values to be matched. + * + * @param obj incoming object + * @param matcher object matcher + * @returns true, if incoming object includes all properties and corresponding values from the matcher + */ +export function isObjectMatched(obj: ArbitraryObject, matcher: ArbitraryObject): boolean { + const matcherKeys = Object.keys(matcher); + for (let i = 0; i < matcherKeys.length; i += 1) { + const key = matcherKeys[i]; + + const value = obj[key]; + if (!isValueMatched(value, matcher[key])) { + return false; + } + + continue; + } + + return true; +} + +/** + * Matches array by partial of the array with the values to be included in the incoming array, + * without considering the order of values. + * + * @param array incoming array + * @param matcher array matcher + * @returns true, if incoming array includes all values from the matcher + */ +export function isArrayMatched(array: unknown[], matcher: unknown[]): boolean { + if (array.length === 0) { + return matcher.length === 0; + } + + // Empty array matcher matches empty array, which is not the case after the previous check + if (matcher.length === 0) { + return false; + } + + for (let i = 0; i < matcher.length; i += 1) { + const matcherValue = matcher[i]; + const isMatching = array.some((arrItem) => isValueMatched(arrItem, matcherValue)); + if (!isMatching) { + return false; + } + + continue; + } + + return true; +} diff --git a/src/scriptlets/scriptlets-list.js b/src/scriptlets/scriptlets-list.js index 51fc085c1..ab5651323 100644 --- a/src/scriptlets/scriptlets-list.js +++ b/src/scriptlets/scriptlets-list.js @@ -68,6 +68,7 @@ export * from './trusted-create-element'; export * from './href-sanitizer'; export * from './json-prune-fetch-response'; export * from './no-protected-audience'; +export * from './trusted-suppress-native-method'; export * from './json-prune-xhr-response'; // redirects as scriptlets // https://github.com/AdguardTeam/Scriptlets/issues/300 diff --git a/src/scriptlets/trusted-suppress-native-method.ts b/src/scriptlets/trusted-suppress-native-method.ts new file mode 100644 index 000000000..e559ff76b --- /dev/null +++ b/src/scriptlets/trusted-suppress-native-method.ts @@ -0,0 +1,233 @@ +import { + hit, + logMessage, + getPropertyInChain, + inferValue, + isValueMatched, + getAbortFunc, + matchStackTrace, + getErrorMessage, + // following helpers should be imported and injected + // because they are used by helpers above + shouldAbortInlineOrInjectedScript, + getNativeRegexpTest, + toRegExp, + nativeIsNaN, + randomId, + createOnErrorHandler, + isEmptyObject, + isArbitraryObject, + isStringMatched, + isArrayMatched, + isObjectMatched, +} from '../helpers/index'; + +/* eslint-disable max-len */ +/** + * @trustedScriptlet trusted-suppress-native-method + * + * @description + * Prevents a call of a given native method, matching the call by incoming arguments. + * + * ### Syntax + * + * ```text + * example.org#%#//scriptlet('trusted-suppress-native-method', methodPath, signatureStr[, how[, stack]]) + * ``` + * + * + * + * - `methodPath` – required, string path to a native method (joined with `.` if needed). The property must be attached to `window`. + * - `signatureStr` – required, string of `|`-separated argument matchers. + * Supported value types with corresponding matchers: + * + * - string – exact string, part of the string or regexp pattern. Empty string `""` to match an empty string. Regexp patterns inside object matchers are not supported. + * - number, boolean, null, undefined – exact value, + * + * - object – partial of the object with the values as mentioned above, i.e by another object, that includes property names and values to be matched, + * - array – partial of the array with the values to be included in the incoming array, without considering the order of values. + * + * To ignore specific argument, explicitly use whitespace as a matcher, e.g `' | |{"prop":"val"}'` to skip matching first and second arguments. + * + * + * + * - `how` – optional, string, one of the following: + * - `abort` – default, aborts the call by throwing an error, + * - `prevent` – replaces the method call with the call of an empty function. + * - `stack` — optional, string or regular expression that must match the current function call stack trace. + * + * ### Examples + * + * 1. Prevent `localStorage.setItem('test-key', 'test-value')` call matching first argument by regexp pattern and the second one by substring: + * + * ```adblock + * example.org#%#//scriptlet('trusted-suppress-native-method', 'localStorage.setItem', '/key/|"value"', 'prevent') + * ``` + * + * 1. Abort `obj.hasOwnProperty('test')` call matching the first argument: + * + * ```adblock + * example.org#%#//scriptlet('trusted-suppress-native-method', 'Object.prototype.hasOwnProperty', '"test"') + * ``` + * + * 1. Prevent `Node.prototype.appendChild` call on element with the id `test-id` by object matcher: + * + * ```adblock + * example.org#%#//scriptlet('trusted-suppress-native-method', 'Node.prototype.appendChild', '{"id":"str"}', 'prevent') + * ``` + * + * 1. Abort all `document.querySelectorAll` calls with `div` as the first argument: + * + * ```adblock + * example.org#%#//scriptlet('trusted-suppress-native-method', 'Document.prototype.querySelectorAll', '"div"') + * ``` + * + * 1. Abort `Array.prototype.concat([1, 'str', true, null])` calls by matching array argument contents: + * + * ```adblock + * example.org#%#//scriptlet('trusted-suppress-native-method', 'Array.prototype.concat', '[1, "str", true]') + * ``` + * + * 1. Use `stack` argument to match by the call, while also matching the second argument: + * + * + * + * ```adblock + * example.org#%#//scriptlet('trusted-suppress-native-method', 'sessionStorage.setItem', ' |"item-value"', 'abort', 'someFuncName') + * ``` + * + * + * + * @added unknown. + */ +/* eslint-enable max-len */ +export function trustedSuppressNativeMethod( + source: Source, + methodPath: string, + signatureStr: string, + how = 'abort', + stack = '', +) { + if (!methodPath || !signatureStr) { + return; + } + + const IGNORE_ARG_SYMBOL = ' '; + + const suppress = how === 'abort' + ? getAbortFunc() + : () => {}; + + let signatureMatcher: unknown[]; + try { + signatureMatcher = signatureStr.split('|').map((value) => { + return value === IGNORE_ARG_SYMBOL ? value : inferValue(value); + }); + } catch (e) { + logMessage(source, `Could not parse the signature matcher: ${getErrorMessage(e)}`); + return; + } + + /** + * getPropertyInChain's return type `ChainBase` only makes sense + * while traversing the chain, but not to outside receivers. + * + * This is done as the least invasive way to make the typings work, + * compared to @ts-ignore or scattered assertions. + */ + const getPathParts = getPropertyInChain as unknown as (base: Window, chain: string) => { + base: Record; + prop: string; + chain?: string; + }; + + const { base, chain, prop } = getPathParts(window, methodPath); + + // Undefined `chain` indicates successful reaching the end prop. + if (typeof chain !== 'undefined') { + logMessage(source, `Could not reach the end of the prop chain: ${methodPath}`); + return; + } + + const nativeMethod = base[prop]; + if (!nativeMethod || typeof nativeMethod !== 'function') { + logMessage(source, `Could not retrieve the method: ${methodPath}`); + return; + } + + /** + * Matches the incoming arguments with the signature matcher. + * + * @param nativeArguments original arguments of the native method call + * @param matchArguments matcher to match against the native argument + * @returns true, if each of the signature matchers match their corresponding argument. + */ + function matchMethodCall( + nativeArguments: unknown[], + matchArguments: unknown[], + ): boolean { + return matchArguments.every((matcher, i) => { + if (matcher === IGNORE_ARG_SYMBOL) { + return true; + } + + const argument = nativeArguments[i]; + return isValueMatched(argument, matcher); + }); + } + + // This flag allows to prevent infinite loops when trapping props that are used by scriptlet's own code. + let isMatchingSuspended = false; + + function apply(target: Function, thisArg: any, argumentsList: unknown[]) { + if (isMatchingSuspended) { + return Reflect.apply(target, thisArg, argumentsList); + } + + isMatchingSuspended = true; + + if (stack && !matchStackTrace(stack, new Error().stack || '')) { + return Reflect.apply(target, thisArg, argumentsList); + } + const isMatching = matchMethodCall(argumentsList, signatureMatcher); + + isMatchingSuspended = false; + + if (isMatching) { + hit(source); + return suppress(); + } + + return Reflect.apply(target, thisArg, argumentsList); + } + + base[prop] = new Proxy(nativeMethod, { apply }); +} + +trustedSuppressNativeMethod.names = [ + 'trusted-suppress-native-method', +]; + +trustedSuppressNativeMethod.injections = [ + hit, + logMessage, + getPropertyInChain, + inferValue, + isValueMatched, + getAbortFunc, + matchStackTrace, + getErrorMessage, + // following helpers should be imported and injected + // because they are used by helpers above + shouldAbortInlineOrInjectedScript, + getNativeRegexpTest, + toRegExp, + nativeIsNaN, + randomId, + createOnErrorHandler, + isEmptyObject, + isArbitraryObject, + isStringMatched, + isArrayMatched, + isObjectMatched, +]; diff --git a/tests/helpers/string-utils.spec.js b/tests/helpers/string-utils.spec.js index 8898a6f9d..a39bcdade 100644 --- a/tests/helpers/string-utils.spec.js +++ b/tests/helpers/string-utils.spec.js @@ -192,5 +192,20 @@ describe('Test string utils', () => { const actual = 'NaN'; expect(Number.isNaN(inferValue(actual))).toBeTruthy(); }); + + test('too big of a number', () => { + const actual = (32767 + 1).toString(); + expect(() => inferValue(actual)).toThrow('number values bigger than 32767 are not allowed'); + }); + + test('string to regexp', () => { + const actual = '/[a-z]{1,9}/'; + const expected = /[a-z]{1,9}/; + + const res = inferValue(actual); + expect(res).toStrictEqual(expected); + expect(res instanceof RegExp).toBeTruthy(); + expect(res.toString()).toStrictEqual(actual); + }); }); }); diff --git a/tests/helpers/value-matchers.spec.js b/tests/helpers/value-matchers.spec.js new file mode 100644 index 000000000..00b0d5a75 --- /dev/null +++ b/tests/helpers/value-matchers.spec.js @@ -0,0 +1,195 @@ +import { + isStringMatched, + isObjectMatched, + isArrayMatched, + isValueMatched, + noopFunc, +} from '../../src/helpers'; + +describe('isStringMatched', () => { + const STR = 'Hello, World!'; + + test('matching with a substring', () => { + expect(isStringMatched(STR, 'Hello')).toBeTruthy(); + expect(isStringMatched(STR, 'World')).toBeTruthy(); + expect(isStringMatched(STR, 'lo, W')).toBeTruthy(); + + expect(isStringMatched(STR, 'hello')).toBeFalsy(); + expect(isStringMatched(STR, 'world')).toBeFalsy(); + expect(isStringMatched(STR, 'lo, w')).toBeFalsy(); + + expect(isStringMatched(STR, null)).toBeFalsy(); + expect(isStringMatched(STR, undefined)).toBeFalsy(); + expect(isStringMatched(STR, { test: 1 })).toBeFalsy(); + + // Empty string matcher is a special case + expect(isStringMatched('', '')).toBeTruthy(); + expect(isStringMatched(STR, '')).toBeFalsy(); + }); + + test('matching with regexp pattern', () => { + expect(isStringMatched(STR, /Hello/)).toBeTruthy(); + expect(isStringMatched(STR, /World/)).toBeTruthy(); + expect(isStringMatched(STR, /lo, W/)).toBeTruthy(); + + expect(isStringMatched(STR, /hello/)).toBeFalsy(); + expect(isStringMatched(STR, /world/)).toBeFalsy(); + expect(isStringMatched(STR, /lo, w/)).toBeFalsy(); + + // More complex regexp patterns + expect(isStringMatched(STR, /^Hello/)).toBeTruthy(); + expect(isStringMatched(STR, /World!$/)).toBeTruthy(); + expect(isStringMatched(STR, /Hello, World!/)).toBeTruthy(); + expect(isStringMatched(STR, /[a-zA-Z]+, [a-zA-Z]+!/)).toBeTruthy(); + + expect(isStringMatched(STR, /hello/)).toBeFalsy(); + expect(isStringMatched(STR, /World$/)).toBeFalsy(); + expect(isStringMatched(STR, /Hello, World$/)).toBeFalsy(); + }); +}); + +describe('isObjectMatched', () => { + const OBJ = { + str: 'Hello, World!', + num: 42, + bool: true, + nil: null, + undef: undefined, + obj: { test: 1 }, + arr: [1, 2, 3], + + // Special cases + empty: {}, + emptyStr: '', + emptyArr: [], + + }; + + test('simple exact matches', () => { + expect(isObjectMatched(OBJ, { str: 'Hello, World!' })).toBeTruthy(); + expect(isObjectMatched(OBJ, { num: 42 })).toBeTruthy(); + expect(isObjectMatched(OBJ, { bool: true })).toBeTruthy(); + expect(isObjectMatched(OBJ, { nil: null })).toBeTruthy(); + expect(isObjectMatched(OBJ, { undef: undefined })).toBeTruthy(); + }); + + test('simple non-matches', () => { + expect(isObjectMatched(OBJ, { str: 'not-a-substring' })).toBeFalsy(); + expect(isObjectMatched(OBJ, { num: 43 })).toBeFalsy(); + expect(isObjectMatched(OBJ, { bool: false })).toBeFalsy(); + expect(isObjectMatched(OBJ, { nil: undefined })).toBeFalsy(); + expect(isObjectMatched(OBJ, { undef: null })).toBeFalsy(); + }); + + test('test matching string values with regexp patterns', () => { + expect(isObjectMatched(OBJ, { str: /[a-zA-Z]+, [a-zA-Z]+!/ })).toBeTruthy(); + expect(isObjectMatched(OBJ, { str: /hello/ })).toBeFalsy(); + }); + + test('matching with object values', () => { + expect(isObjectMatched(OBJ, { obj: { test: 1 } })).toBeTruthy(); + expect(isObjectMatched(OBJ, { arr: [1] })).toBeTruthy(); + }); + + test('matching special cases', () => { + expect(isObjectMatched(OBJ, { empty: {} })).toBeTruthy(); + expect(isObjectMatched(OBJ, { emptyStr: '' })).toBeTruthy(); + expect(isObjectMatched(OBJ, { emptyArr: [] })).toBeTruthy(); + }); +}); + +describe('isArrayMatched', () => { + const ARR = [1, '2', null, undefined, { test: 1 }, [1, 2, 3]]; + + test('simple exact matches', () => { + expect(isArrayMatched(ARR, [1, '2', null, undefined, { test: 1 }, [1, 2, 3]])).toBeTruthy(); + }); + + test('simple non-matches', () => { + expect(isArrayMatched(ARR, [])).toBeFalsy(); + expect(isArrayMatched(ARR, ['not-present-in-arr', '2', null])).toBeFalsy(); + }); + + test('test matching string values with regexp patterns', () => { + expect(isArrayMatched(ARR, [1, /2/])).toBeTruthy(); + expect(isArrayMatched(ARR, [{ test: 1 }, /3/])).toBeFalsy(); + }); + + test('matching with object values', () => { + expect(isArrayMatched(ARR, [{ test: 1 }])).toBeTruthy(); + expect(isArrayMatched(ARR, [[1, 2, 3]])).toBeTruthy(); + }); + + test('matching special cases', () => { + expect(isArrayMatched(ARR, [])).toBeFalsy(); + }); +}); + +describe('isValueMatched', () => { + test('matching simple values', () => { + expect(isValueMatched('Hello, World!', 'Hello')).toBeTruthy(); + expect(isValueMatched('Hello, World!', /Hello/)).toBeTruthy(); + expect(isValueMatched(42, 42)).toBeTruthy(); + expect(isValueMatched(true, true)).toBeTruthy(); + expect(isValueMatched(null, null)).toBeTruthy(); + expect(isValueMatched(undefined, undefined)).toBeTruthy(); + expect(isValueMatched(NaN, NaN)).toBeTruthy(); + + expect(isValueMatched('Hello, World!', 'hello')).toBeFalsy(); + expect(isValueMatched('Hello, World!', /hello/)).toBeFalsy(); + expect(isValueMatched(42, 43)).toBeFalsy(); + expect(isValueMatched(true, false)).toBeFalsy(); + expect(isValueMatched(null, undefined)).toBeFalsy(); + expect(isValueMatched(undefined, null)).toBeFalsy(); + expect(isValueMatched(NaN, 123)).toBeFalsy(); + expect(isValueMatched(13, NaN)).toBeFalsy(); + + // Function matching is not supported + expect(isValueMatched(noopFunc, noopFunc)).toBeFalsy(); + }); + + test('matching with objects', () => { + const obj = { + num: 1, + str: 'Hello, World!', + array: [1, 2, { test: 'str' }], + nil: null, + undef: undefined, + obj: { test: 1 }, + }; + + expect(isValueMatched(obj, { num: 1 })).toBeTruthy(); + expect(isValueMatched(obj, { str: /Hello/ })).toBeTruthy(); + expect(isValueMatched(obj, { nil: null })).toBeTruthy(); + expect(isValueMatched(obj, { array: [1, { test: 'str' }] })).toBeTruthy(); + expect(isValueMatched(obj, { undef: undefined })).toBeTruthy(); + expect(isValueMatched(obj, { obj: { test: 1 } })).toBeTruthy(); + + expect(isValueMatched(obj, { num: 2 })).toBeFalsy(); + expect(isValueMatched(obj, { str: /hello/ })).toBeFalsy(); + expect(isValueMatched(obj, { array: [1, 2, 3] })).toBeFalsy(); + expect(isValueMatched(obj, { nil: undefined })).toBeFalsy(); + expect(isValueMatched(obj, { undef: null })).toBeFalsy(); + expect(isValueMatched(obj, { obj: { test: 2 } })).toBeFalsy(); + + expect(isValueMatched({}, {})).toBeTruthy(); + expect(isValueMatched({}, { test: 1 })).toBeFalsy(); + }); + + test('matching with arrays', () => { + const arr = [400, undefined, { test: 1 }, null, false, 'not-the-string']; + + expect(isValueMatched(arr, [400, { test: 1 }, null, false, 'not-the-string'])).toBeTruthy(); + expect(isValueMatched(arr, [400, undefined, { test: 1 }, undefined, 'the-string', false])).toBeTruthy(); + expect(isValueMatched(arr, [400, undefined, { test: 1 }, null])).toBeTruthy(); + expect(isValueMatched(arr, [{ test: 1 }, null, false])).toBeTruthy(); + expect(isValueMatched(arr, [{ test: 1 }, undefined, /not/])).toBeTruthy(); + + expect(isValueMatched(arr, [123, 'not-the-string', false, null, { test: 1 }, undefined, 400])).toBeFalsy(); + expect(isValueMatched(arr, [true, 'not-the-string', null, { test: 1 }, undefined, 400])).toBeFalsy(); + expect(isValueMatched(arr, [/the/, false, null, { test: 'another' }, undefined, 400])).toBeFalsy(); + + expect(isValueMatched([], [])).toBeTruthy(); + expect(isValueMatched([], [1])).toBeFalsy(); + }); +}); diff --git a/tests/scriptlets/index.test.js b/tests/scriptlets/index.test.js index 83e15880c..8ad40dcef 100644 --- a/tests/scriptlets/index.test.js +++ b/tests/scriptlets/index.test.js @@ -55,3 +55,4 @@ import './inject-css-in-shadow-dom.test'; import './remove-node-text.test'; import './trusted-replace-node-text.test'; import './trusted-prune-inbound-object.test'; +import './trusted-suppress-native-method.test'; diff --git a/tests/scriptlets/trusted-set-constant.test.js b/tests/scriptlets/trusted-set-constant.test.js index d4ddebf8d..5fc35ab42 100644 --- a/tests/scriptlets/trusted-set-constant.test.js +++ b/tests/scriptlets/trusted-set-constant.test.js @@ -145,7 +145,7 @@ if (!isSupported) { }; // not setting constant to illegalNumber - runScriptletFromTag(illegalProp, 32768); + runScriptletFromTag(illegalProp, '32768'); assert.strictEqual(window[illegalProp], undefined); clearGlobalProps(illegalProp); @@ -231,18 +231,18 @@ if (!isSupported) { test('values with same types are not overwritten, values with different types are overwritten', (assert) => { const property = 'customProperty'; - const firstValue = 10; - const anotherValue = 100; - const anotherTypeValue = true; + const firstValue = '10'; + const anotherValue = '100'; + const anotherTypeValue = 'true'; runScriptletFromTag(property, firstValue); - assert.strictEqual(window[property], firstValue); + assert.strictEqual(window[property], Number(firstValue), 'initial value is set'); addSetPropTag(property, anotherValue); - assert.strictEqual(window[property], firstValue, 'values with same types are not overwritten'); + assert.strictEqual(window[property], Number(firstValue), 'values with same types are not overwritten'); addSetPropTag(property, anotherTypeValue); - assert.strictEqual(window[property], anotherTypeValue, 'values with different types are overwritten'); + assert.strictEqual(window[property], true, 'values with different types are overwritten'); clearGlobalProps(property); }); diff --git a/tests/scriptlets/trusted-suppress-native-method.test.js b/tests/scriptlets/trusted-suppress-native-method.test.js new file mode 100644 index 000000000..a2ecc5b8b --- /dev/null +++ b/tests/scriptlets/trusted-suppress-native-method.test.js @@ -0,0 +1,310 @@ +/* eslint-disable no-underscore-dangle, no-console */ +import { runScriptlet, clearGlobalProps } from '../helpers'; +import { noopFunc } from '../../src/helpers'; + +const { test, module } = QUnit; +const name = 'trusted-suppress-native-method'; + +// eslint-disable-next-line no-unused-vars +const testMatching = (arg1, arg2, arg3) => true; + +// Links to the original methods to restore them after each test +const natives = { + 'sessionStorage.getItem': sessionStorage.getItem, + 'localStorage.getItem': localStorage.getItem, + 'Object.prototype.hasOwnProperty': Object.prototype.hasOwnProperty, + 'Array.isArray': Array.isArray, + 'Node.prototype.appendChild': Node.prototype.appendChild, + 'Document.prototype.querySelectorAll': Document.prototype.querySelectorAll, +}; + +const restoreNativeMethod = (path) => { + const pathChunks = path.split('.'); + + switch (pathChunks.length) { + case 2: + window[pathChunks[0]][pathChunks[1]] = natives[path]; + break; + case 3: + window[pathChunks[0]][pathChunks[1]][pathChunks[2]] = natives[path]; + break; + default: + console.error('Unknown path'); + } +}; + +const beforeEach = () => { + window.testMatching = testMatching; + window.__debug = () => { + window.hit = 'FIRED'; + }; +}; + +const afterEach = () => { + window.testMatching = testMatching; + Object.keys(natives).forEach(restoreNativeMethod); + clearGlobalProps('hit', '__debug'); +}; + +module(name, { beforeEach, afterEach }); + +test('Basic prevention', (assert) => { + let item = window.localStorage.getItem('test-key'); + assert.strictEqual(item, null, 'Item is not set'); + + runScriptlet(name, ['localStorage.setItem', '/key/|"test-value"', 'prevent']); + + window.localStorage.setItem('test-key', 'test-value-1'); + item = window.localStorage.getItem('test-key'); + assert.strictEqual(item, null, 'Call was prevented'); + + assert.strictEqual(window.hit, 'FIRED', 'hit func executed'); +}); + +test('Basic abortion', (assert) => { + runScriptlet(name, ['sessionStorage.getItem', '"test-value"']); + + assert.throws( + () => { + window.sessionStorage.getItem('test-value'); + }, + ' Call was aborted with an error', + ); + + assert.strictEqual(window.hit, 'FIRED', 'hit func executed'); +}); + +test('Preventing specific methods', (assert) => { + const obj = { test: 1 }; + runScriptlet(name, ['Object.prototype.hasOwnProperty', '"test"', 'prevent']); + // eslint-disable-next-line no-prototype-builtins + assert.notOk(obj.hasOwnProperty('test'), 'Call was prevented'); + assert.strictEqual(window.hit, 'FIRED', 'hit func executed'); + clearGlobalProps('hit'); + + // Match html element argument as an object + const preventedElement = document.createElement('div'); + preventedElement.id = 'prevented-id'; + const skippedElement = document.createElement('div'); + skippedElement.id = 'skipped-id'; + + assert.notOk(document.getElementById(preventedElement.id), 'Element 1 is not yet appended to the document'); + assert.notOk(document.getElementById(skippedElement.id), 'Element 2 is not yet appended to the document'); + assert.strictEqual(window.hit, undefined, 'hit should not fire'); + runScriptlet(name, ['Node.prototype.appendChild', `{ "id": "${preventedElement.id}" }`, 'prevent']); + + document.body.appendChild(skippedElement); + assert.ok(document.getElementById(skippedElement.id), 'Unmatched element was successfully appended'); + assert.strictEqual(window.hit, undefined, 'hit should not fire'); + + document.body.appendChild(preventedElement); + assert.notOk(document.getElementById(preventedElement.id), 'Prevented matched element from being appended'); + assert.strictEqual(window.hit, 'FIRED', 'hit func executed'); + clearGlobalProps('hit'); + + // Prevent calls to document.querySelectorAll + let divs = document.querySelectorAll('div'); + assert.ok(divs.length, 'Call was not prevented'); + assert.strictEqual(window.hit, undefined, 'hit should not fire'); + runScriptlet(name, ['Document.prototype.querySelectorAll', '"div"', 'prevent']); + + divs = document.querySelectorAll('div'); + assert.strictEqual(divs, undefined, 'Call was prevented'); + assert.strictEqual(window.hit, 'FIRED', 'hit func executed'); + clearGlobalProps('hit'); +}); + +test('Handling possible infinite recursion when trapping methods which are used by the scriptlet', (assert) => { + runScriptlet(name, ['Array.isArray', '[]', 'prevent']); + + assert.strictEqual(Array.isArray([]), undefined, 'Call was prevented'); + + assert.strictEqual(window.hit, 'FIRED', 'hit func executed'); + clearGlobalProps('hit'); +}); + +test('Match: string by string and regexp', (assert) => { + const stringArgument = 'string-arg1234'; + const noMatchArguments = [400, undefined, { test: 1 }, null, false, noopFunc, 'not-the-string']; + + const stringMatcher = 'ng-ar'; + const regexpMatcher = /arg\d+/; + + runScriptlet(name, ['testMatching', `"${stringMatcher}"|${regexpMatcher}`, 'prevent']); + + // Not preventing unmatched calls with single and multiple arguments + assert.ok(window.testMatching(stringArgument, noMatchArguments[0]), 'Unmatched call was not prevented'); + noMatchArguments.forEach((arg) => assert.ok(window.testMatching(arg), 'Unmatched call was not prevented')); + + assert.strictEqual(window.hit, undefined, 'hit should not fire'); + + assert.notOk(window.testMatching(stringArgument, stringArgument), 'Call was prevented'); + assert.strictEqual(window.hit, 'FIRED', 'hit func executed'); +}); + +test('Match: empty string', (assert) => { + const emptyString = ''; + const noMatchArguments = ['string', 400, undefined, { test: 1 }, null, false, noopFunc, 'not-the-string']; + + runScriptlet(name, ['testMatching', '""|""', 'prevent']); + + // Not preventing unmatched calls with single and multiple arguments + assert.ok(window.testMatching(emptyString, noMatchArguments[0]), 'Unmatched call was not prevented'); + noMatchArguments.forEach((arg) => assert.ok(window.testMatching(arg), 'Unmatched call was not prevented')); + + assert.strictEqual(window.hit, undefined, 'hit should not fire'); + + assert.notOk(window.testMatching(emptyString, emptyString), 'Call was prevented'); + assert.strictEqual(window.hit, 'FIRED', 'hit func executed'); +}); + +test('Match: number by exact number', (assert) => { + const numberArgument = 1234; + const noMatchArguments = ['string', undefined, { test: 1 }, null, false, noopFunc, 400]; + + runScriptlet(name, ['testMatching', `${numberArgument}| `, 'prevent']); + + // Not preventing unmatched calls with single and multiple arguments + assert.ok(window.testMatching(noMatchArguments[0], noMatchArguments[1]), 'Unmatched call was not prevented'); + noMatchArguments.forEach((arg) => assert.ok(window.testMatching(arg), 'Unmatched call was not prevented')); + + assert.strictEqual(window.hit, undefined, 'hit should not fire'); + + assert.notOk(window.testMatching(numberArgument, numberArgument), 'Call was prevented'); + assert.strictEqual(window.hit, 'FIRED', 'hit func executed'); +}); + +test('Match: boolean by exact boolean', (assert) => { + const trueArgument = true; + const falseArgument = false; + const noMatchArguments = ['string', undefined, { test: 1 }, null, false, noopFunc, true]; + + runScriptlet(name, ['testMatching', `${trueArgument}|${falseArgument}`, 'prevent']); + + // Not preventing unmatched calls with single and multiple arguments + assert.ok(window.testMatching(falseArgument, trueArgument), 'Unmatched call was not prevented'); + noMatchArguments.forEach((arg) => assert.ok(window.testMatching(arg), 'Unmatched call was not prevented')); + + assert.strictEqual(window.hit, undefined, 'hit should not fire'); + + assert.notOk(window.testMatching(trueArgument, falseArgument), 'Call was prevented'); + assert.strictEqual(window.hit, 'FIRED', 'hit func executed'); +}); + +test('Match: undefined by undefined', (assert) => { + const undefinedArgument = undefined; + const noMatchArguments = ['string', 400, { test: 1 }, null, false, noopFunc, true]; + + runScriptlet(name, ['testMatching', `${undefinedArgument}`, 'prevent']); + + // Not preventing unmatched calls with single and multiple arguments + assert.ok(window.testMatching(noMatchArguments[0], noMatchArguments[1]), 'Unmatched call was not prevented'); + noMatchArguments.forEach((arg) => assert.ok(window.testMatching(arg), 'Unmatched call was not prevented')); + + assert.strictEqual(window.hit, undefined, 'hit should not fire'); + + assert.notOk(window.testMatching(undefinedArgument), 'Call was prevented'); + assert.strictEqual(window.hit, 'FIRED', 'hit func executed'); +}); + +test('Match: null by null', (assert) => { + const nullArgument = null; + const noMatchArguments = ['string', 400, { test: 1 }, undefined, false, noopFunc, true]; + + runScriptlet(name, ['testMatching', `${nullArgument}`, 'prevent']); + + // Not preventing unmatched calls with single and multiple arguments + assert.ok(window.testMatching(noMatchArguments[0], noMatchArguments[1]), 'Unmatched call was not prevented'); + noMatchArguments.forEach((arg) => assert.ok(window.testMatching(arg), 'Unmatched call was not prevented')); + + assert.strictEqual(window.hit, undefined, 'hit should not fire'); + + assert.notOk(window.testMatching(nullArgument), 'Call was prevented'); + assert.strictEqual(window.hit, 'FIRED', 'hit func executed'); +}); + +test('Match: object by object', (assert) => { + const objectArgument = { + test: 1, prop: 'string-test', a: false, b: null, + }; + const noMatchArguments = ['string', 400, null, undefined, false, noopFunc, true, { c: false }]; + + const objectMatcher1 = JSON.stringify({ test: 1 }); + const objectMatcher2 = JSON.stringify({ prop: 'string-test' }); + + runScriptlet(name, ['testMatching', `${objectMatcher1}|${objectMatcher2}`, 'prevent']); + + // Not preventing unmatched calls with single and multiple arguments + assert.ok(window.testMatching(objectMatcher1, { c: false }), 'Unmatched call was not prevented'); + noMatchArguments.forEach((arg) => assert.ok(window.testMatching(arg), 'Unmatched call was not prevented')); + // Not preventing single-argument call when multiple arguments are expected + assert.ok(window.testMatching(objectArgument), 'Unmatched call was not prevented'); + + assert.strictEqual(window.hit, undefined, 'hit should not fire'); + + assert.notOk(window.testMatching(objectArgument, objectArgument), 'Call was prevented'); + assert.strictEqual(window.hit, 'FIRED', 'hit func executed'); +}); + +test('Match: array by array', (assert) => { + const testObject = { prop: 'name' }; + const testArray = [1, 'string-test', false, null]; + + const arrayArgument = [1, 'string-test', false, null, testObject, testArray]; + const noMatchArray = [1, 'string-test', false, null, testObject]; + const noMatchArguments = ['string', 400, null, undefined, false, noopFunc, true, [1, 'string-test', false]]; + + const arrayMatcher = JSON.stringify(['string-test', null, testObject, testArray]); + + runScriptlet(name, ['testMatching', `${arrayMatcher}| `, 'prevent']); + + // Not preventing unmatched calls with single and multiple arguments + assert.ok(window.testMatching(noMatchArray), 'Unmatched call was not prevented'); + noMatchArguments.forEach((arg) => assert.ok(window.testMatching(arg), 'Unmatched call was not prevented')); + + assert.strictEqual(window.hit, undefined, 'hit should not fire'); + + assert.notOk(window.testMatching(arrayArgument), 'Call was prevented'); + assert.strictEqual(window.hit, 'FIRED', 'hit func executed'); +}); + +test('Match: not by function', (assert) => { + const functionArgument = () => {}; + const noMatchArguments = ['string', 400, null, undefined, false, true, { test: 1 }]; + + runScriptlet(name, ['testMatching', `${functionArgument}`, 'prevent']); + + noMatchArguments.forEach((arg) => { + runScriptlet(name, ['testMatching', `${arg}`, 'prevent']); + + assert.ok(window.testMatching(functionArgument), 'Unmatched call was not prevented'); + + window.testMatching = testMatching; + }); + + assert.strictEqual(window.hit, undefined, 'hit should not fire'); +}); + +test('Match: stack trace', (assert) => { + const stackArg = name; + + runScriptlet(name, [ + 'localStorage.getItem', + '"test"', + 'prevent', + 'not-a-stack', + ]); + + assert.ok(testMatching('test'), 'Ignored call with non-matching stack'); + assert.strictEqual(window.hit, undefined, 'hit should not fire'); + + runScriptlet(name, [ + 'testMatching', + '"test"', + 'prevent', + stackArg, + ]); + + assert.notOk(window.testMatching('test'), 'Call was prevented'); + assert.strictEqual(window.hit, 'FIRED', 'hit func executed'); +}); diff --git a/yarn.lock b/yarn.lock index fd5a7df14..2297523d4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1233,6 +1233,13 @@ dependencies: jest-get-type "^29.4.3" +"@jest/expect-utils@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.7.0.tgz#023efe5d26a8a70f21677d0a1afc0f0a44e3a1c6" + integrity sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA== + dependencies: + jest-get-type "^29.6.3" + "@jest/expect@^29.5.0": version "29.5.0" resolved "https://registry.yarnpkg.com/@jest/expect/-/expect-29.5.0.tgz#80952f5316b23c483fbca4363ce822af79c38fba" @@ -1300,6 +1307,13 @@ dependencies: "@sinclair/typebox" "^0.25.16" +"@jest/schemas@^29.6.3": + version "29.6.3" + resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.6.3.tgz#430b5ce8a4e0044a7e3819663305a7b3091c8e03" + integrity sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA== + dependencies: + "@sinclair/typebox" "^0.27.8" + "@jest/source-map@^29.4.3": version "29.4.3" resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-29.4.3.tgz#ff8d05cbfff875d4a791ab679b4333df47951d20" @@ -1362,6 +1376,18 @@ "@types/yargs" "^17.0.8" chalk "^4.0.0" +"@jest/types@^29.6.3": + version "29.6.3" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.6.3.tgz#1131f8cf634e7e84c5e77bab12f052af585fba59" + integrity sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw== + dependencies: + "@jest/schemas" "^29.6.3" + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^17.0.8" + chalk "^4.0.0" + "@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.2": version "0.3.3" resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098" @@ -1550,6 +1576,11 @@ resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.25.24.tgz#8c7688559979f7079aacaf31aa881c3aa410b718" integrity sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ== +"@sinclair/typebox@^0.27.8": + version "0.27.8" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" + integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA== + "@sinonjs/commons@^1", "@sinonjs/commons@^1.3.0", "@sinonjs/commons@^1.4.0", "@sinonjs/commons@^1.7.0": version "1.8.6" resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.6.tgz#80c516a4dc264c2a69115e7578d62581ff455ed9" @@ -1697,6 +1728,14 @@ dependencies: "@types/istanbul-lib-report" "*" +"@types/jest@^29.5.12": + version "29.5.12" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.12.tgz#7f7dc6eb4cf246d2474ed78744b05d06ce025544" + integrity sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw== + dependencies: + expect "^29.0.0" + pretty-format "^29.0.0" + "@types/jsdom@^20.0.0": version "20.0.1" resolved "https://registry.yarnpkg.com/@types/jsdom/-/jsdom-20.0.1.tgz#07c14bc19bd2f918c1929541cdaacae894744808" @@ -2707,6 +2746,11 @@ diff-sequences@^29.4.3: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.4.3.tgz#9314bc1fabe09267ffeca9cbafc457d8499a13f2" integrity sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA== +diff-sequences@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921" + integrity sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q== + diff@^3.5.0: version "3.5.0" resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" @@ -3263,6 +3307,17 @@ exit@^0.1.2: resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ== +expect@^29.0.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/expect/-/expect-29.7.0.tgz#578874590dcb3214514084c08115d8aee61e11bc" + integrity sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw== + dependencies: + "@jest/expect-utils" "^29.7.0" + jest-get-type "^29.6.3" + jest-matcher-utils "^29.7.0" + jest-message-util "^29.7.0" + jest-util "^29.7.0" + expect@^29.5.0: version "29.5.0" resolved "https://registry.yarnpkg.com/expect/-/expect-29.5.0.tgz#68c0509156cb2a0adb8865d413b137eeaae682f7" @@ -4257,6 +4312,16 @@ jest-diff@^29.5.0: jest-get-type "^29.4.3" pretty-format "^29.5.0" +jest-diff@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.7.0.tgz#017934a66ebb7ecf6f205e84699be10afd70458a" + integrity sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw== + dependencies: + chalk "^4.0.0" + diff-sequences "^29.6.3" + jest-get-type "^29.6.3" + pretty-format "^29.7.0" + jest-docblock@^29.4.3: version "29.4.3" resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-29.4.3.tgz#90505aa89514a1c7dceeac1123df79e414636ea8" @@ -4306,6 +4371,11 @@ jest-get-type@^29.4.3: resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.4.3.tgz#1ab7a5207c995161100b5187159ca82dd48b3dd5" integrity sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg== +jest-get-type@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.6.3.tgz#36f499fdcea197c1045a127319c0481723908fd1" + integrity sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw== + jest-haste-map@^29.5.0: version "29.5.0" resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-29.5.0.tgz#69bd67dc9012d6e2723f20a945099e972b2e94de" @@ -4343,6 +4413,16 @@ jest-matcher-utils@^29.5.0: jest-get-type "^29.4.3" pretty-format "^29.5.0" +jest-matcher-utils@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz#ae8fec79ff249fd592ce80e3ee474e83a6c44f12" + integrity sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g== + dependencies: + chalk "^4.0.0" + jest-diff "^29.7.0" + jest-get-type "^29.6.3" + pretty-format "^29.7.0" + jest-message-util@^29.5.0: version "29.5.0" resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.5.0.tgz#1f776cac3aca332ab8dd2e3b41625435085c900e" @@ -4358,6 +4438,21 @@ jest-message-util@^29.5.0: slash "^3.0.0" stack-utils "^2.0.3" +jest-message-util@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.7.0.tgz#8bc392e204e95dfe7564abbe72a404e28e51f7f3" + integrity sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w== + dependencies: + "@babel/code-frame" "^7.12.13" + "@jest/types" "^29.6.3" + "@types/stack-utils" "^2.0.0" + chalk "^4.0.0" + graceful-fs "^4.2.9" + micromatch "^4.0.4" + pretty-format "^29.7.0" + slash "^3.0.0" + stack-utils "^2.0.3" + jest-mock@^29.5.0: version "29.5.0" resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.5.0.tgz#26e2172bcc71d8b0195081ff1f146ac7e1518aed" @@ -4496,6 +4591,18 @@ jest-util@^29.5.0: graceful-fs "^4.2.9" picomatch "^2.2.3" +jest-util@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.7.0.tgz#23c2b62bfb22be82b44de98055802ff3710fc0bc" + integrity sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA== + dependencies: + "@jest/types" "^29.6.3" + "@types/node" "*" + chalk "^4.0.0" + ci-info "^3.2.0" + graceful-fs "^4.2.9" + picomatch "^2.2.3" + jest-validate@^29.5.0: version "29.5.0" resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-29.5.0.tgz#8e5a8f36178d40e47138dc00866a5f3bd9916ffc" @@ -5530,6 +5637,15 @@ prelude-ls@~1.1.2: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w== +pretty-format@^29.0.0, pretty-format@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.7.0.tgz#ca42c758310f365bfa71a0bda0a807160b776812" + integrity sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ== + dependencies: + "@jest/schemas" "^29.6.3" + ansi-styles "^5.0.0" + react-is "^18.0.0" + pretty-format@^29.5.0: version "29.5.0" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.5.0.tgz#283134e74f70e2e3e7229336de0e4fce94ccde5a"