<?php namespace App\Controller; use App\Entity\User; use App\Form\ChangePasswordFormType; use App\Form\ResetPasswordRequestFormType; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bridge\Twig\Mime\TemplatedEmail; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Mailer\MailerInterface; use Symfony\Component\Mime\Address; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; use Symfony\Component\Routing\Annotation\Route; use Symfony\Contracts\Translation\TranslatorInterface; use SymfonyCasts\Bundle\ResetPassword\Controller\ResetPasswordControllerTrait; use SymfonyCasts\Bundle\ResetPassword\Exception\ResetPasswordExceptionInterface; use SymfonyCasts\Bundle\ResetPassword\ResetPasswordHelperInterface; class ResetPasswordController extends AbstractController { use ResetPasswordControllerTrait; public function __construct( private readonly ResetPasswordHelperInterface $resetPasswordHelper, private readonly EntityManagerInterface $entityManager ) { // empty body } /** * Display & process form to request a password reset. */ #[Route(path: '/security/forgot/password', name: 'security_forgot_password')] public function request(Request $request, MailerInterface $mailer): Response { $form = $this->createForm(type: ResetPasswordRequestFormType::class); $form->handleRequest(request: $request); if ($form->isSubmitted() && $form->isValid()) { return $this->processSendingPasswordResetEmail( formData: $form->get(name: 'account')->getData(), mailer: $mailer ); } return $this->render(view: '@default/security/forgot_password.html.twig', parameters: [ 'requestForm' => $form->createView(), ]); } /** * Confirmation page after a user has requested a password reset. */ #[Route(path: '/security/recovery/mail/sent', name: 'security_recovery_mail_sent')] public function checkEmail(): Response { // Generate a fake token if the user does not exist or someone hit this page directly. // This prevents exposing whether a user was found with the given email address or username or not if (null === ($resetToken = $this->getTokenObjectFromSession())) { $resetToken = $this->resetPasswordHelper->generateFakeResetToken(); } return $this->render(view: '@default/security/recovery_mail_sent.html.twig', parameters: [ 'resetToken' => $resetToken, ]); } /** * Validates and process the reset URL that the user clicked in their email. */ #[Route(path: '/security/recovery/reset/{token}', name: 'security_recovery_reset')] public function reset(Request $request, UserPasswordHasherInterface $passwordHasher, TranslatorInterface $translator, string $token = null): Response { if ($token) { // We store the token in session and remove it from the URL, to avoid the URL being // loaded in a browser and potentially leaking the token to 3rd party JavaScript. $this->storeTokenInSession(token: $token); return $this->redirectToRoute(route: 'security_recovery_reset'); } $token = $this->getTokenFromSession(); if (null === $token) { throw $this->createNotFoundException(message: 'No reset password token found in the URL or in the session.'); } try { $user = $this->resetPasswordHelper->validateTokenAndFetchUser(fullToken: $token); } catch (ResetPasswordExceptionInterface $e) { $this->addFlash(type: 'reset_password_error', message: sprintf( '%s - %s', $translator->trans(id: ResetPasswordExceptionInterface::MESSAGE_PROBLEM_VALIDATE, parameters: [], domain: 'ResetPasswordBundle'), $translator->trans(id: $e->getReason(), parameters: [], domain: 'ResetPasswordBundle') )); return $this->redirectToRoute(route: 'app_forgot_password_request'); } // The token is valid; allow the user to change their password. $form = $this->createForm(type: ChangePasswordFormType::class); $form->handleRequest(request: $request); if ($form->isSubmitted() && $form->isValid()) { // A password reset token should be used only once, remove it. $this->resetPasswordHelper->removeResetRequest(fullToken: $token); // Encode(hash) the plain password, and set it. $encodedPassword = $passwordHasher->hashPassword( user: $user, plainPassword: $form->get(name: 'plainPassword')->getData() ); $user->setPassword($encodedPassword); $this->entityManager->flush(); // The session is cleaned up after the password has been changed. $this->cleanSessionAfterReset(); $this->addFlash(type: 'success', message: 'Your password has been changed.'); return $this->redirectToRoute(route: 'app_main'); } return $this->render(view: '@default/security/reset_password.html.twig', parameters: [ 'resetForm' => $form->createView(), ]); } private function processSendingPasswordResetEmail(string $formData, MailerInterface $mailer): RedirectResponse { $user = $this->entityManager->getRepository(entityName: User::class)->findOneBy(criteria: [ 'email' => $formData, ]); if (!$user) { $user = $this->entityManager->getRepository(entityName: User::class)->findOneBy(criteria: [ 'username' => $formData, ]); } // Do not reveal whether a user account was found or not. // if (!$user) { // return $this->redirectToRoute(route: 'app_check_email'); // } try { $resetToken = $this->resetPasswordHelper->generateResetToken(user: $user); } catch (ResetPasswordExceptionInterface $e) { $this->addFlash(type: 'reset_password_error', message: sprintf( '%s - %s', ResetPasswordExceptionInterface::MESSAGE_PROBLEM_HANDLE, $e->getReason() )); return $this->redirectToRoute(route: 'security_forgot_password'); } $email = (new TemplatedEmail()) ->from(new Address(address: 'tracer@24unix.net', name: '24unix.net')) ->to($user->getEmail()) ->subject(subject: 'Your password reset request') ->htmlTemplate(template: '@default/security/mail/recovery.html.twig') ->context(context: [ 'resetToken' => $resetToken, ]); $mailer->send(message: $email); // Store the token object in session for retrieval in check-email route. $this->setTokenObjectInSession(token: $resetToken); return $this->redirectToRoute(route: 'security_recovery_mail_sent'); } }