diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.xml index 4fd3da7a633e..95e35d54ce2e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.xml @@ -61,6 +61,12 @@ + + + %kernel.charset% + + + diff --git a/src/Symfony/Component/Validator/Constraints/NotCompromisedPasswordValidator.php b/src/Symfony/Component/Validator/Constraints/NotCompromisedPasswordValidator.php index 7abdbf23b26e..a432e90fc04e 100644 --- a/src/Symfony/Component/Validator/Constraints/NotCompromisedPasswordValidator.php +++ b/src/Symfony/Component/Validator/Constraints/NotCompromisedPasswordValidator.php @@ -31,14 +31,16 @@ class NotCompromisedPasswordValidator extends ConstraintValidator private const RANGE_API = 'https://api.pwnedpasswords.com/range/%s'; private $httpClient; + private $charset; - public function __construct(HttpClientInterface $httpClient = null) + public function __construct(HttpClientInterface $httpClient = null, string $charset = 'UTF-8') { if (null === $httpClient && !class_exists(HttpClient::class)) { throw new \LogicException(sprintf('The "%s" class requires the "HttpClient" component. Try running "composer require symfony/http-client".', self::class)); } $this->httpClient = $httpClient ?? HttpClient::create(); + $this->charset = $charset; } /** @@ -61,6 +63,10 @@ public function validate($value, Constraint $constraint) return; } + if ('UTF-8' !== $this->charset) { + $value = mb_convert_encoding($value, 'UTF-8', $this->charset); + } + $hash = strtoupper(sha1($value)); $hashPrefix = substr($hash, 0, 5); $url = sprintf(self::RANGE_API, $hashPrefix); diff --git a/src/Symfony/Component/Validator/Tests/Constraints/NotCompromisedPasswordValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/NotCompromisedPasswordValidatorTest.php index d80ed94b52d6..3ea3f821e0ac 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/NotCompromisedPasswordValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/NotCompromisedPasswordValidatorTest.php @@ -28,40 +28,22 @@ class NotCompromisedPasswordValidatorTest extends ConstraintValidatorTestCase private const PASSWORD_TRIGGERING_AN_ERROR_RANGE_URL = 'https://api.pwnedpasswords.com/range/3EF27'; // https://api.pwnedpasswords.com/range/3EF27 is the range for the value "apiError" private const PASSWORD_LEAKED = 'maman'; private const PASSWORD_NOT_LEAKED = ']<0585"%sb^5aa$w6!b38",,72?dp3r4\45b28Hy'; + private const PASSWORD_NON_UTF8_LEAKED = 'мама'; + private const PASSWORD_NON_UTF8_NOT_LEAKED = 'м<в0dp3r4\45b28Hy'; private const RETURN = [ '35E033023A46402F94CFB4F654C5BFE44A1:1', '35F079CECCC31812288257CD770AA7968D7:53', - '36039744C253F9B2A4E90CBEDB02EBFB82D:5', // this is the matching line, password: maman + '36039744C253F9B2A4E90CBEDB02EBFB82D:5', // UTF-8 leaked password: maman + '273CA8A2A78C9B2D724144F4FAF4D221C86:6', // ISO-8859-5 leaked password: мама '3686792BBC66A72D40D928ED15621124CFE:7', '36EEC709091B810AA240179A44317ED415C:2', ]; protected function createValidator() { - $httpClientStub = $this->createMock(HttpClientInterface::class); - $httpClientStub->method('request')->will( - $this->returnCallback(function (string $method, string $url): ResponseInterface { - if (self::PASSWORD_TRIGGERING_AN_ERROR_RANGE_URL === $url) { - throw new class('Problem contacting the Have I been Pwned API.') extends \Exception implements ServerExceptionInterface { - public function getResponse(): ResponseInterface - { - throw new \RuntimeException('Not implemented'); - } - }; - } - - $responseStub = $this->createMock(ResponseInterface::class); - $responseStub - ->method('getContent') - ->willReturn(implode("\r\n", self::RETURN)); - - return $responseStub; - }) - ); - - // Pass HttpClient::create() instead of this mock to run the tests against the real API - return new NotCompromisedPasswordValidator($httpClientStub); + // Pass HttpClient::create() instead of the mock to run the tests against the real API + return new NotCompromisedPasswordValidator($this->createHttpClientStub()); } public function testNullIsValid() @@ -112,6 +94,29 @@ public function testValidPassword() $this->assertNoViolation(); } + public function testNonUtf8CharsetValid() + { + $validator = new NotCompromisedPasswordValidator($this->createHttpClientStub(), 'ISO-8859-5'); + $validator->validate(mb_convert_encoding(self::PASSWORD_NON_UTF8_NOT_LEAKED, 'ISO-8859-5', 'UTF-8'), new NotCompromisedPassword()); + + $this->assertNoViolation(); + } + + public function testNonUtf8CharsetInvalid() + { + $constraint = new NotCompromisedPassword(); + + $this->context = $this->createContext(); + + $validator = new NotCompromisedPasswordValidator($this->createHttpClientStub(), 'ISO-8859-5'); + $validator->initialize($this->context); + $validator->validate(mb_convert_encoding(self::PASSWORD_NON_UTF8_LEAKED, 'ISO-8859-5', 'UTF-8'), $constraint); + + $this->buildViolation($constraint->message) + ->setCode(NotCompromisedPassword::COMPROMISED_PASSWORD_ERROR) + ->assertRaised(); + } + /** * @expectedException \Symfony\Component\Validator\Exception\UnexpectedTypeException */ @@ -142,4 +147,30 @@ public function testApiErrorSkipped() $this->validator->validate(self::PASSWORD_TRIGGERING_AN_ERROR, new NotCompromisedPassword(['skipOnError' => true])); $this->assertTrue(true); // No exception have been thrown } + + private function createHttpClientStub(): HttpClientInterface + { + $httpClientStub = $this->createMock(HttpClientInterface::class); + $httpClientStub->method('request')->will( + $this->returnCallback(function (string $method, string $url): ResponseInterface { + if (self::PASSWORD_TRIGGERING_AN_ERROR_RANGE_URL === $url) { + throw new class('Problem contacting the Have I been Pwned API.') extends \Exception implements ServerExceptionInterface { + public function getResponse(): ResponseInterface + { + throw new \RuntimeException('Not implemented'); + } + }; + } + + $responseStub = $this->createMock(ResponseInterface::class); + $responseStub + ->method('getContent') + ->willReturn(implode("\r\n", self::RETURN)); + + return $responseStub; + }) + ); + + return $httpClientStub; + } }