From 5f4949da5cdc854b010a78a54a56e8d9e6695060 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Wr=C3=B3blewski?= Date: Thu, 10 Aug 2023 19:13:36 +0300 Subject: [PATCH] =?UTF-8?q?AG-24527=20Fix=20'json-prune'=20=E2=80=94=20obl?= =?UTF-8?q?igatoryProps=20and=20stack=20trace.=20#345=20#348?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Squashed commit of the following: commit 38f0da2d075bc9e489d053e0f8440833ec412542 Author: Slava Leleka Date: Thu Aug 10 13:22:40 2023 +0300 Update test description commit 4c9356bf5dac6f40ea4b8ba96d092d89f0287c1c Author: Slava Leleka Date: Thu Aug 10 13:22:08 2023 +0300 Update test description commit 15b3d7c2cea55aa95736bbe0f66c828ea4c4b57f Merge: 0e3b35c1 1b81a020 Author: Adam Wróblewski Date: Thu Aug 10 11:08:30 2023 +0200 Merge branch 'master' into fix/AG-24527 commit 0e3b35c19be9c82de70bf409dc51aaaeaa5ace73 Merge: 31d149d4 975ad118 Author: Adam Wróblewski Date: Fri Aug 4 14:49:41 2023 +0200 Merge branch 'master' into fix/AG-24527 commit 31d149d4625e8024f0bc13daafb98c1e5a93e84b Author: Adam Wróblewski Date: Thu Aug 3 16:24:50 2023 +0200 Fix issue with stack and obligatoryProps in json-prune --- CHANGELOG.md | 9 + src/helpers/get-wildcard-property-in-chain.ts | 11 + src/helpers/prune-utils.ts | 29 +- src/scriptlets/json-prune.js | 10 +- tests/scriptlets/json-prune.test.js | 314 +++++++++++++++++- 5 files changed, 359 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 71a4306f..5b3607fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 +## [Unreleased] + +### Fixed + +- issue with `stack` in `json-prune` scriptlet + [#348](https://github.com/AdguardTeam/Scriptlets/issues/348) +- issue with `obligatoryProps` in `json-prune` scriptlet + [#345](https://github.com/AdguardTeam/Scriptlets/issues/345) ## [v1.9.62] - 2023-08-04 @@ -227,6 +235,7 @@ prevent inline `onerror` and match `link` tag [#276](https://github.com/AdguardT - `metrika-yandex-tag` [#254](https://github.com/AdguardTeam/Scriptlets/issues/254) - `googlesyndication-adsbygoogle` [#252](https://github.com/AdguardTeam/Scriptlets/issues/252) +[Unreleased]: https://github.com/AdguardTeam/Scriptlets/compare/v1.9.62...HEAD [v1.9.62]: https://github.com/AdguardTeam/Scriptlets/compare/v1.9.61...v1.9.62 [v1.9.61]: https://github.com/AdguardTeam/Scriptlets/compare/v1.9.58...v1.9.61 [v1.9.58]: https://github.com/AdguardTeam/Scriptlets/compare/v1.9.57...v1.9.58 diff --git a/src/helpers/get-wildcard-property-in-chain.ts b/src/helpers/get-wildcard-property-in-chain.ts index b9404182..0f0805e7 100644 --- a/src/helpers/get-wildcard-property-in-chain.ts +++ b/src/helpers/get-wildcard-property-in-chain.ts @@ -50,6 +50,17 @@ export function getWildcardPropertyInChain( }); } + // If base is an Array check elements in array + // https://github.com/AdguardTeam/Scriptlets/issues/345 + if (Array.isArray(base)) { + base.forEach((key) => { + const nextBase = key; + if (nextBase !== undefined) { + getWildcardPropertyInChain(nextBase, chain, lookThrough, output); + } + }); + } + const nextBase = base[prop]; chain = chain.slice(pos + 1); if (nextBase !== undefined) { diff --git a/src/helpers/prune-utils.ts b/src/helpers/prune-utils.ts index 8862edc5..b62abfa4 100644 --- a/src/helpers/prune-utils.ts +++ b/src/helpers/prune-utils.ts @@ -2,6 +2,7 @@ import { hit } from './hit'; import { getWildcardPropertyInChain } from './get-wildcard-property-in-chain'; import { logMessage } from './log-message'; import { toRegExp } from './string-utils'; +import { matchStackTrace } from './match-stack'; /** * Checks if prunning is required @@ -18,6 +19,7 @@ export function isPruningNeeded( root: ChainBase, prunePaths: string[], requiredPaths: string[], + stack: string, ): boolean | undefined { if (!root) { return false; @@ -31,7 +33,11 @@ export function isPruningNeeded( const matchRegex = toRegExp(requiredPaths.join('')); const shouldLog = matchRegex.test(rootString); if (shouldLog) { - logMessage(source, `${window.location.hostname}\n${JSON.stringify(root, null, 2)}`, true); + logMessage( + source, + `${window.location.hostname}\n${JSON.stringify(root, null, 2)}\nStack trace:\n${new Error().stack}`, + true, + ); if (root && typeof root === 'object') { logMessage(source, root, true, false); } @@ -40,6 +46,11 @@ export function isPruningNeeded( } } + if (stack && !matchStackTrace(stack, new Error().stack || '')) { + shouldProcess = false; + return shouldProcess; + } + const wildcardSymbols = ['.*.', '*.', '.*', '.[].', '[].', '.[]']; for (let i = 0; i < requiredPaths.length; i += 1) { @@ -50,6 +61,13 @@ export function isPruningNeeded( // if the path has wildcard, getPropertyInChain should 'look through' chain props const details = getWildcardPropertyInChain(root, requiredPath, hasWildcard); + // Do not prune if details is an empty Array + // https://github.com/AdguardTeam/Scriptlets/issues/345 + if (!details.length) { + shouldProcess = false; + return shouldProcess; + } + // start value of 'shouldProcess' due to checking below shouldProcess = !hasWildcard; @@ -85,9 +103,14 @@ export const jsonPruner = ( root: ChainBase, prunePaths: string[], requiredPaths: string[], + stack: string, ): ArbitraryObject => { if (prunePaths.length === 0 && requiredPaths.length === 0) { - logMessage(source, `${window.location.hostname}\n${JSON.stringify(root, null, 2)}`, true); + logMessage( + source, + `${window.location.hostname}\n${JSON.stringify(root, null, 2)}\nStack trace:\n${new Error().stack}`, + true, + ); if (root && typeof root === 'object') { logMessage(source, root, true, false); } @@ -95,7 +118,7 @@ export const jsonPruner = ( } try { - if (isPruningNeeded(source, root, prunePaths, requiredPaths) === false) { + if (isPruningNeeded(source, root, prunePaths, requiredPaths, stack) === false) { return root; } diff --git a/src/scriptlets/json-prune.js b/src/scriptlets/json-prune.js index c81f7d7a..81cedf44 100644 --- a/src/scriptlets/json-prune.js +++ b/src/scriptlets/json-prune.js @@ -98,11 +98,7 @@ import { * @added v1.1.0. */ /* eslint-enable max-len */ -export function jsonPrune(source, propsToRemove, requiredInitialProps, stack) { - if (!!stack && !matchStackTrace(stack, new Error().stack)) { - return; - } - +export function jsonPrune(source, propsToRemove, requiredInitialProps, stack = '') { const prunePaths = propsToRemove !== undefined && propsToRemove !== '' ? propsToRemove.split(/ +/) : []; @@ -115,7 +111,7 @@ export function jsonPrune(source, propsToRemove, requiredInitialProps, stack) { // dealing with stringified json in args, which should be parsed. // so we call nativeJSONParse as JSON.parse which is bound to JSON object const root = nativeJSONParse.apply(JSON, args); - return jsonPruner(source, root, prunePaths, requiredPaths); + return jsonPruner(source, root, prunePaths, requiredPaths, stack); }; // JSON.parse mocking @@ -127,7 +123,7 @@ export function jsonPrune(source, propsToRemove, requiredInitialProps, stack) { const responseJsonWrapper = function () { const promise = nativeResponseJson.apply(this); return promise.then((obj) => { - return jsonPruner(source, obj, prunePaths, requiredPaths); + return jsonPruner(source, obj, prunePaths, requiredPaths, stack); }); }; diff --git a/tests/scriptlets/json-prune.test.js b/tests/scriptlets/json-prune.test.js index a2f6c74b..a3024542 100644 --- a/tests/scriptlets/json-prune.test.js +++ b/tests/scriptlets/json-prune.test.js @@ -124,7 +124,7 @@ test('Response.json() mocking -- remove multiple mixed properties', async (asser done(); }); -test('Response.json() mocking -- remove single properties with wildcard', async (assert) => { +test('Response.json() mocking -- remove single properties with * for any property', async (assert) => { const INPUT_JSON_PATH = `${FETCH_OBJECTS_PATH}/test05.json`; const inputRequest = new Request(INPUT_JSON_PATH); @@ -147,7 +147,7 @@ test('Response.json() mocking -- remove single properties with wildcard', async done(); }); -test('Response.json() mocking -- remove single properties with wildcard', async (assert) => { +test('Response.json() mocking -- remove single properties with [] for any array item', async (assert) => { const INPUT_JSON_PATH = `${FETCH_OBJECTS_PATH}/test06.json`; const inputRequest = new Request(INPUT_JSON_PATH); @@ -579,7 +579,7 @@ test('logs 0', (assert) => { assert.expect(2); console.log = (message) => { assert.ok(message.includes(window.location.hostname), 'should log hostname in console'); - assert.ok(message.endsWith('0'), 'should log parameters in console'); + assert.ok(message.includes('localhost\n0\n'), 'should log parameters in console'); nativeConsole(message); }; runScriptlet('json-prune'); @@ -590,9 +590,315 @@ test('logs 10', (assert) => { assert.expect(2); console.log = (message) => { assert.ok(message.includes(window.location.hostname), 'should log hostname in console'); - assert.ok(message.endsWith('10'), 'should log parameters in console'); + assert.ok(message.includes('localhost\n10\n'), 'should log parameters in console'); nativeConsole(message); }; runScriptlet('json-prune'); JSON.parse(10); }); + +test('check if log contains specific stack trace function', (assert) => { + assert.expect(2); + console.log = (message) => { + assert.ok(message.includes(window.location.hostname), 'should log hostname in console'); + assert.ok(message.includes('logStackFunc'), 'should log parameters in console'); + nativeConsole(message); + }; + runScriptlet('json-prune'); + const logStackFunc = () => { + return JSON.parse(999); + }; + logStackFunc(); +}); + +test('removes propsToRemove + stack match function', (assert) => { + const testFuncStack = () => { + return JSON.parse('{"a":1,"b":2,"c":3}'); + }; + runScriptlet('json-prune', 'c', '', 'testFuncStack'); + assert.deepEqual( + testFuncStack(), + { a: 1, b: 2 }, + 'stack match: should remove single propsToRemove', + ); + + const fewPropsStack = () => { + return JSON.parse('{"a":1,"b":2,"c":3}'); + }; + runScriptlet('json-prune', 'b c', '', 'fewPropsStack'); + assert.deepEqual( + fewPropsStack(), + { a: 1 }, + 'stack match: should remove few propsToRemove', + ); + + const requiredInitialPropsStack = () => { + return JSON.parse('{"x": {"a":1, "b":2}}'); + }; + runScriptlet('json-prune', 'x.b', 'x.a', 'requiredInitialPropsStack'); + assert.deepEqual( + requiredInitialPropsStack(), + { x: { a: 1 } }, + 'stack match: should remove propsToRemove if single nested requiredInitialProps is specified', + ); + + const nestedPropsStack = () => { + return JSON.parse('{"nested":{"a":1,"b":2,"c":3}}'); + }; + runScriptlet('json-prune', 'nested.c nested.b', '', 'nestedPropsStack'); + assert.deepEqual( + nestedPropsStack(), + { nested: { a: 1 } }, + 'stack match: should remove multiple nested propsToRemove', + ); +}); + +test('removes propsToRemove + stack match regex', (assert) => { + const regexFuncStack = () => { + return JSON.parse('{"a":1,"b":2,"c":3}'); + }; + runScriptlet('json-prune', 'c', '', '/regex.*Stack/'); + assert.deepEqual( + regexFuncStack(), + { a: 1, b: 2 }, + 'stack match: should remove single propsToRemove', + ); +}); + +test('obligatory props does not exist, do NOT prune', (assert) => { + runScriptlet('json-prune', 'whatever.qwerty advert', 'path.not.exist'); + assert.deepEqual( + JSON.parse('{ "advert": "hello", "foo": { "bar" :1 } }'), + { advert: 'hello', foo: { bar: 1 } }, + 'should not remove any props', + ); +}); + +test('JSON with Array, wildcard [], should remove props', (assert) => { + runScriptlet('json-prune', '[].foo.ads'); + assert.deepEqual( + // eslint-disable-next-line + JSON.parse('[ {"media":"ertu", "ads":"ad_src_0"}, {"foo": { "bar": 1, "ads":"ad_src_1"} }, {"media":"yhiuo", "ads":"ad_src_2"} ]'), + [{ media: 'ertu', ads: 'ad_src_0' }, { foo: { bar: 1 } }, { media: 'yhiuo', ads: 'ad_src_2' }], + 'should remove propsToRemove -- wildcard in propsToRemove for array', + ); +}); + +test('JSON with Array, wildcard [] + obligatory props, should remove props', (assert) => { + runScriptlet('json-prune', '[].content.[].source', 'state.ready'); + + const expectedJson = [ + { + content: [ + { id: 0 }, + { id: 1 }, + ], + state: { + ready: true, + }, + }, + ]; + + assert.deepEqual( + // eslint-disable-next-line + JSON.parse(` + [{"content":[{"id":0,"source":"example.com"},{"id":1,"source":"example.org"}],"state":{"ready":true}}]`), + expectedJson, + 'should remove propsToRemove -- wildcard in propsToRemove for array', + ); +}); + +test('JSON with Array, wildcard [] + obligatory props, should remove props', (assert) => { + runScriptlet('json-prune', '[].content.[].source', 'state.ready'); + + const expectedJson = [ + { + content: [ + { id: 0 }, + { id: 1 }, + ], + state: { + ready: true, + }, + }, + { + content: [ + { id: 0 }, + { id: 1 }, + ], + state: { + ready: true, + }, + }, + ]; + + assert.deepEqual( + // eslint-disable-next-line + JSON.parse(`[{"content":[{"id":0,"source":"example.com"},{"id":1,"source":"example.org"}],"state":{"ready":true}}, {"content":[{"id":0,"source":"example.com"},{"id":1,"source":"example.org"}],"state":{"ready":true}}]`), + expectedJson, + 'should remove propsToRemove -- wildcard in propsToRemove for array', + ); +}); + +test('JSON with Array, wildcard [] + obligatory props, should remove props', (assert) => { + runScriptlet('json-prune', '[].content2.[].source', 'state.ready'); + + const expectedJson = [ + { + content1: [ + { + id: 0, + source: 'example.com', + }, + { + id: 1, + source: 'example.org', + }, + ], + state: { + ready: true, + }, + }, + { + content2: [ + { id: 0 }, + { id: 1 }, + ], + state: { + ready: true, + }, + }, + ]; + + assert.deepEqual( + // eslint-disable-next-line + JSON.parse(`[{"content1":[{"id":0,"source":"example.com"},{"id":1,"source":"example.org"}],"state":{"ready":true}}, {"content2":[{"id":0,"source":"example.com"},{"id":1,"source":"example.org"}],"state":{"ready":true}}]`), + expectedJson, + 'should remove propsToRemove -- wildcard in propsToRemove for array', + ); +}); + +test('JSON with Array, should prune specific, should remove if obligatory props match', (assert) => { + runScriptlet('json-prune', '1.content2.1.source', 'state.ready'); + + const expectedJson = [ + { + content1: [ + { + id: 0, + source: 'example.com', + }, + { + id: 1, + source: 'example.org', + }, + ], + state: { + ready: true, + }, + }, + { + content2: [ + { + id: 0, + source: 'example.com', + }, + { id: 1 }, + ], + state: { + ready: true, + }, + }, + ]; + + assert.deepEqual( + // eslint-disable-next-line + JSON.parse(`[{"content1":[{"id":0,"source":"example.com"},{"id":1,"source":"example.org"}],"state":{"ready":true}}, {"content2":[{"id":0,"source":"example.com"},{"id":1,"source":"example.org"}],"state":{"ready":true}}]`), + expectedJson, + 'should remove propsToRemove -- wildcard in propsToRemove for array', + ); +}); + +test('JSON with Array and empty Array as first element, should remove if obligatory props match', (assert) => { + runScriptlet('json-prune', '2.content2.1.source', 'state.ready'); + + const expectedJson = [ + [], + { + content1: [ + { + id: 0, + source: 'example.com', + }, + { + id: 1, + source: 'example.org', + }, + ], + state: { + ready: true, + }, + }, + { + content2: [ + { + id: 0, + source: 'example.com', + }, + { id: 1 }, + ], + state: { + ready: true, + }, + }, + ]; + + assert.deepEqual( + // eslint-disable-next-line + JSON.parse(`[[],{"content1":[{"id":0,"source":"example.com"},{"id":1,"source":"example.org"}],"state":{"ready":true}}, {"content2":[{"id":0,"source":"example.com"},{"id":1,"source":"example.org"}],"state":{"ready":true}}]`), + expectedJson, + 'should remove propsToRemove -- wildcard in propsToRemove for array', + ); +}); + +test('JSON with Array and empty Array as first element, should remove', (assert) => { + runScriptlet('json-prune', '2.content2.1.source'); + + const expectedJson = [ + [], + { + content1: [ + { + id: 0, + source: 'example.com', + }, + { + id: 1, + source: 'example.org', + }, + ], + state: { + ready: true, + }, + }, + { + content2: [ + { + id: 0, + source: 'example.com', + }, + { id: 1 }, + ], + state: { + ready: true, + }, + }, + ]; + + assert.deepEqual( + // eslint-disable-next-line + JSON.parse(`[[],{"content1":[{"id":0,"source":"example.com"},{"id":1,"source":"example.org"}],"state":{"ready":true}}, {"content2":[{"id":0,"source":"example.com"},{"id":1,"source":"example.org"}],"state":{"ready":true}}]`), + expectedJson, + 'should remove propsToRemove -- wildcard in propsToRemove for array', + ); +});