src/Controller/AccountController.php line 112

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