diff --git a/src/Forms/ConfirmedPasswordField.php b/src/Forms/ConfirmedPasswordField.php index 479c89d59..1707ef6a5 100644 --- a/src/Forms/ConfirmedPasswordField.php +++ b/src/Forms/ConfirmedPasswordField.php @@ -4,6 +4,7 @@ namespace SilverStripe\Forms; use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObjectInterface; +use SilverStripe\ORM\FieldType\DBField; use SilverStripe\Security\Authenticator; use SilverStripe\Security\Security; use SilverStripe\View\HTML; @@ -200,7 +201,7 @@ class ConfirmedPasswordField extends FormField } } - $fieldContent .= $field->FieldHolder(); + $fieldContent .= $field->FieldHolder(['AttributesHTML' => $this->getAttributesHTMLForChild($field)]); } if (!$this->showOnClick) { @@ -233,6 +234,19 @@ class ConfirmedPasswordField extends FormField ); } + public function Required() + { + return !$this->canBeEmpty || parent::Required(); + } + + public function setForm($form) + { + foreach ($this->getChildren() as $field) { + $field->setForm($form); + } + return parent::setForm($form); + } + /** * Returns the children of this field for use in templating. * @return FieldList @@ -694,4 +708,18 @@ class ConfirmedPasswordField extends FormField { return $this->requireStrongPassword; } + + /** + * Get the AttributesHTML for a child field. + * Includes extra information the child isn't aware of on its own, such as whether + * it's required due to this field as a whole being required. + */ + private function getAttributesHTMLForChild(FormField $child): DBField + { + $attributes = $child->getAttributesHTML(); + if (strpos($attributes, 'required="required"') === false && $this->Required()) { + $attributes .= ' required="required" aria-required="true"'; + } + return DBField::create_field('HTMLFragment', $attributes); + } } diff --git a/tests/php/Forms/ConfirmedPasswordFieldTest.php b/tests/php/Forms/ConfirmedPasswordFieldTest.php index 990402db0..42aa33c35 100644 --- a/tests/php/Forms/ConfirmedPasswordFieldTest.php +++ b/tests/php/Forms/ConfirmedPasswordFieldTest.php @@ -2,6 +2,7 @@ namespace SilverStripe\Forms\Tests; +use SilverStripe\Admin\LeftAndMain; use SilverStripe\Control\Controller; use SilverStripe\Dev\SapphireTest; use SilverStripe\Forms\ConfirmedPasswordField; @@ -11,6 +12,7 @@ use SilverStripe\Forms\ReadonlyField; use SilverStripe\Forms\RequiredFields; use SilverStripe\Security\Member; use SilverStripe\Security\PasswordValidator; +use SilverStripe\View\SSViewer; class ConfirmedPasswordFieldTest extends SapphireTest { @@ -383,4 +385,91 @@ class ConfirmedPasswordFieldTest extends SapphireTest $field->setRequireExistingPassword(false); $this->assertCount(2, $field->getChildren(), 'Current password field should not be removed'); } + + public function provideRequired() + { + return [ + 'can be empty' => [true], + 'cannot be empty' => [false], + ]; + } + + /** + * @dataProvider provideRequired + */ + public function testRequired(bool $canBeEmpty) + { + $field = new ConfirmedPasswordField('Test'); + $field->setCanBeEmpty($canBeEmpty); + $this->assertSame(!$canBeEmpty, $field->Required()); + } + + public function provideChildFieldsAreRequired() + { + return [ + 'not required' => [ + 'canBeEmpty' => true, + 'required' => false, + 'childrenRequired' => false, + 'expectRequired' => false, + ], + 'required via validator' => [ + 'canBeEmpty' => true, + 'required' => true, + 'childrenRequired' => false, + 'expectRequired' => true, + ], + 'children required directly' => [ + 'canBeEmpty' => true, + 'required' => false, + 'childrenRequired' => true, + 'expectRequired' => true, + ], + 'required because cannot be empty' => [ + 'canBeEmpty' => false, + 'required' => false, + 'childrenRequired' => false, + 'expectRequired' => true, + ], + ]; + } + + /** + * @dataProvider provideChildFieldsAreRequired + */ + public function testChildFieldsAreRequired(bool $canBeEmpty, bool $required, bool $childrenRequired, bool $expectRequired) + { + // CWP front-end templates break this logic - but there's no easy fix for that. + // For the most part we are interested in ensuring this works in the CMS with default templates. + $originalThemes = SSViewer::get_themes(); + SSViewer::set_themes(LeftAndMain::config()->uninherited('admin_themes')); + try { + $form = new Form(); + $field = new ConfirmedPasswordField('Test'); + $field->setForm($form); + $field->setCanBeEmpty($canBeEmpty); + $requiredFields = []; + if ($required) { + $requiredFields[] = 'Test'; + } + if ($childrenRequired) { + $requiredFields[] = 'Test[_Password]'; + $requiredFields[] = 'Test[_ConfirmPassword]'; + } + $form->setValidator(new RequiredFields($requiredFields)); + + $rendered = $field->Field(); + $fieldOneRegex = ']*?required="required"\s+aria-required="true"\s[^>]*\/>'; + $fieldTwoRegex = ']*?required="required"\s+aria-required="true"\s[^>]*\/>'; + $regex = '/' . $fieldOneRegex . '.*' . $fieldTwoRegex . '/isu'; + + if ($expectRequired) { + $this->assertMatchesRegularExpression($regex, $rendered); + } else { + $this->assertDoesNotMatchRegularExpression($regex, $rendered); + } + } finally { + SSViewer::set_themes($originalThemes); + } + } }