From 8b106ef601118dc3fb04e4a9a8bc4441bdaa3070 Mon Sep 17 00:00:00 2001 From: binduak Date: Sun, 15 Mar 2026 23:00:43 +0530 Subject: [PATCH 1/3] Bindu | BAH-4360 | Fix Form Scripts saving issue due to html escape from payload --- src/components/Container.jsx | 12 +-- src/helpers/encodingUtils.js | 28 ++++++- src/helpers/scriptRunner.js | 4 +- test/helpers/encodingUtils.spec.js | 127 +++++++++++++++++++++++++++++ 4 files changed, 162 insertions(+), 9 deletions(-) create mode 100644 test/helpers/encodingUtils.spec.js diff --git a/src/components/Container.jsx b/src/components/Container.jsx index cca6f8a5..86cc0c31 100644 --- a/src/components/Container.jsx +++ b/src/components/Container.jsx @@ -11,13 +11,15 @@ import NotificationContainer from '../helpers/Notification'; import Constants from '../constants'; import { IntlProvider } from 'react-intl'; import { executeEventsFromCurrentRecord } from '../helpers/ExecuteEvents'; +import { deepUnescapeStrings } from '../helpers/encodingUtils'; export class Container extends addMoreDecorator(Component) { constructor(props) { super(props); this.childControls = {}; - const { observations, metadata } = this.props; - const controlRecordTree = new ControlRecordTreeBuilder().build(metadata, observations); + const { observations } = this.props; + this.metadata = deepUnescapeStrings(this.props.metadata); + const controlRecordTree = new ControlRecordTreeBuilder().build(this.metadata, observations); this.updatedControlRecordTree = controlRecordTree; this.state = { errors: [], data: controlRecordTree, collapse: props.collapse, notification: {} }; @@ -28,7 +30,7 @@ export class Container extends addMoreDecorator(Component) { this.onEventTrigger = this.onEventTrigger.bind(this); this.showNotification = this.showNotification.bind(this); - const initScript = this.props.metadata.events && this.props.metadata.events.onFormInit; + const initScript = this.metadata.events && this.metadata.events.onFormInit; let updatedTree; try { if (initScript) { @@ -169,8 +171,8 @@ export class Container extends addMoreDecorator(Component) { } render() { - const { metadata: { controls, - name: formName, version: formVersion }, validate, translations, patient } = this.props; + const { controls, name: formName, version: formVersion } = this.metadata; + const { validate, translations, patient } = this.props; const formTranslations = { ...translations.labels, ...translations.concepts }; const patientUuid = patient ? patient.uuid : undefined; const childProps = { diff --git a/src/helpers/encodingUtils.js b/src/helpers/encodingUtils.js index d9d08cf8..d6c0d77c 100644 --- a/src/helpers/encodingUtils.js +++ b/src/helpers/encodingUtils.js @@ -9,6 +9,31 @@ export function utf8ToBase64(str) { return btoa(binaryString); } +export function unescapeHtml(str) { + if (typeof str !== 'string') return str; + return str + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/&/g, '&'); +} + +export function deepUnescapeStrings(obj) { + if (typeof obj === 'string') { + return unescapeHtml(obj); + } + if (Array.isArray(obj)) { + return obj.map(deepUnescapeStrings); + } + if (typeof obj === 'object' && obj !== null) { + const result = {}; + for (const key of Object.keys(obj)) { + result[key] = deepUnescapeStrings(obj[key]); + } + return result; + } + return obj; +} + export function base64ToUtf8(b64) { if (b64 === undefined || b64 === null || b64 === '') { return ''; @@ -22,7 +47,6 @@ export function base64ToUtf8(b64) { const decoder = new TextDecoder(); return decoder.decode(bytes); } catch (e) { - console.error('Error decoding base64 string:', e); - return ''; + throw e; } } diff --git a/src/helpers/scriptRunner.js b/src/helpers/scriptRunner.js index 48f95192..f29a8efb 100644 --- a/src/helpers/scriptRunner.js +++ b/src/helpers/scriptRunner.js @@ -1,6 +1,6 @@ import FormContext from './FormContext'; import { httpInterceptor } from '../helpers/httpInterceptor'; -import { base64ToUtf8 } from './encodingUtils'; +import { base64ToUtf8, unescapeHtml } from './encodingUtils'; export default class ScriptRunner { @@ -22,7 +22,7 @@ export default class ScriptRunner { const formContext = this.formContext; const interceptor = this.interceptor; if (eventJs && interceptor) { - const decodedScript = this.convertToUTF8(eventJs); + const decodedScript = unescapeHtml(this.convertToUTF8(eventJs)); const executiveJs = `(${decodedScript})(formContext,interceptor)`; /* eslint-disable */ eval(executiveJs); diff --git a/test/helpers/encodingUtils.spec.js b/test/helpers/encodingUtils.spec.js new file mode 100644 index 00000000..bd7802a0 --- /dev/null +++ b/test/helpers/encodingUtils.spec.js @@ -0,0 +1,127 @@ +import { expect } from 'chai'; +import { + unescapeHtml, + deepUnescapeStrings, + utf8ToBase64, + base64ToUtf8, +} from 'src/helpers/encodingUtils'; + +describe('encodingUtils', () => { + describe('unescapeHtml', () => { + it('should unescape < to <', () => { + expect(unescapeHtml('a < b')).to.equal('a < b'); + }); + + it('should unescape > to >', () => { + expect(unescapeHtml('a > b')).to.equal('a > b'); + }); + + it('should unescape & to &', () => { + expect(unescapeHtml('a & b')).to.equal('a & b'); + }); + + it('should unescape multiple entities in a single string', () => { + expect(unescapeHtml('if (a < b && c > d)')) + .to.equal('if (a < b && c > d)'); + }); + + it('should return the same string when no entities are present', () => { + expect(unescapeHtml('hello world')).to.equal('hello world'); + }); + + it('should return non-string values as-is', () => { + expect(unescapeHtml(42)).to.equal(42); + expect(unescapeHtml(null)).to.equal(null); + expect(unescapeHtml(undefined)).to.equal(undefined); + }); + }); + + describe('deepUnescapeStrings', () => { + it('should unescape a plain string', () => { + expect(deepUnescapeStrings('a < b')).to.equal('a < b'); + }); + + it('should unescape all string values in an object', () => { + const input = { name: 'test', script: 'if (a < b && c > d) {}' }; + const result = deepUnescapeStrings(input); + expect(result.name).to.equal('test'); + expect(result.script).to.equal('if (a < b && c > d) {}'); + }); + + it('should unescape strings in nested objects', () => { + const input = { + events: { + onFormInit: 'x > 0', + onChange: 'y < 10', + }, + }; + const result = deepUnescapeStrings(input); + expect(result.events.onFormInit).to.equal('x > 0'); + expect(result.events.onChange).to.equal('y < 10'); + }); + + it('should unescape strings inside arrays', () => { + const input = ['a < b', 'c > d']; + const result = deepUnescapeStrings(input); + expect(result).to.deep.equal(['a < b', 'c > d']); + }); + + it('should handle arrays of objects', () => { + const input = [{ label: '<bold>' }, { label: 'plain' }]; + const result = deepUnescapeStrings(input); + expect(result[0].label).to.equal(''); + expect(result[1].label).to.equal('plain'); + }); + + it('should return numbers as-is', () => { + expect(deepUnescapeStrings(42)).to.equal(42); + }); + + it('should return null as-is', () => { + expect(deepUnescapeStrings(null)).to.equal(null); + }); + + it('should return boolean as-is', () => { + expect(deepUnescapeStrings(true)).to.equal(true); + }); + + it('should handle a metadata-like structure with escaped events', () => { + const metadata = { + name: 'Vitals', + controls: [{ id: '1', type: 'label', value: 'Pulse' }], + events: { + onFormInit: 'function(ctx) { if (a < b && c > d) { return; } }', + }, + }; + const result = deepUnescapeStrings(metadata); + expect(result.events.onFormInit) + .to.equal('function(ctx) { if (a < b && c > d) { return; } }'); + expect(result.name).to.equal('Vitals'); + expect(result.controls[0].value).to.equal('Pulse'); + }); + }); + + describe('base64ToUtf8', () => { + it('should return empty string for undefined', () => { + expect(base64ToUtf8(undefined)).to.equal(''); + }); + + it('should return empty string for null', () => { + expect(base64ToUtf8(null)).to.equal(''); + }); + + it('should return empty string for empty string', () => { + expect(base64ToUtf8('')).to.equal(''); + }); + + it('should throw for invalid base64', () => { + expect(() => base64ToUtf8('not-valid-base64!!!')).to.throw(); + }); + + it('should decode a valid base64 string', () => { + const original = 'function(ctx) { return ctx; }'; + const encoded = utf8ToBase64(original); + expect(base64ToUtf8(encoded)).to.equal(original); + }); + }); +}); From cb91bc172db5f1f2ed51778880b266d5fd1f0de3 Mon Sep 17 00:00:00 2001 From: binduak Date: Fri, 20 Mar 2026 17:39:32 +0530 Subject: [PATCH 2/3] Bindu | add html-entities to support all the html entities --- package.json | 1 + src/helpers/encodingUtils.js | 7 +++---- test/helpers/encodingUtils.spec.js | 8 ++++++++ 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 9b8b6c3e..6b61cf7b 100644 --- a/package.json +++ b/package.json @@ -88,6 +88,7 @@ "react-test-renderer": "^16.12.0", "react-textarea-autosize": "^4.0.5", "sinon-as-promised": "^4.0.3", + "html-entities": "^2.6.0", "whatwg-fetch": "^1.0.0" }, "resolutions": { diff --git a/src/helpers/encodingUtils.js b/src/helpers/encodingUtils.js index d6c0d77c..9f2b1a75 100644 --- a/src/helpers/encodingUtils.js +++ b/src/helpers/encodingUtils.js @@ -1,3 +1,5 @@ +import { decode } from 'html-entities'; + export function utf8ToBase64(str) { if (str === undefined || str === null || str === '') { return ''; @@ -11,10 +13,7 @@ export function utf8ToBase64(str) { export function unescapeHtml(str) { if (typeof str !== 'string') return str; - return str - .replace(/</g, '<') - .replace(/>/g, '>') - .replace(/&/g, '&'); + return decode(str); } export function deepUnescapeStrings(obj) { diff --git a/test/helpers/encodingUtils.spec.js b/test/helpers/encodingUtils.spec.js index bd7802a0..22e2f5a1 100644 --- a/test/helpers/encodingUtils.spec.js +++ b/test/helpers/encodingUtils.spec.js @@ -25,6 +25,14 @@ describe('encodingUtils', () => { .to.equal('if (a < b && c > d)'); }); + it('should unescape " to "', () => { + expect(unescapeHtml('a "quoted" b')).to.equal('a "quoted" b'); + }); + + it('should unescape ' to \'', () => { + expect(unescapeHtml('it's')).to.equal("it's"); + }); + it('should return the same string when no entities are present', () => { expect(unescapeHtml('hello world')).to.equal('hello world'); }); From d040632b3548721fbb6f5259c2bceb5c9a4ac991 Mon Sep 17 00:00:00 2001 From: binduak Date: Sat, 21 Mar 2026 05:34:08 +0530 Subject: [PATCH 3/3] Bindu | hadled feedback review comment, removed unescapeHTML for eventJS --- src/helpers/scriptRunner.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/helpers/scriptRunner.js b/src/helpers/scriptRunner.js index f29a8efb..48f95192 100644 --- a/src/helpers/scriptRunner.js +++ b/src/helpers/scriptRunner.js @@ -1,6 +1,6 @@ import FormContext from './FormContext'; import { httpInterceptor } from '../helpers/httpInterceptor'; -import { base64ToUtf8, unescapeHtml } from './encodingUtils'; +import { base64ToUtf8 } from './encodingUtils'; export default class ScriptRunner { @@ -22,7 +22,7 @@ export default class ScriptRunner { const formContext = this.formContext; const interceptor = this.interceptor; if (eventJs && interceptor) { - const decodedScript = unescapeHtml(this.convertToUTF8(eventJs)); + const decodedScript = this.convertToUTF8(eventJs); const executiveJs = `(${decodedScript})(formContext,interceptor)`; /* eslint-disable */ eval(executiveJs);