diff --git a/Controller/ConfirmEmailUpdateController.php b/Controller/ConfirmEmailUpdateController.php new file mode 100644 index 0000000000..38ea118ce3 --- /dev/null +++ b/Controller/ConfirmEmailUpdateController.php @@ -0,0 +1,115 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FOS\UserBundle\Controller; + +use FOS\UserBundle\Event\UserEvent; +use FOS\UserBundle\FOSUserEvents; +use FOS\UserBundle\Model\User; +use FOS\UserBundle\Model\UserInterface; +use FOS\UserBundle\Model\UserManagerInterface; +use FOS\UserBundle\Services\EmailConfirmation\EmailUpdateConfirmation; +use FOS\UserBundle\Util\CanonicalFieldsUpdater; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Security\Core\Exception\AccessDeniedException; +use Symfony\Component\Translation\TranslatorInterface; + +/** + * Controller managing the confirmation of changed user email. + * + * @author Dominik Businger + */ +class ConfirmEmailUpdateController extends AbstractController +{ + /** + * @var EventDispatcherInterface + */ + private $eventDispatcher; + + /** + * @var UserManagerInterface + */ + private $userManager; + + /** + * @var EmailUpdateConfirmation + */ + private $emailUpdateConfirmation; + + /** + * @var TranslatorInterface + */ + private $translator; + /** + * @var CanonicalFieldsUpdater + */ + private $canonicalFieldsUpdater; + + /** + * @param EventDispatcherInterface $eventDispatcher + * @param UserManagerInterface $userManager + * @param EmailUpdateConfirmation $emailUpdateConfirmation + * @param TranslatorInterface $translator + * @param CanonicalFieldsUpdater $canonicalFieldsUpdater + */ + public function __construct(EventDispatcherInterface $eventDispatcher, UserManagerInterface $userManager, EmailUpdateConfirmation $emailUpdateConfirmation, TranslatorInterface $translator, CanonicalFieldsUpdater $canonicalFieldsUpdater) + { + $this->eventDispatcher = $eventDispatcher; + $this->userManager = $userManager; + $this->emailUpdateConfirmation = $emailUpdateConfirmation; + $this->translator = $translator; + $this->canonicalFieldsUpdater = $canonicalFieldsUpdater; + } + + /** + * Confirm user`s email update. + * + * @param Request $request + * @param string $token + * + * @return \Symfony\Component\HttpFoundation\RedirectResponse + */ + public function confirmEmailUpdateAction(Request $request, $token) + { + /** @var User $user */ + $user = $this->userManager->findUserByConfirmationToken($token); + + // If user was not found throw 404 exception + if (!$user) { + throw $this->createNotFoundException($this->translator->trans('email_update.error.message', array(), 'FOSUserBundle')); + } + + // Show invalid token message if the user id found via token does not match the current users id (e.g. anon. or other user) + if (!($this->getUser() instanceof UserInterface) || ($user->getId() !== $this->getUser()->getId())) { + throw new AccessDeniedException($this->translator->trans('email_update.error.message', array(), 'FOSUserBundle')); + } + + $this->emailUpdateConfirmation->setUser($user); + + $newEmail = $this->emailUpdateConfirmation->fetchEncryptedEmailFromConfirmationLink($request->get('target')); + + // Update user email + if ($newEmail) { + $user->setConfirmationToken($this->emailUpdateConfirmation->getEmailConfirmedToken()); + $user->setEmail($newEmail); + $user->setEmail($this->canonicalFieldsUpdater->canonicalizeEmail($newEmail)); + } + + $this->userManager->updateUser($user); + + $event = new UserEvent($user, $request); + $this->eventDispatcher->dispatch(FOSUserEvents::EMAIL_UPDATE_SUCCESS, $event); + + return $this->redirect($this->generateUrl('fos_user_profile_show')); + } +} diff --git a/Controller/ProfileController.php b/Controller/ProfileController.php index ad25e3c0bd..f0cf14e95f 100644 --- a/Controller/ProfileController.php +++ b/Controller/ProfileController.php @@ -14,20 +14,16 @@ use FOS\UserBundle\Event\FilterUserResponseEvent; use FOS\UserBundle\Event\FormEvent; use FOS\UserBundle\Event\GetResponseUserEvent; -use FOS\UserBundle\Event\UserEvent; use FOS\UserBundle\Form\Factory\FactoryInterface; use FOS\UserBundle\FOSUserEvents; -use FOS\UserBundle\Model\User; use FOS\UserBundle\Model\UserInterface; use FOS\UserBundle\Model\UserManagerInterface; -use FOS\UserBundle\Services\EmailConfirmation\EmailUpdateConfirmation; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Security\Core\Exception\AccessDeniedException; -use Symfony\Component\Translation\TranslatorInterface; /** * Controller managing the user profile. @@ -51,30 +47,16 @@ class ProfileController extends AbstractController */ private $userManager; - /** - * @var EmailUpdateConfirmation - */ - private $emailUpdateConfirmation; - - /** - * @var TranslatorInterface - */ - private $translator; - /** * @param EventDispatcherInterface $eventDispatcher * @param FactoryInterface $formFactory * @param UserManagerInterface $userManager - * @param EmailUpdateConfirmation $emailUpdateConfirmation - * @param TranslatorInterface $translator */ - public function __construct(EventDispatcherInterface $eventDispatcher, FactoryInterface $formFactory, UserManagerInterface $userManager, EmailUpdateConfirmation $emailUpdateConfirmation, TranslatorInterface $translator) + public function __construct(EventDispatcherInterface $eventDispatcher, FactoryInterface $formFactory, UserManagerInterface $userManager) { $this->eventDispatcher = $eventDispatcher; $this->formFactory = $formFactory; $this->userManager = $userManager; - $this->emailUpdateConfirmation = $emailUpdateConfirmation; - $this->translator = $translator; } /** @@ -140,45 +122,4 @@ public function editAction(Request $request) 'form' => $form->createView(), )); } - - /** - * Confirm user`s email update. - * - * @param Request $request - * @param string $token - * - * @return \Symfony\Component\HttpFoundation\RedirectResponse - */ - public function confirmEmailUpdateAction(Request $request, $token) - { - /** @var User $user */ - $user = $this->userManager->findUserByConfirmationToken($token); - - // If user was not found throw 404 exception - if (!$user) { - throw $this->createNotFoundException($this->translator->trans('email_update.error.message', array(), 'FOSUserBundle')); - } - - // Show invalid token message if the user id found via token does not match the current users id (e.g. anon. or other user) - if (!($this->getUser() instanceof UserInterface) || ($user->getId() !== $this->getUser()->getId())) { - throw new AccessDeniedException($this->translator->trans('email_update.error.message', array(), 'FOSUserBundle')); - } - - $this->emailUpdateConfirmation->setUser($user); - - $newEmail = $this->emailUpdateConfirmation->fetchEncryptedEmailFromConfirmationLink($request->get('target')); - - // Update user email - if ($newEmail) { - $user->setConfirmationToken($this->emailUpdateConfirmation->getEmailConfirmedToken()); - $user->setEmail($newEmail); - } - - $this->userManager->updateUser($user); - - $event = new UserEvent($user, $request); - $this->eventDispatcher->dispatch(FOSUserEvents::EMAIL_UPDATE_SUCCESS, $event); - - return $this->redirect($this->generateUrl('fos_user_profile_show')); - } } diff --git a/DependencyInjection/FOSUserExtension.php b/DependencyInjection/FOSUserExtension.php index 86e9eb7fd7..7409752dd1 100644 --- a/DependencyInjection/FOSUserExtension.php +++ b/DependencyInjection/FOSUserExtension.php @@ -193,13 +193,12 @@ private function loadProfile(array $config, ContainerBuilder $container, XmlFile if ($config['email_update_confirmation']['enabled']) { if ('custom' !== $dbDriver && isset(self::$doctrineDrivers[$dbDriver])) { - $loader->load('profile_email_update_listener.xml'); + $loader->load('profile_email_update.xml'); } + $container->setParameter('fos_user.email_update_confirmation.template', $config['email_update_confirmation']['email_template']); + $container->setParameter('fos_user.email_update_confirmation.cypher_method', $config['email_update_confirmation']['cypher_method']); } - $container->setParameter('fos_user.email_update_confirmation.template', $config['email_update_confirmation']['email_template']); - $container->setParameter('fos_user.email_update_confirmation.cypher_method', $config['email_update_confirmation']['cypher_method']); - $this->remapParametersNamespaces($config, $container, array( 'form' => 'fos_user.profile.form.%s', )); diff --git a/Doctrine/EmailUpdateListener.php b/Doctrine/EmailUpdateListener.php index 6c34a05524..96ec72428d 100644 --- a/Doctrine/EmailUpdateListener.php +++ b/Doctrine/EmailUpdateListener.php @@ -14,6 +14,7 @@ use Doctrine\ORM\Event\PreUpdateEventArgs; use FOS\UserBundle\Model\UserInterface; use FOS\UserBundle\Services\EmailConfirmation\EmailUpdateConfirmation; +use FOS\UserBundle\Util\CanonicalFieldsUpdater; use Symfony\Component\HttpFoundation\RequestStack; /** @@ -30,17 +31,23 @@ class EmailUpdateListener * @var RequestStack */ private $requestStack; + /** + * @var CanonicalFieldsUpdater + */ + private $canonicalFieldsUpdater; /** * Constructor. * * @param EmailUpdateConfirmation $emailUpdateConfirmation * @param RequestStack $requestStack + * @param CanonicalFieldsUpdater $canonicalFieldsUpdater */ - public function __construct(EmailUpdateConfirmation $emailUpdateConfirmation, RequestStack $requestStack) + public function __construct(EmailUpdateConfirmation $emailUpdateConfirmation, RequestStack $requestStack, CanonicalFieldsUpdater $canonicalFieldsUpdater) { $this->emailUpdateConfirmation = $emailUpdateConfirmation; $this->requestStack = $requestStack; + $this->canonicalFieldsUpdater = $canonicalFieldsUpdater; } /** @@ -58,8 +65,8 @@ public function preUpdate(PreUpdateEventArgs $args) if ($user->getConfirmationToken() != $this->emailUpdateConfirmation->getEmailConfirmedToken() && isset($args->getEntityChangeSet()['email'])) { $oldEmail = $args->getEntityChangeSet()['email'][0]; $newEmail = $args->getEntityChangeSet()['email'][1]; - $user->setEmail($oldEmail); + $user->setEmailCanonical($this->canonicalFieldsUpdater->canonicalizeEmail($oldEmail)); // Configure email confirmation $this->emailUpdateConfirmation->setUser($user); diff --git a/Mailer/Mailer.php b/Mailer/Mailer.php index 17d8785c15..a953d5dc62 100644 --- a/Mailer/Mailer.php +++ b/Mailer/Mailer.php @@ -93,7 +93,7 @@ public function sendResettingEmailMessage(UserInterface $user) */ public function sendUpdateEmailConfirmation(UserInterface $user, $confirmationUrl, $toEmail) { - $template = $this->parameters['template']['email_updating']; + $template = $this->parameters['email_updating.template']; $rendered = $this->templating->render($template, array( 'user' => $user, 'confirmationUrl' => $confirmationUrl, diff --git a/Resources/config/email_confirmation.xml b/Resources/config/email_confirmation.xml index 1e724d3708..ff3be6c9e7 100644 --- a/Resources/config/email_confirmation.xml +++ b/Resources/config/email_confirmation.xml @@ -13,18 +13,6 @@ - - - %fos_user.email_update_confirmation.cypher_method% - - - - - - - - - diff --git a/Resources/config/profile.xml b/Resources/config/profile.xml index 73bf333094..85356e26e3 100644 --- a/Resources/config/profile.xml +++ b/Resources/config/profile.xml @@ -18,12 +18,10 @@ - + - - diff --git a/Resources/config/profile_email_update.xml b/Resources/config/profile_email_update.xml new file mode 100644 index 0000000000..3532f0e96b --- /dev/null +++ b/Resources/config/profile_email_update.xml @@ -0,0 +1,38 @@ + + + + + + + + %fos_user.email_update_confirmation.cypher_method% + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Resources/config/profile_email_update_listener.xml b/Resources/config/profile_email_update_listener.xml deleted file mode 100644 index 0b9e36ad3a..0000000000 --- a/Resources/config/profile_email_update_listener.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - diff --git a/Resources/config/routing/profile.xml b/Resources/config/routing/profile.xml index 606f747621..87cf50b2da 100644 --- a/Resources/config/routing/profile.xml +++ b/Resources/config/routing/profile.xml @@ -5,11 +5,11 @@ xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/routing-1.0.xsd"> - fos_user.profiler.controller:showAction + fos_user.profile.controller:showAction - fos_user.profiler.controller:editAction + fos_user.profile.controller:editAction diff --git a/Resources/config/routing/update_email.xml b/Resources/config/routing/update_email.xml index 131a94aa90..87f8664f7e 100644 --- a/Resources/config/routing/update_email.xml +++ b/Resources/config/routing/update_email.xml @@ -5,7 +5,7 @@ xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/routing-1.0.xsd"> - fos_user.profiler.controller:confirmEmailUpdateAction + fos_user.confirm.email.update.controller:confirmEmailUpdateAction diff --git a/Resources/doc/configuration_reference.rst b/Resources/doc/configuration_reference.rst index 3ccdec2ba3..5a32e400d4 100644 --- a/Resources/doc/configuration_reference.rst +++ b/Resources/doc/configuration_reference.rst @@ -25,7 +25,7 @@ All available configuration options are listed below with their default values. email_update_confirmation: enabled: false # change to force confirmation of changed email by sending a confirmation link to the new address. email_template: '@FOSUser/Profile/email_update_confirmation.txt.twig' - cypher_method: null # the cypher method to be used to encrypt/decrypt the email confirmation tokens. See http://php.net/manual/function.openssl-get-cipher-methods.php + cypher_method: null # the cypher method to be used to encrypt/decrypt the email confirmation tokens. If not specified, the first method returned by openssl_get_cipher_methods will be used. See http://php.net/manual/function.openssl-get-cipher-methods.php change_password: form: type: FOS\UserBundle\Form\Type\ChangePasswordFormType diff --git a/Tests/DependencyInjection/FOSUserExtensionTest.php b/Tests/DependencyInjection/FOSUserExtensionTest.php index 31aef1cb8d..8162f8cd92 100644 --- a/Tests/DependencyInjection/FOSUserExtensionTest.php +++ b/Tests/DependencyInjection/FOSUserExtensionTest.php @@ -134,6 +134,8 @@ public function testDisableProfile() $config['profile'] = false; $loader->load(array($config), $this->configuration); $this->assertNotHasDefinition('fos_user.profile.form.factory'); + $this->assertNotHasDefinition('fos_user.email_update_confirmation.template'); + $this->assertNotHasDefinition('fos_user.email_update_confirmation.cypher_method'); } public function testDisableChangePassword() @@ -243,7 +245,8 @@ public function testUserLoadConfirmationEmailWithDefaults() $this->assertParameter('@FOSUser/Registration/email.txt.twig', 'fos_user.registration.confirmation.template'); $this->assertParameter('@FOSUser/Resetting/email.txt.twig', 'fos_user.resetting.email.template'); $this->assertParameter('@FOSUser/Profile/email_update_confirmation.txt.twig', 'fos_user.email_update_confirmation.template'); - $this->assertParameter(null, 'fos_user.email_update_confirmation.cypher_method'); + $this->assertNotHasDefinition('fos_user.email_update_confirmation.cypher_method'); + $this->assertNotHasDefinition('fos_user.email_update_confirmation.template'); $this->assertParameter(array('admin@acme.org' => 'Acme Corp'), 'fos_user.resetting.email.from_email'); $this->assertParameter(86400, 'fos_user.resetting.token_ttl'); } @@ -258,6 +261,35 @@ public function testUserLoadConfirmationEmail() $this->assertParameter('AcmeMyBundle:Resetting:mail.txt.twig', 'fos_user.resetting.email.template'); $this->assertParameter(array('reset@acme.org' => 'Acme Corp'), 'fos_user.resetting.email.from_email'); $this->assertParameter(7200, 'fos_user.resetting.retry_ttl'); + $this->assertParameter('@FOSUser/Profile/email_update_confirmation.txt.twig', 'fos_user.email_update_confirmation.template'); + $this->assertParameter(null, 'fos_user.email_update_confirmation.cypher_method'); + } + + public function testUserLoadConfirmationEmailAndUpdateConfirmation() + { + $this->configuration = new ContainerBuilder(); + $loader = new FOSUserExtension(); + $config = $this->getFullConfig(); + $loader->load(array($config), $this->configuration); + $this->assertTrue($this->configuration instanceof ContainerBuilder); + + $this->assertParameter(true, 'fos_user.registration.confirmation.enabled'); + $this->assertParameter(null, 'fos_user.email_update_confirmation.cypher_method'); + $this->assertParameter('@FOSUser/Profile/email_update_confirmation.txt.twig', 'fos_user.email_update_confirmation.template'); + } + + public function testUserLoadConfirmationEmailAndNotUpdateConfirmation() + { + $this->configuration = new ContainerBuilder(); + $loader = new FOSUserExtension(); + $config = $this->getFullConfig(); + $config['profile']['email_update_confirmation']['enabled'] = false; + $loader->load(array($config), $this->configuration); + $this->assertTrue($this->configuration instanceof ContainerBuilder); + + $this->assertParameter(true, 'fos_user.registration.confirmation.enabled'); + $this->assertNotHasDefinition('fos_user.email_update_confirmation.cypher_method'); + $this->assertNotHasDefinition('fos_user.email_update_confirmation.template'); } public function testUserLoadUtilServiceWithDefaults() @@ -392,6 +424,8 @@ protected function getFullConfig() type: acme_my_profile name: acme_profile_form validation_groups: [acme_profile] + email_update_confirmation: + enabled: true change_password: form: type: acme_my_change_password diff --git a/Tests/Routing/RoutingTest.php b/Tests/Routing/RoutingTest.php index df68fe8f5f..624fede11d 100644 --- a/Tests/Routing/RoutingTest.php +++ b/Tests/Routing/RoutingTest.php @@ -45,6 +45,9 @@ public function testLoadRouting($routeName, $path, array $methods) $subCollection->addPrefix('/resetting'); $collection->addCollection($subCollection); $collection->addCollection($loader->load(__DIR__.'/../../Resources/config/routing/security.xml')); + $subCollection = $loader->load(__DIR__.'/../../Resources/config/routing/update_email.xml'); + $subCollection->addPrefix('/profile'); + $collection->addCollection($subCollection); $route = $collection->get($routeName); $this->assertNotNull($route, sprintf('The route "%s" should exists', $routeName)); @@ -82,6 +85,8 @@ public function loadRoutingProvider() array('fos_user_security_login', '/login', array('GET', 'POST')), array('fos_user_security_check', '/login_check', array('POST')), array('fos_user_security_logout', '/logout', array('GET', 'POST')), + + array('fos_user_update_email_confirm', '/profile/confirm-email-update/{token}', array('GET')), ); } }