Skip to content

Commit 9b75b35

Browse files
azineXWB
authored andcommitted
add email update confirmation feature (#2612)
* add email update confirmation feature this allows to require the confirmation of changed email-addresses, by clicking on a link, before the new address is written to the database. * implemente requested changes after review - fix whitespaces, - remove redundant line breaks - remove unused 'autoescape' from twig templates - change from deprecated mycrypt functions to openssl-functions & update tests - add option to choose cypher method & update docs * inject fos_user.email_update_confirmation instead of service_container * fixes by php-cs-fixer * added Upgrade instructions * improve "confirm changed email" documentation * use symfony validator to validate email * improve phpDocs * reference UserInterface instead of User * update version in upgrade instructions * changes by php-cs fixer & translation fix
1 parent 855084a commit 9b75b35

30 files changed

Lines changed: 1069 additions & 0 deletions

Controller/ProfileController.php

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,20 @@
1414
use FOS\UserBundle\Event\FilterUserResponseEvent;
1515
use FOS\UserBundle\Event\FormEvent;
1616
use FOS\UserBundle\Event\GetResponseUserEvent;
17+
use FOS\UserBundle\Event\UserEvent;
1718
use FOS\UserBundle\Form\Factory\FactoryInterface;
1819
use FOS\UserBundle\FOSUserEvents;
20+
use FOS\UserBundle\Model\User;
1921
use FOS\UserBundle\Model\UserInterface;
2022
use FOS\UserBundle\Model\UserManagerInterface;
23+
use FOS\UserBundle\Services\EmailConfirmation\EmailUpdateConfirmation;
2124
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
2225
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
2326
use Symfony\Component\HttpFoundation\RedirectResponse;
2427
use Symfony\Component\HttpFoundation\Request;
2528
use Symfony\Component\HttpFoundation\Response;
2629
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
30+
use Symfony\Component\Translation\Translator;
2731

2832
/**
2933
* Controller managing the user profile.
@@ -102,4 +106,54 @@ public function editAction(Request $request)
102106
'form' => $form->createView(),
103107
));
104108
}
109+
110+
/**
111+
* Confirm user`s email update.
112+
*
113+
* @param Request $request
114+
* @param string $token
115+
*
116+
* @return \Symfony\Component\HttpFoundation\RedirectResponse
117+
*/
118+
public function confirmEmailUpdateAction(Request $request, $token)
119+
{
120+
$userManager = $this->container->get('fos_user.user_manager');
121+
122+
/** @var User $user */
123+
$user = $userManager->findUserByConfirmationToken($token);
124+
125+
// If user was not found throw 404 exception
126+
if (!$user) {
127+
/** @var Translator $translator */
128+
$translator = $this->get('translator');
129+
throw $this->createNotFoundException($translator->trans('email_update.error.message', array(), 'FOSUserBundle'));
130+
}
131+
132+
// Show invalid token message if the user id found via token does not match the current users id (e.g. anon. or other user)
133+
if (!($this->getUser() instanceof UserInterface) || ($user->getId() !== $this->getUser()->getId())) {
134+
/** @var Translator $translator */
135+
$translator = $this->get('translator');
136+
throw new AccessDeniedException($translator->trans('email_update.error.message', array(), 'FOSUserBundle'));
137+
}
138+
139+
/** @var EmailUpdateConfirmation $emailUpdateConfirmation */
140+
$emailUpdateConfirmation = $this->get('fos_user.email_update_confirmation');
141+
142+
$emailUpdateConfirmation->setUser($user);
143+
144+
$newEmail = $emailUpdateConfirmation->fetchEncryptedEmailFromConfirmationLink($request->get('target'));
145+
146+
// Update user email
147+
if ($newEmail) {
148+
$user->setConfirmationToken($emailUpdateConfirmation->getEmailConfirmedToken());
149+
$user->setEmail($newEmail);
150+
}
151+
152+
$userManager->updateUser($user);
153+
154+
$event = new UserEvent($user, $request);
155+
$this->get('event_dispatcher')->dispatch(FOSUserEvents::EMAIL_UPDATE_SUCCESS, $event);
156+
157+
return $this->redirect($this->generateUrl('fos_user_profile_show'));
158+
}
105159
}

DependencyInjection/Configuration.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,14 @@ private function addProfileSection(ArrayNodeDefinition $node)
109109
->end()
110110
->end()
111111
->end()
112+
->arrayNode('email_update_confirmation')
113+
->addDefaultsIfNotSet()
114+
->children()
115+
->booleanNode('enabled')->defaultFalse()->end()
116+
->scalarNode('cypher_method')->defaultNull()->end()
117+
->scalarNode('email_template')->defaultValue('@FOSUser/Profile/email_update_confirmation.txt.twig')->end()
118+
->end()
119+
->end()
112120
->end()
113121
->end()
114122
->end();

DependencyInjection/FOSUserExtension.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,9 @@ private function loadProfile(array $config, ContainerBuilder $container, XmlFile
137137
{
138138
$loader->load('profile.xml');
139139

140+
$container->setParameter('fos_user.email_update_confirmation.template', $config['email_update_confirmation']['email_template']);
141+
$container->setParameter('fos_user.email_update_confirmation.cypher_method', $config['email_update_confirmation']['cypher_method']);
142+
140143
$this->remapParametersNamespaces($config, $container, array(
141144
'form' => 'fos_user.profile.form.%s',
142145
));

Doctrine/EmailUpdateListener.php

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the FOSUserBundle package.
5+
*
6+
* (c) FriendsOfSymfony <http://friendsofsymfony.github.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 FOS\UserBundle\Doctrine;
13+
14+
use Doctrine\ORM\Event\PreUpdateEventArgs;
15+
use FOS\UserBundle\Model\UserInterface;
16+
use FOS\UserBundle\Services\EmailConfirmation\EmailUpdateConfirmation;
17+
use Symfony\Component\HttpFoundation\RequestStack;
18+
19+
/**
20+
* Class EmailUpdateListener.
21+
*/
22+
class EmailUpdateListener
23+
{
24+
/**
25+
* @var EmailUpdateConfirmation
26+
*/
27+
private $emailUpdateConfirmation;
28+
29+
/**
30+
* @var RequestStack
31+
*/
32+
private $requestStack;
33+
34+
/**
35+
* Constructor.
36+
*
37+
* @param EmailUpdateConfirmation $emailUpdateConfirmation
38+
* @param RequestStack $requestStack
39+
*/
40+
public function __construct(EmailUpdateConfirmation $emailUpdateConfirmation, RequestStack $requestStack)
41+
{
42+
$this->emailUpdateConfirmation = $emailUpdateConfirmation;
43+
$this->requestStack = $requestStack;
44+
}
45+
46+
/**
47+
* Pre update listener based on doctrine common.
48+
*
49+
* @param PreUpdateEventArgs $args
50+
*/
51+
public function preUpdate(PreUpdateEventArgs $args)
52+
{
53+
$object = $args->getObject();
54+
55+
if ($object instanceof UserInterface && $args instanceof PreUpdateEventArgs) {
56+
$user = $object;
57+
58+
if ($user->getConfirmationToken() != $this->emailUpdateConfirmation->getEmailConfirmedToken() && isset($args->getEntityChangeSet()['email'])) {
59+
$oldEmail = $args->getEntityChangeSet()['email'][0];
60+
$newEmail = $args->getEntityChangeSet()['email'][1];
61+
62+
$user->setEmail($oldEmail);
63+
64+
// Configure email confirmation
65+
$this->emailUpdateConfirmation->setUser($user);
66+
$this->emailUpdateConfirmation->setEmail($newEmail);
67+
$this->emailUpdateConfirmation->setConfirmationRoute('fos_user_update_email_confirm');
68+
$this->emailUpdateConfirmation->getMailer()->sendUpdateEmailConfirmation(
69+
$user,
70+
$this->emailUpdateConfirmation->generateConfirmationLink($this->requestStack->getCurrentRequest()),
71+
$newEmail
72+
);
73+
}
74+
75+
if ($user->getConfirmationToken() == $this->emailUpdateConfirmation->getEmailConfirmedToken()) {
76+
$user->setConfirmationToken(null);
77+
}
78+
}
79+
}
80+
}

EventListener/FlashListener.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ class FlashListener implements EventSubscriberInterface
3030
FOSUserEvents::PROFILE_EDIT_COMPLETED => 'profile.flash.updated',
3131
FOSUserEvents::REGISTRATION_COMPLETED => 'registration.flash.user_created',
3232
FOSUserEvents::RESETTING_RESET_COMPLETED => 'resetting.flash.success',
33+
FOSUserEvents::EMAIL_UPDATE_SUCCESS => 'email_update.flash.success',
34+
FOSUserEvents::EMAIL_UPDATE_INITIALIZE => 'email_update.flash.info',
3335
);
3436

3537
/**
@@ -67,6 +69,8 @@ public static function getSubscribedEvents()
6769
FOSUserEvents::PROFILE_EDIT_COMPLETED => 'addSuccessFlash',
6870
FOSUserEvents::REGISTRATION_COMPLETED => 'addSuccessFlash',
6971
FOSUserEvents::RESETTING_RESET_COMPLETED => 'addSuccessFlash',
72+
FOSUserEvents::EMAIL_UPDATE_SUCCESS => 'addSuccessFlash',
73+
FOSUserEvents::EMAIL_UPDATE_INITIALIZE => 'addInfoFlash',
7074
);
7175
}
7276

@@ -83,6 +87,19 @@ public function addSuccessFlash(Event $event, $eventName)
8387
$this->session->getFlashBag()->add('success', $this->trans(self::$successMessages[$eventName]));
8488
}
8589

90+
/**
91+
* @param Event $event
92+
* @param string $eventName
93+
*/
94+
public function addInfoFlash(Event $event, $eventName)
95+
{
96+
if (!isset(self::$successMessages[$eventName])) {
97+
throw new \InvalidArgumentException('This event does not correspond to a known flash message');
98+
}
99+
100+
$this->session->getFlashBag()->add('info', $this->trans(self::$successMessages[$eventName]));
101+
}
102+
86103
/**
87104
* @param string$message
88105
* @param array $params

FOSUserEvents.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,4 +318,22 @@ final class FOSUserEvents
318318
* @Event("FOS\UserBundle\Event\UserEvent")
319319
*/
320320
const USER_DEMOTED = 'fos_user.user.demoted';
321+
322+
/**
323+
* The EMAIL_UPDATE_INITIALIZE event occurs when the email update process is initialized.
324+
*
325+
* This event allows you to access the user and to add some behaviour after email update is initialized..
326+
*
327+
* @Event("FOS\UserBundle\Event\UserEvent")
328+
*/
329+
const EMAIL_UPDATE_INITIALIZE = 'fos_user.update_email.initialize';
330+
331+
/**
332+
* The EMAIL_UPDATE_SUCCESS event occurs when the email was successfully updated through confirmation link.
333+
*
334+
* This event allows you to access the user and to add some behaviour after email was confirmed and updated..
335+
*
336+
* @Event("FOS\UserBundle\Event\UserEvent")
337+
*/
338+
const EMAIL_UPDATE_SUCCESS = 'fos_user.update_email.success';
321339
}

Mailer/Mailer.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,24 @@ public function sendResettingEmailMessage(UserInterface $user)
8484
$this->sendEmailMessage($rendered, $this->parameters['from_email']['resetting'], (string) $user->getEmail());
8585
}
8686

87+
/**
88+
* Send confirmation link to specified new user email.
89+
*
90+
* @param UserInterface $user
91+
* @param string $confirmationUrl
92+
* @param string $toEmail
93+
*/
94+
public function sendUpdateEmailConfirmation(UserInterface $user, $confirmationUrl, $toEmail)
95+
{
96+
$template = $this->parameters['template']['email_updating'];
97+
$rendered = $this->templating->render($template, array(
98+
'user' => $user,
99+
'confirmationUrl' => $confirmationUrl,
100+
));
101+
102+
$this->sendEmailMessage($rendered, $this->parameters['from_email']['resetting'], $toEmail);
103+
}
104+
87105
/**
88106
* @param string $renderedTemplate
89107
* @param array|string $fromEmail

Mailer/MailerInterface.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,13 @@ public function sendConfirmationEmailMessage(UserInterface $user);
3131
* @param UserInterface $user
3232
*/
3333
public function sendResettingEmailMessage(UserInterface $user);
34+
35+
/**
36+
* Send an email to a user to confirm the changed email address.
37+
*
38+
* @param UserInterface $user
39+
* @param string $confirmationUrl
40+
* @param string $toEmail
41+
*/
42+
public function sendUpdateEmailConfirmation(UserInterface $user, $confirmationUrl, $toEmail);
3443
}

Mailer/NoopMailer.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,12 @@ public function sendResettingEmailMessage(UserInterface $user)
3737
{
3838
// nothing happens.
3939
}
40+
41+
/**
42+
* {@inheritdoc}
43+
*/
44+
public function sendUpdateEmailConfirmation(UserInterface $user, $confirmationUrl, $toEmail)
45+
{
46+
// nothing happens.
47+
}
4048
}

Mailer/TwigSwiftMailer.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,4 +119,24 @@ protected function sendMessage($templateName, $context, $fromEmail, $toEmail)
119119

120120
$this->mailer->send($message);
121121
}
122+
123+
/**
124+
* Send confirmation link to specified new user email.
125+
*
126+
* @param UserInterface $user
127+
* @param $confirmationUrl
128+
* @param $toEmail
129+
*
130+
* @return bool
131+
*/
132+
public function sendUpdateEmailConfirmation(UserInterface $user, $confirmationUrl, $toEmail)
133+
{
134+
$template = $this->parameters['template']['email_updating'];
135+
$context = array(
136+
'user' => $user,
137+
'confirmationUrl' => $confirmationUrl,
138+
);
139+
140+
$this->sendMessage($template, $context, $this->parameters['from_email']['confirmation'], $toEmail);
141+
}
122142
}

0 commit comments

Comments
 (0)