From 9bd1d26ffbd0292fc6c31aa259c20955d648858f Mon Sep 17 00:00:00 2001 From: George Wilson Date: Thu, 28 Dec 2023 20:50:12 +0000 Subject: [PATCH 01/11] First pass at form validation rework --- .../Form/Constraint/AbstractConstraint.php | 106 +++++++++ .../Form/Constraint/ConstraintInterface.php | 50 ++++ .../Form/Constraint/FieldLengthConstraint.php | 70 ++++++ .../Constraint/FieldRequiredConstraint.php | 53 +++++ .../Form/Constraint/LegacyRuleConstraint.php | 103 +++++++++ libraries/src/Form/Field/TextField.php | 26 +++ libraries/src/Form/Form.php | 104 +++++++-- libraries/src/Form/FormField.php | 85 +++---- libraries/src/Form/FormRule.php | 7 +- libraries/src/Form/Rule/AbstractRegexRule.php | 84 +++++++ libraries/src/Form/Rule/BooleanRule.php | 20 +- libraries/src/Form/Rule/FormRuleInterface.php | 44 ++++ libraries/src/Form/Rule/PasswordRule.php | 88 +++---- .../src/Form/Rule/RuleConstraintTrait.php | 75 ++++++ libraries/src/Form/Rule/SubformRule.php | 41 ++-- .../Field/DisabledResponseField.php | 196 ++++++++++++++++ .../Validation/Field/LegacyInvalidField.php | 218 ++++++++++++++++++ .../Validation/Field/LegacyValidField.php | 197 ++++++++++++++++ .../Field/SetupFailedFieldResponse.php | 196 ++++++++++++++++ .../Validation/FieldValidationResponse.php | 171 ++++++++++++++ .../FieldValidationResponseInterface.php | 71 ++++++ .../Validation/FormValidationResponse.php | 115 +++++++++ .../FormValidationResponseInterface.php | 54 +++++ 23 files changed, 2027 insertions(+), 147 deletions(-) create mode 100644 libraries/src/Form/Constraint/AbstractConstraint.php create mode 100644 libraries/src/Form/Constraint/ConstraintInterface.php create mode 100644 libraries/src/Form/Constraint/FieldLengthConstraint.php create mode 100644 libraries/src/Form/Constraint/FieldRequiredConstraint.php create mode 100644 libraries/src/Form/Constraint/LegacyRuleConstraint.php create mode 100644 libraries/src/Form/Rule/AbstractRegexRule.php create mode 100644 libraries/src/Form/Rule/FormRuleInterface.php create mode 100644 libraries/src/Form/Rule/RuleConstraintTrait.php create mode 100644 libraries/src/Form/Validation/Field/DisabledResponseField.php create mode 100644 libraries/src/Form/Validation/Field/LegacyInvalidField.php create mode 100644 libraries/src/Form/Validation/Field/LegacyValidField.php create mode 100644 libraries/src/Form/Validation/Field/SetupFailedFieldResponse.php create mode 100644 libraries/src/Form/Validation/FieldValidationResponse.php create mode 100644 libraries/src/Form/Validation/FieldValidationResponseInterface.php create mode 100644 libraries/src/Form/Validation/FormValidationResponse.php create mode 100644 libraries/src/Form/Validation/FormValidationResponseInterface.php diff --git a/libraries/src/Form/Constraint/AbstractConstraint.php b/libraries/src/Form/Constraint/AbstractConstraint.php new file mode 100644 index 0000000000000..14cfcdbdd8499 --- /dev/null +++ b/libraries/src/Form/Constraint/AbstractConstraint.php @@ -0,0 +1,106 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS\Form\Constraint; + +use Joomla\CMS\Form\Constraint\ConstraintInterface; +use Joomla\CMS\Form\FormField; +use Joomla\CMS\Language\Text; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * Base class for implementing a ConstraintValidationInterface when the validation response has been pre-determined + * outside of the class + * + * @since __DEPLOY_VERSION__ + */ +abstract class AbstractConstraint implements ConstraintInterface +{ + /** + * Was the constraint met? + * + * @var bool + * @since __DEPLOY_VERSION__ + */ + private readonly bool $valid; + + /** + * Was the constraint met? + * + * @var FormField + * @since __DEPLOY_VERSION__ + */ + private readonly FormField $field; + + /** + * Method to instantiate the object. + * + * @param bool $valid Was the result of the constraint valid or not? + * @param FormField $field The form field object being inspected + * + * @since __DEPLOY_VERSION__ + */ + public function __construct(bool $valid, FormField $field) + { + $this->valid = $valid; + $this->field = $field; + } + + /** + * Was the field data valid or not. If there are no constraints on the field this should return true. + * + * @return bool + * + * @since __DEPLOY_VERSION__ + */ + public function isValid(): bool + { + return $this->valid; + } + + /** + * Allows internal access to the form field object which can be useful when creating specific error messages. + * + * @return FormField + * + * @since __DEPLOY_VERSION__ + */ + protected function getField(): FormField + { + return $this->field; + } + + /** + * Gets the human friendly label of the form field. + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + protected function getFieldLabel(): string + { + if ($this->field->getAttribute('label')) { + $fieldLabel = $this->field->getAttribute('label'); + + // Try to translate label if not set to false + $translate = $this->field->getAttribute('translateLabel', ''); + + if (!($translate === 'false' || $translate === 'off' || $translate === '0')) { + $fieldLabel = Text::_($fieldLabel); + } + } else { + $fieldLabel = Text::_($this->field->getAttribute('name')); + } + + return $fieldLabel; + } +} diff --git a/libraries/src/Form/Constraint/ConstraintInterface.php b/libraries/src/Form/Constraint/ConstraintInterface.php new file mode 100644 index 0000000000000..0b1d5253fa619 --- /dev/null +++ b/libraries/src/Form/Constraint/ConstraintInterface.php @@ -0,0 +1,50 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS\Form\Constraint; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * Interface defining a constraint applied against a form field + * + * @since __DEPLOY_VERSION__ + */ +interface ConstraintInterface +{ + /** + * Was the data validated in the constraint. + * + * @return bool + * + * @since __DEPLOY_VERSION__ + */ + public function isValid(): bool; + + /** + * Name of the constraint - note this is for machine access and should be unique for a form field. + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + public function getName(): string; + + /** + * Get an error message that can be displayed to a user about how to remediate the constraint. + * + * @return string + * + * @since __DEPLOY_VERSION__ + * @throws \BadMethodCallException When the constraint is valid + */ + public function getErrorMessage(): string; +} diff --git a/libraries/src/Form/Constraint/FieldLengthConstraint.php b/libraries/src/Form/Constraint/FieldLengthConstraint.php new file mode 100644 index 0000000000000..8268c56ac9c3d --- /dev/null +++ b/libraries/src/Form/Constraint/FieldLengthConstraint.php @@ -0,0 +1,70 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS\Form\Constraint; + +use Joomla\CMS\Form\FormField; +use Joomla\CMS\Language\Text; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * Checks that a required field has been given a value + * + * @since __DEPLOY_VERSION__ + */ +class FieldLengthConstraint extends AbstractConstraint +{ + /** + * Method to instantiate the object. + * + * @param FormField $field The form field object being inspected + * @param string $value Was the result of the constraint valid or not? + * @param int $maxLength Was the result of the constraint valid or not? + * + * @since __DEPLOY_VERSION__ + */ + public function __construct(FormField $field, string $value, int $maxLength) + { + $valid = ($maxLength === 0) || (\strlen($value) <= $maxLength); + + parent::__construct($valid, $field); + } + + /** + * Get an error message that can be displayed to a user about how to remediate the constraint. + * + * @return string + * + * @since __DEPLOY_VERSION__ + * @throws \BadMethodCallException When the constraint is valid + */ + public function getErrorMessage(): string + { + if ($this->isValid()) { + throw new \BadMethodCallException(sprintf('Field %s is valid', $this->getField()->getAttribute('name', ''))); + } + + return Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', $this->getFieldLabel()); + } + + /** + * Name of the constraint - note this is for machine access and should be unique for a form field. + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + public function getName(): string + { + return 'field-required'; + } +} diff --git a/libraries/src/Form/Constraint/FieldRequiredConstraint.php b/libraries/src/Form/Constraint/FieldRequiredConstraint.php new file mode 100644 index 0000000000000..fede7322036e2 --- /dev/null +++ b/libraries/src/Form/Constraint/FieldRequiredConstraint.php @@ -0,0 +1,53 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS\Form\Constraint; + +use Joomla\CMS\Language\Text; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * Checks that a required field has been given a value + * + * @since __DEPLOY_VERSION__ + */ +class FieldRequiredConstraint extends AbstractConstraint +{ + /** + * Get an error message that can be displayed to a user about how to remediate the constraint. + * + * @return string + * + * @since __DEPLOY_VERSION__ + * @throws \BadMethodCallException When the constraint is valid + */ + public function getErrorMessage(): string + { + if ($this->isValid()) { + throw new \BadMethodCallException(sprintf('Field %s is valid', $this->getField()->getAttribute('name', ''))); + } + + return Text::sprintf('JLIB_FORM_VALIDATE_FIELD_REQUIRED', $this->getFieldLabel()); + } + + /** + * Name of the constraint - note this is for machine access and should be unique for a form field. + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + public function getName(): string + { + return 'field-required'; + } +} diff --git a/libraries/src/Form/Constraint/LegacyRuleConstraint.php b/libraries/src/Form/Constraint/LegacyRuleConstraint.php new file mode 100644 index 0000000000000..96c14085979a0 --- /dev/null +++ b/libraries/src/Form/Constraint/LegacyRuleConstraint.php @@ -0,0 +1,103 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS\Form\Constraint; + +use Joomla\CMS\Form\FormField; +use Joomla\CMS\Form\FormRule; +use Joomla\CMS\Language\Text; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * Checks that a required field has been given a value + * + * @since __DEPLOY_VERSION__ + * @deprecated 7.0 This should not be used directly by extensions and is only provided to allow core to provide + * compatibility across Joomla versions. + */ +class LegacyRuleConstraint extends AbstractConstraint +{ + /** + * The form rule that was tested + * + * @var FormRule + * @since __DEPLOY_VERSION__ + */ + private readonly FormRule $rule; + + /** + * Method to instantiate the object. + * + * @param bool $valid Was the result of the constraint valid or not? + * @param FormField $field The form field object being inspected + * @param FormRule $rule The form rule that was run on the field + * + * @since __DEPLOY_VERSION__ + */ + public function __construct(bool $valid, FormField $field, FormRule $rule) + { + parent::__construct($valid, $field); + + $this->rule = $rule; + } + + /** + * Allows internal access to the form rule object which can be useful for creating appropriate extra information + * on error messages. + * + * @return FormRule + * + * @since __DEPLOY_VERSION__ + */ + public function getRule(): FormRule + { + return $this->rule; + } + + /** + * Get an error message that can be displayed to a user about how to remediate the constraint. + * + * @return string + * + * @since __DEPLOY_VERSION__ + * @throws \BadMethodCallException When the constraint is valid + */ + public function getErrorMessage(): string + { + if ($this->isValid()) { + throw new \BadMethodCallException(sprintf('Field %s is valid', $this->getField()->getAttribute('name', ''))); + } + + // Does the field have a defined error message? + $message = (string) $this->getField()->getAttribute('message'); + + if ($message) { + $message = Text::_($message); + } else { + $message = Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', $this->getFieldLabel()); + } + + return $message; + } + + /** + * Name of the constraint - note this is for machine access and should be unique for a form field. + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + public function getName(): string + { + return 'legacy-rule-constraint'; + } +} diff --git a/libraries/src/Form/Field/TextField.php b/libraries/src/Form/Field/TextField.php index 55fa55b5b0db4..43842018849f2 100644 --- a/libraries/src/Form/Field/TextField.php +++ b/libraries/src/Form/Field/TextField.php @@ -12,9 +12,11 @@ use Joomla\CMS\Component\ComponentHelper; use Joomla\CMS\Factory; use Joomla\CMS\Form\FormField; +use Joomla\CMS\Form\Constraint\FieldLengthConstraint; use Joomla\CMS\HTML\HTMLHelper; use Joomla\CMS\Language\Text; use Joomla\CMS\Uri\Uri; +use Joomla\Registry\Registry; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; @@ -312,4 +314,28 @@ protected function getLayoutData() return array_merge($data, $extraData); } + + /** + * Method to validate a FormField object based on field data. + * + * @param mixed $value The optional value to use as the default for the field. + * @param string $group The optional dot-separated form group path on which to find the field. + * @param ?Registry $input An optional Registry object with the entire data set to validate + * against the entire form. + * + * @return \Joomla\CMS\Form\Validation\FieldValidationResponseInterface + * + * @throws \UnexpectedValueException When the field or a rule configuration is invalid + *@since 4.0.0 + */ + public function validate($value, $group = null, Registry $input = null) + { + $validationResults = parent::validate($value, $group, $input); + + $validationResults->addConstraint( + new FieldLengthConstraint($this, $value, $this->maxLength) + ); + + return $validationResults; + } } diff --git a/libraries/src/Form/Form.php b/libraries/src/Form/Form.php index 6680c8f7052f1..f7d06f8a20354 100644 --- a/libraries/src/Form/Form.php +++ b/libraries/src/Form/Form.php @@ -10,6 +10,14 @@ namespace Joomla\CMS\Form; use Joomla\CMS\Factory; +use Joomla\CMS\Form\Exception\ValidationException; +use Joomla\CMS\Form\Validation\Field\DisabledResponseField; +use Joomla\CMS\Form\Validation\Field\LegacyInvalidField; +use Joomla\CMS\Form\Validation\Field\LegacyValidField; +use Joomla\CMS\Form\Validation\Field\SetupFailedFieldResponse; +use Joomla\CMS\Form\Validation\FieldValidationResponseInterface; +use Joomla\CMS\Form\Validation\FormValidationResponse; +use Joomla\CMS\Form\Validation\FormValidationResponseInterface; use Joomla\CMS\Language\Text; use Joomla\CMS\Object\CMSObject; use Joomla\CMS\User\CurrentUserInterface; @@ -58,6 +66,14 @@ class Form implements CurrentUserInterface */ protected $errors = []; + /** + * Use modern error reporting in the object by returning a FormValidationResponse object instead of Exceptions. + * + * @var boolean + * @since __DEPLOY_VERSION__ + */ + protected $modernValidationResponse = false; + /** * The name of the form instance. * @@ -190,6 +206,16 @@ protected function bindLevel($group, $data) */ public function getErrors() { + if ($this->modernValidationResponse === true) { + throw new \BadMethodCallException( + sprintf( + '%1s should not be used when %2s::modernValidationResponse is enabled', + __METHOD__, + static::class + ) + ); + } + return $this->errors; } @@ -947,6 +973,21 @@ public function setFields(&$elements, $group = null, $replace = true, $fieldset return $return; } + /** + * Method to use modern exception reporting for the validate method. If used then validate will throw exceptions + * in catchable groups rather than catching them for the getErrors method + * + * @param boolean $value The value to set for the field. + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function setModernValidationResponse(bool $value): void + { + $this->modernValidationResponse = $value; + } + /** * Method to set the value of a field. If the field does not exist in the form then the method * will return false. @@ -982,7 +1023,7 @@ public function setValue($name, $group = null, $value = null) * @param array $data An array of field values to filter. * @param string $group The dot-separated form group path on which to filter the fields. * - * @return mixed Array or false. + * @return array|boolean array or false. * * @since 4.0.0 */ @@ -1062,9 +1103,12 @@ public function filter($data, $group = null) * @param string $group The optional dot-separated form group path on which to filter the * fields to be validated. * - * @return boolean True on success. + * @return FormValidationResponseInterface|bool Return type depends on {@link static::$modernValidationResponse} being + * set in the class * - * @since 1.7.0 + * @throws \UnexpectedValueException When the form xml is invalid + * @throws \InvalidArgumentException When no fields are found for the supplied form group + *@since 1.7.0 */ public function validate($data, $group = null) { @@ -1073,7 +1117,7 @@ public function validate($data, $group = null) throw new \UnexpectedValueException(sprintf('%s::%s `xml` is not an instance of SimpleXMLElement', \get_class($this), __METHOD__)); } - $return = true; + $validationResponse = new FormValidationResponse(); // Create an input registry object from the data to validate. $input = new Registry($data); @@ -1083,6 +1127,10 @@ public function validate($data, $group = null) if (!$fields) { // PANIC! + if ($this->modernValidationResponse) { + throw new \InvalidArgumentException(sprintf('There were no fields to validate for group %s', $group)); + } + return false; } @@ -1110,7 +1158,7 @@ public function validate($data, $group = null) // If the field is disabled but it is passed in the request this is invalid as disabled fields are not added to the request if ($disabled && $fieldExistsInRequestData) { - throw new \RuntimeException(Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', $fieldLabel)); + $validationResponse->addField(new DisabledResponseField($name, $group, $fieldLabel)); } // Get the field groups for the element. @@ -1122,22 +1170,48 @@ public function validate($data, $group = null) $fieldObj = $this->loadField($field, $attrGroup); - if ($fieldObj) { - $valid = $fieldObj->validate($input->get($key), $attrGroup, $input); - - // Check for an error. - if ($valid instanceof \Exception) { - $this->errors[] = $valid; - $return = false; + if ($fieldObj instanceof FormField) { + $fieldValidationResponse = $fieldObj->validate($input->get($key), $attrGroup, $input); + + if ($fieldValidationResponse instanceof \Exception) { + $validationResponse->addField(new LegacyInvalidField($name, $group, $fieldLabel, $fieldValidationResponse)); + @trigger_error(sprintf('From 7.0 fields must return a class implementing %s.', FieldValidationResponseInterface::class), E_USER_DEPRECATED); + } elseif ($fieldValidationResponse === true) { + $validationResponse->addField(new LegacyValidField($name, $group, $fieldLabel)); + @trigger_error(sprintf('From 7.0 fields must return a class implementing %s.', FieldValidationResponseInterface::class), E_USER_DEPRECATED); + } elseif ($fieldValidationResponse instanceof FieldValidationResponseInterface) { + $validationResponse->addField($fieldValidationResponse); } + + throw new \UnexpectedValueException( + sprintf('Unexpected response from %s::validate', $fieldObj::class) + ); } elseif ($input->exists($key)) { // The field returned false from setup and shouldn't be included in the page body - yet we received // a value for it. This is probably some sort of injection attack and should be rejected - $this->errors[] = new \RuntimeException(Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', $key)); + $validationResponse->addField(new SetupFailedFieldResponse($name, $group, $fieldLabel)); } } - return $return; + if (!$this->modernValidationResponse) { + $isValid = $validationResponse->isValid(); + + if (!$isValid) { + foreach ($validationResponse->getInvalidFields() as $invalidFieldName) { + $invalidField = $validationResponse->getField($invalidFieldName); + + foreach ($invalidField->getInvalidConstraints() as $invalidConstraintName) { + $this->errors[] = new \RuntimeException( + $invalidField->getConstraint($invalidConstraintName)->getErrorMessage() + ); + } + } + } + + return $validationResponse->isValid(); + } + + return $validationResponse; } /** @@ -1147,7 +1221,7 @@ public function validate($data, $group = null) * @param string $group The optional dot-separated form group path on which to filter the * fields to be validated. * - * @return mixed Array or false. + * @return array|boolean Array or false. * * @since 4.0.0 */ diff --git a/libraries/src/Form/FormField.php b/libraries/src/Form/FormField.php index a9f8e6f445a1e..e9763ba643bd6 100644 --- a/libraries/src/Form/FormField.php +++ b/libraries/src/Form/FormField.php @@ -12,6 +12,10 @@ use Joomla\CMS\Factory; use Joomla\CMS\Filter\InputFilter; use Joomla\CMS\Form\Field\SubformField; +use Joomla\CMS\Form\Rule\FormRuleInterface; +use Joomla\CMS\Form\Constraint\FieldRequiredConstraint; +use Joomla\CMS\Form\Constraint\LegacyRuleConstraint; +use Joomla\CMS\Form\Validation\FieldValidationResponse; use Joomla\CMS\Language\Text; use Joomla\CMS\Layout\FileLayout; use Joomla\CMS\Log\Log; @@ -1155,11 +1159,10 @@ public function filter($value, $group = null, Registry $input = null) * @param ?Registry $input An optional Registry object with the entire data set to validate * against the entire form. * - * @return boolean|\Exception Boolean true if field value is valid, Exception on failure. + * @return Validation\FieldValidationResponseInterface * - * @since 4.0.0 - * @throws \InvalidArgumentException - * @throws \UnexpectedValueException + * @throws \UnexpectedValueException When the field or a rule configuration is invalid + *@since 4.0.0 */ public function validate($value, $group = null, Registry $input = null) { @@ -1168,29 +1171,17 @@ public function validate($value, $group = null, Registry $input = null) throw new \UnexpectedValueException(sprintf('%s::validate `element` is not an instance of SimpleXMLElement', \get_class($this))); } - $valid = true; + $response = new FieldValidationResponse($this->name, $this->group); + $valid = true; // Check if the field is required. $required = ((string) $this->element['required'] === 'true' || (string) $this->element['required'] === 'required'); - if ($this->element['label']) { - $fieldLabel = $this->element['label']; - - // Try to translate label if not set to false - $translate = (string) $this->element['translateLabel']; - - if (!($translate === 'false' || $translate === 'off' || $translate === '0')) { - $fieldLabel = Text::_($fieldLabel); - } - } else { - $fieldLabel = Text::_($this->element['name']); - } - // If the field is required and the value is empty return an error message. if ($required && (($value === '') || ($value === null))) { - $message = Text::sprintf('JLIB_FORM_VALIDATE_FIELD_REQUIRED', $fieldLabel); - - return new \RuntimeException($message); + $response->addConstraint(new FieldRequiredConstraint(false, $this)); + } elseif ($required) { + $response->addConstraint(new FieldRequiredConstraint(true, $this)); } // Get the field validation rule. @@ -1216,54 +1207,32 @@ public function validate($value, $group = null, Registry $input = null) $rule->setCurrentUser($this->getCurrentUser()); } - try { + if ($rule instanceof FormRuleInterface) { + $rule->test($this->element, $value, $group, $input, $this->form); + $response->addConstraint($rule); + } else { + @trigger_error( + sprintf('Rules should implement %s from 6.0.', FormRuleInterface::class), + E_USER_DEPRECATED + ); + // Run the field validation rule test. $valid = $rule->test($this->element, $value, $group, $input, $this->form); - } catch (\Exception $e) { - return $e; + $response->addConstraint(new LegacyRuleConstraint($valid, $this, $rule)); } } if ($valid !== false && $this instanceof SubformField) { - // Load the subform validation rule. + /** @var \Joomla\CMS\Form\Rule\SubformRule $rule */ $rule = FormHelper::loadRuleType('Subform'); - if ($rule instanceof DatabaseAwareInterface) { - try { - $rule->setDatabase($this->getDatabase()); - } catch (DatabaseNotFoundException $e) { - @trigger_error(sprintf('Database must be set, this will not be caught anymore in 5.0.'), E_USER_DEPRECATED); - $rule->setDatabase(Factory::getContainer()->get(DatabaseInterface::class)); - } - } - - if ($rule instanceof CurrentUserInterface) { - $rule->setCurrentUser($this->getCurrentUser()); - } - - try { - // Run the field validation rule test. - $valid = $rule->test($this->element, $value, $group, $input, $this->form); - } catch (\Exception $e) { - return $e; - } - } - - // Check if the field is valid. - if ($valid === false) { - // Does the field have a defined error message? - $message = (string) $this->element['message']; - - if ($message) { - $message = Text::_($this->element['message']); - } else { - $message = Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', $fieldLabel); - } + // Run the field validation rule test. + $rule->test($this->element, $value, $group, $input, $this->form); - return new \UnexpectedValueException($message); + $response->addConstraint($rule); } - return $valid; + return $response; } /** diff --git a/libraries/src/Form/FormRule.php b/libraries/src/Form/FormRule.php index 0214326571bb6..3f8faed2ec601 100644 --- a/libraries/src/Form/FormRule.php +++ b/libraries/src/Form/FormRule.php @@ -33,7 +33,12 @@ /** * Form Rule class for the Joomla Platform. * - * @since 1.6 + * @since 1.6 + * @deprecated 7.0 If you are using the test method for regex validation extend from + * {@link \Joomla\CMS\Form\Rule\AbstractRegexRule} or if you are creating a basic rule then implement + * {@link \Joomla\CMS\Form\Rule\FormRuleInterface}. The method signatures and designs have been + * slightly modified in order to allow the inclusion of extra constraints in the form package. View + * the developer documentation for full information. */ class FormRule { diff --git a/libraries/src/Form/Rule/AbstractRegexRule.php b/libraries/src/Form/Rule/AbstractRegexRule.php new file mode 100644 index 0000000000000..b4900ff84481b --- /dev/null +++ b/libraries/src/Form/Rule/AbstractRegexRule.php @@ -0,0 +1,84 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS\Form\Rule; + +\defined('_JEXEC') or die; + +use Joomla\CMS\Form\Form; +use Joomla\Registry\Registry; + +/** + * Class allowing easy implementation of regex rules for data validation. + * + * @since __DEPLOY_VERSION__ + */ +abstract class AbstractRegexRule implements FormRuleInterface +{ + use RuleConstraintTrait; + + /** + * The regular expression to use in testing a form field value. + * + * @var string + * @since __DEPLOY_VERSION__ + */ + protected string $regex; + + /** + * The regular expression modifiers to use when testing a form field value. + * + * @var string + * @since __DEPLOY_VERSION__ + */ + protected string $modifiers = ''; + + /** + * Method to test the value. + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form field object. + * @param mixed $value The form field value to validate. + * @param ?string $group The field name group control value. This acts as as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * @param ?Registry $input An optional Registry object with the entire data set to validate against the entire form. + * @param ?Form $form The form object for which the field is being tested. + * + * @return void + * + * @since 1.6 + * @throws \UnexpectedValueException If regex is invalid. + */ + public function test(\SimpleXMLElement $element, mixed $value, string $group = null, Registry $input = null, Form $form = null): void + { + $this->ruleRun = true; + + // Check for a valid regex. + if (empty($this->regex)) { + throw new \UnexpectedValueException(sprintf('%s has invalid regex.', \get_class($this))); + } + + // Detect if we have full UTF-8 and unicode PCRE support. + static $unicodePropertiesSupport = null; + + if ($unicodePropertiesSupport === null) { + $unicodePropertiesSupport = (bool) @preg_match('/\pL/u', 'a'); + } + + // Add unicode property support if available. + if ($unicodePropertiesSupport) { + $this->modifiers = (strpos($this->modifiers, 'u') !== false) ? $this->modifiers : $this->modifiers . 'u'; + } + + // Test the value against the regular expression. + if (preg_match(\chr(1) . $this->regex . \chr(1) . $this->modifiers, $value)) { + $this->isValid = true; + } + } +} diff --git a/libraries/src/Form/Rule/BooleanRule.php b/libraries/src/Form/Rule/BooleanRule.php index 302c36c641509..813e1dcfe2d40 100644 --- a/libraries/src/Form/Rule/BooleanRule.php +++ b/libraries/src/Form/Rule/BooleanRule.php @@ -9,8 +9,6 @@ namespace Joomla\CMS\Form\Rule; -use Joomla\CMS\Form\FormRule; - // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; // phpcs:enable PSR1.Files.SideEffects @@ -20,7 +18,7 @@ * * @since 1.7.0 */ -class BooleanRule extends FormRule +class BooleanRule extends AbstractRegexRule { /** * The regular expression to use in testing a form field value. @@ -28,7 +26,7 @@ class BooleanRule extends FormRule * @var string * @since 1.7.0 */ - protected $regex = '^(?:[01]|true|false)$'; + protected string $regex = '^(?:[01]|true|false)$'; /** * The regular expression modifiers to use when testing a form field value. @@ -36,5 +34,17 @@ class BooleanRule extends FormRule * @var string * @since 1.7.0 */ - protected $modifiers = 'i'; + protected string $modifiers = 'i'; + + /** + * Name of the constraint - note this is for machine access and should be unique for a form field. + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + public function getName(): string + { + return 'booleanRule'; + } } diff --git a/libraries/src/Form/Rule/FormRuleInterface.php b/libraries/src/Form/Rule/FormRuleInterface.php new file mode 100644 index 0000000000000..262c69d801dde --- /dev/null +++ b/libraries/src/Form/Rule/FormRuleInterface.php @@ -0,0 +1,44 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS\Form\Rule; + +use Joomla\CMS\Form\Constraint\ConstraintInterface; +use Joomla\CMS\Form\Form; +use Joomla\Registry\Registry; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * Interface for a rule class. + * + * @since __DEPLOY_VERSION__ + */ +interface FormRuleInterface extends ConstraintInterface +{ + /** + * Method to test the value. + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * @param ?Registry $input An optional Registry object with the entire data set to validate against the entire form. + * @param ?Form $form The form object for which the field is being tested. + * + * @return void + * + * @since __DEPLOY_VERSION__ + * @throws \UnexpectedValueException if rule is invalid. + */ + public function test(\SimpleXMLElement $element, $value, $group = null, Registry $input = null, Form $form = null): void; +} diff --git a/libraries/src/Form/Rule/PasswordRule.php b/libraries/src/Form/Rule/PasswordRule.php index 27c516bfaa67d..e7ff0765a8335 100644 --- a/libraries/src/Form/Rule/PasswordRule.php +++ b/libraries/src/Form/Rule/PasswordRule.php @@ -12,7 +12,6 @@ use Joomla\CMS\Component\ComponentHelper; use Joomla\CMS\Factory; use Joomla\CMS\Form\Form; -use Joomla\CMS\Form\FormRule; use Joomla\CMS\Language\Text; use Joomla\Registry\Registry; @@ -25,8 +24,10 @@ * * @since 3.1.2 */ -class PasswordRule extends FormRule +class PasswordRule implements FormRuleInterface { + use RuleConstraintTrait; + /** * Method to test if two values are not equal. To use this rule, the form * XML needs a validate attribute of equals and a field attribute @@ -40,13 +41,13 @@ class PasswordRule extends FormRule * @param ?Registry $input An optional Registry object with the entire data set to validate against the entire form. * @param ?Form $form The form object for which the field is being tested. * - * @return boolean True if the value is valid, false otherwise. + * @return void * * @since 3.1.2 * @throws \InvalidArgumentException * @throws \UnexpectedValueException */ - public function test(\SimpleXMLElement $element, $value, $group = null, Registry $input = null, Form $form = null) + public function test(\SimpleXMLElement $element, $value, $group = null, Registry $input = null, Form $form = null): void { $meter = isset($element['strengthmeter']) ? ' meter="0"' : '1'; $threshold = isset($element['threshold']) ? (int) $element['threshold'] : 66; @@ -86,32 +87,28 @@ public function test(\SimpleXMLElement $element, $value, $group = null, Registry } // If the field is empty and not required, the field is valid. - $required = ((string) $element['required'] === 'true' || (string) $element['required'] === 'required'); + $required = ((string) $element['required'] === 'true' || (string) $element['required'] === 'required'); + $this->isValid = true; if (!$required && empty($value)) { - return true; + return; } - $valueLength = \strlen($value); + // Set a variable to check if any errors are made in password + $valueLength = \strlen($value); // We set a maximum length to prevent abuse since it is unfiltered. if ($valueLength > 4096) { - Factory::getApplication()->enqueueMessage(Text::_('JFIELD_PASSWORD_TOO_LONG'), 'error'); + $this->errorMessage = Text::_('JFIELD_PASSWORD_TOO_LONG'); + $this->isValid = false; } // We don't allow white space inside passwords $valueTrim = trim($value); - // Set a variable to check if any errors are made in password - $validPassword = true; - if (\strlen($valueTrim) !== $valueLength) { - Factory::getApplication()->enqueueMessage( - Text::_('JFIELD_PASSWORD_SPACES_IN_PASSWORD'), - 'error' - ); - - $validPassword = false; + $this->errorMessage = Text::_('JFIELD_PASSWORD_SPACES_IN_PASSWORD'); + $this->isValid = false; } // Minimum number of integers required @@ -119,12 +116,8 @@ public function test(\SimpleXMLElement $element, $value, $group = null, Registry $nInts = preg_match_all('/[0-9]/', $value, $imatch); if ($nInts < $minimumIntegers) { - Factory::getApplication()->enqueueMessage( - Text::plural('JFIELD_PASSWORD_NOT_ENOUGH_INTEGERS_N', $minimumIntegers), - 'error' - ); - - $validPassword = false; + $this->errorMessage = Text::plural('JFIELD_PASSWORD_NOT_ENOUGH_INTEGERS_N', $minimumIntegers); + $this->isValid = false; } } @@ -133,12 +126,8 @@ public function test(\SimpleXMLElement $element, $value, $group = null, Registry $nsymbols = preg_match_all('[\W]', $value, $smatch); if ($nsymbols < $minimumSymbols) { - Factory::getApplication()->enqueueMessage( - Text::plural('JFIELD_PASSWORD_NOT_ENOUGH_SYMBOLS_N', $minimumSymbols), - 'error' - ); - - $validPassword = false; + $this->errorMessage = Text::plural('JFIELD_PASSWORD_NOT_ENOUGH_SYMBOLS_N', $minimumSymbols); + $this->isValid = false; } } @@ -147,12 +136,8 @@ public function test(\SimpleXMLElement $element, $value, $group = null, Registry $nUppercase = preg_match_all('/[A-Z]/', $value, $umatch); if ($nUppercase < $minimumUppercase) { - Factory::getApplication()->enqueueMessage( - Text::plural('JFIELD_PASSWORD_NOT_ENOUGH_UPPERCASE_LETTERS_N', $minimumUppercase), - 'error' - ); - - $validPassword = false; + $this->errorMessage = Text::plural('JFIELD_PASSWORD_NOT_ENOUGH_UPPERCASE_LETTERS_N', $minimumUppercase); + $this->isValid = false; } } @@ -161,32 +146,29 @@ public function test(\SimpleXMLElement $element, $value, $group = null, Registry $nLowercase = preg_match_all('/[a-z]/', $value, $umatch); if ($nLowercase < $minimumLowercase) { - Factory::getApplication()->enqueueMessage( - Text::plural('JFIELD_PASSWORD_NOT_ENOUGH_LOWERCASE_LETTERS_N', $minimumLowercase), - 'error' - ); - - $validPassword = false; + $this->errorMessage = Text::plural('JFIELD_PASSWORD_NOT_ENOUGH_LOWERCASE_LETTERS_N', $minimumLowercase); + $this->isValid = false; } } // Minimum length option if (!empty($minimumLength)) { if (\strlen((string) $value) < $minimumLength) { - Factory::getApplication()->enqueueMessage( - Text::plural('JFIELD_PASSWORD_TOO_SHORT_N', $minimumLength), - 'error' - ); - - $validPassword = false; + $this->errorMessage = Text::plural('JFIELD_PASSWORD_TOO_SHORT_N', $minimumLength); + $this->isValid = false; } } + } - // If valid has violated any rules above return false. - if (!$validPassword) { - return false; - } - - return true; + /** + * Name of the constraint - note this is for machine access and should be unique for a form field. + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + public function getName(): string + { + return 'passwordRule'; } } diff --git a/libraries/src/Form/Rule/RuleConstraintTrait.php b/libraries/src/Form/Rule/RuleConstraintTrait.php new file mode 100644 index 0000000000000..7ef715874e2e9 --- /dev/null +++ b/libraries/src/Form/Rule/RuleConstraintTrait.php @@ -0,0 +1,75 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS\Form\Rule; + +/** + * Helps rules to implement {@link \Joomla\CMS\Form\Constraint\ConstraintInterface} + * + * @since __DEPLOY_VERSION__ + */ +trait RuleConstraintTrait +{ + /** + * The regular expression to use in testing a form field value. + * + * @var bool + * @since __DEPLOY_VERSION__ + */ + protected bool $isValid = false; + + /** + * An error message to display to the user. + * + * @var string + * @since __DEPLOY_VERSION__ + */ + protected string $errorMessage; + + /** + * Has the rule been run? + * + * @var bool + * @since __DEPLOY_VERSION__ + */ + protected bool $ruleRun = false; + + /** + * Was the data validated in the constraint. + * + * @return bool + * + * @since __DEPLOY_VERSION__ + */ + public function isValid(): bool + { + return $this->isValid; + } + + /** + * Get an error message that can be displayed to a user about how to remediate the constraint. + * + * @return string + * + * @since __DEPLOY_VERSION__ + * @throws \BadMethodCallException When the constraint is valid + */ + public function getErrorMessage(): string + { + if (!$this->ruleRun) { + throw new \BadMethodCallException(sprintf('The %s::test() method must be run', static::class)); + } + + if ($this->isValid) { + throw new \BadMethodCallException('The rule was valid'); + } + + return $this->errorMessage; + } +} diff --git a/libraries/src/Form/Rule/SubformRule.php b/libraries/src/Form/Rule/SubformRule.php index 6a822763a0c89..40920df5c7612 100644 --- a/libraries/src/Form/Rule/SubformRule.php +++ b/libraries/src/Form/Rule/SubformRule.php @@ -11,7 +11,6 @@ use Joomla\CMS\Form\Field\SubformField; use Joomla\CMS\Form\Form; -use Joomla\CMS\Form\FormRule; use Joomla\Registry\Registry; // phpcs:disable PSR1.Files.SideEffects @@ -23,8 +22,10 @@ * * @since 3.9.7 */ -class SubformRule extends FormRule +class SubformRule implements FormRuleInterface { + use RuleConstraintTrait; + /** * Method to test given values for a subform.. * @@ -36,21 +37,23 @@ class SubformRule extends FormRule * @param ?Registry $input An optional Registry object with the entire data set to validate against the entire form. * @param ?Form $form The form object for which the field is being tested. * - * @return boolean True if the value is valid, false otherwise. + * @return void * * @since 3.9.7 */ - public function test(\SimpleXMLElement $element, $value, $group = null, Registry $input = null, Form $form = null) + public function test(\SimpleXMLElement $element, $value, $group = null, Registry $input = null, Form $form = null): void { // Get the form field object. - $field = $form->getField($element['name'], $group); + $field = $form->getField($element['name'], $group); + $this->isValid = true; + $this->errorMessage = ''; if (!($field instanceof SubformField)) { throw new \UnexpectedValueException(sprintf('%s is no subform field.', $element['name'])); } if ($value === null) { - return true; + return; } $subForm = $field->loadSubForm(); @@ -60,29 +63,37 @@ public function test(\SimpleXMLElement $element, $value, $group = null, Registry foreach ($value as $row) { if ($subForm->validate($row) === false) { // Pass the first error that occurred on the subform validation. - $errors = $subForm->getErrors(); + $errors = $subForm->getErrors(); + $this->isValid = false; if (!empty($errors[0])) { - return $errors[0]; + $this->errorMessage = $errors[0]; } - - return false; } } } else { // Single value. if ($subForm->validate($value) === false) { // Pass the first error that occurred on the subform validation. - $errors = $subForm->getErrors(); + $errors = $subForm->getErrors(); + $this->isValid = false; if (!empty($errors[0])) { - return $errors[0]; + $this->errorMessage = $errors[0]; } - - return false; } } + } - return true; + /** + * Name of the constraint - note this is for machine access and should be unique for a form field. + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + public function getName(): string + { + return 'subformRule'; } } diff --git a/libraries/src/Form/Validation/Field/DisabledResponseField.php b/libraries/src/Form/Validation/Field/DisabledResponseField.php new file mode 100644 index 0000000000000..fbc111fcdbe33 --- /dev/null +++ b/libraries/src/Form/Validation/Field/DisabledResponseField.php @@ -0,0 +1,196 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS\Form\Validation\Field; + +use Joomla\CMS\Form\Constraint\ConstraintInterface; +use Joomla\CMS\Form\Validation\FieldValidationResponseInterface; +use Joomla\CMS\Language\Text; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * Checks that a field that is disabled doesn't exist in the form data + * + * @since __DEPLOY_VERSION__ + */ +class DisabledResponseField implements FieldValidationResponseInterface +{ + /** + * The name of the field + * + * @var string + * @since __DEPLOY_VERSION__ + */ + private string $name = ''; + + /** + * The group of the field + * + * @var string + * @since __DEPLOY_VERSION__ + */ + private string $group = ''; + + /** + * Field label for the error message + * + * @var string + * @since __DEPLOY_VERSION__ + */ + private string $fieldLabel; + + /** + * Method to instantiate the object. + * + * @param string $fieldLabel The human friendly field label. + * @param string $name The name of the field. + * @param string $group The group of the field. + * + * @since __DEPLOY_VERSION__ + */ + public function __construct(string $name, string $group, string $fieldLabel) + { + $this->name = $name; + $this->group = $group; + $this->fieldLabel = $fieldLabel; + } + + /** + * Count constraints tested + * + * @return int<0,max> The custom count as an integer. + * + * @since __DEPLOY_VERSION__ + */ + public function count(): int + { + return 1; + } + + /** + * Was the field data valid or not. If there are no constraints on the field this should return true. + * + * @return bool + * + * @since __DEPLOY_VERSION__ + */ + public function isValid(): bool + { + return false; + } + + /** + * List of validation properties tested that returned a negative result + * + * @return string[] + * + * @since __DEPLOY_VERSION__ + */ + public function getInvalidConstraints(): array + { + return ['disabledFieldInRequest']; + } + + /** + * Method to get a constraint result for the field. + * + * @param string $name The name of the property + * + * @return ConstraintInterface + * + * @since __DEPLOY_VERSION__ + */ + public function getConstraint(string $name): ConstraintInterface + { + return new class ($this->fieldLabel) implements ConstraintInterface { + /** + * Field label for the error message + * + * @var string + * @since __DEPLOY_VERSION__ + */ + private string $fieldLabel; + + /** + * Method to instantiate the object. + * + * @param string $fieldLabel The human friendly field label. + * + * @since __DEPLOY_VERSION__ + */ + public function __construct(string $fieldLabel) + { + $this->fieldLabel = $fieldLabel; + } + + /** + * Was the data validated in the constraint. + * + * @return bool + * + * @since __DEPLOY_VERSION__ + */ + public function isValid(): bool + { + return false; + } + + /** + * Name of the constraint - note this is for machine access and should be unique for a form field. + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + public function getName(): string + { + return 'disabledFieldInRequest'; + } + + /** + * Get an error message that can be displayed to a user about how to remediate the constraint. + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + public function getErrorMessage(): string + { + return Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', $this->fieldLabel); + } + }; + } + + /** + * Name of the field. + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + public function getName(): string + { + return $this->name; + } + + /** + * Group of the field. + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + public function getGroup(): string + { + return $this->group; + } +} diff --git a/libraries/src/Form/Validation/Field/LegacyInvalidField.php b/libraries/src/Form/Validation/Field/LegacyInvalidField.php new file mode 100644 index 0000000000000..fb4c3846cd8f8 --- /dev/null +++ b/libraries/src/Form/Validation/Field/LegacyInvalidField.php @@ -0,0 +1,218 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS\Form\Validation\Field; + +use Joomla\CMS\Form\Constraint\ConstraintInterface; +use Joomla\CMS\Form\Validation\FieldValidationResponseInterface; +use Joomla\CMS\Language\Text; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * Temporary class to handle fields that return \Exceptions for compatibility with old validation responses + * + * @since __DEPLOY_VERSION__ + * @deprecated 7.0 Migrate the response of form fields to {@link \Joomla\CMS\Form\Validation\FieldValidationResponse} + * and constraints. + */ +class LegacyInvalidField implements FieldValidationResponseInterface +{ + /** + * The name of the field + * + * @var string + * @since __DEPLOY_VERSION__ + */ + private string $name = ''; + + /** + * The group of the field + * + * @var string + * @since __DEPLOY_VERSION__ + */ + private string $group = ''; + + /** + * Field label for the error message + * + * @var string + * @since __DEPLOY_VERSION__ + */ + private string $fieldLabel; + + /** + * Validation exception + * + * @var \Exception + * @since __DEPLOY_VERSION__ + */ + private \Exception $validationException; + + /** + * Method to instantiate the object. + * + * @param string $fieldLabel The human friendly field label. + * @param string $name The name of the field. + * @param string $group The group of the field. + * @param \Exception $validationException The exception thrown by the validate method. + * + * @since __DEPLOY_VERSION__ + */ + public function __construct(string $name, string $group, string $fieldLabel, \Exception $validationException) + { + $this->name = $name; + $this->group = $group; + $this->fieldLabel = $fieldLabel; + $this->validationException = $validationException; + } + + /** + * Count constraints tested. Note for this class specifically then this cannot be relied on!! + * + * @return int<0,max> The custom count as an integer. + * + * @since __DEPLOY_VERSION__ + */ + public function count(): int + { + return 1; + } + + /** + * Was the field data valid or not. If there are no constraints on the field this should return true. + * + * @return bool + * + * @since __DEPLOY_VERSION__ + */ + public function isValid(): bool + { + return false; + } + + /** + * List of validation properties tested that returned a negative result + * + * @return string[] + * + * @since __DEPLOY_VERSION__ + */ + public function getInvalidConstraints(): array + { + return ['invalidLegacyFieldResponse']; + } + + /** + * Method to get a constraint result for the field. + * + * @param string $name The name of the property + * + * @return ConstraintInterface + * + * @since __DEPLOY_VERSION__ + */ + public function getConstraint(string $name): ConstraintInterface + { + return new class ($this->fieldLabel, $this->validationException) implements ConstraintInterface { + /** + * Field label for the error message + * + * @var string + * @since __DEPLOY_VERSION__ + */ + private string $fieldLabel; + + /** + * Validation exception + * + * @var \Exception + * @since __DEPLOY_VERSION__ + */ + private \Exception $validationException; + + /** + * Method to instantiate the object. + * + * @param string $fieldLabel The human friendly field label. + * @param \Exception $validationException The human friendly field label. + * + * @since __DEPLOY_VERSION__ + */ + public function __construct(string $fieldLabel, \Exception $validationException) + { + $this->fieldLabel = $fieldLabel; + $this->validationException = $validationException; + } + + /** + * Was the data validated in the constraint. + * + * @return bool + * + * @since __DEPLOY_VERSION__ + */ + public function isValid(): bool + { + return false; + } + + /** + * Name of the constraint - note this is for machine access and should be unique for a form field. + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + public function getName(): string + { + return 'invalidLegacyFieldResponse'; + } + + /** + * Get an error message that can be displayed to a user about how to remediate the constraint. + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + public function getErrorMessage(): string + { + return $this->validationException->getMessage(); + } + }; + } + + /** + * Name of the field. + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + public function getName(): string + { + return $this->name; + } + + /** + * Group of the field. + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + public function getGroup(): string + { + return $this->group; + } +} diff --git a/libraries/src/Form/Validation/Field/LegacyValidField.php b/libraries/src/Form/Validation/Field/LegacyValidField.php new file mode 100644 index 0000000000000..3c031e5c5a315 --- /dev/null +++ b/libraries/src/Form/Validation/Field/LegacyValidField.php @@ -0,0 +1,197 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS\Form\Validation\Field; + +use Joomla\CMS\Form\Constraint\ConstraintInterface; +use Joomla\CMS\Form\Validation\FieldValidationResponseInterface; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * Temporary class to handle fields that return boolean true for compatibility with old validation responses + * + * @since __DEPLOY_VERSION__ + * @deprecated 7.0 Migrate the response of form fields to {@link \Joomla\CMS\Form\Validation\FieldValidationResponse} + * and constraints. + */ +class LegacyValidField implements FieldValidationResponseInterface +{ + /** + * The name of the field + * + * @var string + * @since __DEPLOY_VERSION__ + */ + private string $name = ''; + + /** + * The group of the field + * + * @var string + * @since __DEPLOY_VERSION__ + */ + private string $group = ''; + + /** + * Field label for the error message + * + * @var string + * @since __DEPLOY_VERSION__ + */ + private string $fieldLabel; + + /** + * Method to instantiate the object. + * + * @param string $fieldLabel The human friendly field label. + * @param string $name The name of the field. + * @param string $group The group of the field. + * + * @since __DEPLOY_VERSION__ + */ + public function __construct(string $name, string $group, string $fieldLabel) + { + $this->name = $name; + $this->group = $group; + $this->fieldLabel = $fieldLabel; + } + + /** + * Count constraints tested. Note for this class specifically then this cannot be relied on!! + * + * @return int<0,max> The custom count as an integer. + * + * @since __DEPLOY_VERSION__ + */ + public function count(): int + { + return 1; + } + + /** + * Was the field data valid or not. If there are no constraints on the field this should return true. + * + * @return bool + * + * @since __DEPLOY_VERSION__ + */ + public function isValid(): bool + { + return true; + } + + /** + * List of validation properties tested that returned a negative result + * + * @return string[] + * + * @since __DEPLOY_VERSION__ + */ + public function getInvalidConstraints(): array + { + return []; + } + + /** + * Method to get a constraint result for the field. + * + * @param string $name The name of the property + * + * @return ConstraintInterface + * + * @since __DEPLOY_VERSION__ + */ + public function getConstraint(string $name): ConstraintInterface + { + return new class ($this->fieldLabel) implements ConstraintInterface { + /** + * Field label for the error message + * + * @var string + * @since __DEPLOY_VERSION__ + */ + private string $fieldLabel; + + /** + * Method to instantiate the object. + * + * @param string $fieldLabel The human friendly field label. + * + * @since __DEPLOY_VERSION__ + */ + public function __construct(string $fieldLabel) + { + $this->fieldLabel = $fieldLabel; + } + + /** + * Was the data validated in the constraint. + * + * @return bool + * + * @since __DEPLOY_VERSION__ + */ + public function isValid(): bool + { + return true; + } + + /** + * Name of the constraint - note this is for machine access and should be unique for a form field. + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + public function getName(): string + { + return 'validLegacyFieldResponse'; + } + + /** + * Get an error message that can be displayed to a user about how to remediate the constraint. + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + public function getErrorMessage(): string + { + throw new \BadMethodCallException(sprintf('Field %s is valid', $this->fieldLabel)); + } + }; + } + + /** + * Name of the field. + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + public function getName(): string + { + return $this->name; + } + + /** + * Group of the field. + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + public function getGroup(): string + { + return $this->group; + } +} diff --git a/libraries/src/Form/Validation/Field/SetupFailedFieldResponse.php b/libraries/src/Form/Validation/Field/SetupFailedFieldResponse.php new file mode 100644 index 0000000000000..40aa3b9921b8a --- /dev/null +++ b/libraries/src/Form/Validation/Field/SetupFailedFieldResponse.php @@ -0,0 +1,196 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS\Form\Validation\Field; + +use Joomla\CMS\Form\Constraint\ConstraintInterface; +use Joomla\CMS\Form\Validation\FieldValidationResponseInterface; +use Joomla\CMS\Language\Text; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * Checks that a field that is disabled doesn't exist in the form data + * + * @since __DEPLOY_VERSION__ + */ +class SetupFailedFieldResponse implements FieldValidationResponseInterface +{ + /** + * The name of the field + * + * @var string + * @since __DEPLOY_VERSION__ + */ + private string $name = ''; + + /** + * The group of the field + * + * @var string + * @since __DEPLOY_VERSION__ + */ + private string $group = ''; + + /** + * Field label for the error message + * + * @var string + * @since __DEPLOY_VERSION__ + */ + private string $fieldLabel; + + /** + * Method to instantiate the object. + * + * @param string $fieldLabel The human friendly field label. + * @param string $name The name of the field. + * @param string $group The group of the field. + * + * @since __DEPLOY_VERSION__ + */ + public function __construct(string $name, string $group, string $fieldLabel) + { + $this->name = $name; + $this->group = $group; + $this->fieldLabel = $fieldLabel; + } + + /** + * Count constraints tested + * + * @return int<0,max> The custom count as an integer. + * + * @since __DEPLOY_VERSION__ + */ + public function count(): int + { + return 1; + } + + /** + * Was the field data valid or not. If there are no constraints on the field this should return true. + * + * @return bool + * + * @since __DEPLOY_VERSION__ + */ + public function isValid(): bool + { + return false; + } + + /** + * List of validation properties tested that returned a negative result + * + * @return string[] + * + * @since __DEPLOY_VERSION__ + */ + public function getInvalidConstraints(): array + { + return ['setupFailedFieldInRequest']; + } + + /** + * Method to get a constraint result for the field. + * + * @param string $name The name of the property + * + * @return ConstraintInterface + * + * @since __DEPLOY_VERSION__ + */ + public function getConstraint(string $name): ConstraintInterface + { + return new class ($this->fieldLabel) implements ConstraintInterface { + /** + * Field label for the error message + * + * @var string + * @since __DEPLOY_VERSION__ + */ + private string $fieldLabel; + + /** + * Method to instantiate the object. + * + * @param string $fieldLabel The human friendly field label. + * + * @since __DEPLOY_VERSION__ + */ + public function __construct(string $fieldLabel) + { + $this->fieldLabel = $fieldLabel; + } + + /** + * Was the data validated in the constraint. + * + * @return bool + * + * @since __DEPLOY_VERSION__ + */ + public function isValid(): bool + { + return false; + } + + /** + * Name of the constraint - note this is for machine access and should be unique for a form field. + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + public function getName(): string + { + return 'setupFailedFieldInRequest'; + } + + /** + * Get an error message that can be displayed to a user about how to remediate the constraint. + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + public function getErrorMessage(): string + { + return Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', $this->fieldLabel); + } + }; + } + + /** + * Name of the field. + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + public function getName(): string + { + return $this->name; + } + + /** + * Group of the field. + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + public function getGroup(): string + { + return $this->group; + } +} diff --git a/libraries/src/Form/Validation/FieldValidationResponse.php b/libraries/src/Form/Validation/FieldValidationResponse.php new file mode 100644 index 0000000000000..8e5430dacab5c --- /dev/null +++ b/libraries/src/Form/Validation/FieldValidationResponse.php @@ -0,0 +1,171 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS\Form\Validation; + +// phpcs:disable PSR1.Files.SideEffects +use Joomla\CMS\Form\Constraint\ConstraintInterface; + +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * Base class for implementing a ConstraintValidationInterface + * + * @since __DEPLOY_VERSION__ + */ +class FieldValidationResponse implements FieldValidationResponseInterface +{ + /** + * The name of the field + * + * @var string + * @since __DEPLOY_VERSION__ + */ + private string $name = ''; + + /** + * The group of the field + * + * @var string + * @since __DEPLOY_VERSION__ + */ + private string $group = ''; + + /** + * Is the field valid? + * + * @var bool + * @since __DEPLOY_VERSION__ + */ + private bool $valid = true; + + /** + * List of constraints tested + * + * @var ConstraintInterface[] + * @since __DEPLOY_VERSION__ + */ + private array $constraints = []; + + /** + * List of invalid constraints + * + * @var string[] + * @since __DEPLOY_VERSION__ + */ + private array $invalidConstraints = []; + + /** + * Method to instantiate the object. + * + * @param string $name The name of the field. + * @param string $group The group of the field. + * + * @since __DEPLOY_VERSION__ + */ + public function __construct(string $name, string $group) + { + $this->name = $name; + $this->group = $group; + } + + /** + * Was the field data valid or not. If there are no constraints on the field this should return true. + * + * @return bool + * + * @since __DEPLOY_VERSION__ + */ + public function isValid(): bool + { + return $this->valid; + } + + /** + * List of constraints that returned a negative result. + * + * @return string[] + * + * @since __DEPLOY_VERSION__ + */ + public function getInvalidConstraints(): array + { + return $this->invalidConstraints; + } + + /** + * Method to get a constraint result for the field. + * + * @param string $name The name of the property + * + * @return ConstraintInterface + * + * @since __DEPLOY_VERSION__ + */ + public function getConstraint(string $name): ConstraintInterface + { + return $this->constraints[$name]; + } + + /** + * Adds the result of a constraint to the field validation result. + * + * @param ConstraintInterface $constraint The constraint to add + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function addConstraint(ConstraintInterface $constraint): void + { + if (!$constraint->isValid()) { + $this->valid = false; + $this->invalidConstraints[] = $constraint->getName(); + } + + $this->constraints[$constraint->getName()] = $constraint; + } + + /** + * Count the number of constraints checked on the field + * + * @return int<0,max> The custom count as an integer. + * + * @since __DEPLOY_VERSION__ + */ + public function count(): int + { + return \count($this->constraints); + } + + /** + * Name of the field. + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + public function getName(): string + { + return $this->name; + } + + /** + * Group of the field. + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + public function getGroup(): string + { + return $this->group; + } +} diff --git a/libraries/src/Form/Validation/FieldValidationResponseInterface.php b/libraries/src/Form/Validation/FieldValidationResponseInterface.php new file mode 100644 index 0000000000000..472d4c7fb1843 --- /dev/null +++ b/libraries/src/Form/Validation/FieldValidationResponseInterface.php @@ -0,0 +1,71 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS\Form\Validation; + +// phpcs:disable PSR1.Files.SideEffects +use Joomla\CMS\Form\Constraint\ConstraintInterface; + +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * Interface to get the validation information about a form field + * + * @since __DEPLOY_VERSION__ + */ +interface FieldValidationResponseInterface extends \Countable +{ + /** + * Was the field data valid or not. If there are no constraints on the field this should return true. + * + * @return bool + * + * @since __DEPLOY_VERSION__ + */ + public function isValid(): bool; + + /** + * List of constraints that returned a negative result. + * + * @return string[] + * + * @since __DEPLOY_VERSION__ + */ + public function getInvalidConstraints(): array; + + /** + * Method to get a constraint result for the field. + * + * @param string $name The name of the property + * + * @return ConstraintInterface + * + * @since __DEPLOY_VERSION__ + */ + public function getConstraint(string $name): ConstraintInterface; + + /** + * Name of the field. + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + public function getName(): string; + + /** + * Group of the field. + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + public function getGroup(): string; +} diff --git a/libraries/src/Form/Validation/FormValidationResponse.php b/libraries/src/Form/Validation/FormValidationResponse.php new file mode 100644 index 0000000000000..ca18af4a56461 --- /dev/null +++ b/libraries/src/Form/Validation/FormValidationResponse.php @@ -0,0 +1,115 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS\Form\Validation; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * Base class for implementing a ValidationResponseInterface + * + * @since __DEPLOY_VERSION__ + */ +class FormValidationResponse implements FormValidationResponseInterface +{ + /** + * Is the form valid? + * + * @var bool + * @since __DEPLOY_VERSION__ + */ + private bool $valid = true; + + /** + * List of fields + * + * @var FieldValidationResponseInterface[] + * @since __DEPLOY_VERSION__ + */ + private array $fields = []; + + /** + * List of invalid fields + * + * @var string[] + * @since __DEPLOY_VERSION__ + */ + private array $invalidFields = []; + + /** + * Was the field data valid or not. If there are no constraints on the field this should return true. + * + * @return bool + * + * @since __DEPLOY_VERSION__ + */ + public function isValid(): bool + { + return $this->valid; + } + + /** + * List of validation properties tested that returned a negative result + * + * @return string[] + * + * @since __DEPLOY_VERSION__ + */ + public function getInvalidFields(): array + { + return $this->invalidFields; + } + + /** + * Method to get a constraint result for the field. + * + * @param string $name The name of the field + * + * @return FieldValidationResponseInterface + * + * @since __DEPLOY_VERSION__ + */ + public function getField(string $name): FieldValidationResponseInterface + { + return $this->fields[$name]; + } + + /** + * Adds the result of a constraint to the field validation result. + * + * @param FieldValidationResponseInterface $field The field to add + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function addField(FieldValidationResponseInterface $field): void + { + if (!$field->isValid()) { + $this->valid = false; + $this->invalidFields[] = $field->getGroup() . '.' . $field->getName(); + } + + $this->fields[$field->getGroup() . '.' . $field->getName()] = $field; + } + + /** + * Count the number of fields validated + * + * @return int<0,max> The custom count as an integer. + * + * @since __DEPLOY_VERSION__ + */ + public function count(): int + { + return \count($this->fields); + } +} diff --git a/libraries/src/Form/Validation/FormValidationResponseInterface.php b/libraries/src/Form/Validation/FormValidationResponseInterface.php new file mode 100644 index 0000000000000..4857f09bec9d3 --- /dev/null +++ b/libraries/src/Form/Validation/FormValidationResponseInterface.php @@ -0,0 +1,54 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS\Form\Validation; + +use Joomla\CMS\Form\Validation; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * Interface to get the validation information about a form. + * + * @since __DEPLOY_VERSION__ + */ +interface FormValidationResponseInterface extends \Countable +{ + /** + * Was the data submitted to the form valid or not. + * + * @return bool + * + * @since __DEPLOY_VERSION__ + */ + public function isValid(): bool; + + /** + * List of invalid fields. The names here are not human friendly names but the location of fields that can be + * resolved by {@link Form::getField()} or through {@link static::getFieldValidation()}. + * + * @return string[] + * + * @since __DEPLOY_VERSION__ + */ + public function getInvalidFields(): array; + + /** + * Get the field validation result for a named field. + * + * @param string $name The name of the field to get the result for + * + * @return FieldValidationResponseInterface + * + * @since __DEPLOY_VERSION__ + */ + public function getField(string $name): FieldValidationResponseInterface; +} From 8a7820366c8be90d6ceea3820a2f90283761cf91 Mon Sep 17 00:00:00 2001 From: George Wilson Date: Sun, 4 Feb 2024 10:46:22 +0000 Subject: [PATCH 02/11] Update libraries/src/Form/Constraint/FieldLengthConstraint.php Co-authored-by: Brian Teeman --- libraries/src/Form/Constraint/FieldLengthConstraint.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/src/Form/Constraint/FieldLengthConstraint.php b/libraries/src/Form/Constraint/FieldLengthConstraint.php index 8268c56ac9c3d..90de8bc4ceb41 100644 --- a/libraries/src/Form/Constraint/FieldLengthConstraint.php +++ b/libraries/src/Form/Constraint/FieldLengthConstraint.php @@ -65,6 +65,6 @@ public function getErrorMessage(): string */ public function getName(): string { - return 'field-required'; + return 'field-length'; } } From 40c2af3d3cc165335e06cdf91aa0191f6d5d59f0 Mon Sep 17 00:00:00 2001 From: George Wilson Date: Sun, 4 Feb 2024 10:47:47 +0000 Subject: [PATCH 03/11] Update max length comment --- libraries/src/Form/Constraint/FieldLengthConstraint.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/src/Form/Constraint/FieldLengthConstraint.php b/libraries/src/Form/Constraint/FieldLengthConstraint.php index 90de8bc4ceb41..b6c7ea3d7f5e9 100644 --- a/libraries/src/Form/Constraint/FieldLengthConstraint.php +++ b/libraries/src/Form/Constraint/FieldLengthConstraint.php @@ -17,7 +17,7 @@ // phpcs:enable PSR1.Files.SideEffects /** - * Checks that a required field has been given a value + * Checks that a field's submitted length is less than it's configured maximum length * * @since __DEPLOY_VERSION__ */ From 5746f804315cb6fcc777a536014bbd0f3546f371 Mon Sep 17 00:00:00 2001 From: George Wilson Date: Sun, 25 Feb 2024 01:57:22 +0000 Subject: [PATCH 04/11] Fix process method for the results of validate --- libraries/src/Form/Form.php | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/libraries/src/Form/Form.php b/libraries/src/Form/Form.php index 00eea5f117d80..175ec18c4a162 100644 --- a/libraries/src/Form/Form.php +++ b/libraries/src/Form/Form.php @@ -1023,7 +1023,9 @@ public function setValue($name, $group = null, $value = null) * @param array $data An array of field values to filter. * @param string $group The dot-separated form group path on which to filter the fields. * - * @return array|boolean array or false. + * @return array|boolean|FormValidationResponse array of valid data. If modern validation response enabled then + * the form validation response object when the form isn't valid + * or when disabled boolean false. * * @since 4.0.0 */ @@ -1033,8 +1035,15 @@ public function process($data, $group = null) $valid = $this->validate($data, $group); - if (!$valid) { - return $valid; + if ($this->modernValidationResponse) { + /** @var $valid FormValidationResponse */ + if (!$valid->isValid()) { + return $valid; + } + } else { + if (!$valid) { + return $valid; + } } return $this->postProcess($data, $group); From cbcb4fafe34d39092743c72298a5fbbaf3235e6f Mon Sep 17 00:00:00 2001 From: George Wilson Date: Sun, 25 Feb 2024 02:02:29 +0000 Subject: [PATCH 05/11] Inject group when getting the field --- libraries/src/Form/Validation/FormValidationResponse.php | 7 ++++--- .../Form/Validation/FormValidationResponseInterface.php | 5 +++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/libraries/src/Form/Validation/FormValidationResponse.php b/libraries/src/Form/Validation/FormValidationResponse.php index ca18af4a56461..6201b95d34981 100644 --- a/libraries/src/Form/Validation/FormValidationResponse.php +++ b/libraries/src/Form/Validation/FormValidationResponse.php @@ -71,15 +71,16 @@ public function getInvalidFields(): array /** * Method to get a constraint result for the field. * - * @param string $name The name of the field + * @param string $name The name of the field to get the result for + * @param string $group The group of the field to get the result for * * @return FieldValidationResponseInterface * * @since __DEPLOY_VERSION__ */ - public function getField(string $name): FieldValidationResponseInterface + public function getField(string $name, string $group): FieldValidationResponseInterface { - return $this->fields[$name]; + return $this->fields[$group . '.' . $name]; } /** diff --git a/libraries/src/Form/Validation/FormValidationResponseInterface.php b/libraries/src/Form/Validation/FormValidationResponseInterface.php index 4857f09bec9d3..8add571638e49 100644 --- a/libraries/src/Form/Validation/FormValidationResponseInterface.php +++ b/libraries/src/Form/Validation/FormValidationResponseInterface.php @@ -44,11 +44,12 @@ public function getInvalidFields(): array; /** * Get the field validation result for a named field. * - * @param string $name The name of the field to get the result for + * @param string $name The name of the field to get the result for + * @param string $group The group of the field to get the result for * * @return FieldValidationResponseInterface * * @since __DEPLOY_VERSION__ */ - public function getField(string $name): FieldValidationResponseInterface; + public function getField(string $name, string $group): FieldValidationResponseInterface; } From 57781d35ba52184c26a44b8b03fc727c97ba7635 Mon Sep 17 00:00:00 2001 From: George Wilson Date: Sun, 25 Feb 2024 02:16:26 +0000 Subject: [PATCH 06/11] More changes for the new method --- libraries/src/Form/Validation/FormValidationResponse.php | 5 +++-- .../src/Form/Validation/FormValidationResponseInterface.php | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/libraries/src/Form/Validation/FormValidationResponse.php b/libraries/src/Form/Validation/FormValidationResponse.php index 6201b95d34981..fc0e496268c5e 100644 --- a/libraries/src/Form/Validation/FormValidationResponse.php +++ b/libraries/src/Form/Validation/FormValidationResponse.php @@ -59,7 +59,8 @@ public function isValid(): bool /** * List of validation properties tested that returned a negative result * - * @return string[] + * @return string[][] An array of fields that failed validation. Each field contains a name and group + * * key that can be used in the getField method. * * @since __DEPLOY_VERSION__ */ @@ -96,7 +97,7 @@ public function addField(FieldValidationResponseInterface $field): void { if (!$field->isValid()) { $this->valid = false; - $this->invalidFields[] = $field->getGroup() . '.' . $field->getName(); + $this->invalidFields[] = ['name' => $field->getName(), 'group' => $field->getGroup()]; } $this->fields[$field->getGroup() . '.' . $field->getName()] = $field; diff --git a/libraries/src/Form/Validation/FormValidationResponseInterface.php b/libraries/src/Form/Validation/FormValidationResponseInterface.php index 8add571638e49..b54d60d87ca2a 100644 --- a/libraries/src/Form/Validation/FormValidationResponseInterface.php +++ b/libraries/src/Form/Validation/FormValidationResponseInterface.php @@ -35,7 +35,8 @@ public function isValid(): bool; * List of invalid fields. The names here are not human friendly names but the location of fields that can be * resolved by {@link Form::getField()} or through {@link static::getFieldValidation()}. * - * @return string[] + * @return string[][] An array of fields that failed validation. Each field contains a name and group + * key that can be used in the getField method. * * @since __DEPLOY_VERSION__ */ From f2b6e58227586d340039f9d35ee9b3eb1273b9c9 Mon Sep 17 00:00:00 2001 From: George Wilson Date: Sun, 25 Feb 2024 02:47:34 +0000 Subject: [PATCH 07/11] Fix return type check --- libraries/src/Form/Form.php | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/libraries/src/Form/Form.php b/libraries/src/Form/Form.php index 175ec18c4a162..3e54a735f828e 100644 --- a/libraries/src/Form/Form.php +++ b/libraries/src/Form/Form.php @@ -1190,11 +1190,15 @@ public function validate($data, $group = null) @trigger_error(sprintf('From 7.0 fields must return a class implementing %s.', FieldValidationResponseInterface::class), E_USER_DEPRECATED); } elseif ($fieldValidationResponse instanceof FieldValidationResponseInterface) { $validationResponse->addField($fieldValidationResponse); + } else { + throw new \UnexpectedValueException( + sprintf( + 'Unexpected response from %s::validate, received %s', + $fieldObj::class, + $fieldValidationResponse::class + ) + ); } - - throw new \UnexpectedValueException( - sprintf('Unexpected response from %s::validate', $fieldObj::class) - ); } elseif ($input->exists($key)) { // The field returned false from setup and shouldn't be included in the page body - yet we received // a value for it. This is probably some sort of injection attack and should be rejected From e381bf8d2e10bdc1f803b9bcf34aea5fb5c56b5f Mon Sep 17 00:00:00 2001 From: George Wilson Date: Sun, 25 Feb 2024 02:50:55 +0000 Subject: [PATCH 08/11] Allow field length value to be null --- .../src/Form/Constraint/FieldLengthConstraint.php | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/libraries/src/Form/Constraint/FieldLengthConstraint.php b/libraries/src/Form/Constraint/FieldLengthConstraint.php index b6c7ea3d7f5e9..056ff94625aae 100644 --- a/libraries/src/Form/Constraint/FieldLengthConstraint.php +++ b/libraries/src/Form/Constraint/FieldLengthConstraint.php @@ -26,14 +26,19 @@ class FieldLengthConstraint extends AbstractConstraint /** * Method to instantiate the object. * - * @param FormField $field The form field object being inspected - * @param string $value Was the result of the constraint valid or not? - * @param int $maxLength Was the result of the constraint valid or not? + * @param FormField $field The form field object being inspected + * @param ?string $value Was the result of the constraint valid or not? + * @param int $maxLength Was the result of the constraint valid or not? * * @since __DEPLOY_VERSION__ */ - public function __construct(FormField $field, string $value, int $maxLength) + public function __construct(FormField $field, ?string $value, int $maxLength) { + // For the purposes of a max-length check no value is the same as an empty string + if ($value === null && $maxLength > 0) { + $value = ''; + } + $valid = ($maxLength === 0) || (\strlen($value) <= $maxLength); parent::__construct($valid, $field); From 8597da134693d54db517ae78416efd88009e59a5 Mon Sep 17 00:00:00 2001 From: George Wilson Date: Sun, 25 Feb 2024 18:03:21 +0000 Subject: [PATCH 09/11] PHPCS --- libraries/src/Form/Constraint/AbstractConstraint.php | 1 - libraries/src/Form/Field/TextField.php | 2 +- libraries/src/Form/Form.php | 1 - libraries/src/Form/FormField.php | 4 ++-- libraries/src/Form/Rule/AbstractRegexRule.php | 2 ++ libraries/src/Form/Validation/Field/LegacyInvalidField.php | 1 - 6 files changed, 5 insertions(+), 6 deletions(-) diff --git a/libraries/src/Form/Constraint/AbstractConstraint.php b/libraries/src/Form/Constraint/AbstractConstraint.php index 14cfcdbdd8499..f045c919bdfcd 100644 --- a/libraries/src/Form/Constraint/AbstractConstraint.php +++ b/libraries/src/Form/Constraint/AbstractConstraint.php @@ -9,7 +9,6 @@ namespace Joomla\CMS\Form\Constraint; -use Joomla\CMS\Form\Constraint\ConstraintInterface; use Joomla\CMS\Form\FormField; use Joomla\CMS\Language\Text; diff --git a/libraries/src/Form/Field/TextField.php b/libraries/src/Form/Field/TextField.php index 43842018849f2..1ca6de7824862 100644 --- a/libraries/src/Form/Field/TextField.php +++ b/libraries/src/Form/Field/TextField.php @@ -11,8 +11,8 @@ use Joomla\CMS\Component\ComponentHelper; use Joomla\CMS\Factory; -use Joomla\CMS\Form\FormField; use Joomla\CMS\Form\Constraint\FieldLengthConstraint; +use Joomla\CMS\Form\FormField; use Joomla\CMS\HTML\HTMLHelper; use Joomla\CMS\Language\Text; use Joomla\CMS\Uri\Uri; diff --git a/libraries/src/Form/Form.php b/libraries/src/Form/Form.php index 3e54a735f828e..ab517be53a651 100644 --- a/libraries/src/Form/Form.php +++ b/libraries/src/Form/Form.php @@ -10,7 +10,6 @@ namespace Joomla\CMS\Form; use Joomla\CMS\Factory; -use Joomla\CMS\Form\Exception\ValidationException; use Joomla\CMS\Form\Validation\Field\DisabledResponseField; use Joomla\CMS\Form\Validation\Field\LegacyInvalidField; use Joomla\CMS\Form\Validation\Field\LegacyValidField; diff --git a/libraries/src/Form/FormField.php b/libraries/src/Form/FormField.php index e9763ba643bd6..bba2bd44d0505 100644 --- a/libraries/src/Form/FormField.php +++ b/libraries/src/Form/FormField.php @@ -11,10 +11,10 @@ use Joomla\CMS\Factory; use Joomla\CMS\Filter\InputFilter; -use Joomla\CMS\Form\Field\SubformField; -use Joomla\CMS\Form\Rule\FormRuleInterface; use Joomla\CMS\Form\Constraint\FieldRequiredConstraint; use Joomla\CMS\Form\Constraint\LegacyRuleConstraint; +use Joomla\CMS\Form\Field\SubformField; +use Joomla\CMS\Form\Rule\FormRuleInterface; use Joomla\CMS\Form\Validation\FieldValidationResponse; use Joomla\CMS\Language\Text; use Joomla\CMS\Layout\FileLayout; diff --git a/libraries/src/Form/Rule/AbstractRegexRule.php b/libraries/src/Form/Rule/AbstractRegexRule.php index b4900ff84481b..ff37ca6047366 100644 --- a/libraries/src/Form/Rule/AbstractRegexRule.php +++ b/libraries/src/Form/Rule/AbstractRegexRule.php @@ -9,7 +9,9 @@ namespace Joomla\CMS\Form\Rule; +// phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects use Joomla\CMS\Form\Form; use Joomla\Registry\Registry; diff --git a/libraries/src/Form/Validation/Field/LegacyInvalidField.php b/libraries/src/Form/Validation/Field/LegacyInvalidField.php index fb4c3846cd8f8..7839d2fb99151 100644 --- a/libraries/src/Form/Validation/Field/LegacyInvalidField.php +++ b/libraries/src/Form/Validation/Field/LegacyInvalidField.php @@ -11,7 +11,6 @@ use Joomla\CMS\Form\Constraint\ConstraintInterface; use Joomla\CMS\Form\Validation\FieldValidationResponseInterface; -use Joomla\CMS\Language\Text; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; From ebc97e7c6688626423c1984d32354981b3be8ca4 Mon Sep 17 00:00:00 2001 From: George Wilson Date: Sun, 25 Feb 2024 18:40:57 +0000 Subject: [PATCH 10/11] Fix group concept --- libraries/src/Form/Form.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/src/Form/Form.php b/libraries/src/Form/Form.php index ab517be53a651..b510232819e66 100644 --- a/libraries/src/Form/Form.php +++ b/libraries/src/Form/Form.php @@ -1210,7 +1210,7 @@ public function validate($data, $group = null) if (!$isValid) { foreach ($validationResponse->getInvalidFields() as $invalidFieldName) { - $invalidField = $validationResponse->getField($invalidFieldName); + $invalidField = $validationResponse->getField($invalidFieldName['name'], $invalidFieldName['group']); foreach ($invalidField->getInvalidConstraints() as $invalidConstraintName) { $this->errors[] = new \RuntimeException( From 6379f5cf93b500aef35508e56d066fad1c2e8c92 Mon Sep 17 00:00:00 2001 From: Richard Fath Date: Sun, 15 Sep 2024 13:38:07 +0200 Subject: [PATCH 11/11] Fix PHPCS --- .../src/Form/Constraint/FieldLengthConstraint.php | 2 +- .../src/Form/Constraint/FieldRequiredConstraint.php | 2 +- libraries/src/Form/Constraint/LegacyRuleConstraint.php | 2 +- libraries/src/Form/Field/TextField.php | 2 +- libraries/src/Form/Form.php | 10 +++++----- libraries/src/Form/FormField.php | 2 +- libraries/src/Form/Rule/AbstractRegexRule.php | 4 ++-- libraries/src/Form/Rule/FormRuleInterface.php | 2 +- libraries/src/Form/Rule/RuleConstraintTrait.php | 2 +- .../src/Form/Validation/Field/LegacyValidField.php | 2 +- 10 files changed, 15 insertions(+), 15 deletions(-) diff --git a/libraries/src/Form/Constraint/FieldLengthConstraint.php b/libraries/src/Form/Constraint/FieldLengthConstraint.php index 056ff94625aae..cd26ac64ec10f 100644 --- a/libraries/src/Form/Constraint/FieldLengthConstraint.php +++ b/libraries/src/Form/Constraint/FieldLengthConstraint.php @@ -55,7 +55,7 @@ public function __construct(FormField $field, ?string $value, int $maxLength) public function getErrorMessage(): string { if ($this->isValid()) { - throw new \BadMethodCallException(sprintf('Field %s is valid', $this->getField()->getAttribute('name', ''))); + throw new \BadMethodCallException(\sprintf('Field %s is valid', $this->getField()->getAttribute('name', ''))); } return Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', $this->getFieldLabel()); diff --git a/libraries/src/Form/Constraint/FieldRequiredConstraint.php b/libraries/src/Form/Constraint/FieldRequiredConstraint.php index fede7322036e2..0a81ed2eb5d29 100644 --- a/libraries/src/Form/Constraint/FieldRequiredConstraint.php +++ b/libraries/src/Form/Constraint/FieldRequiredConstraint.php @@ -33,7 +33,7 @@ class FieldRequiredConstraint extends AbstractConstraint public function getErrorMessage(): string { if ($this->isValid()) { - throw new \BadMethodCallException(sprintf('Field %s is valid', $this->getField()->getAttribute('name', ''))); + throw new \BadMethodCallException(\sprintf('Field %s is valid', $this->getField()->getAttribute('name', ''))); } return Text::sprintf('JLIB_FORM_VALIDATE_FIELD_REQUIRED', $this->getFieldLabel()); diff --git a/libraries/src/Form/Constraint/LegacyRuleConstraint.php b/libraries/src/Form/Constraint/LegacyRuleConstraint.php index 96c14085979a0..5266a49ead08e 100644 --- a/libraries/src/Form/Constraint/LegacyRuleConstraint.php +++ b/libraries/src/Form/Constraint/LegacyRuleConstraint.php @@ -74,7 +74,7 @@ public function getRule(): FormRule public function getErrorMessage(): string { if ($this->isValid()) { - throw new \BadMethodCallException(sprintf('Field %s is valid', $this->getField()->getAttribute('name', ''))); + throw new \BadMethodCallException(\sprintf('Field %s is valid', $this->getField()->getAttribute('name', ''))); } // Does the field have a defined error message? diff --git a/libraries/src/Form/Field/TextField.php b/libraries/src/Form/Field/TextField.php index 50138393b8bec..04b7ec9488450 100644 --- a/libraries/src/Form/Field/TextField.php +++ b/libraries/src/Form/Field/TextField.php @@ -328,7 +328,7 @@ protected function getLayoutData() * @throws \UnexpectedValueException When the field or a rule configuration is invalid *@since 4.0.0 */ - public function validate($value, $group = null, Registry $input = null) + public function validate($value, $group = null, ?Registry $input = null) { $validationResults = parent::validate($value, $group, $input); diff --git a/libraries/src/Form/Form.php b/libraries/src/Form/Form.php index ad59069f13e29..802555fa7e7e3 100644 --- a/libraries/src/Form/Form.php +++ b/libraries/src/Form/Form.php @@ -207,7 +207,7 @@ public function getErrors() { if ($this->modernValidationResponse === true) { throw new \BadMethodCallException( - sprintf( + \sprintf( '%1s should not be used when %2s::modernValidationResponse is enabled', __METHOD__, static::class @@ -1136,7 +1136,7 @@ public function validate($data, $group = null) if (!$fields) { // PANIC! if ($this->modernValidationResponse) { - throw new \InvalidArgumentException(sprintf('There were no fields to validate for group %s', $group)); + throw new \InvalidArgumentException(\sprintf('There were no fields to validate for group %s', $group)); } return false; @@ -1183,15 +1183,15 @@ public function validate($data, $group = null) if ($fieldValidationResponse instanceof \Exception) { $validationResponse->addField(new LegacyInvalidField($name, $group, $fieldLabel, $fieldValidationResponse)); - @trigger_error(sprintf('From 7.0 fields must return a class implementing %s.', FieldValidationResponseInterface::class), E_USER_DEPRECATED); + @trigger_error(\sprintf('From 7.0 fields must return a class implementing %s.', FieldValidationResponseInterface::class), E_USER_DEPRECATED); } elseif ($fieldValidationResponse === true) { $validationResponse->addField(new LegacyValidField($name, $group, $fieldLabel)); - @trigger_error(sprintf('From 7.0 fields must return a class implementing %s.', FieldValidationResponseInterface::class), E_USER_DEPRECATED); + @trigger_error(\sprintf('From 7.0 fields must return a class implementing %s.', FieldValidationResponseInterface::class), E_USER_DEPRECATED); } elseif ($fieldValidationResponse instanceof FieldValidationResponseInterface) { $validationResponse->addField($fieldValidationResponse); } else { throw new \UnexpectedValueException( - sprintf( + \sprintf( 'Unexpected response from %s::validate, received %s', $fieldObj::class, $fieldValidationResponse::class diff --git a/libraries/src/Form/FormField.php b/libraries/src/Form/FormField.php index f248b057c29e7..9b5e8b1e49cee 100644 --- a/libraries/src/Form/FormField.php +++ b/libraries/src/Form/FormField.php @@ -1223,7 +1223,7 @@ public function validate($value, $group = null, ?Registry $input = null) $response->addConstraint($rule); } else { @trigger_error( - sprintf('Rules should implement %s from 6.0.', FormRuleInterface::class), + \sprintf('Rules should implement %s from 6.0.', FormRuleInterface::class), E_USER_DEPRECATED ); diff --git a/libraries/src/Form/Rule/AbstractRegexRule.php b/libraries/src/Form/Rule/AbstractRegexRule.php index ff37ca6047366..6d5b8cabd20f4 100644 --- a/libraries/src/Form/Rule/AbstractRegexRule.php +++ b/libraries/src/Form/Rule/AbstractRegexRule.php @@ -57,13 +57,13 @@ abstract class AbstractRegexRule implements FormRuleInterface * @since 1.6 * @throws \UnexpectedValueException If regex is invalid. */ - public function test(\SimpleXMLElement $element, mixed $value, string $group = null, Registry $input = null, Form $form = null): void + public function test(\SimpleXMLElement $element, mixed $value, ?string $group = null, ?Registry $input = null, ?Form $form = null): void { $this->ruleRun = true; // Check for a valid regex. if (empty($this->regex)) { - throw new \UnexpectedValueException(sprintf('%s has invalid regex.', \get_class($this))); + throw new \UnexpectedValueException(\sprintf('%s has invalid regex.', \get_class($this))); } // Detect if we have full UTF-8 and unicode PCRE support. diff --git a/libraries/src/Form/Rule/FormRuleInterface.php b/libraries/src/Form/Rule/FormRuleInterface.php index 262c69d801dde..551115256ae32 100644 --- a/libraries/src/Form/Rule/FormRuleInterface.php +++ b/libraries/src/Form/Rule/FormRuleInterface.php @@ -40,5 +40,5 @@ interface FormRuleInterface extends ConstraintInterface * @since __DEPLOY_VERSION__ * @throws \UnexpectedValueException if rule is invalid. */ - public function test(\SimpleXMLElement $element, $value, $group = null, Registry $input = null, Form $form = null): void; + public function test(\SimpleXMLElement $element, $value, $group = null, ?Registry $input = null, ?Form $form = null): void; } diff --git a/libraries/src/Form/Rule/RuleConstraintTrait.php b/libraries/src/Form/Rule/RuleConstraintTrait.php index 7ef715874e2e9..341dfc821b08e 100644 --- a/libraries/src/Form/Rule/RuleConstraintTrait.php +++ b/libraries/src/Form/Rule/RuleConstraintTrait.php @@ -63,7 +63,7 @@ public function isValid(): bool public function getErrorMessage(): string { if (!$this->ruleRun) { - throw new \BadMethodCallException(sprintf('The %s::test() method must be run', static::class)); + throw new \BadMethodCallException(\sprintf('The %s::test() method must be run', static::class)); } if ($this->isValid) { diff --git a/libraries/src/Form/Validation/Field/LegacyValidField.php b/libraries/src/Form/Validation/Field/LegacyValidField.php index 3c031e5c5a315..483f36d28d984 100644 --- a/libraries/src/Form/Validation/Field/LegacyValidField.php +++ b/libraries/src/Form/Validation/Field/LegacyValidField.php @@ -166,7 +166,7 @@ public function getName(): string */ public function getErrorMessage(): string { - throw new \BadMethodCallException(sprintf('Field %s is valid', $this->fieldLabel)); + throw new \BadMethodCallException(\sprintf('Field %s is valid', $this->fieldLabel)); } }; }