src/Controller/ResetPasswordController.php line 39

Open in your IDE?
  1. <?php
  2. namespace App\Controller;
  3. use App\Entity\User;
  4. use App\Entity\UserPasswordHistory;
  5. use App\Form\ChangePasswordFormType;
  6. use App\Form\ResetPasswordRequestFormType;
  7. use App\Service\Email\Message;
  8. use App\Utils\Utitlity;
  9. use Doctrine\ORM\EntityManagerInterface;
  10. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  11. use Symfony\Component\Form\FormError;
  12. use Symfony\Component\HttpFoundation\RedirectResponse;
  13. use Symfony\Component\HttpFoundation\Request;
  14. use Symfony\Component\HttpFoundation\Response;
  15. use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
  16. use Symfony\Component\Routing\Annotation\Route;
  17. use Symfony\Contracts\Translation\TranslatorInterface;
  18. use SymfonyCasts\Bundle\ResetPassword\Controller\ResetPasswordControllerTrait;
  19. use SymfonyCasts\Bundle\ResetPassword\Exception\ResetPasswordExceptionInterface;
  20. use SymfonyCasts\Bundle\ResetPassword\ResetPasswordHelperInterface;
  21. #[Route('/password')]
  22. class ResetPasswordController extends AbstractController
  23. {
  24. use ResetPasswordControllerTrait;
  25. public function __construct(
  26. private ResetPasswordHelperInterface $resetPasswordHelper,
  27. private EntityManagerInterface $entityManager
  28. ) {
  29. }
  30. /**
  31. * Display & process form to request a password reset.
  32. */
  33. #[Route('', name: 'app_forgot_password_request')]
  34. public function request(Request $request, Message $message, TranslatorInterface $translator): Response
  35. {
  36. $form = $this->createForm(ResetPasswordRequestFormType::class,null,[
  37. 'attr'=>['data-ajax' => 'false'],
  38. ]);
  39. $form->handleRequest($request);
  40. if ($form->isSubmitted() && $form->isValid()) {
  41. return $this->processSendingPasswordResetEmail(
  42. $form->get('email')->getData(),
  43. $message,
  44. $translator
  45. );
  46. }
  47. return $this->render('reset_password/request.html.twig', [
  48. 'requestForm' => $form->createView(),
  49. ]);
  50. }
  51. /**
  52. * Confirmation page after a user has requested a password reset.
  53. */
  54. #[Route('/check-email', name: 'app_check_email')]
  55. public function checkEmail(ResetPasswordHelperInterface $resetPasswordHelper): Response
  56. {
  57. // Generate a fake token if the user does not exist or someone hit this page directly.
  58. // This prevents exposing whether or not a user was found with the given email address or not
  59. if (null === ($resetToken = $this->getTokenObjectFromSession())) {
  60. $resetToken = $resetPasswordHelper->generateFakeResetToken();
  61. }
  62. return $this->render('reset_password/check_email.html.twig', [
  63. 'resetToken' => $resetToken,
  64. ]);
  65. }
  66. /**
  67. * Validates and process the reset URL that the user clicked in their email.
  68. */
  69. #[Route('/reset/{token}', name: 'app_reset_password')]
  70. public function reset(Request $request, UserPasswordHasherInterface $passwordHasher, TranslatorInterface $translator, string $token = null): Response
  71. {
  72. if ($token) {
  73. // We store the token in session and remove it from the URL, to avoid the URL being
  74. // loaded in a browser and potentially leaking the token to 3rd party JavaScript.
  75. $this->storeTokenInSession($token);
  76. return $this->redirectToRoute('app_reset_password');
  77. }
  78. $token = $this->getTokenFromSession();
  79. if (null === $token) {
  80. throw $this->createNotFoundException('No reset password token found in the URL or in the session.');
  81. }
  82. try {
  83. $user = $this->resetPasswordHelper->validateTokenAndFetchUser($token);
  84. } catch (ResetPasswordExceptionInterface $e) {
  85. $this->addFlash('reset_password_error', sprintf(
  86. '%s - %s',
  87. $translator->trans(ResetPasswordExceptionInterface::MESSAGE_PROBLEM_VALIDATE, [], 'ResetPasswordBundle'),
  88. $translator->trans($e->getReason(), [], 'ResetPasswordBundle')
  89. ));
  90. return $this->redirectToRoute('app_forgot_password_request');
  91. }
  92. // The token is valid; allow the user to change their password.
  93. $form = $this->createForm(ChangePasswordFormType::class,null,[
  94. 'attr'=>['data-ajax' => 'false'],
  95. ]);
  96. $form->handleRequest($request);
  97. if ($form->isSubmitted() && $form->isValid()) {
  98. $plain = $form->get('plainPassword')->getData();
  99. // Prevent password reuse: check against existing history
  100. if ($user instanceof User) {
  101. foreach ($user->getPasswordHistory() as $entry) {
  102. if (password_verify($plain, $entry->getPasswordHash())) {
  103. $form->get('plainPassword')->get('first')->addError(new FormError($translator->trans('validator.password.reuse', [], 'validators')));
  104. return $this->render('reset_password/reset.html.twig', [
  105. 'resetForm' => $form->createView(),
  106. ]);
  107. }
  108. }
  109. }
  110. // A password reset token should be used only once, remove it.
  111. $this->resetPasswordHelper->removeResetRequest($token);
  112. // Encode(hash) the plain password, and set it.
  113. $encodedPassword = $passwordHasher->hashPassword(
  114. $user,
  115. $plain
  116. );
  117. $user->setPassword($encodedPassword);
  118. $user->setPasswordChangedAt(new \DateTime());
  119. // Save password into history
  120. if (class_exists(UserPasswordHistory::class)) {
  121. $history = new UserPasswordHistory();
  122. $history->setUser($user);
  123. $history->setPasswordHash($encodedPassword);
  124. $history->setCreatedAt(new \DateTime());
  125. $this->entityManager->persist($history);
  126. }
  127. $this->entityManager->flush();
  128. // The session is cleaned up after the password has been changed.
  129. $this->cleanSessionAfterReset();
  130. return $this->redirectToRoute('security_login');
  131. }
  132. return $this->render('reset_password/reset.html.twig', [
  133. 'resetForm' => $form->createView(),
  134. ]);
  135. }
  136. /**
  137. * Validates and process the reset URL that the user clicked in their email.
  138. */
  139. #[Route('/new/{user}', name: 'app_new_password')]
  140. public function new(Request $request, UserPasswordHasherInterface $passwordHasher, TranslatorInterface $translator, User $user = null): Response
  141. {
  142. $form = $this->createForm(ChangePasswordFormType::class,null,[
  143. 'attr'=>['data-ajax' => 'false'],
  144. ]);
  145. $form->handleRequest($request);
  146. if ($form->isSubmitted() && $form->isValid()) {
  147. $plain = $form->get('plainPassword')->getData();
  148. if ($user && is_object($user) && method_exists($user, 'getPasswordHistory')) {
  149. foreach ($user->getPasswordHistory() as $entry) {
  150. if (password_verify($plain, $entry->getPasswordHash())) {
  151. $form->get('plainPassword')->get('first')->addError(new FormError($translator->trans('validator.password.reuse', [], 'validators')));
  152. return $this->render('reset_password/new.html.twig', [
  153. 'resetForm' => $form->createView(),
  154. 'user' => $user
  155. ]);
  156. }
  157. }
  158. }
  159. // Encode(hash) the plain password, and set it.
  160. $encodedPassword = $passwordHasher->hashPassword(
  161. $user,
  162. $plain
  163. );
  164. $user->setPassword($encodedPassword);
  165. $user->setPasswordChangedAt(new \DateTime());
  166. // Save password into history
  167. if (class_exists(\App\Entity\UserPasswordHistory::class)) {
  168. $history = new \App\Entity\UserPasswordHistory();
  169. $history->setUser($user);
  170. $history->setPasswordHash($encodedPassword);
  171. $history->setCreatedAt(new \DateTime());
  172. $this->entityManager->persist($history);
  173. }
  174. $this->entityManager->flush();
  175. return $this->redirectToRoute('security_login');
  176. }
  177. return $this->render('reset_password/new.html.twig', [
  178. 'resetForm' => $form->createView(),
  179. 'user' => $user
  180. ]);
  181. }
  182. private function processSendingPasswordResetEmail(string $emailFormData, Message $message, TranslatorInterface $translator): RedirectResponse
  183. {
  184. $user = $this->entityManager->getRepository(User::class)->findOneBy([
  185. 'email' => $emailFormData,
  186. ]);
  187. // Do not reveal whether a user account was found or not.
  188. if (!$user) {
  189. return $this->redirectToRoute('app_check_email');
  190. }
  191. try {
  192. $resetToken = $this->resetPasswordHelper->generateResetToken($user);
  193. } catch (ResetPasswordExceptionInterface $e) {
  194. // If you want to tell the user why a reset email was not sent, uncomment
  195. // the lines below and change the redirect to 'app_forgot_password_request'.
  196. // Caution: This may reveal if a user is registered or not.
  197. //
  198. // $this->addFlash('reset_password_error', sprintf(
  199. // '%s - %s',
  200. // $translator->trans(ResetPasswordExceptionInterface::MESSAGE_PROBLEM_HANDLE, [], 'ResetPasswordBundle'),
  201. // $translator->trans($e->getReason(), [], 'ResetPasswordBundle')
  202. // ));
  203. return $this->redirectToRoute('app_check_email');
  204. }
  205. $fromAddress = Utitlity::getSenderAddressByUser($user);
  206. if ($fromAddress) {
  207. $message->setFrom($fromAddress);
  208. } else {
  209. $message->setFrom($this->getParameter('message.email.default'));
  210. }
  211. $message->setTemplate('emails/login/reset_password.html.twig');
  212. $message->setSubject($translator->trans('email.registration.subject.invite'));
  213. $message->setTo($user->getEmail());
  214. //$message->setSubject($translator->trans('email.resetPassword.subject'));
  215. $message->send(['resetToken'=>$resetToken,'user'=>$user]);
  216. // Store the token object in session for retrieval in check-email route.
  217. $this->setTokenObjectInSession($resetToken);
  218. return $this->redirectToRoute('app_check_email');
  219. }
  220. }