diff --git a/core/core.services.yml b/core/core.services.yml index f2ae071..2bd2066 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -1230,7 +1230,7 @@ services: alias: session_handler.storage session_handler.storage: class: Drupal\Core\Session\SessionHandler - arguments: ['@request_stack', '@database'] + arguments: ['@request_stack', '@database', '@current_user'] tags: - { name: backend_overridable } session_handler.write_check: diff --git a/core/lib/Drupal/Core/Authentication/Provider/Cookie.php b/core/lib/Drupal/Core/Authentication/Provider/Cookie.php index 7db5d9b..eaf457a 100644 --- a/core/lib/Drupal/Core/Authentication/Provider/Cookie.php +++ b/core/lib/Drupal/Core/Authentication/Provider/Cookie.php @@ -8,8 +8,13 @@ namespace Drupal\Core\Authentication\Provider; use Drupal\Core\Authentication\AuthenticationProviderInterface; +use Drupal\Core\Database\Connection; +use Drupal\Core\Session\AccountInterface; +use Drupal\Core\Session\AnonymousUserSession; +use Drupal\Core\Session\UserSession; use Drupal\Core\Session\SessionConfigurationInterface; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Session\SessionInterface; /** * Cookie based authentication provider. @@ -24,13 +29,23 @@ class Cookie implements AuthenticationProviderInterface { protected $sessionConfiguration; /** + * The database connection. + * + * @var \Drupal\Core\Database\Connection + */ + protected $connection; + + /** * Constructs a new cookie authentication provider. * * @param \Drupal\Core\Session\SessionConfigurationInterface $session_configuration * The session configuration. + * @param \Drupal\Core\Database\Connection $connection + * The database connection. */ - public function __construct(SessionConfigurationInterface $session_configuration) { + public function __construct(SessionConfigurationInterface $session_configuration, Connection $connection) { $this->sessionConfiguration = $session_configuration; + $this->connection = $connection; } /** @@ -44,13 +59,56 @@ public function applies(Request $request) { * {@inheritdoc} */ public function authenticate(Request $request) { - if ($request->getSession()->start()) { - // @todo Remove global in https://www.drupal.org/node/2228393 - global $_session_user; - return $_session_user; + $session = $request->getSession(); + if ($session->start()) { + return $this->getUserFromSession($session); } - return NULL; } + /** + * Returns the user entity for the session. + * + * @param \Symfony\Component\HttpFoundation\Session\SessionInterface $session + * The session. + * + * @return \Drupal\Core\Session\AccountInterface + * The current user object. + */ + protected function getUserFromSession(SessionInterface $session) { + $values = $this->connection->select('users_field_data', 'u') + ->fields('u') + ->condition('default_langcode', 1) + ->condition('uid', $session->get('uid')) + ->execute() + ->fetchAssoc(); + + if ($values && $values['uid'] > 0 && $values['status'] == 1) { + // UserSession::getLastAccessedTime() returns session save timestamp, + // while User::getLastAccessedTime() returns the user 'access' timestamp. + // This ensures they are synchronized. + $values['timestamp'] = $values['access']; + + // We found the client's session record and they are an authenticated, + // active user. + $rids = $this->connection->select('user__roles', 'ur') + ->fields('ur', ['rid' => 'roles_target_id']) + ->condition('entity_id', $values['uid']) + ->execute() + ->fetchCol(); + // Add user's roles. + $values['roles'] = array_merge([AccountInterface::AUTHENTICATED_ROLE], $rids); + return new UserSession($values); + } + elseif ($values) { + // The user is anonymous or blocked. Only preserve access field from the + // {users} table. + return new UserSession([ + 'access' => $values['access'], + ]); + } + // The session has expired. + return new AnonymousUserSession(); + } + } diff --git a/core/lib/Drupal/Core/Session/SessionHandler.php b/core/lib/Drupal/Core/Session/SessionHandler.php index 92f6741..35c1e71 100644 --- a/core/lib/Drupal/Core/Session/SessionHandler.php +++ b/core/lib/Drupal/Core/Session/SessionHandler.php @@ -37,11 +37,11 @@ class SessionHandler extends AbstractProxy implements \SessionHandlerInterface { protected $connection; /** - * An associative array of obsolete sessions with session id as key, and db-key as value. + * The current user account. * - * @var array + * @var \Drupal\Core\Session\AccountInterface */ - protected $obsoleteSessionIds = array(); + protected $currentUser; /** * Constructs a new SessionHandler instance. @@ -50,10 +50,13 @@ class SessionHandler extends AbstractProxy implements \SessionHandlerInterface { * The request stack. * @param \Drupal\Core\Database\Connection $connection * The database connection. + * @param \Drupal\Core\Session\AccountInterface $current_user + * The current user account. */ - public function __construct(RequestStack $request_stack, Connection $connection) { + public function __construct(RequestStack $request_stack, Connection $connection, AccountInterface $current_user) { $this->requestStack = $request_stack; $this->connection = $connection; + $this->currentUser = $current_user; } /** @@ -67,58 +70,34 @@ public function open($save_path, $name) { * {@inheritdoc} */ public function read($sid) { - // @todo Remove global in https://www.drupal.org/node/2228393 - global $_session_user; - - // Handle the case of first time visitors and clients that don't store - // cookies (eg. web crawlers). - $cookies = $this->requestStack->getCurrentRequest()->cookies; - if (empty($sid) || !$cookies->has($this->getName())) { - $_session_user = new UserSession(); + if (empty($sid)) { return ''; } - $values = $this->connection->query("SELECT u.*, s.* FROM {users_field_data} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE u.default_langcode = 1 AND s.sid = :sid", array( - ':sid' => Crypt::hashBase64($sid), - ))->fetchAssoc(); - - // We found the client's session record and they are an authenticated, - // active user. - if ($values && $values['uid'] > 0 && $values['status'] == 1) { - // Add roles element to $user. - $rids = $this->connection->query("SELECT ur.roles_target_id as rid FROM {user__roles} ur WHERE ur.entity_id = :uid", array( - ':uid' => $values['uid'], - ))->fetchCol(); - $values['roles'] = array_merge(array(AccountInterface::AUTHENTICATED_ROLE), $rids); - $_session_user = new UserSession($values); - } - elseif ($values) { - // The user is anonymous or blocked. Only preserve two fields from the - // {sessions} table. - $_session_user = new UserSession(array( - 'session' => $values['session'], - 'access' => $values['access'], - )); + // Read the session data from the database. + $record = $this->connection->select('sessions', 's') + ->fields('s') + ->condition('sid', Crypt::hashBase64($sid)) + ->execute() + ->fetchAssoc(); + + if (isset($record['session'])) { + return $record['session']; } else { - // The session has expired. - $_session_user = new UserSession(); + return ''; } - - return $_session_user->session; } /** * {@inheritdoc} */ public function write($sid, $value) { - $user = \Drupal::currentUser(); - // The exception handler is not active at this point, so we need to do it // manually. try { $fields = array( - 'uid' => $user->id(), + 'uid' => $this->currentUser->id(), 'hostname' => $this->requestStack->getCurrentRequest()->getClientIP(), 'session' => $value, 'timestamp' => REQUEST_TIME, @@ -127,13 +106,6 @@ public function write($sid, $value) { ->keys(array('sid' => Crypt::hashBase64($sid))) ->fields($fields) ->execute(); - - // Likewise, do not update access time more than once per 180 seconds. - if ($user->isAuthenticated() && REQUEST_TIME - $user->getLastAccessedTime() > Settings::get('session_write_interval', 180)) { - /** @var \Drupal\user\UserStorageInterface $storage */ - $storage = \Drupal::entityManager()->getStorage('user'); - $storage->updateLastAccessTimestamp($user, REQUEST_TIME); - } return TRUE; } catch (\Exception $exception) { @@ -159,21 +131,11 @@ public function close() { * {@inheritdoc} */ public function destroy($sid) { - - // Delete session data. $this->connection->delete('sessions') ->condition('sid', Crypt::hashBase64($sid)) ->execute(); - // Reset $_SESSION and current user to prevent a new session from being - // started in \Drupal\Core\Session\SessionManager::save(). - $_SESSION = array(); - \Drupal::currentUser()->setAccount(new AnonymousUserSession()); - - // Unset the session cookies. - $this->deleteCookie($this->getName()); - return TRUE; } @@ -192,19 +154,4 @@ public function gc($lifetime) { return TRUE; } - /** - * Deletes a session cookie. - * - * @param string $name - * Name of session cookie to delete. - */ - protected function deleteCookie($name) { - $cookies = $this->requestStack->getCurrentRequest()->cookies; - if ($cookies->has($name)) { - $params = session_get_cookie_params(); - setcookie($name, '', REQUEST_TIME - 3600, $params['path'], $params['domain'], $params['secure'], $params['httponly']); - $cookies->remove($name); - } - } - } diff --git a/core/lib/Drupal/Core/Session/SessionManager.php b/core/lib/Drupal/Core/Session/SessionManager.php index 52b3542..3257e39 100644 --- a/core/lib/Drupal/Core/Session/SessionManager.php +++ b/core/lib/Drupal/Core/Session/SessionManager.php @@ -124,10 +124,6 @@ public function start() { } if (empty($result)) { - // @todo Remove global in https://www.drupal.org/node/2228393 - global $_session_user; - $_session_user = new AnonymousUserSession(); - // Randomly generate a session identifier for this request. This is // necessary because \Drupal\user\SharedTempStoreFactory::get() wants to // know the future session ID of a lazily started session in advance. @@ -174,6 +170,7 @@ protected function startNow() { // Restore session data. if ($this->startedLazy) { $_SESSION = $session_data; + $this->loadSession(); } return $result; @@ -194,7 +191,7 @@ public function save() { // There is no session data to store, destroy the session if it was // previously started. if ($this->getSaveHandler()->isActive()) { - session_destroy(); + $this->destroy(); } } else { @@ -214,8 +211,6 @@ public function save() { * {@inheritdoc} */ public function regenerate($destroy = FALSE, $lifetime = NULL) { - $user = \Drupal::currentUser(); - // Nothing to do if we are not allowed to change the session. if ($this->isCli()) { return; @@ -263,6 +258,22 @@ public function delete($uid) { /** * {@inheritdoc} */ + public function destroy() { + session_destroy(); + + // Unset the session cookies. + $session_name = $this->getName(); + $cookies = $this->requestStack->getCurrentRequest()->cookies; + if ($cookies->has($session_name)) { + $params = session_get_cookie_params(); + setcookie($session_name, '', REQUEST_TIME - 3600, $params['path'], $params['domain'], $params['secure'], $params['httponly']); + $cookies->remove($session_name); + } + } + + /** + * {@inheritdoc} + */ public function setWriteSafeHandler(WriteSafeSessionHandlerInterface $handler) { $this->writeSafeHandler = $handler; } diff --git a/core/lib/Drupal/Core/Session/SessionManagerInterface.php b/core/lib/Drupal/Core/Session/SessionManagerInterface.php index d194002..c755687 100644 --- a/core/lib/Drupal/Core/Session/SessionManagerInterface.php +++ b/core/lib/Drupal/Core/Session/SessionManagerInterface.php @@ -23,6 +23,11 @@ public function delete($uid); /** + * Destroys the current session and removes session cookies. + */ + public function destroy(); + + /** * Sets the write safe session handler. * * @todo: This should be removed once all database queries are removed from diff --git a/core/modules/user/src/EventSubscriber/UserRequestSubscriber.php b/core/modules/user/src/EventSubscriber/UserRequestSubscriber.php new file mode 100644 index 0000000..520c727 --- /dev/null +++ b/core/modules/user/src/EventSubscriber/UserRequestSubscriber.php @@ -0,0 +1,73 @@ +account = $account; + $this->entityManager = $entity_manager; + } + + /** + * Updates the current user's last access time. + * + * @param \Symfony\Component\HttpKernel\Event\PostResponseEvent $event + * The event to process. + */ + public function onKernelTerminate(PostResponseEvent $event) { + if ($this->account->isAuthenticated() && REQUEST_TIME - $this->account->getLastAccessedTime() > Settings::get('session_write_interval', 180)) { + // Do that no more than once per 180 seconds. + /** @var \Drupal\user\UserStorageInterface $storage */ + $storage = $this->entityManager->getStorage('user'); + $storage->updateLastAccessTimestamp($this->account, REQUEST_TIME); + } + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + // Should go before other subscribers start to write their caches. + $events[KernelEvents::TERMINATE][] = ['onKernelTerminate', 300]; + return $events; + } + +} diff --git a/core/modules/user/user.module b/core/modules/user/user.module index 5bcea7c..c730a92 100644 --- a/core/modules/user/user.module +++ b/core/modules/user/user.module @@ -514,8 +514,6 @@ function template_preprocess_username(&$variables) { * @see hook_user_login() */ function user_login_finalize(UserInterface $account) { - \Drupal::currentUser()->setAccount($account); - \Drupal::logger('user')->notice('Session opened for %name.', array('%name' => $account->getUsername())); // Update the user table timestamp noting user has logged in. // This is also used to invalidate one-time login links. $account->setLastLoginTime(REQUEST_TIME); @@ -527,8 +525,10 @@ function user_login_finalize(UserInterface $account) { // This is called before hook_user_login() in case one of those functions // fails or incorrectly does a redirect which would leave the old session // in place. + \Drupal::currentUser()->setAccount($account); \Drupal::service('session')->migrate(); - + \Drupal::service('session')->set('uid', $account->id()); + \Drupal::logger('user')->notice('Session opened for %name.', array('%name' => $account->getUsername())); \Drupal::moduleHandler()->invokeAll('user_login', array($account)); } @@ -1386,7 +1386,8 @@ function user_logout() { // Session::invalidate(). Regrettably this method is currently broken and may // lead to the creation of spurious session records in the database. // @see https://github.com/symfony/symfony/issues/12375 - session_destroy(); + \Drupal::service('session_manager')->destroy(); + $user->setAccount(new AnonymousUserSession()); } /** diff --git a/core/modules/user/user.services.yml b/core/modules/user/user.services.yml index 0cf12ef..5a5bb61 100644 --- a/core/modules/user/user.services.yml +++ b/core/modules/user/user.services.yml @@ -17,7 +17,7 @@ services: - { name: access_check, applies_to: _user_is_logged_in } authentication.cookie: class: Drupal\Core\Authentication\Provider\Cookie - arguments: ['@session_configuration'] + arguments: ['@session_configuration', '@database'] tags: - { name: authentication_provider, priority: 0 } user.data: @@ -35,6 +35,11 @@ services: arguments: ['@current_user', '@url_generator'] tags: - { name: event_subscriber } + user_last_access_subscriber: + class: Drupal\user\EventSubscriber\UserRequestSubscriber + arguments: ['@current_user', '@entity.manager'] + tags: + - { name: event_subscriber } theme.negotiator.admin_theme: class: Drupal\user\Theme\AdminNegotiator arguments: ['@current_user', '@config.factory', '@entity.manager', '@router.admin_context']