PageRenderTime 52ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 0ms

/core/modules/user/src/Controller/UserController.php

http://github.com/drupal/drupal
PHP | 364 lines | 160 code | 27 blank | 177 comment | 27 complexity | b931fd55942327602942baa4040043af MD5 | raw file
Possible License(s): GPL-2.0, LGPL-2.1
  1. <?php
  2. namespace Drupal\user\Controller;
  3. use Drupal\Component\Utility\Crypt;
  4. use Drupal\Component\Utility\Xss;
  5. use Drupal\Core\Controller\ControllerBase;
  6. use Drupal\Core\Datetime\DateFormatterInterface;
  7. use Drupal\Core\Flood\FloodInterface;
  8. use Drupal\Core\Url;
  9. use Drupal\user\Form\UserPasswordResetForm;
  10. use Drupal\user\UserDataInterface;
  11. use Drupal\user\UserInterface;
  12. use Drupal\user\UserStorageInterface;
  13. use Psr\Log\LoggerInterface;
  14. use Symfony\Component\DependencyInjection\ContainerInterface;
  15. use Symfony\Component\HttpFoundation\Request;
  16. use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
  17. /**
  18. * Controller routines for user routes.
  19. */
  20. class UserController extends ControllerBase {
  21. /**
  22. * The date formatter service.
  23. *
  24. * @var \Drupal\Core\Datetime\DateFormatterInterface
  25. */
  26. protected $dateFormatter;
  27. /**
  28. * The user storage.
  29. *
  30. * @var \Drupal\user\UserStorageInterface
  31. */
  32. protected $userStorage;
  33. /**
  34. * The user data service.
  35. *
  36. * @var \Drupal\user\UserDataInterface
  37. */
  38. protected $userData;
  39. /**
  40. * A logger instance.
  41. *
  42. * @var \Psr\Log\LoggerInterface
  43. */
  44. protected $logger;
  45. /**
  46. * The flood service.
  47. *
  48. * @var \Drupal\Core\Flood\FloodInterface
  49. */
  50. protected $flood;
  51. /**
  52. * Constructs a UserController object.
  53. *
  54. * @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
  55. * The date formatter service.
  56. * @param \Drupal\user\UserStorageInterface $user_storage
  57. * The user storage.
  58. * @param \Drupal\user\UserDataInterface $user_data
  59. * The user data service.
  60. * @param \Psr\Log\LoggerInterface $logger
  61. * A logger instance.
  62. * @param \Drupal\Core\Flood\FloodInterface $flood
  63. * The flood service.
  64. */
  65. public function __construct(DateFormatterInterface $date_formatter, UserStorageInterface $user_storage, UserDataInterface $user_data, LoggerInterface $logger, FloodInterface $flood = NULL) {
  66. $this->dateFormatter = $date_formatter;
  67. $this->userStorage = $user_storage;
  68. $this->userData = $user_data;
  69. $this->logger = $logger;
  70. if (!$flood) {
  71. @trigger_error('Calling ' . __METHOD__ . ' without the $flood parameter is deprecated in drupal:8.8.0 and is required in drupal:9.0.0. See https://www.drupal.org/node/1681832', E_USER_DEPRECATED);
  72. $flood = \Drupal::service('flood');
  73. }
  74. $this->flood = $flood;
  75. }
  76. /**
  77. * {@inheritdoc}
  78. */
  79. public static function create(ContainerInterface $container) {
  80. return new static(
  81. $container->get('date.formatter'),
  82. $container->get('entity_type.manager')->getStorage('user'),
  83. $container->get('user.data'),
  84. $container->get('logger.factory')->get('user'),
  85. $container->get('flood')
  86. );
  87. }
  88. /**
  89. * Redirects to the user password reset form.
  90. *
  91. * In order to never disclose a reset link via a referrer header this
  92. * controller must always return a redirect response.
  93. *
  94. * @param \Symfony\Component\HttpFoundation\Request $request
  95. * The request.
  96. * @param int $uid
  97. * User ID of the user requesting reset.
  98. * @param int $timestamp
  99. * The current timestamp.
  100. * @param string $hash
  101. * Login link hash.
  102. *
  103. * @return \Symfony\Component\HttpFoundation\RedirectResponse
  104. * The redirect response.
  105. */
  106. public function resetPass(Request $request, $uid, $timestamp, $hash) {
  107. $account = $this->currentUser();
  108. // When processing the one-time login link, we have to make sure that a user
  109. // isn't already logged in.
  110. if ($account->isAuthenticated()) {
  111. // The current user is already logged in.
  112. if ($account->id() == $uid) {
  113. user_logout();
  114. // We need to begin the redirect process again because logging out will
  115. // destroy the session.
  116. return $this->redirect(
  117. 'user.reset',
  118. [
  119. 'uid' => $uid,
  120. 'timestamp' => $timestamp,
  121. 'hash' => $hash,
  122. ]
  123. );
  124. }
  125. // A different user is already logged in on the computer.
  126. else {
  127. /** @var \Drupal\user\UserInterface $reset_link_user */
  128. if ($reset_link_user = $this->userStorage->load($uid)) {
  129. $this->messenger()
  130. ->addWarning($this->t('Another user (%other_user) is already logged into the site on this computer, but you tried to use a one-time link for user %resetting_user. Please <a href=":logout">log out</a> and try using the link again.',
  131. [
  132. '%other_user' => $account->getAccountName(),
  133. '%resetting_user' => $reset_link_user->getAccountName(),
  134. ':logout' => Url::fromRoute('user.logout')->toString(),
  135. ]));
  136. }
  137. else {
  138. // Invalid one-time link specifies an unknown user.
  139. $this->messenger()->addError($this->t('The one-time login link you clicked is invalid.'));
  140. }
  141. return $this->redirect('<front>');
  142. }
  143. }
  144. $session = $request->getSession();
  145. $session->set('pass_reset_hash', $hash);
  146. $session->set('pass_reset_timeout', $timestamp);
  147. return $this->redirect(
  148. 'user.reset.form',
  149. ['uid' => $uid]
  150. );
  151. }
  152. /**
  153. * Returns the user password reset form.
  154. *
  155. * @param \Symfony\Component\HttpFoundation\Request $request
  156. * The request.
  157. * @param int $uid
  158. * User ID of the user requesting reset.
  159. *
  160. * @return array|\Symfony\Component\HttpFoundation\RedirectResponse
  161. * The form structure or a redirect response.
  162. *
  163. * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
  164. * If the pass_reset_timeout or pass_reset_hash are not available in the
  165. * session. Or if $uid is for a blocked user or invalid user ID.
  166. */
  167. public function getResetPassForm(Request $request, $uid) {
  168. $session = $request->getSession();
  169. $timestamp = $session->get('pass_reset_timeout');
  170. $hash = $session->get('pass_reset_hash');
  171. // As soon as the session variables are used they are removed to prevent the
  172. // hash and timestamp from being leaked unexpectedly. This could occur if
  173. // the user does not click on the log in button on the form.
  174. $session->remove('pass_reset_timeout');
  175. $session->remove('pass_reset_hash');
  176. if (!$hash || !$timestamp) {
  177. throw new AccessDeniedHttpException();
  178. }
  179. /** @var \Drupal\user\UserInterface $user */
  180. $user = $this->userStorage->load($uid);
  181. if ($user === NULL || !$user->isActive()) {
  182. // Blocked or invalid user ID, so deny access. The parameters will be in
  183. // the watchdog's URL for the administrator to check.
  184. throw new AccessDeniedHttpException();
  185. }
  186. // Time out, in seconds, until login URL expires.
  187. $timeout = $this->config('user.settings')->get('password_reset_timeout');
  188. $expiration_date = $user->getLastLoginTime() ? $this->dateFormatter->format($timestamp + $timeout) : NULL;
  189. return $this->formBuilder()->getForm(UserPasswordResetForm::class, $user, $expiration_date, $timestamp, $hash);
  190. }
  191. /**
  192. * Validates user, hash, and timestamp; logs the user in if correct.
  193. *
  194. * @param int $uid
  195. * User ID of the user requesting reset.
  196. * @param int $timestamp
  197. * The current timestamp.
  198. * @param string $hash
  199. * Login link hash.
  200. *
  201. * @return \Symfony\Component\HttpFoundation\RedirectResponse
  202. * Returns a redirect to the user edit form if the information is correct.
  203. * If the information is incorrect redirects to 'user.pass' route with a
  204. * message for the user.
  205. *
  206. * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
  207. * If $uid is for a blocked user or invalid user ID.
  208. */
  209. public function resetPassLogin($uid, $timestamp, $hash) {
  210. // The current user is not logged in, so check the parameters.
  211. $current = REQUEST_TIME;
  212. /** @var \Drupal\user\UserInterface $user */
  213. $user = $this->userStorage->load($uid);
  214. // Verify that the user exists and is active.
  215. if ($user === NULL || !$user->isActive()) {
  216. // Blocked or invalid user ID, so deny access. The parameters will be in
  217. // the watchdog's URL for the administrator to check.
  218. throw new AccessDeniedHttpException();
  219. }
  220. // Time out, in seconds, until login URL expires.
  221. $timeout = $this->config('user.settings')->get('password_reset_timeout');
  222. // No time out for first time login.
  223. if ($user->getLastLoginTime() && $current - $timestamp > $timeout) {
  224. $this->messenger()->addError($this->t('You have tried to use a one-time login link that has expired. Please request a new one using the form below.'));
  225. return $this->redirect('user.pass');
  226. }
  227. elseif ($user->isAuthenticated() && ($timestamp >= $user->getLastLoginTime()) && ($timestamp <= $current) && hash_equals($hash, user_pass_rehash($user, $timestamp))) {
  228. user_login_finalize($user);
  229. $this->logger->notice('User %name used one-time login link at time %timestamp.', ['%name' => $user->getDisplayName(), '%timestamp' => $timestamp]);
  230. $this->messenger()->addStatus($this->t('You have just used your one-time login link. It is no longer necessary to use this link to log in. Please change your password.'));
  231. // Let the user's password be changed without the current password
  232. // check.
  233. $token = Crypt::randomBytesBase64(55);
  234. $_SESSION['pass_reset_' . $user->id()] = $token;
  235. // Clear any flood events for this user.
  236. $this->flood->clear('user.password_request_user', $uid);
  237. return $this->redirect(
  238. 'entity.user.edit_form',
  239. ['user' => $user->id()],
  240. [
  241. 'query' => ['pass-reset-token' => $token],
  242. 'absolute' => TRUE,
  243. ]
  244. );
  245. }
  246. $this->messenger()->addError($this->t('You have tried to use a one-time login link that has either been used or is no longer valid. Please request a new one using the form below.'));
  247. return $this->redirect('user.pass');
  248. }
  249. /**
  250. * Redirects users to their profile page.
  251. *
  252. * This controller assumes that it is only invoked for authenticated users.
  253. * This is enforced for the 'user.page' route with the '_user_is_logged_in'
  254. * requirement.
  255. *
  256. * @return \Symfony\Component\HttpFoundation\RedirectResponse
  257. * Returns a redirect to the profile of the currently logged in user.
  258. */
  259. public function userPage() {
  260. return $this->redirect('entity.user.canonical', ['user' => $this->currentUser()->id()]);
  261. }
  262. /**
  263. * Redirects users to their profile edit page.
  264. *
  265. * This controller assumes that it is only invoked for authenticated users.
  266. * This is enforced for the 'user.well-known.change_password' route with the
  267. * '_user_is_logged_in' requirement.
  268. *
  269. * @return \Symfony\Component\HttpFoundation\RedirectResponse
  270. * Returns a redirect to the profile edit form of the currently logged in
  271. * user.
  272. */
  273. public function userEditPage() {
  274. return $this->redirect('entity.user.edit_form', ['user' => $this->currentUser()->id()], [], 301);
  275. }
  276. /**
  277. * Route title callback.
  278. *
  279. * @param \Drupal\user\UserInterface $user
  280. * The user account.
  281. *
  282. * @return string|array
  283. * The user account name as a render array or an empty string if $user is
  284. * NULL.
  285. */
  286. public function userTitle(UserInterface $user = NULL) {
  287. return $user ? ['#markup' => $user->getDisplayName(), '#allowed_tags' => Xss::getHtmlTagList()] : '';
  288. }
  289. /**
  290. * Logs the current user out.
  291. *
  292. * @return \Symfony\Component\HttpFoundation\RedirectResponse
  293. * A redirection to home page.
  294. */
  295. public function logout() {
  296. user_logout();
  297. return $this->redirect('<front>');
  298. }
  299. /**
  300. * Confirms cancelling a user account via an email link.
  301. *
  302. * @param \Drupal\user\UserInterface $user
  303. * The user account.
  304. * @param int $timestamp
  305. * The timestamp.
  306. * @param string $hashed_pass
  307. * The hashed password.
  308. *
  309. * @return \Symfony\Component\HttpFoundation\RedirectResponse
  310. * A redirect response.
  311. */
  312. public function confirmCancel(UserInterface $user, $timestamp = 0, $hashed_pass = '') {
  313. // Time out in seconds until cancel URL expires; 24 hours = 86400 seconds.
  314. $timeout = 86400;
  315. $current = REQUEST_TIME;
  316. // Basic validation of arguments.
  317. $account_data = $this->userData->get('user', $user->id());
  318. if (isset($account_data['cancel_method']) && !empty($timestamp) && !empty($hashed_pass)) {
  319. // Validate expiration and hashed password/login.
  320. if ($timestamp <= $current && $current - $timestamp < $timeout && $user->id() && $timestamp >= $user->getLastLoginTime() && hash_equals($hashed_pass, user_pass_rehash($user, $timestamp))) {
  321. $edit = [
  322. 'user_cancel_notify' => isset($account_data['cancel_notify']) ? $account_data['cancel_notify'] : $this->config('user.settings')->get('notify.status_canceled'),
  323. ];
  324. user_cancel($edit, $user->id(), $account_data['cancel_method']);
  325. // Since user_cancel() is not invoked via Form API, batch processing
  326. // needs to be invoked manually and should redirect to the front page
  327. // after completion.
  328. return batch_process('<front>');
  329. }
  330. else {
  331. $this->messenger()->addError($this->t('You have tried to use an account cancellation link that has expired. Please request a new one using the form below.'));
  332. return $this->redirect('entity.user.cancel_form', ['user' => $user->id()], ['absolute' => TRUE]);
  333. }
  334. }
  335. throw new AccessDeniedHttpException();
  336. }
  337. }