diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c0e9b71..f4ec9ae8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,10 +14,12 @@ The format is based on [Keep a Changelog], and this project adheres to [Semantic ### Fixed -- issue with `TrustedScriptURL` in `prevent-element-src-loading` scriptlet [#514]. +- Incorrectly escaped quotes in `trusted-replace-node-text` scriptlet [#517]. +- `TrustedScriptURL` in `prevent-element-src-loading` scriptlet [#514]. [Unreleased]: https://github.com/AdguardTeam/Scriptlets/compare/v2.2.8...HEAD [#514]: https://github.com/AdguardTeam/Scriptlets/issues/514 +[#517]: https://github.com/AdguardTeam/Scriptlets/issues/517 ## [v2.2.8] - 2025-07-08 diff --git a/src/helpers/injector.ts b/src/helpers/injector.ts index 46bd8ab6..eba85354 100644 --- a/src/helpers/injector.ts +++ b/src/helpers/injector.ts @@ -19,7 +19,9 @@ export async function attachDependencies(scriptlet: Scriptlet | Redirect): Promi try { const depStr = dep.toString(); const result = await minify(depStr, { - compress: true, + compress: { + drop_debugger: false, + }, mangle: { // injection functions should be accessible by the same name // so we preserve their names diff --git a/src/helpers/node-text-utils.ts b/src/helpers/node-text-utils.ts index 56e90bf1..d56c276d 100644 --- a/src/helpers/node-text-utils.ts +++ b/src/helpers/node-text-utils.ts @@ -111,11 +111,7 @@ export const replaceNodeText = ( ): void => { const { textContent } = node; if (textContent) { - // Remove quotes' escapes for cases where scriptlet rule argument has own escaped quotes - // https://github.com/AdguardTeam/Scriptlets/issues/440 - let modifiedText = textContent.replace(pattern, replacement) - .replace(/\\'/g, "'") - .replace(/\\"/g, '"'); + let modifiedText = textContent.replace(pattern, replacement); // For websites that use Trusted Types // https://w3c.github.io/webappsec-trusted-types/dist/spec/ diff --git a/src/scriptlets/trusted-replace-node-text.js b/src/scriptlets/trusted-replace-node-text.js index 29625bea..f047a1a9 100644 --- a/src/scriptlets/trusted-replace-node-text.js +++ b/src/scriptlets/trusted-replace-node-text.js @@ -99,12 +99,26 @@ import { */ /* eslint-enable max-len */ export function trustedReplaceNodeText(source, nodeName, textMatch, pattern, replacement, ...extraArgs) { + // Remove quotes' escapes for cases where scriptlet rule argument has own escaped quotes + // https://github.com/AdguardTeam/Scriptlets/issues/440 + const fixQuotes = (str) => { + if (typeof str !== 'string') { + return str; + } + return str + .replace(/\\'/g, "'") + .replace(/\\"/g, '"'); + }; + + const fixedPattern = fixQuotes(pattern); + const fixedReplacement = fixQuotes(replacement); + const { selector, nodeNameMatch, textContentMatch, patternMatch, - } = parseNodeTextParams(nodeName, textMatch, pattern); + } = parseNodeTextParams(nodeName, textMatch, fixedPattern); const shouldLog = extraArgs.includes('verbose'); @@ -130,7 +144,7 @@ export function trustedReplaceNodeText(source, nodeName, textMatch, pattern, rep logMessage(source, `Original text content: ${originalText}`); } } - replaceNodeText(source, node, patternMatch, replacement); + replaceNodeText(source, node, patternMatch, fixedReplacement); if (shouldLog) { const modifiedText = node.textContent; if (modifiedText) { diff --git a/tests/scriptlets/trusted-replace-node-text.test.js b/tests/scriptlets/trusted-replace-node-text.test.js index 3d115b70..43122004 100644 --- a/tests/scriptlets/trusted-replace-node-text.test.js +++ b/tests/scriptlets/trusted-replace-node-text.test.js @@ -72,7 +72,35 @@ test('simple case', (assert) => { }, 1); }); -test('simple case - check if quotes are correctly escaped', (assert) => { +test('simple case - should not throw error when pattern and replacement are not provided', (assert) => { + const done = assert.async(); + + const nodeName = 'div'; + const textMatch = 'qwerty!'; + + const text = 'qwerty!1'; + const expectedText = 'qwerty!1'; + + const nodeBefore = addNode('div', text); + const safeNodeBefore = addNode('a', text); + + runScriptlet(name, [nodeName, textMatch]); + + const nodeAfter = addNode('div', text); + const safeNodeAfter = addNode('span', text); + setTimeout(() => { + assert.strictEqual(nodeAfter.textContent, expectedText, 'text content should not be modified'); + assert.strictEqual(nodeBefore.textContent, expectedText, 'text content should not be modified'); + + assert.strictEqual(safeNodeAfter.textContent, text, 'non-matched node should not be affected'); + assert.strictEqual(safeNodeBefore.textContent, text, 'non-matched node should not be affected'); + + assert.strictEqual(window.hit, 'FIRED', 'hit function should fire'); + done(); + }, 1); +}); + +test('simple case - check if quotes are correctly escaped in pattern and replacement', (assert) => { const done = assert.async(); const nodeName = 'div'; @@ -101,6 +129,35 @@ test('simple case - check if quotes are correctly escaped', (assert) => { }, 1); }); +test('simple case - check if quotes are correctly escaped in result', (assert) => { + const done = assert.async(); + + const nodeName = 'div'; + const textMatch = 'alert'; + const pattern = 'foo'; + const replacement = 'bar'; + const text = 'alert("\\"foo\\"")'; + const expectedText = 'alert("\\"bar\\"")'; + + const nodeBefore = addNode('div', text); + const safeNodeBefore = addNode('a', text); + + runScriptlet(name, [nodeName, textMatch, pattern, replacement]); + + const nodeAfter = addNode('div', text); + const safeNodeAfter = addNode('span', text); + setTimeout(() => { + assert.strictEqual(nodeAfter.textContent, expectedText, 'text content should be modified'); + assert.strictEqual(nodeBefore.textContent, expectedText, 'text content should be modified'); + + assert.strictEqual(safeNodeAfter.textContent, text, 'non-matched node should not be affected'); + assert.strictEqual(safeNodeBefore.textContent, text, 'non-matched node should not be affected'); + + assert.strictEqual(window.hit, 'FIRED', 'hit function should fire'); + done(); + }, 1); +}); + test('using matchers as regexes', (assert) => { const done = assert.async();