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`;