diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc index 5579dbf..6acf24f 100644 --- a/core/includes/install.core.inc +++ b/core/includes/install.core.inc @@ -141,6 +141,9 @@ function install_drupal($class_loader, $settings = array()) { // installations can send output to the browser or redirect the user to the // next page. if ($state['interactive']) { + if ($session = \Drupal::request()->getSession()) { + $session->save(); + } if ($state['parameters_changed']) { // Redirect to the correct page if the URL parameters have changed. install_goto(install_redirect_url($state)); @@ -596,8 +599,9 @@ function install_run_task($task, &$install_state) { $url = Url::fromUri('base:install.php', ['query' => $install_state['parameters'], 'script' => '']); $response = batch_process($url, clone $url); if ($response instanceof Response) { - // Save $_SESSION data from batch. - \Drupal::service('session')->save(); + if ($session = \Drupal::request()->getSession()) { + $session->save(); + } // Send the response. $response->send(); exit; @@ -1553,8 +1557,10 @@ function install_load_profile(&$install_state) { * @param $install_state * An array of information about the current installation state. */ -function install_bootstrap_full() { - \Drupal::service('session')->start(); +function install_bootstrap_full(&$install_state) { + $session = \Drupal::service('session'); + \Drupal::request()->setSession($session); + $session->start(); } /** 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..999d4c4 100644 --- a/core/lib/Drupal/Core/Session/SessionHandler.php +++ b/core/lib/Drupal/Core/Session/SessionHandler.php @@ -10,7 +10,6 @@ use Drupal\Component\Utility\Crypt; use Drupal\Core\Database\Connection; use Drupal\Core\DependencyInjection\DependencySerializationTrait; -use Drupal\Core\Site\Settings; use Drupal\Core\Utility\Error; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Session\Storage\Proxy\AbstractProxy; @@ -37,13 +36,6 @@ class SessionHandler extends AbstractProxy implements \SessionHandlerInterface { protected $connection; /** - * An associative array of obsolete sessions with session id as key, and db-key as value. - * - * @var array - */ - protected $obsoleteSessionIds = array(); - - /** * Constructs a new SessionHandler instance. * * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack @@ -67,58 +59,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->requestStack->getCurrentRequest()->getSession()->get('uid', 0), 'hostname' => $this->requestStack->getCurrentRequest()->getClientIP(), 'session' => $value, 'timestamp' => REQUEST_TIME, @@ -127,13 +95,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 +120,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 +143,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 3c71930..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. @@ -195,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 { @@ -215,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; @@ -264,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..3ca4698 100644 --- a/core/modules/user/user.module +++ b/core/modules/user/user.module @@ -528,7 +528,7 @@ function user_login_finalize(UserInterface $account) { // fails or incorrectly does a redirect which would leave the old session // in place. \Drupal::service('session')->migrate(); - + \Drupal::service('session')->set('uid', $account->id()); \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']