Skip to content

Commit 2724b72

Browse files
committed
[Form] Add hash_mapping option to PasswordType
1 parent c70be09 commit 2724b72

File tree

9 files changed

+306
-1
lines changed

9 files changed

+306
-1
lines changed

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@
170170
use Symfony\Component\Notifier\Notifier;
171171
use Symfony\Component\Notifier\Recipient\Recipient;
172172
use Symfony\Component\Notifier\Transport\TransportFactoryInterface as NotifierTransportFactoryInterface;
173+
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasher;
173174
use Symfony\Component\PropertyAccess\PropertyAccessor;
174175
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
175176
use Symfony\Component\PropertyInfo\Extractor\PhpStanExtractor;
@@ -723,6 +724,10 @@ private function registerFormConfiguration(array $config, ContainerBuilder $cont
723724
->clearTag('kernel.reset')
724725
;
725726
}
727+
728+
if (!ContainerBuilder::willBeAvailable('symfony/security-bundle', UserPasswordHasher::class, [], true)) {
729+
$container->removeDefinition('form.type_extension.password.password_hasher');
730+
}
726731
}
727732

728733
private function registerHttpCacheConfiguration(array $config, ContainerBuilder $container, bool $httpMethodOverride)

src/Symfony/Bundle/FrameworkBundle/Resources/config/form.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,13 @@
1818
use Symfony\Component\Form\Extension\Core\Type\ColorType;
1919
use Symfony\Component\Form\Extension\Core\Type\FileType;
2020
use Symfony\Component\Form\Extension\Core\Type\FormType;
21+
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
2122
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
2223
use Symfony\Component\Form\Extension\Core\Type\TransformationFailureExtension;
2324
use Symfony\Component\Form\Extension\DependencyInjection\DependencyInjectionExtension;
2425
use Symfony\Component\Form\Extension\HttpFoundation\HttpFoundationRequestHandler;
2526
use Symfony\Component\Form\Extension\HttpFoundation\Type\FormTypeHttpFoundationExtension;
27+
use Symfony\Component\Form\Extension\PasswordHasher\Type\PasswordTypePasswordHasherExtension;
2628
use Symfony\Component\Form\Extension\Validator\Type\FormTypeValidatorExtension;
2729
use Symfony\Component\Form\Extension\Validator\Type\RepeatedTypeValidatorExtension;
2830
use Symfony\Component\Form\Extension\Validator\Type\SubmitTypeValidatorExtension;
@@ -144,5 +146,12 @@
144146
param('validator.translation_domain'),
145147
])
146148
->tag('form.type_extension')
149+
150+
->set('form.type_extension.password.password_hasher', PasswordTypePasswordHasherExtension::class)
151+
->args([
152+
service('security.password_hasher'),
153+
service('form.property_accessor'),
154+
])
155+
->tag('form.type_extension', ['extended-type' => PasswordType::class])
147156
;
148157
};

src/Symfony/Component/Form/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ CHANGELOG
55
---
66

77
* Add a `prototype_options` option to `CollectionType`
8+
* Add a `hash_mapping` option to `PasswordType`
89

910
6.0
1011
---
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Form\Extension\PasswordHasher\EventListener;
13+
14+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
15+
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
16+
use Symfony\Component\Form\FormEvent;
17+
use Symfony\Component\Form\FormEvents;
18+
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
19+
use Symfony\Component\PropertyAccess\PropertyAccess;
20+
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
21+
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
22+
23+
/**
24+
* @author Sébastien Alfaiate <s.alfaiate@webarea.fr>
25+
*/
26+
class PasswordHasherListener implements EventSubscriberInterface
27+
{
28+
private $passwordHasher;
29+
private $propertyAccessor;
30+
31+
public function __construct(UserPasswordHasherInterface $passwordHasher, PropertyAccessorInterface $propertyAccessor = null)
32+
{
33+
$this->passwordHasher = $passwordHasher;
34+
$this->propertyAccessor = $propertyAccessor ?? PropertyAccess::createPropertyAccessor();
35+
}
36+
37+
public static function getSubscribedEvents(): array
38+
{
39+
return [
40+
FormEvents::SUBMIT => ['hashPassword', -256],
41+
];
42+
}
43+
44+
public function hashPassword(FormEvent $event)
45+
{
46+
$form = $event->getForm();
47+
$parentForm = $form->getParent();
48+
49+
if ($parentForm && $parentForm->getConfig()->getType()->getInnerType() instanceof RepeatedType) {
50+
$parentForm = $parentForm->getParent();
51+
}
52+
53+
if ($parentForm && ($user = $parentForm->getData()) && ($user instanceof PasswordAuthenticatedUserInterface)) {
54+
$this->propertyAccessor->setValue(
55+
$user,
56+
$form->getConfig()->getOption('hash_mapping'),
57+
$this->passwordHasher->hashPassword($user, $event->getData())
58+
);
59+
}
60+
}
61+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Form\Extension\PasswordHasher;
13+
14+
use Symfony\Component\Form\AbstractExtension;
15+
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
16+
use Symfony\Component\PropertyAccess\PropertyAccess;
17+
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
18+
19+
/**
20+
* Integrates the PasswordHasher component with the Form library.
21+
*
22+
* @author Sébastien Alfaiate <s.alfaiate@webarea.fr>
23+
*/
24+
class PasswordHasherExtension extends AbstractExtension
25+
{
26+
private $passwordHasher;
27+
private $propertyAccessor;
28+
29+
public function __construct(UserPasswordHasherInterface $passwordHasher, PropertyAccessorInterface $propertyAccessor = null)
30+
{
31+
$this->passwordHasher = $passwordHasher;
32+
$this->propertyAccessor = $propertyAccessor ?? PropertyAccess::createPropertyAccessor();
33+
}
34+
35+
/**
36+
* {@inheritdoc}
37+
*/
38+
protected function loadTypeExtensions(): array
39+
{
40+
return [
41+
new Type\PasswordTypePasswordHasherExtension($this->passwordHasher, $this->propertyAccessor),
42+
];
43+
}
44+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Form\Extension\PasswordHasher\Type;
13+
14+
use Symfony\Component\Form\AbstractTypeExtension;
15+
use Symfony\Component\Form\Exception\InvalidConfigurationException;
16+
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
17+
use Symfony\Component\Form\Extension\PasswordHasher\EventListener\PasswordHasherListener;
18+
use Symfony\Component\Form\FormBuilderInterface;
19+
use Symfony\Component\OptionsResolver\OptionsResolver;
20+
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
21+
use Symfony\Component\PropertyAccess\PropertyAccess;
22+
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
23+
24+
/**
25+
* @author Sébastien Alfaiate <s.alfaiate@webarea.fr>
26+
*/
27+
class PasswordTypePasswordHasherExtension extends AbstractTypeExtension
28+
{
29+
private $passwordHasher;
30+
private $propertyAccessor;
31+
32+
public function __construct(UserPasswordHasherInterface $passwordHasher, PropertyAccessorInterface $propertyAccessor = null)
33+
{
34+
$this->passwordHasher = $passwordHasher;
35+
$this->propertyAccessor = $propertyAccessor ?? PropertyAccess::createPropertyAccessor();
36+
}
37+
38+
/**
39+
* {@inheritdoc}
40+
*/
41+
public function buildForm(FormBuilderInterface $builder, array $options)
42+
{
43+
if ($options['hash_mapping']) {
44+
if ($options['mapped']) {
45+
throw new InvalidConfigurationException('The hash_mapping option cannot be used on mapped field.');
46+
}
47+
$builder->addEventSubscriber(new PasswordHasherListener($this->passwordHasher, $this->propertyAccessor));
48+
}
49+
}
50+
51+
/**
52+
* {@inheritdoc}
53+
*/
54+
public function configureOptions(OptionsResolver $resolver)
55+
{
56+
$resolver->setDefaults([
57+
'hash_mapping' => null,
58+
]);
59+
}
60+
61+
/**
62+
* {@inheritdoc}
63+
*/
64+
public static function getExtendedTypes(): iterable
65+
{
66+
return [PasswordType::class];
67+
}
68+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Form\Tests\Extension\PasswordHasher\Type;
13+
14+
use PHPUnit\Framework\MockObject\MockObject;
15+
use Symfony\Component\Form\Exception\InvalidConfigurationException;
16+
use Symfony\Component\Form\Extension\PasswordHasher\PasswordHasherExtension;
17+
use Symfony\Component\Form\Test\TypeTestCase;
18+
use Symfony\Component\Form\Tests\Fixtures\User;
19+
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
20+
21+
class PasswordTypePasswordHasherExtensionTest extends TypeTestCase
22+
{
23+
/**
24+
* @var MockObject&UserPasswordHasherInterface
25+
*/
26+
protected $passwordHasher;
27+
28+
protected function setUp(): void
29+
{
30+
$this->passwordHasher = $this->createMock(UserPasswordHasherInterface::class);
31+
32+
parent::setUp();
33+
}
34+
35+
protected function getExtensions()
36+
{
37+
return array_merge(parent::getExtensions(), [
38+
new PasswordHasherExtension($this->passwordHasher),
39+
]);
40+
}
41+
42+
public function testPasswordHashSuccess()
43+
{
44+
$user = new User();
45+
46+
$plainPassword = 'PlainPassword';
47+
$hashedPassword = 'HashedPassword';
48+
49+
$this->passwordHasher->expects($this->once())
50+
->method('hashPassword')
51+
->with($user, $plainPassword)
52+
->willReturn($hashedPassword)
53+
;
54+
55+
$this->assertNull($user->getPassword());
56+
57+
$form = $this->factory
58+
->createBuilder('Symfony\Component\Form\Extension\Core\Type\FormType', $user)
59+
->add('plainPassword', 'Symfony\Component\Form\Extension\Core\Type\PasswordType', [
60+
'hash_mapping' => 'password',
61+
'mapped' => false,
62+
])
63+
->getForm()
64+
;
65+
66+
$form->submit(['plainPassword' => $plainPassword]);
67+
68+
$this->assertSame($user->getPassword(), $hashedPassword);
69+
}
70+
71+
public function testPasswordHashOnMappedFieldForbidden()
72+
{
73+
$this->expectException(InvalidConfigurationException::class);
74+
$this->expectExceptionMessage('The hash_mapping option cannot be used on mapped field.');
75+
76+
$this->factory
77+
->createBuilder('Symfony\Component\Form\Extension\Core\Type\FormType', new User())
78+
->add('password', 'Symfony\Component\Form\Extension\Core\Type\PasswordType', [
79+
'hash_mapping' => 'password',
80+
'mapped' => true,
81+
])
82+
->getForm()
83+
;
84+
}
85+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Form\Tests\Fixtures;
13+
14+
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
15+
16+
class User implements PasswordAuthenticatedUserInterface
17+
{
18+
private $password;
19+
20+
public function getPassword(): ?string
21+
{
22+
return $this->password;
23+
}
24+
25+
public function setPassword(string $password): self
26+
{
27+
$this->password = $password;
28+
29+
return $this;
30+
}
31+
}

src/Symfony/Component/Form/composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@
5656
"suggest": {
5757
"symfony/validator": "For form validation.",
5858
"symfony/security-csrf": "For protecting forms against CSRF attacks.",
59-
"symfony/twig-bridge": "For templating with Twig."
59+
"symfony/twig-bridge": "For templating with Twig.",
60+
"symfony/password-hasher": "For password hashing."
6061
},
6162
"autoload": {
6263
"psr-4": { "Symfony\\Component\\Form\\": "" },

0 commit comments

Comments
 (0)