mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
Compare commits
4 Commits
77c922f632
...
f9aad1365d
Author | SHA1 | Date | |
---|---|---|---|
|
f9aad1365d | ||
|
6bb9a0b33d | ||
|
ebbd6427b2 | ||
|
b5fee199c8 |
@ -10,8 +10,13 @@ use SilverStripe\Security\Authenticator;
|
|||||||
use SilverStripe\Security\Security;
|
use SilverStripe\Security\Security;
|
||||||
use SilverStripe\View\HTML;
|
use SilverStripe\View\HTML;
|
||||||
use Closure;
|
use Closure;
|
||||||
|
use SilverStripe\Control\HTTP;
|
||||||
use SilverStripe\Core\Validation\ConstraintValidator;
|
use SilverStripe\Core\Validation\ConstraintValidator;
|
||||||
use Symfony\Component\Validator\Constraints\PasswordStrength;
|
use Symfony\Component\Validator\Constraints\PasswordStrength;
|
||||||
|
use SilverStripe\Forms\LiteralField;
|
||||||
|
use SilverStripe\Control\HTTPRequest;
|
||||||
|
use SilverStripe\Control\HTTPResponse;
|
||||||
|
use SilverStripe\Security\Validation\PasswordValidator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Two masked input fields, checks for matching passwords.
|
* Two masked input fields, checks for matching passwords.
|
||||||
@ -24,6 +29,9 @@ use Symfony\Component\Validator\Constraints\PasswordStrength;
|
|||||||
*/
|
*/
|
||||||
class ConfirmedPasswordField extends FormField
|
class ConfirmedPasswordField extends FormField
|
||||||
{
|
{
|
||||||
|
private static $allowed_actions = [
|
||||||
|
'strength',
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Minimum character length of the password.
|
* Minimum character length of the password.
|
||||||
@ -106,6 +114,8 @@ class ConfirmedPasswordField extends FormField
|
|||||||
|
|
||||||
protected ?PasswordField $passwordField;
|
protected ?PasswordField $passwordField;
|
||||||
|
|
||||||
|
protected ?LiteralField $passwordStrengthField;
|
||||||
|
|
||||||
protected ?PasswordField $confirmPasswordfield;
|
protected ?PasswordField $confirmPasswordfield;
|
||||||
|
|
||||||
protected ?HiddenField $hiddenField = null;
|
protected ?HiddenField $hiddenField = null;
|
||||||
@ -132,6 +142,10 @@ class ConfirmedPasswordField extends FormField
|
|||||||
"{$name}[_Password]",
|
"{$name}[_Password]",
|
||||||
$title
|
$title
|
||||||
),
|
),
|
||||||
|
$this->passwordStrengthField = LiteralField::create(
|
||||||
|
"{$name}[_PasswordStrength]",
|
||||||
|
'<div class="passwordstrength"></div>'
|
||||||
|
),
|
||||||
$this->confirmPasswordfield = PasswordField::create(
|
$this->confirmPasswordfield = PasswordField::create(
|
||||||
"{$name}[_ConfirmPassword]",
|
"{$name}[_ConfirmPassword]",
|
||||||
(isset($titleConfirmField)) ? $titleConfirmField : _t('SilverStripe\\Security\\Member.CONFIRMPASSWORD', 'Confirm Password')
|
(isset($titleConfirmField)) ? $titleConfirmField : _t('SilverStripe\\Security\\Member.CONFIRMPASSWORD', 'Confirm Password')
|
||||||
@ -154,6 +168,50 @@ class ConfirmedPasswordField extends FormField
|
|||||||
$this->setValue($value);
|
$this->setValue($value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides feedback for the current and required level of password strength
|
||||||
|
*/
|
||||||
|
public function strength(HTTPRequest $request): HTTPResponse
|
||||||
|
{
|
||||||
|
$response = HTTPResponse::create();
|
||||||
|
$json = json_decode($request->getBody(), true);
|
||||||
|
if (!$json || !array_key_exists('password', $json) || !$request->isPOST()) {
|
||||||
|
$response->setStatusCode(400);
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
$password = $json['password'];
|
||||||
|
$validator = PasswordValidator::create();
|
||||||
|
if ($this->getRequireStrongPassword()) {
|
||||||
|
$requiredStrength = $this->getMinPasswordStrength();
|
||||||
|
} else {
|
||||||
|
$requiredStrength = $validator->getRequiredStrength();
|
||||||
|
}
|
||||||
|
$requiredLevel = $validator->getStrengthLevel($requiredStrength);
|
||||||
|
$passwordStrength = $validator->evaluateStrength($password);
|
||||||
|
$passwordLevel = $validator->getStrengthLevel($passwordStrength);
|
||||||
|
if ($passwordStrength < $requiredStrength) {
|
||||||
|
$valid = false;
|
||||||
|
$message = _t(
|
||||||
|
__CLASS__ . '.STRENGTH',
|
||||||
|
'Password strength is {passwordLevel}, must be at least {requiredLevel}',
|
||||||
|
['passwordLevel' => $passwordLevel, 'requiredLevel' => $requiredLevel]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$valid = true;
|
||||||
|
$message = _t(
|
||||||
|
__CLASS__ . '.STRENGTH',
|
||||||
|
'Password strength is {passwordLevel}',
|
||||||
|
['passwordLevel' => $passwordLevel]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$body = json_encode((object) [
|
||||||
|
'valid' => $valid,
|
||||||
|
'message' => $message,
|
||||||
|
]);
|
||||||
|
$response->setBody($body);
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
public function Title()
|
public function Title()
|
||||||
{
|
{
|
||||||
// Title is displayed on nested field, not on the top level field
|
// Title is displayed on nested field, not on the top level field
|
||||||
@ -173,6 +231,7 @@ class ConfirmedPasswordField extends FormField
|
|||||||
*/
|
*/
|
||||||
public function Field($properties = [])
|
public function Field($properties = [])
|
||||||
{
|
{
|
||||||
|
$canEvaluateStrength = PasswordValidator::singleton()->canEvaluateStrength();
|
||||||
// Build inner content
|
// Build inner content
|
||||||
$fieldContent = '';
|
$fieldContent = '';
|
||||||
foreach ($this->getChildren() as $field) {
|
foreach ($this->getChildren() as $field) {
|
||||||
@ -184,6 +243,9 @@ class ConfirmedPasswordField extends FormField
|
|||||||
$field->setAttribute($name, $value);
|
$field->setAttribute($name, $value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if ($canEvaluateStrength && is_a($field, PasswordField::class)) {
|
||||||
|
$field->setAttribute('data-strengthurl', $this->Link('strength'));
|
||||||
|
}
|
||||||
|
|
||||||
$fieldContent .= $field->FieldHolder(['AttributesHTML' => $this->getAttributesHTMLForChild($field)]);
|
$fieldContent .= $field->FieldHolder(['AttributesHTML' => $this->getAttributesHTMLForChild($field)]);
|
||||||
}
|
}
|
||||||
|
@ -49,6 +49,8 @@ class GridFieldFilterHeader extends AbstractGridFieldComponent implements GridFi
|
|||||||
*/
|
*/
|
||||||
protected ?string $searchField = null;
|
protected ?string $searchField = null;
|
||||||
|
|
||||||
|
private string $placeHolderText = '';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritDoc
|
* @inheritDoc
|
||||||
*/
|
*/
|
||||||
@ -245,6 +247,24 @@ class GridFieldFilterHeader extends AbstractGridFieldComponent implements GridFi
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the text to be used as a placeholder in the search field.
|
||||||
|
* If blank, the placeholder will be generated based on the class held in the GridField.
|
||||||
|
*/
|
||||||
|
public function getPlaceHolderText(): string
|
||||||
|
{
|
||||||
|
return $this->placeHolderText;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the text to be used as a placeholder in the search field.
|
||||||
|
* If blank, this text will be generated based on the class held in the GridField.
|
||||||
|
*/
|
||||||
|
public function setPlaceHolderText(string $placeHolderText): static
|
||||||
|
{
|
||||||
|
$this->placeHolderText = $placeHolderText;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a search context based on the model class of the of the GridField
|
* Generate a search context based on the model class of the of the GridField
|
||||||
@ -318,7 +338,7 @@ class GridFieldFilterHeader extends AbstractGridFieldComponent implements GridFi
|
|||||||
$schema = [
|
$schema = [
|
||||||
'formSchemaUrl' => $schemaUrl,
|
'formSchemaUrl' => $schemaUrl,
|
||||||
'name' => $searchField,
|
'name' => $searchField,
|
||||||
'placeholder' => _t(__CLASS__ . '.Search', 'Search "{name}"', ['name' => $this->getTitle($gridField, $inst)]),
|
'placeholder' => $this->getPlaceHolder($inst),
|
||||||
'filters' => $filters ?: new \stdClass, // stdClass maps to empty json object '{}'
|
'filters' => $filters ?: new \stdClass, // stdClass maps to empty json object '{}'
|
||||||
'gridfield' => $gridField->getName(),
|
'gridfield' => $gridField->getName(),
|
||||||
'searchAction' => $searchAction->getAttribute('name'),
|
'searchAction' => $searchAction->getAttribute('name'),
|
||||||
@ -330,19 +350,6 @@ class GridFieldFilterHeader extends AbstractGridFieldComponent implements GridFi
|
|||||||
return json_encode($schema);
|
return json_encode($schema);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getTitle(GridField $gridField, object $inst): string
|
|
||||||
{
|
|
||||||
if ($gridField->Title) {
|
|
||||||
return $gridField->Title;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ClassInfo::hasMethod($inst, 'i18n_plural_name')) {
|
|
||||||
return $inst->i18n_plural_name();
|
|
||||||
}
|
|
||||||
|
|
||||||
return ClassInfo::shortName($inst);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the search form for the component
|
* Returns the search form for the component
|
||||||
*
|
*
|
||||||
@ -386,7 +393,7 @@ class GridFieldFilterHeader extends AbstractGridFieldComponent implements GridFi
|
|||||||
$field->addExtraClass('stacked no-change-track');
|
$field->addExtraClass('stacked no-change-track');
|
||||||
}
|
}
|
||||||
|
|
||||||
$name = $this->getTitle($gridField, singleton($gridField->getModelClass()));
|
$name = $this->getTitle(singleton($gridField->getModelClass()));
|
||||||
|
|
||||||
$this->searchForm = $form = new Form(
|
$this->searchForm = $form = new Form(
|
||||||
$gridField,
|
$gridField,
|
||||||
@ -456,4 +463,32 @@ class GridFieldFilterHeader extends AbstractGridFieldComponent implements GridFi
|
|||||||
)
|
)
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the text that will be used as a placeholder in the search field.
|
||||||
|
*
|
||||||
|
* @param object $obj An instance of the class that will be searched against.
|
||||||
|
* If getPlaceHolderText is empty, this object will be used to build the placeholder
|
||||||
|
* e.g. 'Search "My Data Object"'
|
||||||
|
*/
|
||||||
|
private function getPlaceHolder(object $obj): string
|
||||||
|
{
|
||||||
|
$placeholder = $this->getPlaceHolderText();
|
||||||
|
if (!empty($placeholder)) {
|
||||||
|
return $placeholder;
|
||||||
|
}
|
||||||
|
if ($obj) {
|
||||||
|
return _t(__CLASS__ . '.Search', 'Search "{name}"', ['name' => $this->getTitle($obj)]);
|
||||||
|
}
|
||||||
|
return _t(__CLASS__ . '.Search_Default', 'Search');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getTitle(object $inst): string
|
||||||
|
{
|
||||||
|
if (ClassInfo::hasMethod($inst, 'i18n_plural_name')) {
|
||||||
|
return $inst->i18n_plural_name();
|
||||||
|
}
|
||||||
|
|
||||||
|
return ClassInfo::shortName($inst);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,12 @@
|
|||||||
|
|
||||||
namespace SilverStripe\Forms;
|
namespace SilverStripe\Forms;
|
||||||
|
|
||||||
|
use SilverStripe\Control\Director;
|
||||||
|
use SilverStripe\Security\Security;
|
||||||
|
use SilverStripe\Security\Validation\PasswordValidator;
|
||||||
|
use SilverStripe\Control\HTTPRequest;
|
||||||
|
use SilverStripe\Control\HTTPResponse;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Password input field.
|
* Password input field.
|
||||||
*/
|
*/
|
||||||
|
@ -20,7 +20,7 @@ class EntropyPasswordValidator extends PasswordValidator
|
|||||||
* The strength of a valid password.
|
* The strength of a valid password.
|
||||||
* See https://symfony.com/doc/current/reference/constraints/PasswordStrength.html#minscore
|
* See https://symfony.com/doc/current/reference/constraints/PasswordStrength.html#minscore
|
||||||
*/
|
*/
|
||||||
private static int $password_strength = PasswordStrength::STRENGTH_STRONG;
|
private static int $password_strength = PasswordStrength::STRENGTH_MEDIUM;
|
||||||
|
|
||||||
public function validate(string $password, Member $member): ValidationResult
|
public function validate(string $password, Member $member): ValidationResult
|
||||||
{
|
{
|
||||||
@ -30,4 +30,35 @@ class EntropyPasswordValidator extends PasswordValidator
|
|||||||
$this->extend('updateValidatePassword', $password, $member, $result, $this);
|
$this->extend('updateValidatePassword', $password, $member, $result, $this);
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getRequiredStrength(): int
|
||||||
|
{
|
||||||
|
return static::config()->get('password_strength');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function canEvaluateStrength(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function evaluateStrength(string $password): int
|
||||||
|
{
|
||||||
|
$strengths = [
|
||||||
|
PasswordStrength::STRENGTH_WEAK,
|
||||||
|
PasswordStrength::STRENGTH_MEDIUM,
|
||||||
|
PasswordStrength::STRENGTH_STRONG,
|
||||||
|
PasswordStrength::STRENGTH_VERY_STRONG,
|
||||||
|
];
|
||||||
|
// STRENGTH_VERY_WEAK is not validatable, it's just the default value
|
||||||
|
$lastPassedStrength = PasswordStrength::STRENGTH_VERY_WEAK;
|
||||||
|
foreach ($strengths as $strength) {
|
||||||
|
$result = ConstraintValidator::validate($password, new PasswordStrength(minScore: $strength));
|
||||||
|
if ($result->isValid()) {
|
||||||
|
$lastPassedStrength = $strength;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $lastPassedStrength;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ use SilverStripe\ORM\DataObject;
|
|||||||
use SilverStripe\Core\Validation\ValidationResult;
|
use SilverStripe\Core\Validation\ValidationResult;
|
||||||
use SilverStripe\Security\Member;
|
use SilverStripe\Security\Member;
|
||||||
use SilverStripe\Security\MemberPassword;
|
use SilverStripe\Security\MemberPassword;
|
||||||
|
use Symfony\Component\Validator\Constraints\PasswordStrength;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstract validator with functionality for checking for reusing old passwords.
|
* Abstract validator with functionality for checking for reusing old passwords.
|
||||||
@ -69,4 +70,63 @@ abstract class PasswordValidator
|
|||||||
$this->historicalPasswordCount = $count;
|
$this->historicalPasswordCount = $count;
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the required strength of a password based on the consts in
|
||||||
|
* Symfony\Component\Validator\Constraints\PasswordStrength
|
||||||
|
* Default return -1 for validators that do not support this
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public function getRequiredStrength(): int
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this validator can evaluate password strength.
|
||||||
|
*/
|
||||||
|
public function canEvaluateStrength(): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluate the strength of a password based on the consts in
|
||||||
|
* Symfony\Component\Validator\Constraints\PasswordStrength
|
||||||
|
* Default return -1 for validators that do not support this
|
||||||
|
*/
|
||||||
|
public function evaluateStrength(string $password): int
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Textual representation of an evaluated password strength
|
||||||
|
*/
|
||||||
|
public static function getStrengthLevel(int $strength): string
|
||||||
|
{
|
||||||
|
return match ($strength) {
|
||||||
|
PasswordStrength::STRENGTH_VERY_WEAK => _t(
|
||||||
|
PasswordValidator::class . '.VERYWEAK',
|
||||||
|
'very weak'
|
||||||
|
),
|
||||||
|
PasswordStrength::STRENGTH_WEAK => _t(
|
||||||
|
PasswordValidator::class . '.WEAK',
|
||||||
|
'weak'
|
||||||
|
),
|
||||||
|
PasswordStrength::STRENGTH_MEDIUM => _t(
|
||||||
|
PasswordValidator::class . '.MEDIUM',
|
||||||
|
'medium'
|
||||||
|
),
|
||||||
|
PasswordStrength::STRENGTH_STRONG => _t(
|
||||||
|
PasswordValidator::class . '.STRONG',
|
||||||
|
'strong'
|
||||||
|
),
|
||||||
|
PasswordStrength::STRENGTH_VERY_STRONG => _t(
|
||||||
|
PasswordValidator::class . '.VERYSTRONG',
|
||||||
|
'very strong'
|
||||||
|
),
|
||||||
|
default => '',
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
namespace SilverStripe\Forms\Tests\GridField;
|
namespace SilverStripe\Forms\Tests\GridField;
|
||||||
|
|
||||||
use LogicException;
|
use LogicException;
|
||||||
|
use ReflectionMethod;
|
||||||
use SilverStripe\Control\HTTPRequest;
|
use SilverStripe\Control\HTTPRequest;
|
||||||
use SilverStripe\Core\Config\Config;
|
use SilverStripe\Core\Config\Config;
|
||||||
use SilverStripe\Dev\SapphireTest;
|
use SilverStripe\Dev\SapphireTest;
|
||||||
@ -117,6 +118,29 @@ class GridFieldFilterHeaderTest extends SapphireTest
|
|||||||
$this->assertEquals('testfield', $searchSchema->gridfield);
|
$this->assertEquals('testfield', $searchSchema->gridfield);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests the private method that returns the placeholder for the search field
|
||||||
|
*/
|
||||||
|
public function testGetPlaceHolder()
|
||||||
|
{
|
||||||
|
$gridField = new GridField('test');
|
||||||
|
$filterHeader = new GridFieldFilterHeader();
|
||||||
|
$reflectionGetPlaceHolder = new ReflectionMethod($filterHeader, 'getPlaceHolder');
|
||||||
|
$reflectionGetPlaceHolder->setAccessible(true);
|
||||||
|
|
||||||
|
// No explicit placeholder or model i18n_plural_name method
|
||||||
|
$this->assertSame('Search "ArrayData"', $reflectionGetPlaceHolder->invoke($filterHeader, new ArrayData()));
|
||||||
|
|
||||||
|
// No explicit placeholder, but model has i18n_plural_name method
|
||||||
|
$model = new DataObject();
|
||||||
|
$this->assertSame('Search "' . $model->i18n_plural_name() . '"', $reflectionGetPlaceHolder->invoke($filterHeader, $model));
|
||||||
|
|
||||||
|
// Explicit placeholder is set, which overrides both of the above cases
|
||||||
|
$filterHeader->setPlaceHolderText('This is the text');
|
||||||
|
$this->assertSame('This is the text', $reflectionGetPlaceHolder->invoke($filterHeader, $model));
|
||||||
|
$this->assertSame('This is the text', $reflectionGetPlaceHolder->invoke($filterHeader, new ArrayData()));
|
||||||
|
}
|
||||||
|
|
||||||
public function testHandleActionReset()
|
public function testHandleActionReset()
|
||||||
{
|
{
|
||||||
// Init Grid state with some pre-existing filters
|
// Init Grid state with some pre-existing filters
|
||||||
|
Loading…
Reference in New Issue
Block a user