Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/master' into snyk-upgrade-d47742…
Browse files Browse the repository at this point in the history
…1429c531126d3ac51303cb0350
  • Loading branch information
lane-formio committed Feb 22, 2024
2 parents afaab02 + e63fafc commit 7af8eee
Show file tree
Hide file tree
Showing 30 changed files with 3,980 additions and 2,036 deletions.
6 changes: 6 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- FIO-7786: Fixing Datagrid issue in Settings JSON
- FIO-4905: File upload (multi) - aborting upload always cancels the last one in the list
- FIO-7642: fixed issues where calculated value with allow override is not recalculated after form/component/row values are reset
- FIO-7632: Fixes an issue where HTML tags are added to the HTML5 Select metadata
- FIO-4871: fixed calculated value issues
- FIO 7603: fixed Edit Grid With Empty Rows Not Submitting Form
- FIO-7445: fixed an issue where the interpolated data does not show up on PDF
- FIO-7421: Adds ReCaptcha error messages to the translations config
- FIO-7804: Added PKCE method for OIDC

## 5.0.0-rc.37
### Fixed
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@
"jwt-decode": "^3.1.2",
"lodash": "^4.17.21",
"moment": "^2.29.4",
"moment-timezone": "^0.5.43",
"moment-timezone": "^0.5.44",
"quill": "^2.0.0-dev.3",
"signature_pad": "^4.1.4",
"string-hash": "^1.1.3",
Expand Down
4 changes: 4 additions & 0 deletions src/Webform.js
Original file line number Diff line number Diff line change
Expand Up @@ -613,6 +613,10 @@ export default class Webform extends NestedDataComponent {
if (form && form.properties) {
this.options.properties = form.properties;
}
// Use the sanitize config from the form settings or the global sanitize config if it is not provided in the options
if (!this.options.sanitizeConfig && !this.builderMode) {
this.options.sanitizeConfig = _.get(form, 'settings.sanitizeConfig') || _.get(form, 'globalSettings.sanitizeConfig');
}

if ('schema' in form && compareVersions(form.schema, '1.x') > 0) {
this.ready.then(() => {
Expand Down
244 changes: 243 additions & 1 deletion src/Webform.unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ import calculateValueOnServerForEditGrid from '../test/forms/calculateValueOnSer
import formsWithAllowOverride from '../test/forms/formsWithAllowOverrideComps';
import formWithDeeplyNestedConditionalComps from '../test/forms/formWithDeeplyNestedConditionalComps';
import formWithValidation from '../test/forms/formWithValidation';
import formWithNotAllowedTags from '../test/forms/formWithNotAllowedTags';
import formWithValidateWhenHidden from '../test/forms/formWithValidateWhenHidden';

global.requestAnimationFrame = (cb) => cb();
global.cancelAnimationFrame = () => {};
Expand All @@ -88,6 +90,122 @@ if (_.has(Formio, 'Components.setComponents')) {
describe('Webform tests', function() {
this.retries(3);

it('Should validate hidden and conditionally hidden components when validateWhenHidden is enabled for those components', done => {
const formElement = document.createElement('div');

Formio.createForm(formElement, formWithValidateWhenHidden)
.then(form => {
const errorClasses = ['has-error', 'has-message', form.options.componentErrorClass];
const number1 = form.getComponent('number1');
const number2 = form.getComponent('number2');
const number = form.getComponent('number');
const textField = form.getComponent('textField');
const textArea = form.getComponent('textArea');
const checkbox = form.getComponent('checkbox');

assert.equal(form.errors.length, 0);

number1.setValue(5);
number2.setValue(7);
setTimeout(()=> {
assert.equal(form.errors.length, 1);
assert.equal(!!number.error, true);

errorClasses.forEach(cl => assert.equal(number.element.classList.contains(cl), false, '(1) Should not set error classes for hidden components.'));
number2.setValue(3);

setTimeout(() => {
assert.equal(form.errors.length, 0);
assert.equal(!!number.error, false);
errorClasses.forEach(cl => assert.equal(number.element.classList.contains(cl), false, '(2) Should not set error classes for hidden components.'));

textField.setValue('test');
setTimeout(() => {
assert.equal(form.errors.length, 1);
assert.equal(!!textArea.error, true);
assert.equal(textArea.visible, true);

checkbox.setValue(true);
setTimeout(()=> {
assert.equal(textArea.visible, false);
assert.equal(form.errors.length, 1);
assert.equal(!!textArea.error, true);
errorClasses.forEach(cl => assert.equal(textArea.element.classList.contains(cl), false));

number2.setValue(9);
form.submit();
setTimeout(()=> {
assert.equal(form.errors.length, 2);
assert.equal(!!textArea.error, true);
assert.equal(!!number.error, true);
assert.equal(!!form.alert, true);
assert.equal(form.refs.errorRef.length, 2);
errorClasses.forEach(cl => assert.equal(number.element.classList.contains(cl), false));
errorClasses.forEach(cl => assert.equal(textArea.element.classList.contains(cl), false));

textField.setValue('test test test');
number2.setValue(1);
setTimeout(()=> {
assert.equal(form.errors.length, 0);
assert.equal(!!textArea.error, false);
assert.equal(!!number.error, false);
assert.equal(!!form.alert, false);

done();
}, 300);
}, 300);
}, 300);
}, 300);
}, 300);
}, 300);
})
.catch(done);
});

it('Should not validate hidden and conditionally hidden components when validateWhenHidden is not enabled for those components', done => {
const formElement = document.createElement('div');
const testForm = fastCloneDeep(formWithValidateWhenHidden);

_.each(testForm.components, comp => {
comp.validateWhenHidden = false;
});

Formio.createForm(formElement, testForm)
.then(form => {
const number1 = form.getComponent('number1');
const number2 = form.getComponent('number2');
const number = form.getComponent('number');
const textField = form.getComponent('textField');
const textArea = form.getComponent('textArea');
const checkbox = form.getComponent('checkbox');

assert.equal(form.errors.length, 0);

number1.setValue(5);
number2.setValue(7);
setTimeout(()=> {
assert.equal(form.errors.length, 0);
assert.equal(!!number.error, false);

textField.setValue('test');
setTimeout(() => {
assert.equal(form.errors.length, 1);
assert.equal(!!textArea.error, true);
assert.equal(textArea.visible, true);

checkbox.setValue(true);
setTimeout(()=> {
assert.equal(textArea.visible, false);
assert.equal(form.errors.length, 0);
assert.equal(!!textArea.error, false);
done();
}, 300);
}, 300);
}, 300);
})
.catch(done);
});

it('Should not lose values of conditionally visible components on setValue when server option is passed', function(done) {
const formElement = document.createElement('div');
Formio.createForm(formElement, formWithDeeplyNestedConditionalComps, { server: true }).then((form) => {
Expand Down Expand Up @@ -194,7 +312,6 @@ describe('Webform tests', function() {
Formio.makeRequest = function() {
return new Promise((res, rej) => {
setTimeout(() => {
console.log(8888);
rej(errorText);
}, 50);
});
Expand Down Expand Up @@ -4341,6 +4458,131 @@ describe('Webform tests', function() {
.catch((err) => done(err));
});

describe('Test sanitizeConfig', () => {
it('Should sanitize components using default sanitizeConfig', function(done) {
const formElement = document.createElement('div');
const form = new Webform(formElement);
const testForm = fastCloneDeep(formWithNotAllowedTags);

form.setForm(testForm).then(() => {
const textFieldWithScript = form.getComponent('textFieldWithScript');
const textAreaWithIframe = form.getComponent('textAreaWithIframe');

assert.equal(textFieldWithScript.element?.getElementsByTagName('script').length, 0, 'Should not render srcipt tag');
assert.equal(textAreaWithIframe.element?.getElementsByTagName('iframe').length, 0, 'Should not render iframe tag');

done();
}).catch((err) => done(err));
});

it('Should sanitize components using sanitizeConfig from form settings', function(done) {
const formElement = document.createElement('div');
const form = new Webform(formElement);
const testForm = fastCloneDeep(formWithNotAllowedTags);
testForm.settings.sanitizeConfig = {
addTags: ['iframe', 'script'],
},

form.setForm(testForm).then(() => {
const textFieldWithScript = form.getComponent('textFieldWithScript');
const textAreaWithIframe = form.getComponent('textAreaWithIframe');

assert.equal(textFieldWithScript.element?.getElementsByTagName('script').length, 1, 'Should render srcipt tag');
assert.equal(textAreaWithIframe.element?.getElementsByTagName('iframe').length, 1, 'Should render iframe tag');

done();
}).catch((err) => done(err));
});

it('Should sanitize components using sanitizeConfig from global settings', function(done) {
const formElement = document.createElement('div');
const form = new Webform(formElement);
const testForm = fastCloneDeep(formWithNotAllowedTags);
testForm.globalSettings.sanitizeConfig = {
addTags: ['iframe', 'script'],
},

form.setForm(testForm).then(() => {
const textFieldWithScript = form.getComponent('textFieldWithScript');
const textAreaWithIframe = form.getComponent('textAreaWithIframe');

assert.equal(textFieldWithScript.element?.getElementsByTagName('script').length, 1, 'Should render srcipt tag');
assert.equal(textAreaWithIframe.element?.getElementsByTagName('iframe').length, 1, 'Should render iframe tag');

done();
}).catch((err) => done(err));
});

it('sanitizeConfig from form options must not be overriden by sanitizeConfig from global settings', function(done) {
const formElement = document.createElement('div');
const form = new Webform(formElement, {
sanitizeConfig: {
addTags: ['iframe'],
}
});
const testForm = fastCloneDeep(formWithNotAllowedTags);
testForm.globalSettings.sanitizeConfig = {
addTags: ['script'],
},

form.setForm(testForm).then(() => {
const textFieldWithScript = form.getComponent('textFieldWithScript');
const textAreaWithIframe = form.getComponent('textAreaWithIframe');

assert.equal(textFieldWithScript.element?.getElementsByTagName('script').length, 0, 'Should not render srcipt tag');
assert.equal(textAreaWithIframe.element?.getElementsByTagName('iframe').length, 1, 'Should render iframe tag');

done();
}).catch((err) => done(err));
});

it('sanitizeConfig from form options must not be overriden by sanitizeConfig from form settings', function(done) {
const formElement = document.createElement('div');
const form = new Webform(formElement, {
sanitizeConfig: {
addTags: ['iframe'],
}
});
const testForm = fastCloneDeep(formWithNotAllowedTags);
testForm.settings.sanitizeConfig = {
addTags: ['script'],
},

form.setForm(testForm).then(() => {
const textFieldWithScript = form.getComponent('textFieldWithScript');
const textAreaWithIframe = form.getComponent('textAreaWithIframe');

assert.equal(textFieldWithScript.element?.getElementsByTagName('script').length, 0, 'Should not render srcipt tag');
assert.equal(textAreaWithIframe.element?.getElementsByTagName('iframe').length, 1, 'Should render iframe tag');

done();
}).catch((err) => done(err));
});

it('sanitizeConfig from form settings must not be overriden by sanitizeConfig from global settings', function(done) {
const formElement = document.createElement('div');
const form = new Webform(formElement);
const testForm = fastCloneDeep(formWithNotAllowedTags);
testForm.settings.sanitizeConfig = {
addTags: ['iframe'],
},

testForm.globalSettings.sanitizeConfig = {
addTags: ['script'],
},

form.setForm(testForm).then(() => {
const textFieldWithScript = form.getComponent('textFieldWithScript');
const textAreaWithIframe = form.getComponent('textAreaWithIframe');

assert.equal(textFieldWithScript.element?.getElementsByTagName('script').length, 0, 'Should not render srcipt tag');
assert.equal(textAreaWithIframe.element?.getElementsByTagName('iframe').length, 1, 'Should render iframe tag');

done();
}).catch((err) => done(err));
});
});

for (const formTest of FormTests) {
const useDoneInsteadOfPromise = formTest.useDone;

Expand Down
6 changes: 4 additions & 2 deletions src/WebformBuilder.js
Original file line number Diff line number Diff line change
Expand Up @@ -1138,6 +1138,7 @@ export default class WebformBuilder extends Component {
}

updateComponent(component, changed) {
const sanitizeConfig = _.get(this.webform, 'form.settings.sanitizeConfig') || _.get(this.webform, 'form.globalSettings.sanitizeConfig');
// Update the preview.
if (this.preview) {
this.preview.form = {
Expand All @@ -1149,7 +1150,8 @@ export default class WebformBuilder extends Component {
'autofocus',
'customConditional',
])],
config: this.options.formConfig || {}
config: this.options.formConfig || {},
sanitizeConfig,
};

const fieldsToRemoveDoubleQuotes = ['label', 'tooltip'];
Expand All @@ -1158,7 +1160,7 @@ export default class WebformBuilder extends Component {

const previewElement = this.componentEdit.querySelector('[ref="preview"]');
if (previewElement) {
this.setContent(previewElement, this.preview.render());
this.setContent(previewElement, this.preview.render(), null, sanitizeConfig);
this.preview.attach(previewElement);
}
}
Expand Down
14 changes: 10 additions & 4 deletions src/components/_classes/component/Component.js
Original file line number Diff line number Diff line change
Expand Up @@ -2152,6 +2152,10 @@ export default class Component extends Element {
this.setElementInvalid(this.performInputMapping(element), false);
});
this.setInputWidgetErrorClasses(elements, hasErrors);
// do not set error classes for hidden components
if (!this.visible) {
return;
}

if (hasErrors) {
// Add error classes
Expand Down Expand Up @@ -2853,8 +2857,8 @@ export default class Component extends Element {

/* eslint-disable max-statements */
calculateComponentValue(data, flags, row) {
// Skip value calculation for the component if we don't have entire form data set
if (_.isUndefined(_.get(this, 'root.data'))) {
// Skip value calculation for the component if we don't have entire form data set or in builder mode
if (this.builderMode || _.isUndefined(_.get(this, 'root.data'))) {
return false;
}
// If no calculated value or
Expand Down Expand Up @@ -2924,6 +2928,7 @@ export default class Component extends Element {

// Check to ensure that the calculated value is different than the previously calculated value.
if (previousCalculatedValue && previousChanged && !calculationChanged) {
this.calculatedValue = null;
return false;
}

Expand Down Expand Up @@ -3335,6 +3340,7 @@ export default class Component extends Element {
}

shouldSkipValidation(data, dirty, row) {
const { validateWhenHidden = false } = this.component || {};
const rules = [
// Force valid if component is read-only
() => this.options.readOnly,
Expand All @@ -3343,9 +3349,9 @@ export default class Component extends Element {
// Check to see if we are editing and if so, check component persistence.
() => this.isValueHidden(),
// Force valid if component is hidden.
() => !this.visible,
() => !this.visible && !validateWhenHidden,
// Force valid if component is conditionally hidden.
() => !this.checkCondition(row, data)
() => !this.checkCondition(row, data) && !validateWhenHidden
];

return rules.some(pred => pred());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ export default [
key: 'unique',
input: true
},
{
weight: 100,
type: 'checkbox',
label: 'Validate When Hidden',
tooltip: 'Validates the component when it is hidden/conditionally hidden. Vaildation errors are displayed in the error alert on the form submission.',
key: 'validateWhenHidden',
input: true
},
{
weight: 0,
type: 'select',
Expand Down
Loading

0 comments on commit 7af8eee

Please sign in to comment.