Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
12 changes: 7 additions & 5 deletions src/components/Container.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@
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: {} };
Expand All @@ -28,7 +30,7 @@
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) {
Expand All @@ -37,7 +39,7 @@
updatedTree = updatedTree || this.state.data;
updatedTree = executeEventsFromCurrentRecord(updatedTree, updatedTree, this.props.patient);
} catch (error) {
console.error('Error executing form init script:', error);

Check warning on line 42 in src/components/Container.jsx

View workflow job for this annotation

GitHub Actions / build

Unexpected console statement
}
if (updatedTree) {
this.state.data = updatedTree;
Expand Down Expand Up @@ -169,8 +171,8 @@
}

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 = {
Expand Down
27 changes: 25 additions & 2 deletions src/helpers/encodingUtils.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { decode } from 'html-entities';

export function utf8ToBase64(str) {
if (str === undefined || str === null || str === '') {
return '';
Expand All @@ -9,6 +11,28 @@ export function utf8ToBase64(str) {
return btoa(binaryString);
}

export function unescapeHtml(str) {
if (typeof str !== 'string') return str;
return decode(str);
}

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 '';
Expand All @@ -22,7 +46,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;
}
}
135 changes: 135 additions & 0 deletions test/helpers/encodingUtils.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { expect } from 'chai';
import {
unescapeHtml,
deepUnescapeStrings,
utf8ToBase64,
base64ToUtf8,
} from 'src/helpers/encodingUtils';

describe('encodingUtils', () => {
describe('unescapeHtml', () => {
it('should unescape &lt; to <', () => {
expect(unescapeHtml('a &lt; b')).to.equal('a < b');
});

it('should unescape &gt; to >', () => {
expect(unescapeHtml('a &gt; b')).to.equal('a > b');
});

it('should unescape &amp; to &', () => {
expect(unescapeHtml('a &amp; b')).to.equal('a & b');
});

it('should unescape multiple entities in a single string', () => {
expect(unescapeHtml('if (a &lt; b &amp;&amp; c &gt; d)'))
.to.equal('if (a < b && c > d)');
});

it('should unescape &quot; to "', () => {
expect(unescapeHtml('a &quot;quoted&quot; b')).to.equal('a "quoted" b');
});

it('should unescape &#39; to \'', () => {
expect(unescapeHtml('it&#39;s')).to.equal("it's");
});

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 &lt; b')).to.equal('a < b');
});

it('should unescape all string values in an object', () => {
const input = { name: 'test', script: 'if (a &lt; b &amp;&amp; c &gt; 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 &gt; 0',
onChange: 'y &lt; 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 &lt; b', 'c &gt; d'];
const result = deepUnescapeStrings(input);
expect(result).to.deep.equal(['a < b', 'c > d']);
});

it('should handle arrays of objects', () => {
const input = [{ label: '&lt;bold&gt;' }, { label: 'plain' }];
const result = deepUnescapeStrings(input);
expect(result[0].label).to.equal('<bold>');
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 &lt; b &amp;&amp; c &gt; 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);
});
});
});
Loading