Skip to content

Commit

Permalink
Merge pull request #5 from JanDC/master
Browse files Browse the repository at this point in the history
Allow the validator to be used as a property validator as well
  • Loading branch information
JanDC authored Jan 21, 2021
2 parents 233fac4 + 163f875 commit d027808
Show file tree
Hide file tree
Showing 8 changed files with 157 additions and 66 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
vendor
.idea
composer.lock
composer.lock
.php_cs.cache
13 changes: 13 additions & 0 deletions .php_cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

$finder = PhpCsFixer\Finder::create()
->in(__DIR__)
;

return PhpCsFixer\Config::create()
->setRules([
'@Symfony' => true,
'array_syntax' => ['syntax' => 'short'],
])
->setFinder($finder)
;
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
],
"require": {
"symfony/validator": "^4",
"symfony/security-core": "^4"
"symfony/security-core": "^4",
"friendsofphp/php-cs-fixer": "^2.18"
},
"autoload": {
"psr-4": {
Expand Down
13 changes: 3 additions & 10 deletions src/Constraints/Password.php
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
<?php

declare(strict_types=1);

namespace PasswordValidator\Constraints;

use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Exception\MissingOptionsException;

/**
* @Annotation
Expand All @@ -15,7 +14,6 @@ class Password extends Constraint
const TOO_SHORT_ERROR = '9ff3fdc4-b214-49db-8718-39c315e33d45';
const TOO_LONG_ERROR = 'd94b19cc-114f-4f44-9cc4-4138e80a87b9';


protected static $errorNames = [
self::TOO_SHORT_ERROR => 'TOO_SHORT_ERROR',
self::TOO_LONG_ERROR => 'TOO_LONG_ERROR',
Expand All @@ -32,13 +30,8 @@ class Password extends Constraint
public $max;
public $min;


public function __construct($options = null)
{
if (!isset($options['plainPasswordAccessor']) || !isset($options['plainPasswordProperty'])) {
throw new MissingOptionsException('The plainPasswordAccessor and plainPasswordProperty options are required.', ['plainPasswordAccessor', 'plainPasswordProperty']);
}

if (!isset($options['min'])) {
$options['min'] = 8;
}
Expand All @@ -50,8 +43,8 @@ public function __construct($options = null)
parent::__construct($options);
}

public function getTargets(): string
public function getTargets(): array
{
return self::CLASS_CONSTRAINT;
return [self::CLASS_CONSTRAINT, self::PROPERTY_CONSTRAINT];
}
}
85 changes: 48 additions & 37 deletions src/Constraints/PasswordValidator.php
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
<?php

declare(strict_types=1);

namespace PasswordValidator\Constraints;

use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\MissingOptionsException;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
use Symfony\Component\Validator\Exception\UnexpectedValueException;

class PasswordValidator extends ConstraintValidator
{
/** @var string|null */
private $plainPasswordProperty;

/**
* {@inheritdoc}
*/
Expand All @@ -20,73 +24,80 @@ public function validate($value, Constraint $constraint)
throw new UnexpectedTypeException($constraint, __NAMESPACE__.'\Password');
}

if (!($value instanceof UserInterface)) {
throw new UnexpectedTypeException($value, UserInterface::class);
}
if ($value instanceof UserInterface) {
$plainPasswordAccessor = $constraint->plainPasswordAccessor;
$this->plainPasswordProperty = $constraint->plainPasswordProperty;

$plainPasswordAccessor = $constraint->plainPasswordAccessor;
$plainPasswordProperty = $constraint->plainPasswordProperty;
if (null === $plainPasswordAccessor || null === $this->plainPasswordProperty) {
throw new MissingOptionsException('The plainPasswordAccessor and plainPasswordProperty options are required when using the class constraint.', ['plainPasswordAccessor', 'plainPasswordProperty']);
}

$stringValue = (string) $value->$plainPasswordAccessor();
$stringValue = (string) $value->$plainPasswordAccessor();
} else {
$stringValue = (string) $value;
}

$length = mb_strlen($stringValue);

if (null !== $constraint->max && $length > $constraint->max) {
$this->context->buildViolation($constraint->min == $constraint->max ? $constraint->exactMessage : $constraint->maxMessage)
->setParameter('{{ value }}', $this->formatValue($stringValue))
->setParameter('{{ limit }}', $constraint->max)
->setInvalidValue($value)
->setPlural((int)$constraint->max)
->atPath($plainPasswordProperty)
$this->buildViolation(
$value,
$constraint->min === $constraint->max ? $constraint->exactMessage : $constraint->maxMessage,
['{{ value }}' => $this->formatValue($stringValue), '{{ limit }}' => $constraint->max]
)
->setPlural((int) $constraint->max)
->setCode(Password::TOO_LONG_ERROR)
->addViolation();
}

if (null !== $constraint->min && $length < $constraint->min) {
$this->context->buildViolation($constraint->min == $constraint->max ? $constraint->exactMessage : $constraint->minMessage)
->setParameter('{{ value }}', $this->formatValue($stringValue))
->setParameter('{{ limit }}', $constraint->min)
->setInvalidValue($value)
->atPath($plainPasswordProperty)
->setPlural((int)$constraint->min)
$this->buildViolation($value,
$constraint->min === $constraint->max ? $constraint->exactMessage : $constraint->minMessage,
['{{ value }}' => $this->formatValue($stringValue), '{{ limit }}' => $constraint->min]
)
->setPlural((int) $constraint->min)
->setCode(Password::TOO_SHORT_ERROR)
->addViolation();
}

if (null !== $plainPasswordAccessor && false !== strpos($stringValue, $value->getUserName())) {
if ($value instanceof UserInterface && false !== strpos($stringValue, $value->getUserName())) {
$this->context
->buildViolation($constraint->usernameMessage)
->setInvalidValue($value)
->atPath($plainPasswordProperty)
->atPath($this->plainPasswordProperty)
->addViolation();
}

// Check whether there is at least one upper cased character present
if (!\preg_match('/[A-Z]/', $stringValue)) {
$this->context
->buildViolation($constraint->upperCaseCharacterMissingMessage)
->setInvalidValue($value)
->atPath($plainPasswordProperty)
->addViolation();
$this->buildViolation($value, $constraint->upperCaseCharacterMissingMessage)->addViolation();
}

// Check whether there is at least one lower cased character present
if (!\preg_match('/[a-z]/', $stringValue)) {
$this->context
->buildViolation($constraint->lowerCaseCharacterMissingMessage)
->setInvalidValue($value)
->atPath($plainPasswordProperty)
->addViolation();
$this->buildViolation($value, $constraint->lowerCaseCharacterMissingMessage)->addViolation();
}


// Check whether there is at least one number present
if (!\preg_match('/[0-9]/', $stringValue)) {
$this->context
->buildViolation($constraint->numberMissingMessage)
->setInvalidValue($value)
->atPath($plainPasswordProperty)
->addViolation();
$this->buildViolation($value, $constraint->numberMissingMessage)->addViolation();
}
}

private function buildViolation($value, string $message, array $parameters = [])
{
$isClassConstraint = $value instanceof UserInterface;

$violation = $this->context->buildViolation($message);
$violation->setInvalidValue($value);
foreach ($parameters as $parameterKey => $parameter) {
$violation->setParameter($parameterKey, $parameter);
}

if ($isClassConstraint) {
$violation->atPath($this->plainPasswordProperty);
}

return $violation;
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
<?php

declare(strict_types=1);

use PasswordValidator\Constraints\Password;
use PasswordValidator\Constraints\PasswordValidator;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Validator\Exception\MissingOptionsException;

class PasswordValidatorTest extends \Symfony\Component\Validator\Test\ConstraintValidatorTestCase
{
Expand Down Expand Up @@ -38,7 +39,6 @@ public function testLength()

$this->assertSame(1, $this->context->getViolations()->count());
$this->assertSame($passwordConstraint->minMessage, $this->context->getViolations()->get(0)->getMessageTemplate());

}

public function testNumber()
Expand All @@ -61,7 +61,6 @@ public function testUpperCase()

$this->assertSame(1, $this->context->getViolations()->count());
$this->assertSame($passwordConstraint->upperCaseCharacterMissingMessage, $this->context->getViolations()->get(0)->getMessageTemplate());

}

public function testLowerCase()
Expand All @@ -73,7 +72,6 @@ public function testLowerCase()

$this->assertSame(1, $this->context->getViolations()->count());
$this->assertSame($passwordConstraint->lowerCaseCharacterMissingMessage, $this->context->getViolations()->get(0)->getMessageTemplate());

}

public function testProperPassword()
Expand All @@ -85,6 +83,16 @@ public function testProperPassword()
$this->assertNoViolation();
}

// Check whether class and property options are mixed
public function testInvalidUserConfiguration()
{
$user = new Symfony\Component\Security\Core\User\User('[email protected]', '[email protected]');
$passwordConstraint = new Password();

$this->expectException(MissingOptionsException::class);
$this->validator->validate($user, $passwordConstraint);
}

protected function createValidator()
{
return new PasswordValidator();
Expand Down
14 changes: 1 addition & 13 deletions tests/src/Constraints/PasswordTest.php
Original file line number Diff line number Diff line change
@@ -1,24 +1,12 @@
<?php

declare(strict_types=1);

use PasswordValidator\Constraints\Password;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Validator\Exception\MissingOptionsException;

class PasswordTest extends TestCase
{
public function testConstructionWithoutAccessor()
{
$this->expectException(MissingOptionsException::class);
new Password(['plainPasswordProperty' => 'foo']);
}

public function testConstructionWithoutFieldDefinition()
{
$this->expectException(MissingOptionsException::class);
new Password(['plainPasswordAccessor' => 'getFoo']);
}

public function testConstructionWithProperParameters()
{
$this->assertInstanceOf(Password::class, new Password(['plainPasswordAccessor' => 'getFoo', 'plainPasswordProperty' => 'foo']));
Expand Down
76 changes: 76 additions & 0 deletions tests/src/Constraints/PropertyConstraintTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php

declare(strict_types=1);

use PasswordValidator\Constraints\Password;
use PasswordValidator\Constraints\PasswordValidator;

class PropertyConstraintTest extends \Symfony\Component\Validator\Test\ConstraintValidatorTestCase
{
public function testLength()
{
$password = 'Foo1';
$passwordConstraint = new Password();

$this->validator->validate($password, $passwordConstraint);

$this->assertSame(1, $this->context->getViolations()->count());
$this->assertSame($passwordConstraint->minMessage, $this->context->getViolations()->get(0)->getMessageTemplate());
}

public function testNumber()
{
$password = '[email protected]';
$passwordConstraint = new Password();
$this->validator->validate($password, $passwordConstraint);

$this->assertSame(1, $this->context->getViolations()->count());
$this->assertSame($passwordConstraint->numberMissingMessage, $this->context->getViolations()->get(0)->getMessageTemplate());
}

public function testUpperCase()
{
$password = '[email protected]';
$passwordConstraint = new Password();

$this->validator->validate($password, $passwordConstraint);

$this->assertSame(1, $this->context->getViolations()->count());
$this->assertSame($passwordConstraint->upperCaseCharacterMissingMessage, $this->context->getViolations()->get(0)->getMessageTemplate());
}

public function testLowerCase()
{
$password = 'FOOBARBAZ1';
$passwordConstraint = new Password();

$this->validator->validate($password, $passwordConstraint);

$this->assertSame(1, $this->context->getViolations()->count());
$this->assertSame($passwordConstraint->lowerCaseCharacterMissingMessage, $this->context->getViolations()->get(0)->getMessageTemplate());
}

public function testProperPassword()
{
$password = '[email protected]';
$passwordConstraint = new Password();

$this->validator->validate($password, $passwordConstraint);
$this->assertNoViolation();
}

// Check whether class and property options are mixed
public function testInvalidPropertyConfiguration()
{
$password = '[email protected]';
$passwordConstraint = new Password(['plainPasswordAccessor' => 'getPassword', 'plainPasswordProperty' => 'password']);

$this->validator->validate($password, $passwordConstraint);
$this->assertNoViolation();
}

protected function createValidator()
{
return new PasswordValidator();
}
}

0 comments on commit d027808

Please sign in to comment.