src/Bidcoz/Bundle/FrontendBundle/Controller/CampaignResettingController.php line 49

Open in your IDE?
  1. <?php
  2. namespace Bidcoz\Bundle\FrontendBundle\Controller;
  3. use Bidcoz\Bundle\CoreBundle\Controller\CoreController;
  4. use Bidcoz\Bundle\CoreBundle\Entity\Campaign;
  5. use Bidcoz\Bundle\CoreBundle\Entity\Organization;
  6. use FOS\UserBundle\Event\FilterUserResponseEvent;
  7. use FOS\UserBundle\Event\FormEvent;
  8. use FOS\UserBundle\Event\GetResponseUserEvent;
  9. use FOS\UserBundle\FOSUserEvents;
  10. use FOS\UserBundle\Util\TokenGeneratorInterface;
  11. use Symfony\Component\HttpFoundation\RedirectResponse;
  12. use Symfony\Component\HttpFoundation\Request;
  13. use Symfony\Component\HttpFoundation\Response;
  14. use Symfony\Component\Routing\Annotation\Route;
  15. use Symfony\Component\Routing\RouterInterface;
  16. use Symfony\Component\Security\Core\Security;
  17. /**
  18.  * CAU-350 Part 2: the in-campaign password-reset variant.
  19.  *
  20.  * Mirrors LoginController's org/campaign scoping (`/{orgSlug}/...` and
  21.  * `/{orgSlug}/{campaignSlug}/...`) so a donor who starts a reset from inside a
  22.  * campaign is never ejected to the generic `/resetting` flow — the campaign context
  23.  * is carried through the request page, the emailed link, and the "return to sign in"
  24.  * link. The reset LOGIC is identical to the standalone
  25.  * {@see \Bidcoz\Bundle\UserBundle\Controller\ResettingController}; only the surrounding
  26.  * links and the emailed URL differ, so the two stay behaviourally in step.
  27.  *
  28.  * "resetting" is a reserved slug (see Constants), so these literal paths are never
  29.  * mistaken for a campaign slug by the campaign catch-all routes.
  30.  *
  31.  * @Route("/{orgSlug}")
  32.  */
  33. class CampaignResettingController extends CoreController
  34. {
  35.     /**
  36.      * @Route("/resetting/request", name="organization_resetting_request", methods={"GET"})
  37.      */
  38.     public function organizationRequest(Request $requestOrganization $organization)
  39.     {
  40.         return $this->doRequest($request$organization);
  41.     }
  42.     /**
  43.      * @Route("/{campaignSlug}/resetting/request", name="campaign_resetting_request", methods={"GET"})
  44.      */
  45.     public function campaignRequest(Request $requestOrganization $organizationCampaign $campaign)
  46.     {
  47.         return $this->doRequest($request$organization$campaign);
  48.     }
  49.     /**
  50.      * @Route("/resetting/send-email", name="organization_resetting_send_email", methods={"POST"})
  51.      */
  52.     public function organizationSendEmail(Request $requestOrganization $organizationTokenGeneratorInterface $tokenGenerator)
  53.     {
  54.         return $this->doSendEmail($request$tokenGenerator$organization);
  55.     }
  56.     /**
  57.      * @Route("/{campaignSlug}/resetting/send-email", name="campaign_resetting_send_email", methods={"POST"})
  58.      */
  59.     public function campaignSendEmail(Request $requestOrganization $organizationCampaign $campaignTokenGeneratorInterface $tokenGenerator)
  60.     {
  61.         return $this->doSendEmail($request$tokenGenerator$organization$campaign);
  62.     }
  63.     /**
  64.      * @Route("/resetting/check-email", name="organization_resetting_check_email", methods={"GET"})
  65.      */
  66.     public function organizationCheckEmail(Request $requestOrganization $organization)
  67.     {
  68.         return $this->doCheckEmail($request$organization);
  69.     }
  70.     /**
  71.      * @Route("/{campaignSlug}/resetting/check-email", name="campaign_resetting_check_email", methods={"GET"})
  72.      */
  73.     public function campaignCheckEmail(Request $requestOrganization $organizationCampaign $campaign)
  74.     {
  75.         return $this->doCheckEmail($request$organization$campaign);
  76.     }
  77.     /**
  78.      * @Route("/resetting/reset/{token}", name="organization_resetting_reset")
  79.      */
  80.     public function organizationReset(Request $requestOrganization $organizationstring $token)
  81.     {
  82.         return $this->doReset($request$token$organization);
  83.     }
  84.     /**
  85.      * @Route("/{campaignSlug}/resetting/reset/{token}", name="campaign_resetting_reset")
  86.      */
  87.     public function campaignReset(Request $requestOrganization $organizationCampaign $campaignstring $token)
  88.     {
  89.         return $this->doReset($request$token$organization$campaign);
  90.     }
  91.     protected function doRequest(Request $requestOrganization $organization, ?Campaign $campaign null): Response
  92.     {
  93.         $session      $request->getSession();
  94.         $lastUsername = (null === $session) ? '' $session->get(Security::LAST_USERNAME);
  95.         return $this->render('@FOSUser/Resetting/request.html.twig', [
  96.             'last_username'     => $lastUsername,
  97.             'organization'      => $organization,
  98.             'campaign'          => $campaign,
  99.             'send_email_action' => $this->scopedUrl('resetting_send_email'$organization$campaign),
  100.             'sign_in_path'      => $this->loginUrl($organization$campaign),
  101.         ]);
  102.     }
  103.     /**
  104.      * Mirrors FOSUser's sendEmailAction, but builds an org/campaign-scoped reset link
  105.      * and sends it through EmailManager (mechanism (a) — the standalone FOSMailer is
  106.      * left untouched). CAU-338 non-enumeration is preserved: the response is the same
  107.      * redirect to check-email whether or not an account exists.
  108.      */
  109.     protected function doSendEmail(Request $requestTokenGeneratorInterface $tokenGeneratorOrganization $organization, ?Campaign $campaign null): Response
  110.     {
  111.         $username $request->request->get('username');
  112.         $user     $this->getUserManager()->findUserByUsernameOrEmail($username);
  113.         $retryTtl = (int) $this->getParameter('fos_user.resetting.retry_ttl');
  114.         if (null !== $user && !$user->isPasswordRequestNonExpired($retryTtl)) {
  115.             if (null === $user->getConfirmationToken()) {
  116.                 $user->setConfirmationToken($tokenGenerator->generateToken());
  117.             }
  118.             $resetUrl $this->scopedUrl(
  119.                 'resetting_reset',
  120.                 $organization,
  121.                 $campaign,
  122.                 ['token' => $user->getConfirmationToken()],
  123.                 RouterInterface::ABSOLUTE_URL
  124.             );
  125.             $this->getEmailManager()->sendEmail($organization$user'user.reset_password', [
  126.                 'user'            => $user,
  127.                 'confirmationUrl' => $resetUrl,
  128.             ]);
  129.             $user->setPasswordRequestedAt(new \DateTime());
  130.             $this->getUserManager()->updateUser($user);
  131.         }
  132.         return new RedirectResponse(
  133.             $this->scopedUrl('resetting_check_email'$organization$campaign, ['username' => $username])
  134.         );
  135.     }
  136.     protected function doCheckEmail(Request $requestOrganization $organization, ?Campaign $campaign null): Response
  137.     {
  138.         $username $request->query->get('username');
  139.         // NO user lookup here, deliberately — same enumeration guard as the standalone
  140.         // flow (CAU-338). An empty username means the visitor did not arrive from the
  141.         // send-email step, so bounce back to the (scoped) request page.
  142.         if (empty($username)) {
  143.             return new RedirectResponse($this->scopedUrl('resetting_request'$organization$campaign));
  144.         }
  145.         return $this->render('@FOSUser/Resetting/check_email.html.twig', [
  146.             'tokenLifetime' => ceil($this->getParameter('fos_user.resetting.token_ttl') / 3600),
  147.             'organization'  => $organization,
  148.             'campaign'      => $campaign,
  149.             'sign_in_path'  => $this->loginUrl($organization$campaign),
  150.         ]);
  151.     }
  152.     protected function doReset(Request $requeststring $tokenOrganization $organization, ?Campaign $campaign null): Response
  153.     {
  154.         $formFactory $this->get('fos_user.resetting.form.factory');
  155.         $userManager $this->getUserManager();
  156.         $dispatcher  $this->getEventDispatcher();
  157.         $user $userManager->findUserByConfirmationToken($token);
  158.         if (null === $user) {
  159.             // Expired, already used, or never valid — deliberately indistinguishable
  160.             // (CAU-349). 410 Gone, token never echoed back.
  161.             return $this->render('@FOSUser/Resetting/invalid_token.html.twig', [
  162.                 'organization' => $organization,
  163.                 'campaign'     => $campaign,
  164.                 'request_path' => $this->scopedUrl('resetting_request'$organization$campaign),
  165.                 'sign_in_path' => $this->loginUrl($organization$campaign),
  166.             ], new Response(''Response::HTTP_GONE));
  167.         }
  168.         $event = new GetResponseUserEvent($user$request);
  169.         $dispatcher->dispatch($eventFOSUserEvents::RESETTING_RESET_INITIALIZE);
  170.         if (null !== $event->getResponse()) {
  171.             return $event->getResponse();
  172.         }
  173.         $form $formFactory->createForm();
  174.         $form->setData($user);
  175.         $form->handleRequest($request);
  176.         if ($form->isSubmitted() && $form->isValid()) {
  177.             $event = new FormEvent($form$request);
  178.             $dispatcher->dispatch($eventFOSUserEvents::RESETTING_RESET_SUCCESS);
  179.             $userManager->updateUser($user);
  180.             if (null === $response $event->getResponse()) {
  181.                 // Land back inside the campaign/org the reset was started from.
  182.                 $response = new RedirectResponse($this->homeUrl($organization$campaign));
  183.             }
  184.             $dispatcher->dispatch(new FilterUserResponseEvent($user$request$response), FOSUserEvents::RESETTING_RESET_COMPLETED);
  185.             return $response;
  186.         }
  187.         return $this->render('@FOSUser/Resetting/reset.html.twig', [
  188.             'token'        => $token,
  189.             'form'         => $form->createView(),
  190.             'organization' => $organization,
  191.             'campaign'     => $campaign,
  192.             'reset_action' => $this->scopedUrl('resetting_reset'$organization$campaign, ['token' => $token]),
  193.             'sign_in_path' => $this->loginUrl($organization$campaign),
  194.         ]);
  195.     }
  196.     /**
  197.      * Generate a scoped resetting URL, picking the organization_* or campaign_*
  198.      * route by whether a campaign is in play.
  199.      */
  200.     private function scopedUrl(string $suffixOrganization $organization, ?Campaign $campaign, array $extra = [], int $referenceType RouterInterface::ABSOLUTE_PATH): string
  201.     {
  202.         $prefix null === $campaign 'organization_' 'campaign_';
  203.         return $this->getRouter()->generate($prefix.$suffix$this->routeParams($organization$campaign$extra), $referenceType);
  204.     }
  205.     private function loginUrl(Organization $organization, ?Campaign $campaign): string
  206.     {
  207.         $route null === $campaign 'organization_login_user' 'campaign_login_user';
  208.         return $this->getRouter()->generate($route$this->routeParams($organization$campaign));
  209.     }
  210.     private function homeUrl(Organization $organization, ?Campaign $campaign): string
  211.     {
  212.         $route null === $campaign 'organization_home' 'campaign_home';
  213.         return $this->getRouter()->generate($route$this->routeParams($organization$campaign));
  214.     }
  215.     private function routeParams(Organization $organization, ?Campaign $campaign, array $extra = []): array
  216.     {
  217.         $params array_merge(['orgSlug' => $organization->getSlug()], $extra);
  218.         if (null !== $campaign) {
  219.             $params['campaignSlug'] = $campaign->getSlug();
  220.         }
  221.         return $params;
  222.     }
  223. }