From 2375b00a085f92f6252e8b045cdb235f9c275c1d Mon Sep 17 00:00:00 2001 From: viettruongq Date: Wed, 1 Nov 2023 22:02:31 +0700 Subject: [PATCH] MDL-70760 admin: Limited length config setting validate client-side --- admin/templates/setting_configtext.mustache | 13 +- .../tests/behat/incoming_mail.feature | 10 ++ lang/en/admin.php | 1 + lib/adminlib.php | 34 ++++- .../amd/build/configtext_maxlength.min.js | 3 + .../amd/build/configtext_maxlength.min.js.map | 1 + lib/form/amd/src/configtext_maxlength.js | 121 ++++++++++++++++++ .../setting_validation_failure.mustache | 36 ++++++ 8 files changed, 216 insertions(+), 3 deletions(-) create mode 100644 lib/form/amd/build/configtext_maxlength.min.js create mode 100644 lib/form/amd/build/configtext_maxlength.min.js.map create mode 100644 lib/form/amd/src/configtext_maxlength.js create mode 100644 lib/form/templates/setting_validation_failure.mustache diff --git a/admin/templates/setting_configtext.mustache b/admin/templates/setting_configtext.mustache index 1238c8e3e98..e9bb7642c9b 100644 --- a/admin/templates/setting_configtext.mustache +++ b/admin/templates/setting_configtext.mustache @@ -27,6 +27,8 @@ * forceltr - always display as ltr * attributes - list of additional attributes containing name, value * readonly - bool + * data - list of arbitrary data attributes + * maxcharacter - maximum character Example context (json): { @@ -36,12 +38,19 @@ "size": "21", "forceltr": false, "readonly": false, - "attributes": [ { "name": "readonly", "value": "readonly" } ] + "attributes": [ { "name": "readonly", "value": "readonly" } ], + "data": [ { "key": "validation_max_length", "value": "10" } ], + "maxcharacter": false } }} {{! Setting configtext. }}
- + + {{#data}} + {{#maxcharacter}} +
{{#str}} maxcharacter, admin, {{value}} {{/str}}
+ {{/maxcharacter}} + {{/data}}
diff --git a/admin/tool/messageinbound/tests/behat/incoming_mail.feature b/admin/tool/messageinbound/tests/behat/incoming_mail.feature index b8974244a12..2ed2e3d0752 100644 --- a/admin/tool/messageinbound/tests/behat/incoming_mail.feature +++ b/admin/tool/messageinbound/tests/behat/incoming_mail.feature @@ -23,3 +23,13 @@ Feature: Incoming mail configuration When I navigate to "Server > Email > Incoming mail configuration" in site administration Then "OAuth 2 service" "select" should exist And I should see "Testing service" in the "OAuth 2 service" "select" + + @javascript + Scenario: Check character limitations of mailbox name + When I navigate to "Server > Email > Incoming mail configuration" in site administration + And I set the field "Mailbox name" to "frogfrogfrogfrog" + Then I should see "Maximum of 15 characters" + And the "disabled" attribute of "form#adminsettings button[type='submit']" "css_element" should contain "true" + And I set the field "Mailbox name" to "frogfrogfrogfro" + And I should not see "Maximum of 15 characters" + And the "disabled" attribute of "form#adminsettings button[type='submit']" "css_element" should not be set diff --git a/lang/en/admin.php b/lang/en/admin.php index c02559f49ae..8e0b7b48b82 100644 --- a/lang/en/admin.php +++ b/lang/en/admin.php @@ -871,6 +871,7 @@ sites. If this is not what you wanted then you should make sure you are updating from a STABLE branch of the Moodle code. See Moodle Docs for more details.'; $string['maxbytes'] = 'Maximum uploaded file size'; $string['maxconsecutiveidentchars'] = 'Consecutive identical characters'; +$string['maxcharacter'] = '{$a} character maximum'; $string['maxsizeperdownloadcoursefile'] = 'Maximum size per file'; $string['maxsizeperdownloadcoursefile_desc'] = 'The maximum size of each file when downloading course content. Files exceeding this size will be omitted from the download.'; $string['maxeditingtime'] = 'Maximum time to edit posts'; diff --git a/lib/adminlib.php b/lib/adminlib.php index a4c6ad400c6..4382479843a 100644 --- a/lib/adminlib.php +++ b/lib/adminlib.php @@ -2447,6 +2447,8 @@ class admin_setting_configtext extends admin_setting { /** @var int default field size */ public $size; + /** @var array List of arbitrary data attributes */ + protected $datavalues = []; /** * Config text constructor @@ -2530,11 +2532,24 @@ class admin_setting_configtext extends admin_setting { } } + /** + * Set arbitrary data attributes for template. + * + * @param string $key Attribute key for template. + * @param string $value Attribute value for template. + */ + public function set_data_attribute(string $key, string $value): void { + $this->datavalues[] = [ + 'key' => $key, + 'value' => $value, + ]; + } + /** * Return an XHTML string for the setting * @return string Returns an XHTML string */ - public function output_html($data, $query='') { + public function output_html($data, $query = '') { global $OUTPUT; $default = $this->get_defaultsetting(); @@ -2545,6 +2560,8 @@ class admin_setting_configtext extends admin_setting { 'value' => $data, 'forceltr' => $this->get_force_ltr(), 'readonly' => $this->is_readonly(), + 'data' => $this->datavalues, + 'maxcharacter' => array_key_exists('validation-max-length', $this->datavalues), ]; $element = $OUTPUT->render_from_template('core_admin/setting_configtext', $context); @@ -2578,6 +2595,7 @@ class admin_setting_configtext_with_maxlength extends admin_setting_configtext { public function __construct($name, $visiblename, $description, $defaultsetting, $paramtype=PARAM_RAW, $size=null, $maxlength = 0) { $this->maxlength = $maxlength; + $this->set_data_attribute('validation-max-length', $maxlength); parent::__construct($name, $visiblename, $description, $defaultsetting, $paramtype, $size); } @@ -2604,6 +2622,20 @@ class admin_setting_configtext_with_maxlength extends admin_setting_configtext { return $parentvalidation; } } + + /** + * Return an XHTML string for the setting. + * + * @param string $data data. + * @param string $query query statement. + * @return string Returns an XHTML string + */ + public function output_html($data, $query = ''): string { + global $PAGE; + $PAGE->requires->js_call_amd('core_form/configtext_maxlength', 'init'); + + return parent::output_html($data, $query); + } } /** diff --git a/lib/form/amd/build/configtext_maxlength.min.js b/lib/form/amd/build/configtext_maxlength.min.js new file mode 100644 index 00000000000..67d5447e700 --- /dev/null +++ b/lib/form/amd/build/configtext_maxlength.min.js @@ -0,0 +1,3 @@ +define("core_form/configtext_maxlength",["exports","core/str","core/templates","core/notification","core/prefetch"],(function(_exports,_str,_templates,_notification,_prefetch){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_templates=_interopRequireDefault(_templates),_notification=_interopRequireDefault(_notification);let registered=!1;_exports.init=()=>{registered||((0,_prefetch.prefetchStrings)("core",["maximumchars"]),(0,_prefetch.prefetchTemplates)(["core_form/setting_validation_failure"]),registered=!0,document.addEventListener("input",(e=>{const maxLengthField=e.target.closest("[data-validation-max-length]");if(maxLengthField)if(maxLengthField.value.length>maxLengthField.dataset.validationMaxLength)maxLengthField.form.addEventListener("submit",submissionCheck),(0,_str.get_string)("maximumchars","core",maxLengthField.dataset.validationMaxLength).then((errorMessage=>_templates.default.renderForPromise("core_form/setting_validation_failure",{fieldid:maxLengthField.id,message:errorMessage}))).then((errorTemplate=>{if(!maxLengthField.dataset.validationFailureId){const formWrapper=maxLengthField.closest(".form-text");_templates.default.prependNodeContents(formWrapper,errorTemplate.html,errorTemplate.js),maxLengthField.dataset.validationFailureId="maxlength_error_".concat(maxLengthField.id),updateSubmitButton()}})).then((()=>{maxLengthField.setAttribute("aria-invalid",!0);const errorField=document.getElementById(maxLengthField.dataset.validationFailureId);errorField&&errorField.setAttribute("aria-describedby",maxLengthField.id)})).catch(_notification.default.exception);else{const validationMessage=document.getElementById(maxLengthField.dataset.validationFailureId);validationMessage&&(validationMessage.parentElement.remove(),delete maxLengthField.dataset.validationFailureId,maxLengthField.removeAttribute("aria-invalid"),updateSubmitButton())}})))};const submissionCheck=e=>{const maxLengthFields=e.target.querySelectorAll("[data-validation-max-length]");Array.from(maxLengthFields).some((maxLengthField=>maxLengthField.value.length>maxLengthField.dataset.validationMaxLength&&(e.preventDefault(),maxLengthField.focus(),!0)))},updateSubmitButton=()=>{const shouldDisable=document.querySelector("form#adminsettings .error");document.querySelector('form#adminsettings button[type="submit"]').disabled=!!shouldDisable}})); + +//# sourceMappingURL=configtext_maxlength.min.js.map \ No newline at end of file diff --git a/lib/form/amd/build/configtext_maxlength.min.js.map b/lib/form/amd/build/configtext_maxlength.min.js.map new file mode 100644 index 00000000000..6b337e2049d --- /dev/null +++ b/lib/form/amd/build/configtext_maxlength.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"configtext_maxlength.min.js","sources":["../src/configtext_maxlength.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Validation for configtext_maxlength.\n *\n * @module core_form/configtext-maxlength\n * @copyright 2021 The Open University\n */\nimport {get_string as getString} from 'core/str';\nimport Templates from 'core/templates';\nimport Notification from 'core/notification';\nimport {prefetchStrings, prefetchTemplates} from 'core/prefetch';\n\nlet registered = false;\n\n/**\n * Initialisation function.\n */\nexport const init = () => {\n if (registered) {\n return;\n }\n prefetchStrings('core', [\n 'maximumchars',\n ]);\n\n prefetchTemplates([\n 'core_form/setting_validation_failure',\n ]);\n\n registered = true;\n\n document.addEventListener('input', e => {\n const maxLengthField = e.target.closest('[data-validation-max-length]');\n if (!maxLengthField) {\n return;\n }\n\n if (maxLengthField.value.length > maxLengthField.dataset.validationMaxLength) {\n // Disable the form for this field.\n maxLengthField.form.addEventListener('submit', submissionCheck);\n // Display an error.\n getString('maximumchars', 'core', maxLengthField.dataset.validationMaxLength)\n .then(errorMessage => {\n return Templates.renderForPromise('core_form/setting_validation_failure', {\n fieldid: maxLengthField.id,\n message: errorMessage,\n });\n })\n .then(errorTemplate => {\n if (!maxLengthField.dataset.validationFailureId) {\n const formWrapper = maxLengthField.closest('.form-text');\n Templates.prependNodeContents(formWrapper, errorTemplate.html, errorTemplate.js);\n maxLengthField.dataset.validationFailureId = `maxlength_error_${maxLengthField.id}`;\n // Disable submit button when the message is displayed.\n updateSubmitButton();\n }\n return;\n })\n .then(() => {\n maxLengthField.setAttribute('aria-invalid', true);\n const errorField = document.getElementById(maxLengthField.dataset.validationFailureId);\n if (errorField) {\n errorField.setAttribute('aria-describedby', maxLengthField.id);\n }\n return;\n })\n .catch(Notification.exception);\n } else {\n // Remove the old message.\n const validationMessage = document.getElementById(maxLengthField.dataset.validationFailureId);\n if (validationMessage) {\n validationMessage.parentElement.remove();\n delete maxLengthField.dataset.validationFailureId;\n maxLengthField.removeAttribute('aria-invalid');\n // Enable submit button when the message was removed.\n updateSubmitButton();\n }\n }\n });\n};\n\n/**\n * Handle form submission.\n *\n * @param {Event} e The event.\n */\nconst submissionCheck = e => {\n const maxLengthFields = e.target.querySelectorAll('[data-validation-max-length]');\n const maxLengthFieldsArray = Array.from(maxLengthFields);\n maxLengthFieldsArray.some(maxLengthField => {\n // Focus on the first validation failure.\n if (maxLengthField.value.length > maxLengthField.dataset.validationMaxLength) {\n e.preventDefault();\n maxLengthField.focus();\n return true;\n }\n return false;\n });\n};\n\n/**\n * Update submit button.\n */\nconst updateSubmitButton = () => {\n const shouldDisable = document.querySelector('form#adminsettings .error');\n document.querySelector('form#adminsettings button[type=\"submit\"]').disabled = !!shouldDisable;\n};\n"],"names":["registered","document","addEventListener","e","maxLengthField","target","closest","value","length","dataset","validationMaxLength","form","submissionCheck","then","errorMessage","Templates","renderForPromise","fieldid","id","message","errorTemplate","validationFailureId","formWrapper","prependNodeContents","html","js","updateSubmitButton","setAttribute","errorField","getElementById","catch","Notification","exception","validationMessage","parentElement","remove","removeAttribute","maxLengthFields","querySelectorAll","Array","from","some","preventDefault","focus","shouldDisable","querySelector","disabled"],"mappings":"qbA0BIA,YAAa,gBAKG,KACZA,2CAGY,OAAQ,CACpB,iDAGc,CACd,yCAGJA,YAAa,EAEbC,SAASC,iBAAiB,SAASC,UACzBC,eAAiBD,EAAEE,OAAOC,QAAQ,mCACnCF,kBAIDA,eAAeG,MAAMC,OAASJ,eAAeK,QAAQC,oBAErDN,eAAeO,KAAKT,iBAAiB,SAAUU,qCAErC,eAAgB,OAAQR,eAAeK,QAAQC,qBACpDG,MAAKC,cACKC,mBAAUC,iBAAiB,uCAAwC,CACtEC,QAASb,eAAec,GACxBC,QAASL,iBAGhBD,MAAKO,oBACGhB,eAAeK,QAAQY,oBAAqB,OACvCC,YAAclB,eAAeE,QAAQ,iCACjCiB,oBAAoBD,YAAaF,cAAcI,KAAMJ,cAAcK,IAC7ErB,eAAeK,QAAQY,8CAAyCjB,eAAec,IAE/EQ,yBAIPb,MAAK,KACFT,eAAeuB,aAAa,gBAAgB,SACtCC,WAAa3B,SAAS4B,eAAezB,eAAeK,QAAQY,qBAC9DO,YACAA,WAAWD,aAAa,mBAAoBvB,eAAec,OAIlEY,MAAMC,sBAAaC,eACrB,OAEGC,kBAAoBhC,SAAS4B,eAAezB,eAAeK,QAAQY,qBACrEY,oBACAA,kBAAkBC,cAAcC,gBACzB/B,eAAeK,QAAQY,oBAC9BjB,eAAegC,gBAAgB,gBAE/BV,kCAWVd,gBAAkBT,UACdkC,gBAAkBlC,EAAEE,OAAOiC,iBAAiB,gCACrBC,MAAMC,KAAKH,iBACnBI,MAAKrC,gBAElBA,eAAeG,MAAMC,OAASJ,eAAeK,QAAQC,sBACrDP,EAAEuC,iBACFtC,eAAeuC,SACR,MASbjB,mBAAqB,WACjBkB,cAAgB3C,SAAS4C,cAAc,6BAC7C5C,SAAS4C,cAAc,4CAA4CC,WAAaF"} \ No newline at end of file diff --git a/lib/form/amd/src/configtext_maxlength.js b/lib/form/amd/src/configtext_maxlength.js new file mode 100644 index 00000000000..7a4c29bed01 --- /dev/null +++ b/lib/form/amd/src/configtext_maxlength.js @@ -0,0 +1,121 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Validation for configtext_maxlength. + * + * @module core_form/configtext-maxlength + * @copyright 2021 The Open University + */ +import {get_string as getString} from 'core/str'; +import Templates from 'core/templates'; +import Notification from 'core/notification'; +import {prefetchStrings, prefetchTemplates} from 'core/prefetch'; + +let registered = false; + +/** + * Initialisation function. + */ +export const init = () => { + if (registered) { + return; + } + prefetchStrings('core', [ + 'maximumchars', + ]); + + prefetchTemplates([ + 'core_form/setting_validation_failure', + ]); + + registered = true; + + document.addEventListener('input', e => { + const maxLengthField = e.target.closest('[data-validation-max-length]'); + if (!maxLengthField) { + return; + } + + if (maxLengthField.value.length > maxLengthField.dataset.validationMaxLength) { + // Disable the form for this field. + maxLengthField.form.addEventListener('submit', submissionCheck); + // Display an error. + getString('maximumchars', 'core', maxLengthField.dataset.validationMaxLength) + .then(errorMessage => { + return Templates.renderForPromise('core_form/setting_validation_failure', { + fieldid: maxLengthField.id, + message: errorMessage, + }); + }) + .then(errorTemplate => { + if (!maxLengthField.dataset.validationFailureId) { + const formWrapper = maxLengthField.closest('.form-text'); + Templates.prependNodeContents(formWrapper, errorTemplate.html, errorTemplate.js); + maxLengthField.dataset.validationFailureId = `maxlength_error_${maxLengthField.id}`; + // Disable submit button when the message is displayed. + updateSubmitButton(); + } + return; + }) + .then(() => { + maxLengthField.setAttribute('aria-invalid', true); + const errorField = document.getElementById(maxLengthField.dataset.validationFailureId); + if (errorField) { + errorField.setAttribute('aria-describedby', maxLengthField.id); + } + return; + }) + .catch(Notification.exception); + } else { + // Remove the old message. + const validationMessage = document.getElementById(maxLengthField.dataset.validationFailureId); + if (validationMessage) { + validationMessage.parentElement.remove(); + delete maxLengthField.dataset.validationFailureId; + maxLengthField.removeAttribute('aria-invalid'); + // Enable submit button when the message was removed. + updateSubmitButton(); + } + } + }); +}; + +/** + * Handle form submission. + * + * @param {Event} e The event. + */ +const submissionCheck = e => { + const maxLengthFields = e.target.querySelectorAll('[data-validation-max-length]'); + const maxLengthFieldsArray = Array.from(maxLengthFields); + maxLengthFieldsArray.some(maxLengthField => { + // Focus on the first validation failure. + if (maxLengthField.value.length > maxLengthField.dataset.validationMaxLength) { + e.preventDefault(); + maxLengthField.focus(); + return true; + } + return false; + }); +}; + +/** + * Update submit button. + */ +const updateSubmitButton = () => { + const shouldDisable = document.querySelector('form#adminsettings .error'); + document.querySelector('form#adminsettings button[type="submit"]').disabled = !!shouldDisable; +}; diff --git a/lib/form/templates/setting_validation_failure.mustache b/lib/form/templates/setting_validation_failure.mustache new file mode 100644 index 00000000000..dafc693f286 --- /dev/null +++ b/lib/form/templates/setting_validation_failure.mustache @@ -0,0 +1,36 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_form/setting_validation_failure + Error displayed when field fails validation (longer than max length). + + Context variables required for this template: + * fieldid - element field id + * message - error message + + Example context (json): + { + "fieldid": "test0", + "message": "test message" + } +}} +{{! + Setting error template for configtext_maxlength. +}} +
+ {{message}} +