annotate core/modules/user/src/Controller/UserController.php @ 19:fa3358dc1485 tip

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