From bf49cab67818552df3252daef4667757243a2b20 Mon Sep 17 00:00:00 2001 From: Steve Boyd Date: Wed, 31 May 2023 17:33:35 +1200 Subject: [PATCH 1/2] FIX Prevent infinite recursion when field display rules are co-dependent --- code/Model/EditableFormField.php | 24 ++++++++++- tests/php/Model/EditableFormFieldTest.php | 51 +++++++++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/code/Model/EditableFormField.php b/code/Model/EditableFormField.php index 4cd748d..4f60370 100755 --- a/code/Model/EditableFormField.php +++ b/code/Model/EditableFormField.php @@ -187,6 +187,11 @@ class EditableFormField extends DataObject private static $cascade_duplicates = false; + /** + * This is protected rather that private so that it's unit testable + */ + protected static $isDisplayedRecursionProtection = []; + /** * @var bool */ @@ -1009,6 +1014,20 @@ class EditableFormField extends DataObject return (count($result['selectors'] ?? [])) ? $result : null; } + /** + * Used to prevent infinite recursion when checking a CMS user has setup two or more fields to have + * their display rules dependent on one another + * + * There will be several thousand calls to isDisplayed before memory is likely to be hit, so 100 + * calls is a reasonable limit that ensures that this doesn't prevent legit use cases from being + * identified as recursion + */ + private function checkIsDisplayedRecursionProtection(): bool + { + $count = count(array_filter(static::$isDisplayedRecursionProtection, fn($id) => $id === $this->ID)); + return $count < 100; + } + /** * Check if this EditableFormField is displayed based on its DisplayRules and the provided data. * @param array $data @@ -1016,6 +1035,7 @@ class EditableFormField extends DataObject */ public function isDisplayed(array $data) { + static::$isDisplayedRecursionProtection[] = $this->ID; $displayRules = $this->DisplayRules(); if ($displayRules->count() === 0) { @@ -1033,7 +1053,9 @@ class EditableFormField extends DataObject $controllingField = $rule->ConditionField(); // recursively check - if any of the dependant fields are hidden, assume the rule can not be satisfied - $ruleSatisfied = $controllingField->isDisplayed($data) && $rule->validateAgainstFormData($data); + $ruleSatisfied = $this->checkIsDisplayedRecursionProtection() + && $controllingField->isDisplayed($data) + && $rule->validateAgainstFormData($data); if ($conjunction === '||' && $ruleSatisfied) { $conditionsSatisfied = true; diff --git a/tests/php/Model/EditableFormFieldTest.php b/tests/php/Model/EditableFormFieldTest.php index 3ebe75d..d2be2b7 100644 --- a/tests/php/Model/EditableFormFieldTest.php +++ b/tests/php/Model/EditableFormFieldTest.php @@ -16,6 +16,7 @@ use SilverStripe\UserForms\Model\EditableFormField\EditableRadioField; use SilverStripe\UserForms\Model\EditableFormField\EditableTextField; use SilverStripe\UserForms\Model\UserDefinedForm; use SilverStripe\Dev\Deprecation; +use SilverStripe\UserForms\Model\EditableCustomRule; /** * @package userforms @@ -358,4 +359,54 @@ class EditableFormFieldTest extends FunctionalTest $updatedField = EditableFormField::get()->byId($fieldId); $this->assertFalse((bool)$updatedField->Required); } + + public function testRecursionProtection() + { + $radioOne = EditableRadioField::create(); + $radioOneID = $radioOne->write(); + $optionOneOne = EditableOption::create(); + $optionOneOne->Value = 'yes'; + $optionOneOne->ParentID = $radioOneID; + $optionOneTwo = EditableOption::create(); + $optionOneTwo->Value = 'no'; + $optionOneTwo->ParentID = $radioOneID; + + $radioTwo = EditableRadioField::create(); + $radioTwoID = $radioTwo->write(); + $optionTwoOne = EditableOption::create(); + $optionTwoOne->Value = 'yes'; + $optionTwoOne->ParentID = $radioOneID; + $optionTwoTwo = EditableOption::create(); + $optionTwoTwo->Value = 'no'; + $optionTwoTwo->ParentID = $radioTwoID; + + $conditionOne = EditableCustomRule::create(); + $conditionOne->ParentID = $radioOneID; + $conditionOne->ConditionFieldID = $radioTwoID; + $conditionOne->ConditionOption = 'HasValue'; + $conditionOne->FieldValue = 'yes'; + $conditionOne->write(); + $radioOne->DisplayRules()->add($conditionOne); + + $conditionTwo = EditableCustomRule::create(); + $conditionTwo->ParentID = $radioTwoID; + $conditionTwo->ConditionFieldID = $radioOneID; + $conditionTwo->ConditionOption = 'IsNotBlank'; + $conditionTwo->write(); + $radioTwo->DisplayRules()->add($conditionTwo); + + $testField = new class extends EditableFormField + { + public function countIsDisplayedRecursionProtection(int $fieldID) + { + return count(array_filter(static::$isDisplayedRecursionProtection, function ($id) use ($fieldID) { + return $id === $fieldID; + })); + } + }; + + $this->assertSame(0, $testField->countIsDisplayedRecursionProtection($radioOneID)); + $radioOne->isDisplayed([]); + $this->assertSame(100, $testField->countIsDisplayedRecursionProtection($radioOneID)); + } } From 0dfd2990dde1cd5ee474d02d248d38b39ec1f582 Mon Sep 17 00:00:00 2001 From: Michal Kleiner Date: Wed, 7 Jun 2023 15:27:20 +1200 Subject: [PATCH 2/2] Use window.ss.config provided adminUrl (#1211) Co-authored-by: Michal Kleiner --- client/dist/js/userforms-cms.js | 2 +- client/src/bundles/ConfirmFolder.js | 6 ++---- code/Control/UserDefinedFormAdmin.php | 3 +-- code/Extension/UserFormFieldEditorExtension.php | 3 +-- 4 files changed, 5 insertions(+), 9 deletions(-) diff --git a/client/dist/js/userforms-cms.js b/client/dist/js/userforms-cms.js index 3e2a5fd..ba5a251 100644 --- a/client/dist/js/userforms-cms.js +++ b/client/dist/js/userforms-cms.js @@ -1 +1 @@ -!function(e){function o(t){if(r[t])return r[t].exports;var n=r[t]={i:t,l:!1,exports:{}};return e[t].call(n.exports,n,n.exports,o),n.l=!0,n.exports}var r={};o.m=e,o.c=r,o.i=function(e){return e},o.d=function(e,r,t){o.o(e,r)||Object.defineProperty(e,r,{configurable:!1,enumerable:!0,get:t})},o.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return o.d(r,"a",r),r},o.o=function(e,o){return Object.prototype.hasOwnProperty.call(e,o)},o.p="",o(o.s="./client/src/bundles/bundle-cms.js")}({"./client/src/bundles/ConfirmFolder.js":function(e,o,r){"use strict";function t(e){return e&&e.__esModule?e:{default:e}}var n=Object.assign||function(e){for(var o=1;o');var r=e(this).closest("tr").data("id"),t=e(this).closest(".uf-field-editor").data("adminUrl");o.data("id",r),o.data("adminUrl",t),e("body").append(o),o.open()}}}),e("#confirm-folder__dialog-wrapper").entwine({onunmatch:function(){this._clearModal()},open:function(){this._renderModal(!0)},close:function(o){if(!o){var r=e("#confirm-folder__dialog-wrapper").data("id");e(".ss-gridfield-item[data-id='"+r+"'] .dropdown.editable-column-field.form-group--no-label[data-folderconfirmed='0']").val("SilverStripe\\UserForms\\Model\\EditableFormField\\EditableTextField")}this._renderModal(!1)},_renderModal:function(o){var r=this,t=function(){return r._handleHideModal.apply(r,arguments)},i=function(){return r._handleSubmitModal.apply(r,arguments)},l=d.default._t("UserForms.FILE_CONFIRMATION_TITLE","Select file upload folder"),s=e(this).data("id"),a=e(this).data("adminUrl"),f=F.default.parse(a+"user-forms/confirmfolderformschema"),m=h.default.parse(f.query);m.ID=s;var p=F.default.format(n({},f,{search:h.default.stringify(m)}));u.default.render(c.default.createElement(g,{title:l,isOpen:o,onSubmit:i,onClosed:t,schemaUrl:p,bodyClassName:"modal__dialog",className:"confirm-folder-modal",responseClassBad:"modal__response modal__response--error",responseClassGood:"modal__response modal__response--good",identifier:"UserForms.ConfirmFolder"}),this[0])},_clearModal:function(){u.default.unmountComponentAtNode(this[0])},_handleHideModal:function(){return this.close()},_handleSubmitModal:function(o,r,t){var n=this;return t().then(function(){s.default.noticeAdd({text:d.default._t("UserForms.FILE_CONFIRMATION_CONFIRMATION","Folder confirmed successfully."),stay:!1,type:"success"}),n.close(!0),e("[name=action_doSave], [name=action_save]").click()}).catch(function(e){s.default.noticeAdd({text:e.message,stay:!1,type:"error"})})}}),e("#Form_ConfirmFolderForm_action_cancel").entwine({onclick:function(){e("#confirm-folder__dialog-wrapper").close()}})})},"./client/src/bundles/FieldEditor.js":function(e,o,r){"use strict";var t=r(0);(function(e){return e&&e.__esModule?e:{default:e}})(t).default.entwine("ss",function(e){var o=null;e(".uf-field-editor .ss-gridfield-items").entwine({onmatch:function(){var r=0,t=0,n=e(".uf-field-editor .ss-gridfield-buttonrow").addClass("sticky-buttons"),i=e(".cms-content-header.north").first().height()+parseInt(e(".sticky-buttons").css("padding-top"),10),d=e(".uf-field-editor");this._super(),this.find(".ss-gridfield-item").each(function(o,n){switch(e(n).data("class")){case"SilverStripe\\UserForms\\Model\\EditableFormField\\EditableFormStep":return void(t=0);case"SilverStripe\\UserForms\\Model\\EditableFormField\\EditableFieldGroup":t+=1,r=t;break;case"SilverStripe\\UserForms\\Model\\EditableFormField\\EditableFieldGroupEnd":r=t,t-=1;break;default:r=t}e(n).toggleClass("infieldgroup",r>0);for(var i=1;i<=5;i++)e(n).toggleClass("infieldgroup-level-"+i,r>=i)}),o=setInterval(function(){var e=d.offset().top;n.width("100%"),e>i||0===e?n.removeClass("sticky-buttons"):n.addClass("sticky-buttons")},300)},onunmatch:function(){this._super(),clearInterval(o)}}),e(".uf-field-editor .ss-gridfield-buttonrow .action").entwine({onclick:function(e){this._super(e),this.trigger("addnewinline")}}),e(".uf-field-editor").entwine({onmatch:function(){var o=this;this._super(),this.on("addnewinline",function(){o.one("reload",function(){var r=o.find(".ss-gridfield-item").last(),t=null;"SilverStripe\\UserForms\\Model\\EditableFormField\\EditableFieldGroupEnd"===r.attr("data-class")?(t=r,t.prev().find(".col-Title input").focus(),r=t.add(t.prev()),t.css("visibility","hidden")):r.find(".col-Title input").focus(),r.addClass("flashBackground");var n=e(".cms-content-fields");n.length>0&&n.scrollTop(n[0].scrollHeight),t&&t.css("visibility","visible")})})},onummatch:function(){this._super()}})})},"./client/src/bundles/Recipient.js":function(e,o,r){"use strict";var t=r(0);(function(e){return e&&e.__esModule?e:{default:e}})(t).default.entwine("ss",function(e){var o={updateFormatSpecificFields:function(){var o=e('input[name="SendPlain"]').is(":checked");e(".field.toggle-html-only")[o?"hide":"show"](),e(".field.toggle-plain-only")[o?"show":"hide"]()}};e("#Form_ItemEditForm .EmailRecipientForm").entwine({onmatch:function(){o.updateFormatSpecificFields()},onunmatch:function(){(void 0)._super()}}),e('#Form_ItemEditForm .EmailRecipientForm input[name="SendPlain"]').entwine({onchange:function(){o.updateFormatSpecificFields()}})})},"./client/src/bundles/bundle-cms.js":function(e,o,r){"use strict";r("./client/src/bundles/FieldEditor.js"),r("./client/src/bundles/ConfirmFolder.js"),r("./client/src/bundles/Recipient.js")},0:function(e,o){e.exports=jQuery},1:function(e,o){e.exports=Injector},2:function(e,o){e.exports=NodeUrl},3:function(e,o){e.exports=React},4:function(e,o){e.exports=ReactDom},5:function(e,o){e.exports=i18n},6:function(e,o){e.exports=qs}}); \ No newline at end of file +!function(e){function o(n){if(r[n])return r[n].exports;var t=r[n]={i:n,l:!1,exports:{}};return e[n].call(t.exports,t,t.exports,o),t.l=!0,t.exports}var r={};o.m=e,o.c=r,o.i=function(e){return e},o.d=function(e,r,n){o.o(e,r)||Object.defineProperty(e,r,{configurable:!1,enumerable:!0,get:n})},o.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return o.d(r,"a",r),r},o.o=function(e,o){return Object.prototype.hasOwnProperty.call(e,o)},o.p="",o(o.s="./client/src/bundles/bundle-cms.js")}({"./client/src/bundles/ConfirmFolder.js":function(e,o,r){"use strict";function n(e){return e&&e.__esModule?e:{default:e}}var t=Object.assign||function(e){for(var o=1;o');var r=e(this).closest("tr").data("id");o.data("id",r),e("body").append(o),o.open()}}}),e("#confirm-folder__dialog-wrapper").entwine({onunmatch:function(){this._clearModal()},open:function(){this._renderModal(!0)},close:function(o){if(!o){var r=e("#confirm-folder__dialog-wrapper").data("id");e(".ss-gridfield-item[data-id='"+r+"'] .dropdown.editable-column-field.form-group--no-label[data-folderconfirmed='0']").val("SilverStripe\\UserForms\\Model\\EditableFormField\\EditableTextField")}this._renderModal(!1)},_renderModal:function(o){var r=this,n=function(){return r._handleHideModal.apply(r,arguments)},i=function(){return r._handleSubmitModal.apply(r,arguments)},l=d.default._t("UserForms.FILE_CONFIRMATION_TITLE","Select file upload folder"),s=e(this).data("id"),a=window.ss.config.adminUrl||"/admin/",f=F.default.parse(a+"user-forms/confirmfolderformschema"),m=h.default.parse(f.query);m.ID=s;var p=F.default.format(t({},f,{search:h.default.stringify(m)}));u.default.render(c.default.createElement(g,{title:l,isOpen:o,onSubmit:i,onClosed:n,schemaUrl:p,bodyClassName:"modal__dialog",className:"confirm-folder-modal",responseClassBad:"modal__response modal__response--error",responseClassGood:"modal__response modal__response--good",identifier:"UserForms.ConfirmFolder"}),this[0])},_clearModal:function(){u.default.unmountComponentAtNode(this[0])},_handleHideModal:function(){return this.close()},_handleSubmitModal:function(o,r,n){var t=this;return n().then(function(){s.default.noticeAdd({text:d.default._t("UserForms.FILE_CONFIRMATION_CONFIRMATION","Folder confirmed successfully."),stay:!1,type:"success"}),t.close(!0),e("[name=action_doSave], [name=action_save]").click()}).catch(function(e){s.default.noticeAdd({text:e.message,stay:!1,type:"error"})})}}),e("#Form_ConfirmFolderForm_action_cancel").entwine({onclick:function(){e("#confirm-folder__dialog-wrapper").close()}})})},"./client/src/bundles/FieldEditor.js":function(e,o,r){"use strict";var n=r(0);(function(e){return e&&e.__esModule?e:{default:e}})(n).default.entwine("ss",function(e){var o=null;e(".uf-field-editor .ss-gridfield-items").entwine({onmatch:function(){var r=0,n=0,t=e(".uf-field-editor .ss-gridfield-buttonrow").addClass("sticky-buttons"),i=e(".cms-content-header.north").first().height()+parseInt(e(".sticky-buttons").css("padding-top"),10),d=e(".uf-field-editor");this._super(),this.find(".ss-gridfield-item").each(function(o,t){switch(e(t).data("class")){case"SilverStripe\\UserForms\\Model\\EditableFormField\\EditableFormStep":return void(n=0);case"SilverStripe\\UserForms\\Model\\EditableFormField\\EditableFieldGroup":n+=1,r=n;break;case"SilverStripe\\UserForms\\Model\\EditableFormField\\EditableFieldGroupEnd":r=n,n-=1;break;default:r=n}e(t).toggleClass("infieldgroup",r>0);for(var i=1;i<=5;i++)e(t).toggleClass("infieldgroup-level-"+i,r>=i)}),o=setInterval(function(){var e=d.offset().top;t.width("100%"),e>i||0===e?t.removeClass("sticky-buttons"):t.addClass("sticky-buttons")},300)},onunmatch:function(){this._super(),clearInterval(o)}}),e(".uf-field-editor .ss-gridfield-buttonrow .action").entwine({onclick:function(e){this._super(e),this.trigger("addnewinline")}}),e(".uf-field-editor").entwine({onmatch:function(){var o=this;this._super(),this.on("addnewinline",function(){o.one("reload",function(){var r=o.find(".ss-gridfield-item").last(),n=null;"SilverStripe\\UserForms\\Model\\EditableFormField\\EditableFieldGroupEnd"===r.attr("data-class")?(n=r,n.prev().find(".col-Title input").focus(),r=n.add(n.prev()),n.css("visibility","hidden")):r.find(".col-Title input").focus(),r.addClass("flashBackground");var t=e(".cms-content-fields");t.length>0&&t.scrollTop(t[0].scrollHeight),n&&n.css("visibility","visible")})})},onummatch:function(){this._super()}})})},"./client/src/bundles/Recipient.js":function(e,o,r){"use strict";var n=r(0);(function(e){return e&&e.__esModule?e:{default:e}})(n).default.entwine("ss",function(e){var o={updateFormatSpecificFields:function(){var o=e('input[name="SendPlain"]').is(":checked");e(".field.toggle-html-only")[o?"hide":"show"](),e(".field.toggle-plain-only")[o?"show":"hide"]()}};e("#Form_ItemEditForm .EmailRecipientForm").entwine({onmatch:function(){o.updateFormatSpecificFields()},onunmatch:function(){(void 0)._super()}}),e('#Form_ItemEditForm .EmailRecipientForm input[name="SendPlain"]').entwine({onchange:function(){o.updateFormatSpecificFields()}})})},"./client/src/bundles/bundle-cms.js":function(e,o,r){"use strict";r("./client/src/bundles/FieldEditor.js"),r("./client/src/bundles/ConfirmFolder.js"),r("./client/src/bundles/Recipient.js")},0:function(e,o){e.exports=jQuery},1:function(e,o){e.exports=Injector},2:function(e,o){e.exports=NodeUrl},3:function(e,o){e.exports=React},4:function(e,o){e.exports=ReactDom},5:function(e,o){e.exports=i18n},6:function(e,o){e.exports=qs}}); \ No newline at end of file diff --git a/client/src/bundles/ConfirmFolder.js b/client/src/bundles/ConfirmFolder.js index 24dab2a..702cc64 100644 --- a/client/src/bundles/ConfirmFolder.js +++ b/client/src/bundles/ConfirmFolder.js @@ -60,7 +60,7 @@ jQuery.entwine('ss', ($) => { $('#Form_ConfirmFolderForm_FolderID_Holder .treedropdownfield.is-open,#Form_ItemEditForm_FolderID .treedropdownfield.is-open').entwine({ onunmatch() { // Build url - const adminUrl = $(this).closest('#Form_ConfirmFolderForm').data('adminUrl'); + const adminUrl = window.ss.config.adminUrl || '/admin/'; const parsedURL = url.parse(`${adminUrl}user-forms/getfoldergrouppermissions`); const parsedQs = qs.parse(parsedURL.query); parsedQs.FolderID = $(this).find('input[name=FolderID]').val(); @@ -105,9 +105,7 @@ jQuery.entwine('ss', ($) => { dialog = $('
'); const id = $(this).closest('tr').data('id'); - const adminUrl = $(this).closest('.uf-field-editor').data('adminUrl'); dialog.data('id', id); - dialog.data('adminUrl', adminUrl); $('body').append(dialog); dialog.open(); @@ -143,7 +141,7 @@ jQuery.entwine('ss', ($) => { const editableFileFieldID = $(this).data('id'); // Build schema url - const adminUrl = $(this).data('adminUrl'); + const adminUrl = window.ss.config.adminUrl || '/admin/'; const parsedURL = url.parse(`${adminUrl}user-forms/confirmfolderformschema`); const parsedQs = qs.parse(parsedURL.query); parsedQs.ID = editableFileFieldID; diff --git a/code/Control/UserDefinedFormAdmin.php b/code/Control/UserDefinedFormAdmin.php index 780fdb1..5ced698 100644 --- a/code/Control/UserDefinedFormAdmin.php +++ b/code/Control/UserDefinedFormAdmin.php @@ -200,8 +200,7 @@ class UserDefinedFormAdmin extends LeftAndMain return Form::create($this, 'ConfirmFolderForm', $fields, $actions, RequiredFields::create('ID')) ->setFormAction($this->Link('ConfirmFolderForm')) - ->addExtraClass('form--no-dividers') - ->setAttribute('data-admin-url', AdminRootController::admin_url()); + ->addExtraClass('form--no-dividers'); } /** diff --git a/code/Extension/UserFormFieldEditorExtension.php b/code/Extension/UserFormFieldEditorExtension.php index 173c960..d9057e1 100644 --- a/code/Extension/UserFormFieldEditorExtension.php +++ b/code/Extension/UserFormFieldEditorExtension.php @@ -126,8 +126,7 @@ class UserFormFieldEditorExtension extends DataExtension $fields, $config ) - ->addExtraClass('uf-field-editor') - ->setAttribute('data-admin-url', AdminRootController::admin_url()); + ->addExtraClass('uf-field-editor'); return $fieldEditor; }