diff --git a/core/core.services.yml b/core/core.services.yml index 589447d..dbb27fb 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -1200,6 +1200,8 @@ services: arguments: ['@private_key', '@cache.default'] current_user: class: Drupal\Core\Session\AccountProxy + tags: + - { name: needs_destruction } session_configuration: class: Drupal\Core\Session\SessionConfiguration arguments: ['%session.storage.options%'] @@ -1216,7 +1218,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..9bc26c7 100644 --- a/core/lib/Drupal/Core/Authentication/Provider/Cookie.php +++ b/core/lib/Drupal/Core/Authentication/Provider/Cookie.php @@ -7,7 +7,12 @@ namespace Drupal\Core\Authentication\Provider; +use Drupal\Component\Utility\Crypt; use Drupal\Core\Authentication\AuthenticationProviderInterface; +use Drupal\Core\Database\Connection; +use Drupal\Core\Session\AnonymousUserSession; +use Drupal\Core\Session\UserSession; +use Drupal\Core\Site\Settings; use Drupal\Core\Session\SessionConfigurationInterface; use Symfony\Component\HttpFoundation\Request; @@ -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,12 +59,42 @@ 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()) { + // Handle the case of first time visitors and clients that don't store + // cookies (eg. web crawlers). + if (!$this->sessionConfiguration->hasSession($request)) { + return new AnonymousUserSession(); + } + + $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($session->getId()), + ))->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(DRUPAL_AUTHENTICATED_RID), $rids); + $user = new UserSession($values); + } + elseif ($values) { + // The user is anonymous or blocked. Only preserve two fields from the + // {sessions} table. + $user = new UserSession(array( + 'session' => $values['session'], + 'access' => $values['access'], + )); + } + else { + // The session has expired. + $user = new AnonymousUserSession(); + } + return $user; + } return NULL; } diff --git a/core/lib/Drupal/Core/Session/AccountProxy.php b/core/lib/Drupal/Core/Session/AccountProxy.php index eae71e7..bc36c46 100644 --- a/core/lib/Drupal/Core/Session/AccountProxy.php +++ b/core/lib/Drupal/Core/Session/AccountProxy.php @@ -7,6 +7,9 @@ namespace Drupal\Core\Session; +use Drupal\Core\DestructableInterface; +use Drupal\Core\Site\Settings; + /** * A proxied implementation of AccountInterface. * @@ -18,7 +21,7 @@ * allows legacy code to change the current user where the user cannot be * directly injected into dependent code. */ -class AccountProxy implements AccountProxyInterface { +class AccountProxy implements AccountProxyInterface, DestructableInterface { /** * The instantiated account. @@ -183,6 +186,18 @@ public function setInitialAccountId($account_id) { } /** + * {@inheritdoc} + */ + public function destruct() { + // Update last access time (no more than once per 180 seconds). + if ($this->account->isAuthenticated() && REQUEST_TIME - $this->account->getLastAccessedTime() > Settings::get('session_write_interval', 180)) { + /** @var \Drupal\user\UserStorageInterface $storage */ + $storage = \Drupal::entityManager()->getStorage('user'); + $storage->updateLastAccessTimestamp($this->account, REQUEST_TIME); + } + } + + /** * Load a user entity. * * The entity manager requires additional initialization code and cache diff --git a/core/lib/Drupal/Core/Session/SessionHandler.php b/core/lib/Drupal/Core/Session/SessionHandler.php index 8d9aa96..05b5887 100644 --- a/core/lib/Drupal/Core/Session/SessionHandler.php +++ b/core/lib/Drupal/Core/Session/SessionHandler.php @@ -34,11 +34,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. @@ -47,10 +47,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; } /** @@ -64,58 +67,30 @@ 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(); - return ''; - } + // Read the session data from the database. + $record = $this->connection->select('sessions', 's') + ->fields('s') + ->condition('sid', Crypt::hashBase64($sid)) + ->execute() + ->fetchAssoc(); - $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'], - )); + 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, @@ -124,13 +99,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) { @@ -156,8 +124,6 @@ public function close() { * {@inheritdoc} */ public function destroy($sid) { - - // Delete session data. $this->connection->delete('sessions') ->condition('sid', Crypt::hashBase64($sid)) @@ -166,8 +132,12 @@ public function destroy($sid) { // 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()); + // @todo: Manipulating the current user is not the business of the session + // handler. This should be moved to the authentication mananger. + $this->currentUser->setAccount(new AnonymousUserSession()); + + // @todo: cookie management should be moved to Cookie auth manager. // Unset the session cookies. $this->deleteCookie($this->getName()); @@ -194,6 +164,8 @@ public function gc($lifetime) { * * @param string $name * Name of session cookie to delete. + * + * @todo: cookie management should be moved to Cookie auth manager. */ protected function deleteCookie($name) { $cookies = $this->requestStack->getCurrentRequest()->cookies; diff --git a/core/lib/Drupal/Core/Session/SessionManager.php b/core/lib/Drupal/Core/Session/SessionManager.php index 7c3ac21..2c6f09a 100644 --- a/core/lib/Drupal/Core/Session/SessionManager.php +++ b/core/lib/Drupal/Core/Session/SessionManager.php @@ -121,10 +121,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. @@ -187,7 +183,7 @@ public function save() { return; } - if ($user->isAnonymous() && $this->isSessionObsolete()) { + if (!$user || ($user->isAnonymous() && $this->isSessionObsolete())) { // There is no session data to store, destroy the session if it was // previously started. if ($this->getSaveHandler()->isActive()) { @@ -211,8 +207,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; diff --git a/core/modules/user/user.module b/core/modules/user/user.module index 2b08e73..d95061f 100644 --- a/core/modules/user/user.module +++ b/core/modules/user/user.module @@ -568,8 +568,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); @@ -581,7 +579,9 @@ 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::logger('user')->notice('Session opened for %name.', array('%name' => $account->getUsername())); \Drupal::moduleHandler()->invokeAll('user_login', array($account)); } diff --git a/core/modules/user/user.services.yml b/core/modules/user/user.services.yml index b520772..792eec0 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: