vendor/ibexa/user/src/bundle/Controller/PasswordResetController.php line 83

Open in your IDE?
  1. <?php
  2. /**
  3. * @copyright Copyright (C) Ibexa AS. All rights reserved.
  4. * @license For full copyright and license information view LICENSE file distributed with this source code.
  5. */
  6. declare(strict_types=1);
  7. namespace Ibexa\Bundle\User\Controller;
  8. use DateInterval;
  9. use DateTime;
  10. use Ibexa\Bundle\User\Type\UserForgotPasswordReason;
  11. use Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException;
  12. use Ibexa\Contracts\Core\Repository\PermissionResolver;
  13. use Ibexa\Contracts\Core\Repository\UserService;
  14. use Ibexa\Contracts\Core\Repository\Values\User\User;
  15. use Ibexa\Contracts\Core\Repository\Values\User\UserTokenUpdateStruct;
  16. use Ibexa\Contracts\Core\SiteAccess\ConfigResolverInterface;
  17. use Ibexa\Contracts\Notifications\Service\NotificationServiceInterface;
  18. use Ibexa\Contracts\Notifications\Value\Notification\SymfonyNotificationAdapter;
  19. use Ibexa\Contracts\Notifications\Value\Recipent\SymfonyRecipientAdapter;
  20. use Ibexa\Contracts\Notifications\Value\Recipent\UserRecipient;
  21. use Ibexa\Contracts\User\Notification\UserPasswordReset;
  22. use Ibexa\User\ExceptionHandler\ActionResultHandler;
  23. use Ibexa\User\Form\Data\UserPasswordResetData;
  24. use Ibexa\User\Form\Factory\FormFactory;
  25. use Ibexa\User\View\ForgotPassword\FormView;
  26. use Ibexa\User\View\ForgotPassword\LoginView;
  27. use Ibexa\User\View\ForgotPassword\SuccessView;
  28. use Ibexa\User\View\ResetPassword\FormView as UserResetPasswordFormView;
  29. use Ibexa\User\View\ResetPassword\InvalidLinkView;
  30. use Ibexa\User\View\ResetPassword\SuccessView as UserResetPasswordSuccessView;
  31. use Swift_Mailer;
  32. use Swift_Message;
  33. use Symfony\Component\HttpFoundation\Request;
  34. use Symfony\Component\HttpFoundation\Response;
  35. use Twig\Environment;
  36. class PasswordResetController extends Controller
  37. {
  38. private FormFactory $formFactory;
  39. private UserService $userService;
  40. private Swift_Mailer $mailer;
  41. private Environment $twig;
  42. private ActionResultHandler $actionResultHandler;
  43. private PermissionResolver $permissionResolver;
  44. private ConfigResolverInterface $configResolver;
  45. private NotificationServiceInterface $notificationService;
  46. public function __construct(
  47. FormFactory $formFactory,
  48. UserService $userService,
  49. Swift_Mailer $mailer,
  50. Environment $twig,
  51. ActionResultHandler $actionResultHandler,
  52. PermissionResolver $permissionResolver,
  53. ConfigResolverInterface $configResolver,
  54. NotificationServiceInterface $notificationService
  55. ) {
  56. $this->formFactory = $formFactory;
  57. $this->userService = $userService;
  58. $this->mailer = $mailer;
  59. $this->twig = $twig;
  60. $this->actionResultHandler = $actionResultHandler;
  61. $this->permissionResolver = $permissionResolver;
  62. $this->configResolver = $configResolver;
  63. $this->notificationService = $notificationService;
  64. }
  65. /**
  66. * @return \Ibexa\User\View\ForgotPassword\FormView|\Ibexa\User\View\ForgotPassword\SuccessView|\Symfony\Component\HttpFoundation\RedirectResponse
  67. *
  68. * @throws \Ibexa\Core\Base\Exceptions\InvalidArgumentType
  69. */
  70. public function userForgotPasswordAction(Request $request, ?string $reason = null)
  71. {
  72. $form = $this->formFactory->forgotUserPassword();
  73. $form->handleRequest($request);
  74. if ($form->isSubmitted() && $form->isValid()) {
  75. $data = $form->getData();
  76. $users = $this->userService->loadUsersByEmail($data->getEmail());
  77. /** Because is is possible to have multiple user accounts with same email address we must gain a user login. */
  78. if (\count($users) > 1) {
  79. return $this->redirectToRoute('ibexa.user.forgot_password.login');
  80. }
  81. if (!empty($users)) {
  82. $user = reset($users);
  83. $token = $this->updateUserToken($user);
  84. $this->sendResetPasswordMessage($user, $token);
  85. }
  86. return new SuccessView(null);
  87. }
  88. return new FormView(null, [
  89. 'form_forgot_user_password' => $form->createView(),
  90. 'reason' => $reason,
  91. 'userForgotPasswordReasonMigration' => UserForgotPasswordReason::MIGRATION,
  92. ]);
  93. }
  94. /**
  95. * @param \Symfony\Component\HttpFoundation\Request $request
  96. *
  97. * @return \Ibexa\User\View\ForgotPassword\LoginView|\Ibexa\User\View\ForgotPassword\SuccessView
  98. *
  99. * @throws \Ibexa\Core\Base\Exceptions\InvalidArgumentType
  100. */
  101. public function userForgotPasswordLoginAction(Request $request)
  102. {
  103. $form = $this->formFactory->forgotUserPasswordWithLogin();
  104. $form->handleRequest($request);
  105. if ($form->isSubmitted() && $form->isValid()) {
  106. $data = $form->getData();
  107. try {
  108. $user = $this->userService->loadUserByLogin($data->getLogin());
  109. } catch (NotFoundException $e) {
  110. $user = null;
  111. }
  112. if (!$user || \count($this->userService->loadUsersByEmail($user->email)) < 2) {
  113. return new SuccessView(null);
  114. }
  115. $token = $this->updateUserToken($user);
  116. $this->sendResetPasswordMessage($user, $token);
  117. return new SuccessView(null);
  118. }
  119. return new LoginView(null, [
  120. 'form_forgot_user_password_with_login' => $form->createView(),
  121. ]);
  122. }
  123. /**
  124. * @param \Symfony\Component\HttpFoundation\Request $request
  125. * @param string $hashKey
  126. *
  127. * @return \Ibexa\User\View\ResetPassword\FormView|\Ibexa\User\View\ResetPassword\InvalidLinkView|\Ibexa\User\View\ResetPassword\SuccessView
  128. *
  129. * @throws \Ibexa\Core\Base\Exceptions\InvalidArgumentType
  130. */
  131. public function userResetPasswordAction(Request $request, string $hashKey)
  132. {
  133. $response = new Response();
  134. $response->headers->set('X-Robots-Tag', 'noindex');
  135. try {
  136. $user = $this->userService->loadUserByToken($hashKey);
  137. } catch (NotFoundException $e) {
  138. $view = new InvalidLinkView(null);
  139. $view->setResponse($response);
  140. return $view;
  141. }
  142. $userPasswordResetData = new UserPasswordResetData();
  143. $form = $this->formFactory->resetUserPassword(
  144. $userPasswordResetData,
  145. null,
  146. $user->getContentType(),
  147. $user
  148. );
  149. $form->handleRequest($request);
  150. if ($form->isSubmitted() && $form->isValid()) {
  151. try {
  152. $currentUser = $this->permissionResolver->getCurrentUserReference();
  153. $this->permissionResolver->setCurrentUserReference($user);
  154. } catch (NotFoundException $e) {
  155. $view = new InvalidLinkView(null);
  156. $view->setResponse($response);
  157. return $view;
  158. }
  159. $data = $form->getData();
  160. try {
  161. $this->userService->updateUserPassword($user, $data->getNewPassword());
  162. $this->userService->expireUserToken($hashKey);
  163. $this->permissionResolver->setCurrentUserReference($currentUser);
  164. $view = new UserResetPasswordSuccessView(null);
  165. $view->setResponse($response);
  166. return $view;
  167. } catch (\Exception $e) {
  168. $this->actionResultHandler->error($e->getMessage());
  169. }
  170. }
  171. $view = new UserResetPasswordFormView(null, [
  172. 'form_reset_user_password' => $form->createView(),
  173. ]);
  174. $view->setResponse($response);
  175. return $view;
  176. }
  177. /**
  178. * @param \Ibexa\Contracts\Core\Repository\Values\User\User $user
  179. *
  180. * @return string
  181. *
  182. * @throws \Exception
  183. */
  184. private function updateUserToken(User $user): string
  185. {
  186. $struct = new UserTokenUpdateStruct();
  187. $struct->hashKey = bin2hex(random_bytes(16));
  188. $date = new DateTime();
  189. $date->add(new DateInterval($this->configResolver->getParameter('security.token_interval_spec')));
  190. $struct->time = $date;
  191. $this->userService->updateUserToken($user, $struct);
  192. return $struct->hashKey;
  193. }
  194. private function sendResetPasswordMessage(User $user, string $hashKey): void
  195. {
  196. if ($this->isNotifierConfigured()) {
  197. $this->sendNotification($user, $hashKey);
  198. return;
  199. }
  200. // Swiftmailer delivery has to be kept to maintain backwards compatibility
  201. $template = $this->twig->load($this->configResolver->getParameter('user_forgot_password.templates.mail'));
  202. $senderAddress = $this->configResolver->hasParameter('sender_address', 'swiftmailer.mailer')
  203. ? $this->configResolver->getParameter('sender_address', 'swiftmailer.mailer')
  204. : '';
  205. $subject = $template->renderBlock('subject', []);
  206. $from = $template->renderBlock('from', []) ?: $senderAddress;
  207. $body = $template->renderBlock('body', ['hash_key' => $hashKey]);
  208. $message = (new Swift_Message())
  209. ->setSubject($subject)
  210. ->setTo($user->email)
  211. ->setBody($body, 'text/html');
  212. if (empty($from) === false) {
  213. $message->setFrom($from);
  214. }
  215. $this->mailer->send($message);
  216. }
  217. private function sendNotification($user, string $token): void
  218. {
  219. $this->notificationService->send(
  220. new SymfonyNotificationAdapter(
  221. new UserPasswordReset($user, $token),
  222. ),
  223. [new SymfonyRecipientAdapter(new UserRecipient($user))],
  224. );
  225. }
  226. private function isNotifierConfigured(): bool
  227. {
  228. $subscriptions = $this->configResolver->getParameter('notifications.subscriptions');
  229. return array_key_exists(UserPasswordReset::class, $subscriptions)
  230. && !empty($subscriptions[UserPasswordReset::class]['channels']);
  231. }
  232. }
  233. class_alias(PasswordResetController::class, 'EzSystems\EzPlatformUserBundle\Controller\PasswordResetController');