Skip to content

Commit

Permalink
ENH Generate a random password if a blank password is entered
Browse files Browse the repository at this point in the history
  • Loading branch information
emteknetnz committed Oct 12, 2023
1 parent 87958e7 commit 2216a3e
Show file tree
Hide file tree
Showing 4 changed files with 63 additions and 8 deletions.
1 change: 1 addition & 0 deletions lang/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.'
Expand Down
47 changes: 40 additions & 7 deletions src/Forms/ConfirmedPasswordField.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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->RightTitle() === $text) {
$this->passwordField->setRightTitle('');
}
return $this;
}

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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)
Expand All @@ -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() ?: 20);
// 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;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/Security/Member.php
Original file line number Diff line number Diff line change
Expand Up @@ -670,7 +670,7 @@ public function getMemberPasswordField()
$password->setRequireExistingPassword(true);
}

$password->setCanBeEmpty(false);
$password->setCanBeEmpty(true);
$this->extend('updateMemberPasswordField', $password);

return $password;
Expand Down
21 changes: 21 additions & 0 deletions tests/php/Forms/ConfirmedPasswordFieldTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -381,4 +381,25 @@ public function testSetRequireExistingPasswordOnlyRunsOnce()
$field->setRequireExistingPassword(false);
$this->assertCount(2, $field->getChildren(), 'Current password field should not be removed');
}

public function testSetCanBeEmptySaveInto()
{
$field = new ConfirmedPasswordField('Test', 'Change it');
$field->setCanBeEmpty(true);
$this->assertEmpty($field->Value());
$member = new Member();
$field->saveInto($member);
$this->assertNotEmpty($field->Value());
$expected = $field->getMaxLength() ?: 20;
$this->assertSame($expected, strlen($field->Value()));
}

public function testSetCanBeEmptyRightTitle()
{
$field = new ConfirmedPasswordField('Test', 'Change it');
$passwordField = $field->getPasswordField();
$this->assertEmpty($passwordField->RightTitle());
$field->setCanBeEmpty(true);
$this->assertNotEmpty($passwordField->RightTitle());
}
}

0 comments on commit 2216a3e

Please sign in to comment.