From 9ca4a781263fddb0b29bfdb41ca4c80bf001b28f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Wr=C3=B3blewski?= Date: Fri, 7 Jul 2023 14:27:38 +0300 Subject: [PATCH] =?UTF-8?q?AG-21081=20Improve=20'trusted-replace-fetch-res?= =?UTF-8?q?ponse'/'trusted-replace-xhr-response'=20=E2=80=94=20add=20abili?= =?UTF-8?q?ty=20to=20replace=20all=20matched=20content.=20#303?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Squashed commit of the following: commit ea24d90a13a979888a3663cd47a2111b535904ee Merge: 1d3046c8 10e4c544 Author: Adam Wróblewski Date: Fri Jul 7 13:16:11 2023 +0200 Merge branch 'master' into fix/AG-21081 commit 1d3046c8392ea6e74c971532e1bd0777915a15e8 Merge: 59be7390 d6713c7a Author: Adam Wróblewski Date: Fri Jul 7 13:06:30 2023 +0200 Merge branch 'master' into fix/AG-21081 commit 59be73902a84575dc2c65e0bca2809d98a2ad9ab Author: Slava Leleka Date: Fri Jul 7 12:28:00 2023 +0300 Update changelog commit 71c74b50ed83a81ac8ead16a40f1b1f050e1bd57 Merge: e2888344 c647f9a7 Author: Adam Wróblewski Date: Fri Jul 7 08:55:44 2023 +0200 Merge branch 'master' into fix/AG-21081 commit e2888344483b8ccc596595c5962ba5e640c54342 Merge: 18cfaca3 2b35bd53 Author: Adam Wróblewski Date: Thu Jun 29 14:19:55 2023 +0200 Merge branch 'master' into fix/AG-21081 commit 18cfaca38c5b5024be7994ccad377eca8bab2edd Merge: 9395fa5f c6b7edac Author: Adam Wróblewski Date: Thu Jun 29 12:27:45 2023 +0200 Merge branch 'master' into fix/AG-21081 commit 9395fa5fd854c4a22db3e3b6a63143779ad887ac Author: Adam Wróblewski Date: Thu Jun 29 10:23:20 2023 +0200 Rename hasRegExpFlags to getRegExpFlags Update JSDoc Get rid of unnecessary flagsStr commit 8f02390dae6947c9ee07b5dffada87e908e3db99 Author: Adam Wróblewski Date: Wed Jun 28 11:03:32 2023 +0200 Do not return boolean in hasRegExpFlags Use isValidRegExpFlag instead of match method commit b26f700a8b4cf8004c799381e0b8754cdb93a8ff Author: Adam Wróblewski Date: Tue Jun 27 15:14:17 2023 +0200 Update JSDoc Check only valid flags commit 23c7526fbaa34754b232a49886ae3d85cdfb7abf Merge: c5114da0 97e7764b Author: Adam Wróblewski Date: Tue Jun 27 11:52:50 2023 +0200 Merge branch 'master' into fix/AG-21081 commit c5114da072210a6c171d9b5d45cd6337eafb7a26 Author: Adam Wróblewski Date: Tue Jun 27 10:08:04 2023 +0200 Add JSDoc comments Update toRegExp description Rename regExpHasFlags to hasRegExpFlags Refactor the statement commit 0e46a06c454a5f138bc7fced5459d817a6be83dd Author: Slava Leleka Date: Mon Jun 26 11:53:50 2023 +0300 Update changelog commit 23640990b927ad13bc75a6636327fd58f5f3020e Author: Adam Wróblewski Date: Mon Jun 26 09:09:09 2023 +0200 Add ability to use regexp flags in toRegExp helper --- CHANGELOG.md | 5 ++ src/helpers/string-utils.ts | 56 +++++++++++++-- tests/helpers/string-utils.spec.js | 68 ++++++++++++++++++- .../trusted-replace-fetch-response.test.js | 24 +++++++ .../trusted-replace-xhr-response.test.js | 28 ++++++++ 5 files changed, 176 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5846aa39..c7bb87cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- ability to use flags in regular expression scriptlet parameters + [#303](https://github.com/AdguardTeam/Scriptlets/issues/303) + ### Fixed - issue with printing unnecessary logs to the console in `log-addEventListener` scriptlet diff --git a/src/helpers/string-utils.ts b/src/helpers/string-utils.ts index bb82ba19..7c5b050d 100644 --- a/src/helpers/string-utils.ts +++ b/src/helpers/string-utils.ts @@ -46,12 +46,12 @@ export const replaceAll = ( export const escapeRegExp = (str: string): string => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); /** - * Converts string to the regexp + * Converts string to the regexp, + * if string contains valid regexp flags it will be converted to regexp with flags * TODO think about nested dependencies, but be careful with dependency loops * * @param input literal string or regexp pattern; defaults to '' (empty string) * @returns regular expression; defaults to /.?/ - * @throws Throw an error for invalid regex pattern */ export const toRegExp = (input: RawStrPattern = ''): RegExp => { const DEFAULT_VALUE = '.?'; @@ -59,8 +59,56 @@ export const toRegExp = (input: RawStrPattern = ''): RegExp => { if (input === '') { return new RegExp(DEFAULT_VALUE); } - if (input[0] === FORWARD_SLASH && input[input.length - 1] === FORWARD_SLASH) { - return new RegExp(input.slice(1, -1)); + + const delimiterIndex = input.lastIndexOf(FORWARD_SLASH); + const flagsPart = input.substring(delimiterIndex + 1); + const regExpPart = input.substring(0, delimiterIndex + 1); + + /** + * Checks whether the string is a valid regexp flag + * + * @param flag string + * @returns True if regexp flag is valid, otherwise false. + */ + const isValidRegExpFlag = (flag: string): boolean => { + if (!flag) { + return false; + } + try { + // eslint-disable-next-line no-new + new RegExp('', flag); + return true; + } catch (ex) { + return false; + } + }; + + /** + * Checks whether the text string contains valid regexp flags, + * and returns `flagsStr` if valid, otherwise empty string. + * + * @param regExpStr string + * @param flagsStr string + * @returns `flagsStr` if it is valid, otherwise empty string. + */ + const getRegExpFlags = (regExpStr: string, flagsStr: string): string => { + if ( + regExpStr.startsWith(FORWARD_SLASH) + && regExpStr.endsWith(FORWARD_SLASH) + // Not a correct regex if ends with '\\/' + && !regExpStr.endsWith('\\/') + && isValidRegExpFlag(flagsStr) + ) { + return flagsStr; + } + return ''; + }; + + const flags = getRegExpFlags(regExpPart, flagsPart); + + if ((input.startsWith(FORWARD_SLASH) && input.endsWith(FORWARD_SLASH)) || flags) { + const regExpInput = flags ? regExpPart : input; + return new RegExp(regExpInput.slice(1, -1), flags); } const escaped = input diff --git a/tests/helpers/string-utils.spec.js b/tests/helpers/string-utils.spec.js index ff5ef752..8898a6f9 100644 --- a/tests/helpers/string-utils.spec.js +++ b/tests/helpers/string-utils.spec.js @@ -1,6 +1,6 @@ import { toRegExp, inferValue } from '../../src/helpers'; -describe('Test inferValue', () => { +describe('Test string utils', () => { describe('Test toRegExp for valid inputs', () => { const DEFAULT_VALUE = '.?'; const defaultRegexp = new RegExp(DEFAULT_VALUE); @@ -28,6 +28,72 @@ describe('Test inferValue', () => { }); }); + describe('Test toRegExp with flag', () => { + const DEFAULT_VALUE = '.?'; + const defaultRegexp = new RegExp(DEFAULT_VALUE); + + const testCases = [ + { + actual: '/qwerty/g', + expected: /qwerty/g, + }, + { + actual: '/[a-z]{1,9}/gm', + expected: /[a-z]{1,9}/gm, + }, + { + actual: '', + expected: defaultRegexp, + }, + { + actual: undefined, + expected: defaultRegexp, + }, + ]; + test.each(testCases)('"$actual"', ({ actual, expected }) => { + expect(toRegExp(actual)).toStrictEqual(expected); + }); + }); + + describe('Test toRegExp with not a valid flag', () => { + const DEFAULT_VALUE = '.?'; + const defaultRegexp = new RegExp(DEFAULT_VALUE); + + const testCases = [ + { + actual: 'g', + expected: /g/, + }, + { + actual: 'qwerty/g', + expected: /qwerty\/g/, + }, + { + actual: '/asdf/gmtest', + expected: /\/asdf\/gmtest/, + }, + { + actual: '/qwert/ggm', + expected: /\/qwert\/ggm/, + }, + { + actual: '/test\\/g', + expected: /\/test\\\/g/, + }, + { + actual: '', + expected: defaultRegexp, + }, + { + actual: undefined, + expected: defaultRegexp, + }, + ]; + test.each(testCases)('"$actual"', ({ actual, expected }) => { + expect(toRegExp(actual)).toStrictEqual(expected); + }); + }); + describe('Test toRegExp for invalid inputs', () => { const invalidRegexpPatterns = [ '/\\/', diff --git a/tests/scriptlets/trusted-replace-fetch-response.test.js b/tests/scriptlets/trusted-replace-fetch-response.test.js index 1376f68d..aa63427d 100644 --- a/tests/scriptlets/trusted-replace-fetch-response.test.js +++ b/tests/scriptlets/trusted-replace-fetch-response.test.js @@ -112,6 +112,30 @@ if (!isSupported) { done(); }); + test('Match all requests, replace by regex with flag', async (assert) => { + const INPUT_JSON_PATH = `${FETCH_OBJECTS_PATH}/test03.json`; + const TEST_METHOD = 'GET'; + const init = { + method: TEST_METHOD, + }; + + const done = assert.async(); + + const PATTERN = '/inner/g'; + const REPLACEMENT = 'qwerty'; + runScriptlet(name, [PATTERN, REPLACEMENT]); + + const response = await fetch(INPUT_JSON_PATH, init); + const actualJson = await response.json(); + + const textContent = JSON.stringify(actualJson); + + assert.strictEqual(window.hit, 'FIRED', 'hit function fired'); + assert.notOk(textContent.includes(PATTERN), 'Pattern is removed'); + assert.ok(textContent.includes(REPLACEMENT), 'New content is set'); + done(); + }); + test('Match all requests, replace multiline content', async (assert) => { const INPUT_JSON_PATH = `${FETCH_OBJECTS_PATH}/empty.html`; const TEST_METHOD = 'GET'; diff --git a/tests/scriptlets/trusted-replace-xhr-response.test.js b/tests/scriptlets/trusted-replace-xhr-response.test.js index 191952a5..770f9a2a 100644 --- a/tests/scriptlets/trusted-replace-xhr-response.test.js +++ b/tests/scriptlets/trusted-replace-xhr-response.test.js @@ -113,6 +113,34 @@ if (isSupported) { xhr.send(); }); + test('Matched, regex pattern with flag', async (assert) => { + const METHOD = 'GET'; + const URL = `${FETCH_OBJECTS_PATH}/test03.json`; + + const PATTERN = /inner/g; + const REPLACEMENT = 'qwerty'; + const MATCH_DATA = [`${PATTERN}`, REPLACEMENT, `${URL} method:${METHOD}`]; + + runScriptlet(name, MATCH_DATA); + + const done = assert.async(); + + const xhr = new XMLHttpRequest(); + xhr.open(METHOD, URL); + xhr.onload = () => { + assert.strictEqual(xhr.readyState, 4, 'Response done'); + assert.ok(xhr.response.includes(REPLACEMENT) && !PATTERN.test(xhr.response), 'Response has been modified'); + assert.ok( + xhr.responseText.includes(REPLACEMENT) && !PATTERN.test(xhr.responseText), + 'Response text has been modified', + ); + + assert.strictEqual(window.hit, 'FIRED', 'hit function fired'); + done(); + }; + xhr.send(); + }); + test('Matched, replaces multiline content', async (assert) => { const METHOD = 'GET'; const URL = `${FETCH_OBJECTS_PATH}/empty.html`;