src/Controller/PasswordRestoreController.php line 46

Open in your IDE?
  1. <?php
  2. namespace App\Controller;
  3. use App\Form\EventListener\ReCaptchaValidatorListener;
  4. use App\Service\Cypher\Cypher;
  5. use App\Service\UserManager;
  6. use Psr\Log\LoggerInterface;
  7. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  8. use Symfony\Component\Form\Extension\Core\Type\EmailType;
  9. use Symfony\Component\Form\Extension\Core\Type\FormType;
  10. use Symfony\Component\Form\Extension\Core\Type\HiddenType;
  11. use Symfony\Component\Form\Extension\Core\Type\PasswordType;
  12. use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
  13. use Symfony\Component\Form\Extension\Core\Type\SubmitType;
  14. use Symfony\Component\Form\Extension\Core\Type\TelType;
  15. use Symfony\Component\Form\FormError;
  16. use Symfony\Component\HttpFoundation\Request;
  17. use Symfony\Component\Routing\Annotation\Route;
  18. use Symfony\Component\HttpFoundation\Response;
  19. use Symfony\Component\Validator\Constraints\Email;
  20. use Symfony\Component\Validator\Constraints\Length;
  21. use Symfony\Component\Validator\Constraints\NotBlank;
  22. use Symfony\Component\Validator\Constraints\Type;
  23. /**
  24.  * @Route("/restablecer", name="password_restore_", 
  25.  *  requirements={"_locale":"%app.supported_locales%"},
  26.  *  defaults={"_locale" = "es", "_title"="Pasarela restablecer clave de la Universidad de Murcia"})
  27.  */
  28. class PasswordRestoreController extends AbstractController
  29. {
  30.     const PANEL_STEPS 4;
  31.     const PASSWORD_MIN_LEN 12;
  32.     const PHONE_MIN_LEN 7;
  33.     const PHONE_MAX_LEN 20;
  34.     const MAIL_MIN_LEN 5;
  35.     const MAIL_MAX_LEN 64;
  36.     const DEFAULT_ERRORS = [
  37.         500 => 'error.default.500'// Se ha producido un error inesperado, vuelva a intentarlo más tarde.
  38.     ];
  39.     /**
  40.      * @Route("/{_locale}", name="index", requirements={"_locale"="[a-zA-Z]+"})
  41.      */
  42.     public function index(Request $requestUserManager $userManagerCypher $cypherLoggerInterface $logger): Response
  43.     {
  44.         // Creación de formularios de usuario y validación
  45.         $userForm $this->createUserForm();
  46.         $userForm->handleRequest($request);
  47.         $phoneValidationForm $this->createPhoneValidationForm();
  48.         $phoneValidationForm->handleRequest($request);
  49.         $emailValidationForm $this->createEmailValidationForm();
  50.         $emailValidationForm->handleRequest($request);
  51.         $validationForm null;
  52.         // Función anónima que renderiza el paso 1
  53.         $render1 = function () use ($logger$userForm) {
  54.             return $this->renderForm('password_restore_step1.html.twig', [
  55.                 'panel_steps' => self::PANEL_STEPS,
  56.                 'panel_current_step' => 1,
  57.                 'user_form' => $userForm
  58.             ]);
  59.         };
  60.         // Función anónima que renderizar el paso 2 con todos sus formularios
  61.         $render2 = function (string $username, array $contacts$formWithErrors null) use ($logger$cypher$phoneValidationForm$emailValidationForm) {
  62.             // Preparar los datos de los formularios a renderizar
  63.             $defaultMethod null;
  64.             foreach ($contacts as &$contact) {
  65.                 $form = &${$contact['method'].'ValidationForm'};
  66.                 // Se recibió un formulario con errores que se deberá renderizar por defecto 
  67.                 if (isset($formWithErrors) && $form === $formWithErrors) {
  68.                     $defaultMethod $contact['method'];
  69.                 }
  70.                 // Preparar datos de otros formularios
  71.                 else {
  72.                     $form->get('username')->setData($cypher->encrypt([$username]));
  73.                     $form->get('method')->setData($contact['method']);
  74.                     $form->get('hint')->setData($contact['hint']);
  75.                     if ('phone' == $contact['method'])
  76.                         $defaultMethod $contact['method'];
  77.                 }
  78.                 $contact['tabid'] = 'recovery_method_'.$contact['method'];
  79.                 $contact['form'] = $form->createView();
  80.             }
  81.             unset($contact);
  82.             $logger->info(sprintf('[restablecer] inicio de solicitud de cambio [username=%s, methods=%s]'$usernameimplode(','array_column($contacts'method'))));
  83.             return $this->renderForm("password_restore_step2.html.twig", [
  84.                 'panel_steps' => self::PANEL_STEPS,
  85.                 'panel_current_step' => 2,
  86.                 'contacts' => $contacts,
  87.                 'default_method' => $defaultMethod
  88.             ]);
  89.         };
  90.         // Función anónima que renderizar el paso 3, de envío de código al método elegido
  91.         $render3 = function (string $usernamestring $methodbool $extern false) use ($logger) {
  92.             $logger->info(sprintf('[restablecer] envío de solicitud de cambio [username=%s, method=%s, extern=%s]'$username$method$extern 'true' 'false'));
  93.             return $this->render('password_restore_step3.html.twig', [
  94.                 'panel_steps' => self::PANEL_STEPS,
  95.                 'panel_current_step' => 3,
  96.                 'recovery_method' => $method,
  97.             ]);
  98.         };
  99.         // Obtener datos de los formularios ya enviados y setear variables
  100.         $method null;
  101.         $data = [];
  102.         if ($userForm->isSubmitted() && $userForm->isValid()) {
  103.             $data $userForm->getData();
  104.         }
  105.         elseif ($phoneValidationForm->isSubmitted()) {
  106.             $method UserManager::RECOVERY_METHOD_PHONE;
  107.             $validationForm = &$phoneValidationForm;
  108.             $data $phoneValidationForm->getData();
  109.             $data['username'] = $cypher->decrypt($data['username'])[0] ?? '';
  110.             if ($phoneValidationForm->isValid()) {
  111.                 $code $data['country_code'];
  112.                 $contact strlen($code) == || $code == 34 $data['contact'] : '+'.$code.$data['contact'];
  113.                 $contactError 'El teléfono móvil introducido no coincide con el registrado.';
  114.             }
  115.         }
  116.         elseif ($emailValidationForm->isSubmitted()) {
  117.             $method UserManager::RECOVERY_METHOD_MAIL;
  118.             $validationForm = &$emailValidationForm;
  119.             $data $emailValidationForm->getData();
  120.             $data['username'] = $cypher->decrypt($data['username'])[0] ?? '';
  121.             if ($emailValidationForm->isValid()) {
  122.                 $contact $data['contact'];
  123.                 $contactError 'El correo introducido no coincide con el registrado.';
  124.             }
  125.         }
  126.         // Comprobar proactivamente que el usuario es válido, no está bloqueado y obtener información actualizada
  127.         if (! empty($data)) {
  128.             try {
  129.                 $userManager->infoAccount($data['username'], true);
  130.                 $userInfo $userManager->validateUserV2($data['username']);
  131.             } catch (\Throwable $th) {
  132.                 // 404 debería volver a rederizar $userForm seteando error en username
  133.                 if (404 == $th->getCode()) {
  134.                     $userForm->get('username')->addError(new FormError($th->getMessage()));
  135.                     return $render1();
  136.                 }
  137.                 // Error inesperado renderizar pantalla de error
  138.                 return $this->render('error.html.twig', [
  139.                     'error_code' => $th->getCode(),
  140.                     'error_body' => $th->getMessage(),
  141.                 ]);
  142.             }
  143.         }
  144.         // Validar paso 1, ir a paso 2 o 3
  145.         if ($userForm->isSubmitted()) {
  146.             if ($userForm->isValid()) {
  147.                 // Ir a paso 3 cuando es un usuario externo
  148.                 if ($userInfo['externo']) {
  149.                     return $render3($data['username'], UserManager::RECOVERY_METHOD_MAILtrue);
  150.                 }
  151.                 // Ir al paso 2, con todos los métodos disponibles
  152.                 return $render2($data['username'], $userInfo['contacto']);
  153.             }
  154.         }
  155.         // Validar paso 2, ir a paso 3
  156.         elseif (isset($validationForm) && $validationForm->isSubmitted()) {
  157.             if ($validationForm->isValid()) {
  158.                 try {
  159.                     $result $userManager->validateRecoveryMethod($data['username'], $userInfo['dni'], $contact$method);
  160.                 } catch (\Throwable $th) {
  161.                     // 404 debería volver a renderizar $phoneValidationForm seteando error en phone
  162.                     if ($th->getCode() == 404) {
  163.                         // Sobrescribir el mensaje del UserManager por teléfono erróneo, por descarte y puesto que el usuario ya está validado
  164.                         //$validationForm->get('contact')->addError(new FormError($th->getMessage()));
  165.                         $validationForm->get('contact')->addError(new FormError($contactError));
  166.                         return $render2($data['username'], $userInfo['contacto'], $validationForm);
  167.                     // Error inesperado renderizar pantalla de error
  168.                     } else {
  169.                         return $this->render('error.html.twig', [
  170.                             'error_code' => $th->getCode(),
  171.                             'error_body' => $th->getMessage(),
  172.                         ]);
  173.                     }
  174.                 }
  175.                 // Paso 3 tras comprobar todo ok
  176.                 return $render3($data['username'], $method);
  177.             }
  178.             // El formulario se envío pero es erróneo, volver a mostrar paso 2
  179.             return $render2($data['username'], $userInfo['contacto'], $validationForm);
  180.         }
  181.         // Paso 1 por defecto
  182.         return $render1();
  183.     }
  184.     /**
  185.      * Redirección cunado no hay locale establecida.
  186.      * @Route("/{recovery_code}", name="passwd_redirect", defaults={"recovery_code" = "0"}, requirements={"recovery_code"="\d+"})
  187.      */
  188.     public function passwdNoLocale(Request $requeststring $recovery_code 'empty')
  189.     {
  190.         return $this->redirectToRoute('password_restore_passwd', ['_locale' => $request->getLocale(), 'recovery_code' => $recovery_code]);
  191.     }
  192.     /**
  193.      * @Route("/{_locale}/{recovery_code}", name="passwd", defaults={"recovery_code" = "0"}, requirements={"recovery_code"="\d+"})
  194.      */
  195.     public function passwd(string $recovery_codeRequest $requestReCaptchaValidatorListener $recaptchaValidatorUserManager $userManagerLoggerInterface $logger): Response
  196.     {
  197.         // Formulario nueva contraseña
  198.         $passwdForm $this->createPasswdForm($recaptchaValidator);
  199.         $passwdForm->handleRequest($request);
  200.         if ($passwdForm->isSubmitted()) {
  201.             if ($passwdForm->isValid()) {
  202.                 // Realizar el cambio de contraseña
  203.                 $data $passwdForm->getData();
  204.                 try {
  205.                     $result $userManager->changePasswordWithToken($data['token'], $data['password']);
  206.                 } catch (\Throwable $th) {
  207.                     // Contraseña no válida
  208.                     if (400 == $th->getCode() || 406 == $th->getCode()) {
  209.                         //$passwdForm->addError(new FormError($th->getMessage()));
  210.                         $passwdForm->get('password')['first']->addError(new FormError($th->getMessage()));
  211.                         return $this->renderForm('password_restore_step4.html.twig', [
  212.                             'panel_steps' => self::PANEL_STEPS,
  213.                             'panel_current_step' => 4,
  214.                             'passwd_form' => $passwdForm
  215.                         ]);
  216.                     // Token no válido
  217.                     } else if (403 == $th->getCode()) {
  218.                         return $this->render('error.html.twig', [
  219.                             'error_code' => $th->getCode(),
  220.                             'error_body' => $th->getMessage(),
  221.                             'back_btn_href' => $this->generateUrl('password_restore_index', ['_locale' => $request->getLocale() ]),
  222.                         ]);
  223.                     // Cualquier otro error
  224.                     } else {
  225.                         return $this->render('error.html.twig', [
  226.                             'error_code' => $th->getCode(),
  227.                             'error_body' => $th->getMessage(),
  228.                         ]);
  229.                     }
  230.                 }
  231.                 $logger->info('[restablecer] cambio de clave '.$data['token']);
  232.                 return $this->render('success.html.twig', [ 'show_chpwd_help' => true ]);
  233.             }
  234.         }
  235.         // Establecer token del formulario si no se envió
  236.         else
  237.             $passwdForm->get('token')->setData($recovery_code);
  238.         // Renderizar
  239.         return $this->renderForm('password_restore_step4.html.twig', [
  240.             'panel_steps' => self::PANEL_STEPS,
  241.             'panel_current_step' => 4,
  242.             'passwd_form' => $passwdForm
  243.         ]);
  244.     }
  245.     /**
  246.      * Crea un formulario de validación de usuario.
  247.      */
  248.     private function createUserForm(string $name 'userForm'string $formid 'step1') {
  249.         //return $this->get('form.factory')->createNamedBuilder($name, FormType::class, [ 'formid' => $formid ])
  250.         return $this->createFormBuilder()->getFormFactory()->createNamedBuilder($nameFormType::class, [ 'formid' => $formid ])
  251.             ->add('username'EmailType::class, [
  252.                 'constraints' => [ 
  253.                     new NotBlank(), 
  254.                     new Email(),
  255.                 ],
  256.                 'label' => 'Correo electrónico',
  257.                 'attr' => [ 'autofocus' => 'autofocus' ],
  258.                 'row_attr' => [ 'class' => 'form-floating'],
  259.             ])->add('next'SubmitType::class, [
  260.                 'label' => 'Siguiente',
  261.             ])->getForm();
  262.     }
  263.     /**
  264.      * Crea un formulario de validación de teléfono.
  265.      * Los campos username, uid y phonehint son ocultos y se deben proporcionar.
  266.      */
  267.     private function createPhoneValidationForm(string $name 'phoneForm'string $formid 'step2') {
  268.         //return $this->get('form.factory')->createNamedBuilder($name, FormType::class, [ 'formid' => $formid ])
  269.         return $this->createFormBuilder()->getFormFactory()->createNamedBuilder($nameFormType::class, [ 'formid' => $formid ])
  270.             ->add('method'HiddenType::class, [
  271.                 'constraints' => [
  272.                     new NotBlank(),
  273.                 ],
  274.                 'attr' => [
  275.                     'readonly' => true,
  276.                 ],
  277.             ])->add('username'HiddenType::class, [
  278.                 'constraints' => [
  279.                     new NotBlank(),
  280.                     //new Email(), Va encriptado
  281.                 ],
  282.                 'attr' => [
  283.                     'readonly' => true,
  284.                 ],
  285.             ])->add('hint'HiddenType::class, [
  286.                 'constraints' => [
  287.                     new NotBlank(),
  288.                 ],
  289.                 'attr' => [
  290.                     'readonly' => true,
  291.                 ],
  292.             ])->add('country_code'TelType::class, [
  293.                 'constraints' => [
  294.                     new Type('numeric'),
  295.                     new Length([ 'max' => 4]),
  296.                 ],
  297.                 'label' => false,
  298.                 'required' => false,
  299.                 'attr' => [
  300.                     'placeholder' => 34,
  301.                     'maxlength' => 4,
  302.                     'autocomplete' => 'off',
  303.                     'title' => 'Código de país',
  304.                     'class' => 'input-numeric-code',
  305.                 ],
  306.             ])->add('contact'TelType::class, [
  307.                 'constraints' => [
  308.                     new NotBlank(),
  309.                     new Type('numeric'),
  310.                     new Length([ 'min' => self::PHONE_MIN_LEN'max' => self::PHONE_MAX_LEN]),
  311.                 ],
  312.                 'label' => false,
  313.                 'attr' => [
  314.                     'autofocus' => 'autofocus',
  315.                     'minlength' => self::PHONE_MIN_LEN,
  316.                     'maxlength' => self::PHONE_MAX_LEN,
  317.                     'autocomplete' => 'off',
  318.                     'title' => 'Teléfono',
  319.                     'class' => 'input-numeric-code',
  320.                     'aria-describedby' => 'tab_recovery_method_phone_desc',
  321.                 ],
  322.             ])->add('next'SubmitType::class, [
  323.                 'label' => 'Siguiente',
  324.             ])->getForm();
  325.     }
  326.     /**
  327.      * Crea un formulario de validación de teléfono.
  328.      * Los campos username, uid y phoneint son ocultos y se deben proporcionar.
  329.      */
  330.     private function createEmailValidationForm(string $name 'mailForm'string $formid 'step2') {
  331.         //return $this->get('form.factory')->createNamedBuilder($name, FormType::class, [ 'formid' => $formid ])
  332.         return $this->createFormBuilder()->getFormFactory()->createNamedBuilder($nameFormType::class, [ 'formid' => $formid ])
  333.             ->add('method'HiddenType::class, [
  334.                 'constraints' => [
  335.                     new NotBlank(),
  336.                 ],
  337.                 'attr' => [
  338.                     'readonly' => true,
  339.                 ],
  340.             ])->add('username'HiddenType::class, [
  341.                 'constraints' => [
  342.                     new NotBlank(),
  343.                     //new Email(), Va encriptado
  344.                 ],
  345.                 'attr' => [
  346.                     'readonly' => true,
  347.                 ],
  348.             ])->add('hint'HiddenType::class, [
  349.                 'constraints' => [
  350.                     new NotBlank(),
  351.                 ],
  352.                 'attr' => [
  353.                     'readonly' => true,
  354.                 ],
  355.             ])->add('contact'EmailType::class, [
  356.                 'constraints' => [
  357.                     new NotBlank(),
  358.                     new Email(),
  359.                     new Length([ 'min' => self::MAIL_MIN_LEN'max' => self::MAIL_MAX_LEN]),
  360.                 ],
  361.                 'label' => false,
  362.                 'attr' => [
  363.                     'autofocus' => 'autofocus',
  364.                     'minlength' => self::MAIL_MIN_LEN,
  365.                     'maxlength' => self::MAIL_MAX_LEN,
  366.                     'autocomplete' => 'off',
  367.                     'title' => 'Correo electrónico',
  368.                     'aria-describedby' => 'tab_recovery_method_email_desc',
  369.                 ],
  370.             ])->add('next'SubmitType::class, [
  371.                 'label' => 'Siguiente',
  372.             ])->getForm();
  373.     }
  374.     /**
  375.      * Crea un formulario de restauración de clave.
  376.      */
  377.     private function createPasswdForm(ReCaptchaValidatorListener $recaptchaValidatorstring $name 'passwdForm'string $formid 'step4') {
  378.         //return $this->get('form.factory')->createNamedBuilder($name, FormType::class, [ 'formid' => $formid ])
  379.         return $this->createFormBuilder()->getFormFactory()->createNamedBuilder($nameFormType::class, [ 'formid' => $formid ])
  380.             ->add('token'HiddenType::class, [
  381.                 'constraints' => [ 
  382.                     new NotBlank(),
  383.                     new Type('numeric'),
  384.                 ],
  385.             ])->add('password'RepeatedType::class, [
  386.                 'type' => PasswordType::class,
  387.                 'constraints' => [
  388.                     new NotBlank(),
  389.                     new Length([ 'min' => self::PASSWORD_MIN_LEN ]), 
  390.                 ],
  391.                 'options' => [ 
  392.                     'attr' => [ 'minglength' => 12'autofocus' => 'autofocus' ],
  393.                     'row_attr' => [ 'class' => 'form-floating form-caps-lock'],
  394.                 ],
  395.                 'required' => true,
  396.                 'first_options' => [ 'label' => 'Nueva clave' ],
  397.                 'second_options' => [ 'label' => 'Repita la clave' ],
  398.                 'invalid_message' => 'Las claves deben coincidir.',
  399.             ])->add('next'SubmitType::class, [
  400.                 'label' => 'Restaurar clave',
  401.             ])->addEventSubscriber($recaptchaValidator)
  402.             ->getForm();
  403.         }
  404. }
  405. ?>