src/Controller/AccountController.php line 56

Open in your IDE?
  1. <?php
  2. /**
  3.  * Pimcore
  4.  *
  5.  * This source file is available under two different licenses:
  6.  * - GNU General Public License version 3 (GPLv3)
  7.  * - Pimcore Enterprise License (PEL)
  8.  * Full copyright and license information is available in
  9.  * LICENSE.md which is distributed with this source code.
  10.  *
  11.  *  @copyright  Copyright (c) Pimcore GmbH (http://www.pimcore.org)
  12.  *  @license    http://www.pimcore.org/license     GPLv3 and PEL
  13.  */
  14. namespace App\Controller;
  15. use App\EventListener\AuthenticationLoginListener;
  16. use App\Form\LoginFormType;
  17. use App\Form\PasswordMaxLengthTrait;
  18. use App\Form\RegistrationFormHandler;
  19. use App\Form\RegistrationFormType;
  20. use App\Model\Customer;
  21. use App\Services\NewsletterDoubleOptInService;
  22. use App\Services\PasswordRecoveryService;
  23. use CustomerManagementFrameworkBundle\CustomerProvider\CustomerProviderInterface;
  24. use CustomerManagementFrameworkBundle\CustomerSaveValidator\Exception\DuplicateCustomerException;
  25. use CustomerManagementFrameworkBundle\Model\CustomerInterface;
  26. use CustomerManagementFrameworkBundle\Security\Authentication\LoginManagerInterface;
  27. use CustomerManagementFrameworkBundle\Security\OAuth\Exception\AccountNotLinkedException;
  28. use CustomerManagementFrameworkBundle\Security\OAuth\OAuthRegistrationHandler;
  29. use CustomerManagementFrameworkBundle\Security\SsoIdentity\SsoIdentityServiceInterface;
  30. use HWI\Bundle\OAuthBundle\OAuth\Response\UserResponseInterface;
  31. use HWI\Bundle\OAuthBundle\Security\Core\Authentication\Token\OAuthToken;
  32. use Pimcore\Bundle\EcommerceFrameworkBundle\Factory;
  33. use Pimcore\Bundle\EcommerceFrameworkBundle\OrderManager\Order\Listing\Filter\CustomerObject;
  34. use Pimcore\DataObject\Consent\Service;
  35. use Pimcore\Translation\Translator;
  36. use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
  37. use Symfony\Component\HttpFoundation\RedirectResponse;
  38. use Symfony\Component\HttpFoundation\Request;
  39. use Symfony\Component\HttpFoundation\Response;
  40. use Symfony\Component\HttpFoundation\Session\SessionInterface;
  41. use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
  42. use Symfony\Component\Routing\Annotation\Route;
  43. use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
  44. use Symfony\Component\Security\Core\User\UserInterface;
  45. use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
  46. use Symfony\Component\Uid\Uuid;
  47. /**
  48.  * Class AccountController
  49.  *
  50.  * Controller that handles all account functionality, including register, login and connect to SSO profiles
  51.  */
  52. class AccountController extends BaseController
  53. {
  54.     use PasswordMaxLengthTrait;
  55.     /**
  56.      * @Route("/account/login", name="account-login")
  57.      *
  58.      * @param AuthenticationUtils $authenticationUtils
  59.      * @param OAuthRegistrationHandler $oAuthHandler
  60.      * @param SessionInterface $session
  61.      * @param Request $request
  62.      * @param UserInterface|null $user
  63.      *
  64.      * @return Response|RedirectResponse
  65.      */
  66.     public function loginAction(
  67.         AuthenticationUtils $authenticationUtils,
  68.         OAuthRegistrationHandler $oAuthHandler,
  69.         SessionInterface $session,
  70.         Request $request,
  71.         UserInterface $user null
  72.     ) {
  73.         //redirect user to index page if logged in
  74.         if ($user && $this->isGranted('ROLE_USER')) {
  75.             return $this->redirectToRoute('account-index');
  76.         }
  77.         // get the login error if there is one
  78.         $error $authenticationUtils->getLastAuthenticationError();
  79.         // OAuth handling - the OAuth authenticator is configured to return to the login page on errors
  80.         // (see failure_path configuration) - therefore we can fetch the last authentication error
  81.         // here. If the error is an AccountNotLinkedException (as thrown by our user provider) save the
  82.         // OAuth token to the session and redirect to registration with a special key which can be used
  83.         // to load the token to prepopulate the registration form with account data.
  84.         if ($error instanceof AccountNotLinkedException) {
  85.             // this can be anything - for simplicity we just use an UUID as it is unique and random
  86.             $registrationKey = (string) Uuid::v4()->toRfc4122();
  87.             $oAuthHandler->saveToken($registrationKey$error->getToken());
  88.             return $this->redirectToRoute('account-register', [
  89.                 'registrationKey' => $registrationKey
  90.             ]);
  91.         }
  92.         // last username entered by the user
  93.         $lastUsername $authenticationUtils->getLastUsername();
  94.         $formData = [
  95.             '_username' => $lastUsername
  96.         ];
  97.         $form $this->createForm(LoginFormType::class, $formData, [
  98.             'action' => $this->generateUrl('account-login'),
  99.         ]);
  100.         //store referer in session to get redirected after login
  101.         if (!$request->get('no-referer-redirect')) {
  102.             $session->set('_security.demo_frontend.target_path'$request->headers->get('referer'));
  103.         }
  104.         return $this->render('account/login.html.twig', [
  105.             'form' => $form->createView(),
  106.             'error' => $error,
  107.             'hideBreadcrumbs' => true
  108.         ]);
  109.     }
  110.     /**
  111.      * If registration is called with a registration key, the key will be used to look for an existing OAuth token in
  112.      * the session. This OAuth token will be used to fetch user info which can be used to pre-populate the form and to
  113.      * link a SSO identity to the created customer object.
  114.      *
  115.      * This could be further separated into services, but was kept as single method for demonstration purposes as the
  116.      * registration process is different on every project.
  117.      *
  118.      * @Route("/account/register", name="account-register")
  119.      *
  120.      * @param Request $request
  121.      * @param CustomerProviderInterface $customerProvider
  122.      * @param OAuthRegistrationHandler $oAuthHandler
  123.      * @param LoginManagerInterface $loginManager
  124.      * @param RegistrationFormHandler $registrationFormHandler
  125.      * @param SessionInterface $session
  126.      * @param AuthenticationLoginListener $authenticationLoginListener
  127.      * @param Translator $translator
  128.      * @param Service $consentService
  129.      * @param UrlGeneratorInterface $urlGenerator
  130.      * @param NewsletterDoubleOptInService $newsletterDoubleOptInService
  131.      * @param UserInterface|null $user
  132.      *
  133.      * @return Response|RedirectResponse
  134.      */
  135.     public function registerAction(
  136.         Request $request,
  137.         CustomerProviderInterface $customerProvider,
  138.         OAuthRegistrationHandler $oAuthHandler,
  139.         LoginManagerInterface $loginManager,
  140.         RegistrationFormHandler $registrationFormHandler,
  141.         SessionInterface $session,
  142.         AuthenticationLoginListener $authenticationLoginListener,
  143.         Translator $translator,
  144.         Service $consentService,
  145.         UrlGeneratorInterface $urlGenerator,
  146.         NewsletterDoubleOptInService $newsletterDoubleOptInService,
  147.         UserInterface $user null
  148.     ) {
  149.         //redirect user to index page if logged in
  150.         if ($user && $this->isGranted('ROLE_USER')) {
  151.             return $this->redirectToRoute('account-index');
  152.         }
  153.         $registrationKey $request->get('registrationKey');
  154.         // create a new, empty customer instance
  155.         /** @var CustomerInterface|\Pimcore\Model\DataObject\Customer $customer */
  156.         $customer $customerProvider->create();
  157.         /** @var OAuthToken $oAuthToken */
  158.         $oAuthToken null;
  159.         /** @var UserResponseInterface $oAuthUserInfo */
  160.         $oAuthUserInfo null;
  161.         // load previously stored token from the session and try to load user profile
  162.         // from provider
  163.         if (null !== $registrationKey) {
  164.             $oAuthToken $oAuthHandler->loadToken($registrationKey);
  165.             $oAuthUserInfo $oAuthHandler->loadUserInformation($oAuthToken);
  166.         }
  167.         if (null !== $oAuthUserInfo) {
  168.             // try to load a customer with the given identity from our storage. if this succeeds, we can't register
  169.             // the customer and should either log in the existing identity or show an error. for simplicity, we just
  170.             // throw an exception here.
  171.             // this shouldn't happen as the login would log in the user if found
  172.             if ($oAuthHandler->getCustomerFromUserResponse($oAuthUserInfo)) {
  173.                 throw new \RuntimeException('Customer is already registered');
  174.             }
  175.         }
  176.         // the registration form handler is just a utility class to map pimcore object data to form
  177.         // and vice versa.
  178.         $formData $registrationFormHandler->buildFormData($customer);
  179.         $hidePassword false;
  180.         if (null !== $oAuthToken) {
  181.             $formData $this->mergeOAuthFormData($formData$oAuthUserInfo);
  182.             $hidePassword true;
  183.         }
  184.         // build the registration form and pre-fill it with customer data
  185.         $form $this->createForm(RegistrationFormType::class, $formData, ['hidePassword' => $hidePassword]);
  186.         $form->handleRequest($request);
  187.         $errors = [];
  188.         if ($form->isSubmitted() && $form->isValid()) {
  189.             $registrationFormHandler->updateCustomerFromForm($customer$form);
  190.             $customer->setCustomerLanguage($request->getLocale());
  191.             $customer->setActive(true);
  192.             try {
  193.                 if(!$hidePassword) {
  194.                     $this->checkPassword($form->getData()['password']);
  195.                 }
  196.                 $customer->save();
  197.                 if ($form->getData()['newsletter']) {
  198.                     $consentService->giveConsent($customer'newsletter'$translator->trans('general.newsletter'));
  199.                     $newsletterDoubleOptInService->sendDoubleOptInMail($customer$this->document->getProperty('newsletter_confirm_mail'));
  200.                 }
  201.                 if ($form->getData()['profiling']) {
  202.                     $consentService->giveConsent($customer'profiling'$translator->trans('general.profiling'));
  203.                 }
  204.                 // add SSO identity from OAuth data
  205.                 if (null !== $oAuthUserInfo) {
  206.                     $oAuthHandler->connectSsoIdentity($customer$oAuthUserInfo);
  207.                 }
  208.                 //check if special redirect is necessary
  209.                 if ($session->get('referrer')) {
  210.                     $response $this->redirect($session->get('referrer'));
  211.                     $session->remove('referrer');
  212.                 } else {
  213.                     $response $this->redirectToRoute('account-index');
  214.                 }
  215.                 // log user in manually
  216.                 // pass response to login manager as it adds potential remember me cookies
  217.                 $loginManager->login($customer$request$response);
  218.                 //do ecommerce framework login
  219.                 $authenticationLoginListener->doEcommerceFrameworkLogin($customer);
  220.                 return $response;
  221.             } catch (DuplicateCustomerException $e) {
  222.                 $errors[] = $translator->trans(
  223.                     'account.customer-already-exists',
  224.                     [
  225.                         $customer->getEmail(),
  226.                         $urlGenerator->generate('account-password-send-recovery', ['email' => $customer->getEmail()])
  227.                     ]
  228.                 );
  229.             } catch (\Exception $e) {
  230.                 $errors[] = $e->getMessage();
  231.             }
  232.         }
  233.         if ($form->isSubmitted() && !$form->isValid()) {
  234.             foreach ($form->getErrors() as $error) {
  235.                 $errors[] = $error->getMessage();
  236.             }
  237.         }
  238.         // re-save user info to session as we need it in subsequent requests (e.g. after form errors) or
  239.         // when form is rendered for the first time
  240.         if (null !== $registrationKey && null !== $oAuthToken) {
  241.             $oAuthHandler->saveToken($registrationKey$oAuthToken);
  242.         }
  243.         return $this->render('account/register.html.twig', [
  244.             'customer' => $customer,
  245.             'form' => $form->createView(),
  246.             'errors' => $errors,
  247.             'hideBreadcrumbs' => true,
  248.             'hidePassword' => $hidePassword
  249.         ]);
  250.     }
  251.     /**
  252.      * Special route for connecting to social profiles that saves referrer in session for later
  253.      * redirect to that referrer
  254.      *
  255.      * @param Request $request
  256.      * @param SessionInterface $session
  257.      * @param $service
  258.      *
  259.      * @return Response
  260.      * @Route("/auth/oauth/referrerLogin/{service}", name="app_auth_oauth_login_referrer")
  261.      */
  262.     public function connectAction(Request $requestSessionInterface $session$service)
  263.     {
  264.         // we overwrite this route to store user's referrer in the session
  265.         $session->set('referrer'$request->headers->get('referer'));
  266.         return $this->forward('HWIOAuthBundle:Connect:redirectToService', ['service' => $service]);
  267.     }
  268.     /**
  269.      * Connects an already logged in user to an auth provider
  270.      *
  271.      * @Route("/oauth/connect/{service}", name="app_auth_oauth_connect")
  272.      * @Security("is_granted('ROLE_USER')")
  273.      *
  274.      * @param Request $request
  275.      * @param OAuthRegistrationHandler $oAuthHandler
  276.      * @param UserInterface $user
  277.      * @param string $service
  278.      *
  279.      * @return RedirectResponse
  280.      */
  281.     public function oAuthConnectAction(
  282.         Request $request,
  283.         OAuthRegistrationHandler $oAuthHandler,
  284.         UserInterface $user,
  285.         string $service
  286.     ) {
  287.         $resourceOwner $oAuthHandler->getResourceOwner($service);
  288.         $redirectUrl $this->generateUrl('app_auth_oauth_connect', [
  289.             'service' => $service
  290.         ], UrlGeneratorInterface::ABSOLUTE_URL);
  291.         // redirect to authorization
  292.         if (!$resourceOwner->handles($request)) {
  293.             $authorizationUrl $oAuthHandler->getAuthorizationUrl($request$service$redirectUrl);
  294.             return $this->redirect($authorizationUrl);
  295.         }
  296.         // get access token from URL
  297.         $accessToken $resourceOwner->getAccessToken($request$redirectUrl);
  298.         // e.g. user cancelled auth on provider side
  299.         if (null === $accessToken) {
  300.             return $this->redirectToRoute('account-index');
  301.         }
  302.         $oAuthUserInfo $resourceOwner->getUserInformation($accessToken);
  303.         // we don't want to allow linking an OAuth account to multiple customers
  304.         if ($oAuthHandler->getCustomerFromUserResponse($oAuthUserInfo)) {
  305.             throw new \RuntimeException('There\'s already a customer registered with this provider identity');
  306.         }
  307.         // create a SSO identity object and save it to the user
  308.         $oAuthHandler->connectSsoIdentity($user$oAuthUserInfo);
  309.         // redirect to secure page which should now list the newly linked profile
  310.         return $this->redirectToRoute('account-index');
  311.     }
  312.     /**
  313.      *
  314.      * @param array $formData
  315.      * @param UserResponseInterface $userInformation
  316.      *
  317.      * @return array
  318.      */
  319.     private function mergeOAuthFormData(
  320.         array $formData,
  321.         UserResponseInterface $userInformation
  322.     ): array {
  323.         return array_replace([
  324.             'firstname' => $userInformation->getFirstName(),
  325.             'lastname' => $userInformation->getLastName(),
  326.             'email' => $userInformation->getEmail()
  327.         ], $formData);
  328.     }
  329.     /**
  330.      * Index page for account - it is restricted to ROLE_USER via security annotation
  331.      *
  332.      * @Route("/account/index", name="account-index")
  333.      * @Security("is_granted('ROLE_USER')")
  334.      *
  335.      * @param SsoIdentityServiceInterface $identityService
  336.      * @param UserInterface|null $user
  337.      *
  338.      * @return Response
  339.      */
  340.     public function indexAction(SsoIdentityServiceInterface $identityServiceUserInterface $user null)
  341.     {
  342.         $blacklist = [];
  343.         foreach ($identityService->getSsoIdentities($user) as $identity) {
  344.             $blacklist[] = $identity->getProvider();
  345.         }
  346.         $orderManager Factory::getInstance()->getOrderManager();
  347.         $orderList $orderManager->createOrderList();
  348.         $orderList->addFilter(new CustomerObject($user));
  349.         $orderList->setOrder('orderDate DESC');
  350.         return $this->render('account/index.html.twig', [
  351.             'blacklist' => $blacklist,
  352.             'orderList' => $orderList,
  353.             'hideBreadcrumbs' => true
  354.         ]);
  355.     }
  356.     /**
  357.      * @Route("/account/update-marketing", name="account-update-marketing-permission")
  358.      * @Security("is_granted('ROLE_USER')")
  359.      *
  360.      * @param Request $request
  361.      * @param Service $consentService
  362.      * @param Translator $translator
  363.      * @param NewsletterDoubleOptInService $newsletterDoubleOptInService
  364.      * @param UserInterface|null $user
  365.      *
  366.      * @return RedirectResponse
  367.      *
  368.      * @throws \Exception
  369.      */
  370.     public function updateMarketingPermissionAction(Request $requestService $consentServiceTranslator $translatorNewsletterDoubleOptInService $newsletterDoubleOptInServiceUserInterface $user null)
  371.     {
  372.         if ($user instanceof Customer) {
  373.             $currentNewsletterPermission $user->getNewsletter()->getConsent();
  374.             if (!$currentNewsletterPermission && $request->get('newsletter')) {
  375.                 $consentService->giveConsent($user'newsletter'$translator->trans('general.newsletter'));
  376.                 $newsletterDoubleOptInService->sendDoubleOptInMail($user$this->document->getProperty('newsletter_confirm_mail'));
  377.             } elseif ($currentNewsletterPermission && !$request->get('newsletter')) {
  378.                 $user->setNewsletterConfirmed(false);
  379.                 $consentService->revokeConsent($user'newsletter');
  380.             }
  381.             $currentProfilingPermission $user->getProfiling()->getConsent();
  382.             if (!$currentProfilingPermission && $request->get('profiling')) {
  383.                 $consentService->giveConsent($user'profiling'$translator->trans('general.profiling'));
  384.             } elseif ($currentProfilingPermission && !$request->get('profiling')) {
  385.                 $consentService->revokeConsent($user'profiling');
  386.             }
  387.             $user->save();
  388.             $this->addFlash('success'$translator->trans('account.marketing-permissions-updated'));
  389.         }
  390.         return $this->redirectToRoute('account-index');
  391.     }
  392.     /**
  393.      * @Route("/account/confirm-newsletter", name="account-confirm-newsletter")
  394.      *
  395.      * @param Request $request
  396.      * @param NewsletterDoubleOptInService $newsletterDoubleOptInService
  397.      * @param Translator $translator
  398.      *
  399.      * @return RedirectResponse
  400.      */
  401.     public function confirmNewsletterAction(Request $requestNewsletterDoubleOptInService $newsletterDoubleOptInServiceTranslator $translator)
  402.     {
  403.         $token $request->get('token');
  404.         $customer $newsletterDoubleOptInService->handleDoubleOptInConfirmation($token);
  405.         if ($customer) {
  406.             $this->addFlash('success'$translator->trans('account.marketing-permissions-confirmed-newsletter'));
  407.             return $this->redirectToRoute('account-index');
  408.         } else {
  409.             throw new NotFoundHttpException('Invalid token');
  410.         }
  411.     }
  412.     /**
  413.      * @Route("/account/send-password-recovery", name="account-password-send-recovery")
  414.      *
  415.      * @param Request $request
  416.      * @param PasswordRecoveryService $service
  417.      * @param Translator $translator
  418.      *
  419.      * @return Response
  420.      *
  421.      * @throws \Exception
  422.      */
  423.     public function sendPasswordRecoveryMailAction(Request $requestPasswordRecoveryService $serviceTranslator $translator)
  424.     {
  425.         if ($request->isMethod(Request::METHOD_POST)) {
  426.             try {
  427.                 $customer $service->sendRecoveryMail($request->get('email'''), $this->document->getProperty('password_reset_mail'));
  428.                 if (!$customer instanceof CustomerInterface) {
  429.                     throw new \Exception('Invalid Customer');
  430.                 }
  431.                 $this->addFlash('success'$translator->trans('account.reset-mail-sent-when-possible'));
  432.             } catch (\Exception $e) {
  433.                 $this->addFlash('danger'$e->getMessage());
  434.             }
  435.             return $this->redirectToRoute('account-login', ['no-referer-redirect' => true]);
  436.         }
  437.         return $this->render('account/send_password_recovery_mail.html.twig', [
  438.             'hideBreadcrumbs' => true,
  439.             'emailPrefill' => $request->get('email')
  440.         ]);
  441.     }
  442.     /**
  443.      * @Route("/account/reset-password", name="account-reset-password")
  444.      *
  445.      * @param Request $request
  446.      * @param PasswordRecoveryService $service
  447.      * @param Translator $translator
  448.      *
  449.      * @return Response|RedirectResponse
  450.      */
  451.     public function resetPasswordAction(Request $requestPasswordRecoveryService $serviceTranslator $translator)
  452.     {
  453.         $token $request->get('token');
  454.         $customer $service->getCustomerByToken($token);
  455.         $error null;
  456.         try {
  457.             if (!$customer) {
  458.                 throw new NotFoundHttpException('Invalid token');
  459.             }
  460.             if ($request->isMethod(Request::METHOD_POST)) {
  461.                 $newPassword $request->get('password');
  462.                 $this->checkPassword($newPassword);
  463.                 $service->setPassword($token$newPassword);
  464.                 $this->addFlash('success'$translator->trans('account.password-reset-successful'));
  465.                 return $this->redirectToRoute('account-login', ['no-referer-redirect' => true]);
  466.             }
  467.         } catch (\Exception $exception) {
  468.             $error $exception->getMessage();
  469.         }
  470.         return $this->render('account/reset_password.html.twig', [
  471.             'hideBreadcrumbs' => true,
  472.             'token' => $token,
  473.             'email' => $customer?->getEmail(),
  474.             'error' => $error
  475.         ]);
  476.     }
  477. }