diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 65c1c0b..b71b58a 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -22,8 +22,4 @@ ./src - - - - diff --git a/src/DependencyInjection/RollerworksPasswordStrengthExtension.php b/src/DependencyInjection/RollerworksPasswordStrengthExtension.php index 8a5c9f9..598867e 100644 --- a/src/DependencyInjection/RollerworksPasswordStrengthExtension.php +++ b/src/DependencyInjection/RollerworksPasswordStrengthExtension.php @@ -36,6 +36,7 @@ public function load(array $configs, ContainerBuilder $container) $container->setAlias('rollerworks_password_strength.blacklist_provider', $config['blacklist']['default_provider']); $loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); + $loader->load('strength_validator.xml'); $loader->load('blacklist.xml'); if (isset($config['blacklist']['providers']['sqlite'])) { diff --git a/src/Resources/config/strength_validator.xml b/src/Resources/config/strength_validator.xml new file mode 100644 index 0000000..0371e77 --- /dev/null +++ b/src/Resources/config/strength_validator.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/src/Resources/translations/validators.en.xlf b/src/Resources/translations/validators.en.xlf new file mode 100644 index 0000000..d78884b --- /dev/null +++ b/src/Resources/translations/validators.en.xlf @@ -0,0 +1,79 @@ + + + + + + Your password must be at least {{length}} characters long. + Password must be at least {{length}} characters long. + + + Your password must include at least one letter. + Password must include at least one letter. + + + Your password must include both upper and lower case letters. + Password must include both upper and lower case letters. + + + Your password must include at least one number. + Password must include at least one number. + + + Your password must contain at least one special character. + Password must contain at least one special character. + + + password_too_weak + Password needs to be at least at strength level "{{ min_strength }}", current level is "{{ current_strength }}", try the following {{ strength_tips }}. + + + + + rollerworks_password.strength_level.very_weak + Very Weak + + + rollerworks_password.strength_level.weak + Weak + + + rollerworks_password.strength_level.medium + Medium + + + rollerworks_password.strength_level.strong + Strong + + + rollerworks_password.strength_level.very_strong + Very strong + + + + + rollerworks_password.tip.letters + add (upper/lowercase) letters + + + rollerworks_password.tip.numbers + add numbers + + + rollerworks_password.tip.lowercase_letters + add uppercase letters + + + rollerworks_password.tip.uppercase_letters + add uppercase letters + + + rollerworks_password.tip.special_chars + add special characters + + + rollerworks_password.tip.length + add more characters + + + + diff --git a/src/Resources/translations/validators.nl.xlf b/src/Resources/translations/validators.nl.xlf new file mode 100644 index 0000000..4bc47f1 --- /dev/null +++ b/src/Resources/translations/validators.nl.xlf @@ -0,0 +1,79 @@ + + + + + + Your password must be at least {{length}} characters long. + Wachtwoord moet minstens {{length}} tekens lang zijn. + + + Your password must include at least one letter. + Wachtwoord moet ten minste één letter bevatten. + + + Your password must include both upper and lower case letters. + Wachtwoord moet ten minste één hoofdletter en kleine letter bevatten. + + + Your password must include at least one number. + Wachtwoord moet ten minste één nummer bevatten. + + + Your password must contain at least one special character. + Wachtwoord moet ten minste één speciaal teken of leesteken bevatten. + + + password_too_weak + Wachtwoord moet minstens aan sterkte niveau "{{ min_strength }}" voldoen, huidig niveau is “{{ current_strength }}” probeer het volgende: {{ strength_tips }}. + + + + + rollerworks_password.strength_level.very_weak + Erg zwak + + + rollerworks_password.strength_level.weak + Zwak + + + rollerworks_password.strength_level.medium + Gemiddeld + + + rollerworks_password.strength_level.strong + Sterk + + + rollerworks_password.strength_level.very_strong + Zeer sterk + + + + + rollerworks_password.tip.letters + voeg (hoofd/kleine) letters toe + + + rollerworks_password.tip.numbers + voeg nummers toe + + + rollerworks_password.tip.lowercase_letters + voeg hoofdletters toe + + + rollerworks_password.tip.uppercase_letters + voeg kleine letters toe + + + rollerworks_password.tip.special_chars + voeg speciaal tekens of leestekens toe + + + rollerworks_password.tip.length + gebruik meer tekens + + + + diff --git a/src/Validator/Constraints/PasswordStrength.php b/src/Validator/Constraints/PasswordStrength.php index dd5bcb1..3e0d42c 100644 --- a/src/Validator/Constraints/PasswordStrength.php +++ b/src/Validator/Constraints/PasswordStrength.php @@ -34,4 +34,9 @@ public function getRequiredOptions() { return array('minStrength'); } + + public function validatedBy() + { + return 'rollerworks_password_strength'; + } } diff --git a/src/Validator/Constraints/PasswordStrengthValidator.php b/src/Validator/Constraints/PasswordStrengthValidator.php index e414970..4251e8c 100644 --- a/src/Validator/Constraints/PasswordStrengthValidator.php +++ b/src/Validator/Constraints/PasswordStrengthValidator.php @@ -11,6 +11,9 @@ namespace Rollerworks\Bundle\PasswordStrengthBundle\Validator\Constraints; +use Symfony\Component\Translation\Loader\XliffFileLoader; +use Symfony\Component\Translation\Translator; +use Symfony\Component\Translation\TranslatorInterface; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Context\ExecutionContextInterface; @@ -38,6 +41,35 @@ */ class PasswordStrengthValidator extends ConstraintValidator { + /** + * @var TranslatorInterface + */ + private $translator; + + /** + * @var array + */ + private static $levelToLabel = array( + 1 => 'very_weak', + 2 => 'weak', + 3 => 'medium', + 4 => 'strong', + 5 => 'very_strong', + ); + + public function __construct(TranslatorInterface $translator = null) + { + // If translator is missing create a new translator. + // With the 'en' locale and 'validators' domain. + if (null === $translator) { + $translator = new Translator('en'); + $translator->addLoader('xlf', new XliffFileLoader()); + $translator->addResource('xlf', dirname(dirname(__DIR__)).'/Resources/translations/validators.en.xlf', 'en', 'validators'); + } + + $this->translator = $translator; + } + /** * @param string $password * @param PasswordStrength|Constraint $constraint @@ -69,35 +101,65 @@ public function validate($password, Constraint $constraint) return; } + $tips = array(); + if (preg_match('/[a-zA-Z]/', $password)) { ++$passwordStrength; - if (preg_match('/[a-z]/', $password) && preg_match('/[A-Z]/', $password)) { + + if (!preg_match('/[a-z]/', $password)) { + $tips[] = 'lowercase_letters'; + } elseif (preg_match('/[A-Z]/', $password)) { ++$passwordStrength; + } else { + $tips[] = 'uppercase_letters'; } + } else { + $tips[] = 'letters'; } if (preg_match('/\d+/', $password)) { ++$passwordStrength; + } else { + $tips[] = 'numbers'; } if (preg_match('/[^a-zA-Z0-9]/', $password)) { ++$passwordStrength; + } else { + $tips[] = 'special_chars'; } if ($passLength > 12) { ++$passwordStrength; + } else { + $tips[] = 'length'; } // No decrease strength on weak combinations if ($passwordStrength < $constraint->minStrength) { + $parameters = array( + '{{ length }}' => $constraint->minLength, + '{{ min_strength }}' => $this->translator->trans('rollerworks_password.strength_level.'.self::$levelToLabel[$constraint->minStrength], array(), 'validators'), + '{{ current_strength }}' => $this->translator->trans('rollerworks_password.strength_level.'.self::$levelToLabel[$passwordStrength], array(), 'validators'), + '{{ strength_tips }}' => implode(', ', array_map(array($this, 'translateTips'), $tips)), + ); + if ($this->context instanceof ExecutionContextInterface) { $this->context->buildViolation($constraint->message) - ->setParameters(array('{{ length }}' => $constraint->minLength)) + ->setParameters($parameters) ->addViolation(); } else { - $this->context->addViolation($constraint->message, array('{{ length }}' => $constraint->minLength)); + $this->context->addViolation($constraint->message, $parameters); } } } + + /** + * @internal + */ + public function translateTips($tip) + { + return $this->translator->trans('rollerworks_password.tip.'.$tip, array(), 'validators'); + } } diff --git a/tests/DependencyInjection/ExtensionTest.php b/tests/DependencyInjection/ExtensionTest.php index 6724d8a..64349af 100644 --- a/tests/DependencyInjection/ExtensionTest.php +++ b/tests/DependencyInjection/ExtensionTest.php @@ -13,8 +13,10 @@ use Rollerworks\Bundle\PasswordStrengthBundle\DependencyInjection\RollerworksPasswordStrengthExtension; use Rollerworks\Bundle\PasswordStrengthBundle\Validator\Constraints\Blacklist as BlacklistConstraint; +use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddConstraintValidatorsPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; +use Symfony\Component\Validator\Tests\Fixtures\Reference; class ExtensionTest extends \PHPUnit_Framework_TestCase { @@ -107,6 +109,30 @@ public function testLoadWithChainConfiguration() $this->assertFalse($provider->isBlacklisted('leeRoy')); } + public function testPasswordStrengthValidatorService() + { + $container = $this->createContainer(); + $container->registerExtension(new RollerworksPasswordStrengthExtension()); + $container->loadFromExtension('rollerworks_password_strength'); + + $container->addCompilerPass(new AddConstraintValidatorsPass()); + $container->register( + 'validator.validator_factory', + 'Symfony\Bundle\FrameworkBundle\Validator\ConstraintValidatorFactory' + )->setArguments(array(new Reference('service_container'), array())); + + $this->compileContainer($container); + + $validatorFactory = $container->getDefinition('validator.validator_factory'); + $factoryArguments = $validatorFactory->getArguments(); + + $this->assertArrayHasKey('rollerworks_password_strength', $factoryArguments[1]); + $this->assertEquals( + 'rollerworks_password_strength.validator.password_strength', + $factoryArguments[1]['rollerworks_password_strength'] + ); + } + /** * @return ContainerBuilder */ diff --git a/tests/Validator/PasswordStrengthTest.php b/tests/Validator/PasswordStrengthTest.php index 283486b..92284ee 100644 --- a/tests/Validator/PasswordStrengthTest.php +++ b/tests/Validator/PasswordStrengthTest.php @@ -13,11 +13,23 @@ use Rollerworks\Bundle\PasswordStrengthBundle\Validator\Constraints\PasswordStrength; use Rollerworks\Bundle\PasswordStrengthBundle\Validator\Constraints\PasswordStrengthValidator; +use Symfony\Component\Translation\Translator; use Symfony\Component\Validator\Tests\Constraints\AbstractConstraintValidatorTest; use Symfony\Component\Validator\Validation; class PasswordStrengthTest extends AbstractConstraintValidatorTest { + /** + * @var array + */ + private static $levelToLabel = array( + 1 => 'very_weak', + 2 => 'weak', + 3 => 'medium', + 4 => 'strong', + 5 => 'very_strong', + ); + protected function getApiVersion() { return Validation::API_VERSION_2_5; @@ -25,7 +37,7 @@ protected function getApiVersion() protected function createValidator() { - return new PasswordStrengthValidator(); + return new PasswordStrengthValidator(new Translator('en')); } public function testNullIsValid() @@ -50,35 +62,31 @@ public function testExpectsStringCompatibleType() $this->validator->validate(new \stdClass(), new PasswordStrength(5)); } - public static function getVeryWeakPasswords() + public function getWeakPasswords() { - return array( - array('weaker'), - array('123456'), - array('foobar'), - array('!.!.!.'), - ); - } + $pre = 'rollerworks_password.tip.'; - public static function getWeakPasswords() - { return array( - array('wee6eak'), - array('foobar!'), - array('Foobar'), - array('123456!'), - array('7857375923752947'), - array('fjsfjdljfsjsjjlsj'), - ); - } - - public static function getMediumPasswords() - { - return array( - array('Foobar!'), - array('foo-b0r!'), - array('fjsfjdljfsjsjjls1'), - array('785737592375294b'), + // Very weak + array(2, 'weaker', 1, "{$pre}uppercase_letters, {$pre}numbers, {$pre}special_chars, {$pre}length"), + array(2, '123456', 1, "{$pre}letters, {$pre}special_chars, {$pre}length"), + array(2, 'foobar', 1, "{$pre}uppercase_letters, {$pre}numbers, {$pre}special_chars, {$pre}length"), + array(2, '!.!.!.', 1, "{$pre}letters, {$pre}numbers, {$pre}length"), + + // Weak + array(3, 'wee6eak', 2, "{$pre}uppercase_letters, {$pre}special_chars, {$pre}length"), + array(3, 'foobar!', 2, "{$pre}uppercase_letters, {$pre}numbers, {$pre}length"), + array(3, 'Foobar', 2, "{$pre}numbers, {$pre}special_chars, {$pre}length"), + array(3, '123456!', 2, "{$pre}letters, {$pre}length"), + array(3, '7857375923752947', 2, "{$pre}letters, {$pre}special_chars"), + array(3, 'FSDFJSLKFFSDFDSF', 2, "{$pre}lowercase_letters, {$pre}numbers, {$pre}special_chars"), + array(3, 'fjsfjdljfsjsjjlsj', 2, "{$pre}uppercase_letters, {$pre}numbers, {$pre}special_chars"), + + // Medium + array(4, 'Foobar!', 3, "{$pre}numbers, {$pre}length"), + array(4, 'foo-b0r!', 3, "{$pre}uppercase_letters, {$pre}length"), + array(4, 'fjsfjdljfsjsjjls1', 3, "{$pre}uppercase_letters, {$pre}special_chars"), + array(4, '785737592375294b', 3, "{$pre}uppercase_letters, {$pre}special_chars"), ); } @@ -100,66 +108,31 @@ public static function getVeryStrongPasswords() ); } - /** - * @dataProvider getVeryWeakPasswords - */ - public function testVeryWeakPasswords($value) - { - $constraint = new PasswordStrength(2); - - $this->validator->validate($value, $constraint); - - $this->buildViolation('password_too_weak') - ->setParameters(array('{{ length }}' => 6)) - ->assertRaised(); - } - /** * @dataProvider getWeakPasswords */ - public function testWeakPasswords($value) - { - $constraint = new PasswordStrength(array('minStrength' => 3, 'minLength' => 7)); - - $this->validator->validate($value, $constraint); - - $this->buildViolation('password_too_weak') - ->setParameters(array('{{ length }}' => 7)) - ->assertRaised(); - } - - /** - * @dataProvider getMediumPasswords - */ - public function testMediumPasswords($value) + public function testWeakPasswordsWillNotPass($minStrength, $value, $currentStrength, $tips = '') { - $constraint = new PasswordStrength(4); + $constraint = new PasswordStrength(array('minStrength' => $minStrength, 'minLength' => 6)); $this->validator->validate($value, $constraint); - $this->buildViolation('password_too_weak') - ->setParameters(array('{{ length }}' => 6)) - ->assertRaised(); - } - - /** - * @dataProvider getStrongPasswords - */ - public function testStrongPasswords($value) - { - $constraint = new PasswordStrength(5); - - $this->validator->validate($value, $constraint); + $parameters = array( + '{{ length }}' => 6, + '{{ min_strength }}' => 'rollerworks_password.strength_level.'.self::$levelToLabel[$minStrength], + '{{ current_strength }}' => 'rollerworks_password.strength_level.'.self::$levelToLabel[$currentStrength], + '{{ strength_tips }}' => $tips, + ); $this->buildViolation('password_too_weak') - ->setParameters(array('{{ length }}' => 6)) + ->setParameters($parameters) ->assertRaised(); } /** * @dataProvider getVeryStrongPasswords */ - public function testVeryStrongPasswords($value) + public function testStrongPasswordsWillPass($value) { $constraint = new PasswordStrength(5); @@ -168,66 +141,31 @@ public function testVeryStrongPasswords($value) $this->assertNoViolation(); } - /** - * @dataProvider getVeryWeakPasswords - */ - public function testVeryWeakPasswordWillNotPass($value) + public function testConstraintGetDefaultOption() { - $constraint = new PasswordStrength(2); - - $this->validator->validate($value, $constraint); + $constraint = new PasswordStrength(5); - $this->buildViolation('password_too_weak') - ->setParameters(array('{{ length }}' => 6)) - ->assertRaised(); + $this->assertEquals(5, $constraint->minStrength); } - /** - * @dataProvider getWeakPasswords - */ - public function testWeakPasswordsWillNotPass($value) + public function testParametersAreTranslatedWhenTranslatorIsMissing() { - $constraint = new PasswordStrength(3); + $this->validator = new PasswordStrengthValidator(); + $this->validator->initialize($this->context); - $this->validator->validate($value, $constraint); - - $this->buildViolation('password_too_weak') - ->setParameters(array('{{ length }}' => 6)) - ->assertRaised(); - } + $constraint = new PasswordStrength(array('minStrength' => 5, 'minLength' => 6)); - /** - * @dataProvider getMediumPasswords - */ - public function testMediumPasswordWillNotPass($value) - { - $constraint = new PasswordStrength(4); + $this->validator->validate('FD43f.!', $constraint); - $this->validator->validate($value, $constraint); - - $this->buildViolation('password_too_weak') - ->setParameters(array('{{ length }}' => 6)) - ->assertRaised(); - } - - /** - * @dataProvider getStrongPasswords - */ - public function testStrongPasswordWillNotPass($value) - { - $constraint = new PasswordStrength(5); - - $this->validator->validate($value, $constraint); + $parameters = array( + '{{ length }}' => 6, + '{{ current_strength }}' => 'Strong', + '{{ min_strength }}' => 'Very strong', + '{{ strength_tips }}' => 'add more characters', + ); $this->buildViolation('password_too_weak') - ->setParameters(array('{{ length }}' => 6)) + ->setParameters($parameters) ->assertRaised(); } - - public function testConstraintGetDefaultOption() - { - $constraint = new PasswordStrength(5); - - $this->assertEquals(5, $constraint->minStrength); - } }