diff --git a/lang/en.yml b/lang/en.yml index fbd74a2c0a4..e36a8815117 100644 --- a/lang/en.yml +++ b/lang/en.yml @@ -36,6 +36,7 @@ en: SilverStripe\Forms\ConfirmedPasswordField: ATLEAST: 'Passwords must be at least {min} characters long.' BETWEEN: 'Passwords must be {min} to {max} characters long.' + CANBEEMTPYRIGHT: 'If a password is not set then a hidden random password will be automatically generated. The user will need to click the "I\'ve lost my password" link to receive a password reset email.' CURRENT_PASSWORD_ERROR: 'The current password you have entered is not correct.' CURRENT_PASSWORD_MISSING: 'You must enter your current password.' LOGGED_IN_ERROR: 'You must be logged in to change your password.' diff --git a/src/Forms/ConfirmedPasswordField.php b/src/Forms/ConfirmedPasswordField.php index ec5fabe22fe..cbe8c087043 100644 --- a/src/Forms/ConfirmedPasswordField.php +++ b/src/Forms/ConfirmedPasswordField.php @@ -43,7 +43,8 @@ class ConfirmedPasswordField extends FormField public $requireStrongPassword = false; /** - * Allow empty fields in serverside validation + * Allow empty fields in when entering the password for the first time + * If this is set to true then a random password will be created before saving * * @var boolean */ @@ -119,6 +120,8 @@ class ConfirmedPasswordField extends FormField */ protected $hiddenField = null; + private string $strongPasswordRegex = '/^(([a-zA-Z]+\d+)|(\d+[a-zA-Z]+))[a-zA-Z0-9]*$/'; + /** * @param string $name * @param string $title @@ -255,7 +258,15 @@ public function getChildren() public function setCanBeEmpty($value) { $this->canBeEmpty = (bool)$value; - + $text = _t( + __CLASS__ . '.CANBEEMTPYRIGHT', + 'If a password is not set then a hidden random password will be automatically generated. The user will need to click the "I\'ve lost my password" link to receive a password reset email.' + ); + if ($this->canBeEmpty) { + $this->passwordField->setRightTitle($text); + } elseif (!$this->canBeEmpty && $this->passwordField->getRightTitle() === $text) { + $this->passwordField->setRightTitle(''); + } return $this; } @@ -488,7 +499,7 @@ public function validate($validator) } if ($this->getRequireStrongPassword()) { - if (!preg_match('/^(([a-zA-Z]+\d+)|(\d+[a-zA-Z]+))[a-zA-Z0-9]*$/', $value ?? '')) { + if (!preg_match($this->strongPasswordRegex, $value ?? '')) { $validator->validationError( $name, _t( @@ -552,8 +563,6 @@ public function validate($validator) } /** - * Only save if field was shown on the client, and is not empty. - * * @param DataObjectInterface $record */ public function saveInto(DataObjectInterface $record) @@ -562,9 +571,33 @@ public function saveInto(DataObjectInterface $record) return; } - if (!($this->canBeEmpty && !$this->value)) { - parent::saveInto($record); + // Create a random password if password is blank + if ($this->canBeEmpty && !$this->value) { + $this->value = $this->createStrongRandomPassword(); } + + parent::saveInto($record); + } + + private function createStrongRandomPassword(): string + { + $value = ''; + do { + // random_int() in OK to use for random password generation + $randomInt = random_int(0, PHP_INT_MAX); + // add in a-f as extra possible characters + $hashed = substr(md5($randomInt), 0, $this->getMaxLength()); + // uppercase half of the characters + $hashed = implode('', [ + strtoupper(substr($hashed, 0, floor(strlen($hashed) / 2))), + substr($hashed, ceil(strlen($hashed) / 2)), + ]); + // shuffle the characters + $arr = str_split($hashed); + shuffle($arr); + $value = implode('', $arr); + } while (!preg_match($this->strongPasswordRegex, $value)); + return $value; } /** diff --git a/src/Security/Member.php b/src/Security/Member.php index 2dffce485e2..9f6d838042f 100644 --- a/src/Security/Member.php +++ b/src/Security/Member.php @@ -670,7 +670,7 @@ public function getMemberPasswordField() $password->setRequireExistingPassword(true); } - $password->setCanBeEmpty(false); + $password->setCanBeEmpty(true); $this->extend('updateMemberPasswordField', $password); return $password;