From 12abe621e1aecb4690b93fa33253cb153aebc70c Mon Sep 17 00:00:00 2001 From: Alex Pott Date: Thu, 10 Apr 2014 13:30:54 -0400 Subject: [PATCH] Issue #2228341 by sun, znerol, skipyT, ParisLiakos: Objectify session management functions + remove session.inc. --- core/authorize.php | 3 +- core/core.services.yml | 8 +- core/includes/bootstrap.inc | 18 +- core/includes/common.inc | 2 +- core/includes/install.core.inc | 6 +- core/includes/session.inc | 241 -------------- core/lib/Drupal.php | 2 +- .../Drupal/Core/Access/CsrfTokenGenerator.php | 2 +- .../Core/Authentication/Provider/Cookie.php | 24 +- core/lib/Drupal/Core/Cron.php | 21 +- .../Drupal/Core/Session/SessionHandler.php | 51 +-- .../Drupal/Core/Session/SessionManager.php | 300 ++++++++++++++++++ .../Core/Session/SessionManagerInterface.php | 80 +++++ .../Drupal/simpletest/DrupalUnitTestBase.php | 4 + .../lib/Drupal/simpletest/TestBase.php | 5 +- .../lib/Drupal/simpletest/WebTestBase.php | 4 +- core/modules/system/core.api.php | 1 - .../system/Tests/Common/FormatDateTest.php | 4 +- .../Tests/Datetime/DrupalDateTimeTest.php | 4 +- .../system/Tests/Session/SessionTest.php | 11 +- .../Controller/SessionTestController.php | 8 +- .../user/lib/Drupal/user/Entity/User.php | 7 +- .../user/Tests/Views/ArgumentDefaultTest.php | 4 +- core/modules/user/user.module | 4 +- core/update.php | 5 +- 25 files changed, 502 insertions(+), 317 deletions(-) delete mode 100644 core/includes/session.inc create mode 100644 core/lib/Drupal/Core/Session/SessionManager.php create mode 100644 core/lib/Drupal/Core/Session/SessionManagerInterface.php diff --git a/core/authorize.php b/core/authorize.php index 97fbe24df9..108bda1f4e 100644 --- a/core/authorize.php +++ b/core/authorize.php @@ -46,8 +46,7 @@ * TRUE if the current user can run authorize.php, and FALSE if not. */ function authorize_access_allowed() { - require_once DRUPAL_ROOT . '/' . Settings::get('session_inc', 'core/includes/session.inc'); - drupal_session_initialize(); + \Drupal::service('session_manager')->initialize(); return Settings::get('allow_authorize_operations', TRUE) && user_access('administer software updates'); } diff --git a/core/core.services.yml b/core/core.services.yml index fe8d80d1ea..cb3a2e065b 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -102,7 +102,7 @@ services: arguments: ['@config.storage', '@config.storage.schema', '@cache.config'] cron: class: Drupal\Core\Cron - arguments: ['@module_handler', '@lock', '@queue', '@state', '@current_user'] + arguments: ['@module_handler', '@lock', '@queue', '@state', '@current_user', '@session_manager'] database: class: Drupal\Core\Database\Connection factory_class: Drupal\Core\Database\Database @@ -712,6 +712,7 @@ services: class: Drupal\Core\Authentication\AuthenticationManager authentication.cookie: class: Drupal\Core\Authentication\Provider\Cookie + arguments: ['@session_manager'] tags: - { name: authentication_provider, priority: 0 } authentication_subscriber: @@ -722,6 +723,11 @@ services: current_user: class: Drupal\Core\Session\AccountProxy arguments: ['@authentication', '@request'] + session_manager: + class: Drupal\Core\Session\SessionManager + arguments: ['@request_stack', '@database'] + tags: + - { name: persist } asset.css.collection_renderer: class: Drupal\Core\Asset\CssCollectionRenderer arguments: [ '@state' ] diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc index 36a0627af8..1fe4e42485 100644 --- a/core/includes/bootstrap.inc +++ b/core/includes/bootstrap.inc @@ -1466,7 +1466,9 @@ function drupal_handle_request($test_only = FALSE) { // Create a request object from the HttpFoundation. $request = Request::createFromGlobals(); - \Drupal::getContainer()->set('request', $request); + $container = \Drupal::getContainer(); + $container->set('request', $request); + $container->get('request_stack')->push($request); drupal_bootstrap(DRUPAL_BOOTSTRAP_CODE); @@ -1616,7 +1618,9 @@ function _drupal_bootstrap_kernel() { $kernel = new DrupalKernel('prod', drupal_classloader()); $kernel->boot(); $request = Request::createFromGlobals(); - \Drupal::getContainer()->set('request', $request); + $container = \Drupal::getContainer(); + $container->set('request', $request); + $container->get('request_stack')->push($request); } } @@ -2173,12 +2177,12 @@ function drupal_classloader_register($name, $path) { * * Example: * @code - * function drupal_session_started($set = NULL) { - * static $session_started = FALSE; - * if (isset($set)) { - * $session_started = $set; + * function mymodule_log_stream_handle($new_handle = NULL) { + * static $handle; + * if (isset($new_handle)) { + * $handle = $new_handle; * } - * return $session_started && session_id(); + * return $handle; * } * @endcode * diff --git a/core/includes/common.inc b/core/includes/common.inc index 658c1b6272..1f72423f78 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -2877,7 +2877,7 @@ function drupal_get_private_key() { * * @see drupal_get_hash_salt() * @see \Drupal\Core\Access\CsrfTokenGenerator - * @see drupal_session_start() + * @see \Drupal\Core\Session\SessionManager::start() * * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0. * Use \Drupal::csrfToken()->get(). diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc index aa158a2823..a0a6038801 100644 --- a/core/includes/install.core.inc +++ b/core/includes/install.core.inc @@ -306,7 +306,6 @@ function install_begin_request(&$install_state) { // Load module basics (needed for hook invokes). include_once __DIR__ . '/module.inc'; - include_once __DIR__ . '/session.inc'; require_once __DIR__ . '/entity.inc'; // Create a minimal mocked container to support calls to t() in the pre-kernel @@ -546,7 +545,7 @@ function install_run_task($task, &$install_state) { $response = batch_process(install_redirect_url($install_state), install_full_redirect_url($install_state)); if ($response instanceof Response) { // Save $_SESSION data from batch. - drupal_session_commit(); + \Drupal::service('session_manager')->save(); // Send the response. $response->send(); exit; @@ -1780,8 +1779,7 @@ function install_load_profile(&$install_state) { */ function install_bootstrap_full() { drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL); - require_once DRUPAL_ROOT . '/' . Settings::get('session_inc', 'core/includes/session.inc'); - drupal_session_initialize(); + \Drupal::service('session_manager')->initialize(); } /** diff --git a/core/includes/session.inc b/core/includes/session.inc deleted file mode 100644 index 76aa83b024..0000000000 --- a/core/includes/session.inc +++ /dev/null @@ -1,241 +0,0 @@ -isSecure(); - $cookies = \Drupal::request()->cookies; - if (($cookies->has(session_name()) && ($session_name = $cookies->get(session_name()))) || ($is_https && Settings::get('mixed_mode_sessions', FALSE) && ($cookies->has(substr(session_name(), 1))) && ($session_name = $cookies->get(substr(session_name(), 1))))) { - // If a session cookie exists, initialize the session. Otherwise the - // session is only started on demand in drupal_session_commit(), making - // anonymous users not use a session cookie unless something is stored in - // $_SESSION. This allows HTTP proxies to cache anonymous pageviews. - drupal_session_start(); - if ($user->isAuthenticated() || !empty($_SESSION)) { - drupal_page_is_cacheable(FALSE); - } - } - else { - // Set a session identifier for this request. This is necessary because - // we lazily start sessions at the end of this request, and some - // processes (like drupal_get_token()) needs to know the future - // session ID in advance. - $GLOBALS['lazy_session'] = TRUE; - $user = new AnonymousUserSession(); - // Less random sessions (which are much faster to generate) are used for - // anonymous users than are generated in drupal_session_regenerate() when - // a user becomes authenticated. - session_id(Crypt::randomBytesBase64()); - if ($is_https && Settings::get('mixed_mode_sessions', FALSE)) { - $insecure_session_name = substr(session_name(), 1); - $session_id = Crypt::randomBytesBase64(); - $cookies->set($insecure_session_name, $session_id); - } - } - date_default_timezone_set(drupal_get_user_timezone()); -} - -/** - * Starts a session forcefully, preserving already set session data. - * - * @ingroup php_wrappers - */ -function drupal_session_start() { - // Command line clients do not support cookies nor sessions. - if (!drupal_session_started() && !drupal_is_cli()) { - // Save current session data before starting it, as PHP will destroy it. - $session_data = isset($_SESSION) ? $_SESSION : NULL; - - session_start(); - drupal_session_started(TRUE); - - // Restore session data. - if (!empty($session_data)) { - $_SESSION += $session_data; - } - } -} - -/** - * Commits the current session, if necessary. - * - * If an anonymous user already have an empty session, destroy it. - */ -function drupal_session_commit() { - global $user; - - if (!drupal_save_session()) { - // We don't have anything to do if we are not allowed to save the session. - return; - } - - if ($user->isAnonymous() && empty($_SESSION)) { - // There is no session data to store, destroy the session if it was - // previously started. - if (drupal_session_started()) { - session_destroy(); - } - } - else { - // There is session data to store. Start the session if it is not already - // started. - if (!drupal_session_started()) { - drupal_session_start(); - if (\Drupal::request()->isSecure() && Settings::get('mixed_mode_sessions', FALSE)) { - $insecure_session_name = substr(session_name(), 1); - $params = session_get_cookie_params(); - $expire = $params['lifetime'] ? REQUEST_TIME + $params['lifetime'] : 0; - $cookie_params = \Drupal::request()->cookies; - setcookie($insecure_session_name, $cookie_params->get($insecure_session_name), $expire, $params['path'], $params['domain'], FALSE, $params['httponly']); - } - } - // Write the session data. - session_write_close(); - } -} - -/** - * Returns whether a session has been started. - */ -function drupal_session_started($set = NULL) { - static $session_started = FALSE; - if (isset($set)) { - $session_started = $set; - } - return $session_started && session_status() === \PHP_SESSION_ACTIVE; -} - -/** - * Called when an anonymous user becomes authenticated or vice-versa. - * - * @ingroup php_wrappers - */ -function drupal_session_regenerate() { - global $user; - - // Nothing to do if we are not allowed to change the session. - if (!drupal_save_session()) { - return; - } - - $is_https = \Drupal::request()->isSecure(); - $cookies = \Drupal::request()->cookies; - - if ($is_https && Settings::get('mixed_mode_sessions', FALSE)) { - $insecure_session_name = substr(session_name(), 1); - if (!isset($GLOBALS['lazy_session']) && $cookies->has($insecure_session_name)) { - $old_insecure_session_id = $cookies->get($insecure_session_name); - } - $params = session_get_cookie_params(); - $session_id = Crypt::randomBytesBase64(); - // If a session cookie lifetime is set, the session will expire - // $params['lifetime'] seconds from the current request. If it is not set, - // it will expire when the browser is closed. - $expire = $params['lifetime'] ? REQUEST_TIME + $params['lifetime'] : 0; - setcookie($insecure_session_name, $session_id, $expire, $params['path'], $params['domain'], FALSE, $params['httponly']); - $cookies->set($insecure_session_name, $session_id); - } - - if (drupal_session_started()) { - $old_session_id = session_id(); - } - session_id(Crypt::randomBytesBase64()); - - if (isset($old_session_id)) { - $params = session_get_cookie_params(); - $expire = $params['lifetime'] ? REQUEST_TIME + $params['lifetime'] : 0; - setcookie(session_name(), session_id(), $expire, $params['path'], $params['domain'], $params['secure'], $params['httponly']); - $fields = array('sid' => Crypt::hashBase64(session_id())); - if ($is_https) { - $fields['ssid'] = Crypt::hashBase64(session_id()); - // If the "secure pages" setting is enabled, use the newly-created - // insecure session identifier as the regenerated sid. - if (Settings::get('mixed_mode_sessions', FALSE)) { - $fields['sid'] = Crypt::hashBase64($session_id); - } - } - db_update('sessions') - ->fields($fields) - ->condition($is_https ? 'ssid' : 'sid', Crypt::hashBase64($old_session_id)) - ->execute(); - } - elseif (isset($old_insecure_session_id)) { - // If logging in to the secure site, and there was no active session on the - // secure site but a session was active on the insecure site, update the - // insecure session with the new session identifiers. - db_update('sessions') - ->fields(array('sid' => Crypt::hashBase64($session_id), 'ssid' => Crypt::hashBase64(session_id()))) - ->condition('sid', Crypt::hashBase64($old_insecure_session_id)) - ->execute(); - } - else { - // Start the session when it doesn't exist yet. - // Preserve the logged in user, as it will be reset to anonymous - // by _drupal_session_read. - $account = $user; - drupal_session_start(); - $user = $account; - } - date_default_timezone_set(drupal_get_user_timezone()); -} - -/** - * Ends a specific user's session(s). - * - * @param $uid - * User ID. - */ -function drupal_session_destroy_uid($uid) { - // Nothing to do if we are not allowed to change the session. - if (!drupal_save_session()) { - return; - } - - db_delete('sessions') - ->condition('uid', $uid) - ->execute(); -} - -/** - * Determines whether to save session data of the current request. - * - * This function allows the caller to temporarily disable writing of - * session data, should the request end while performing potentially - * dangerous operations, such as manipulating the global $user object. - * See http://drupal.org/node/218104 for usage. - * - * @param $status - * Disables writing of session data when FALSE, (re-)enables - * writing when TRUE. - * - * @return - * FALSE if writing session data has been disabled. Otherwise, TRUE. - */ -function drupal_save_session($status = NULL) { - // PHP session ID, session, and cookie handling happens in the global scope. - // This value has to persist across calls to drupal_static_reset(), since a - // potentially wrong or disallowed session would be written otherwise. - static $save_session = TRUE; - if (isset($status)) { - $save_session = $status; - } - return $save_session; -} diff --git a/core/lib/Drupal.php b/core/lib/Drupal.php index c4622197b8..faeb0818c5 100644 --- a/core/lib/Drupal.php +++ b/core/lib/Drupal.php @@ -586,7 +586,7 @@ public static function languageManager() { * @return \Drupal\Core\Access\CsrfTokenGenerator * The CSRF token manager. * - * @see drupal_session_start() + * @see \Drupal\Core\Session\SessionManager::start() */ public static function csrfToken() { return static::$container->get('csrf_token'); diff --git a/core/lib/Drupal/Core/Access/CsrfTokenGenerator.php b/core/lib/Drupal/Core/Access/CsrfTokenGenerator.php index 527fffda38..2e4e44d9cb 100644 --- a/core/lib/Drupal/Core/Access/CsrfTokenGenerator.php +++ b/core/lib/Drupal/Core/Access/CsrfTokenGenerator.php @@ -69,7 +69,7 @@ public function setCurrentUser(AccountInterface $current_user = NULL) { * 'drupal_private_key' configuration variable. * * @see drupal_get_hash_salt() - * @see drupal_session_start() + * @see \Drupal\Core\Session\SessionManager::start() */ public function get($value = '') { return Crypt::hmacBase64($value, session_id() . $this->privateKey->get() . drupal_get_hash_salt()); diff --git a/core/lib/Drupal/Core/Authentication/Provider/Cookie.php b/core/lib/Drupal/Core/Authentication/Provider/Cookie.php index 1a7b256fcc..379d97e3a0 100644 --- a/core/lib/Drupal/Core/Authentication/Provider/Cookie.php +++ b/core/lib/Drupal/Core/Authentication/Provider/Cookie.php @@ -8,6 +8,7 @@ namespace Drupal\Core\Authentication\Provider; use Drupal\Core\Authentication\AuthenticationProviderInterface; +use Drupal\Core\Session\SessionManagerInterface; use Drupal\Component\Utility\Settings; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent; @@ -17,6 +18,23 @@ */ class Cookie implements AuthenticationProviderInterface { + /** + * The session manager. + * + * @var \Drupal\Core\Session\SessionManagerInterface + */ + protected $sessionManager; + + /** + * Constructs a new Cookie authentication provider instance. + * + * @param \Drupal\Core\Session\SessionManagerInterface $session_manager + * The session manager. + */ + public function __construct(SessionManagerInterface $session_manager) { + $this->sessionManager = $session_manager; + } + /** * {@inheritdoc} */ @@ -30,9 +48,7 @@ public function applies(Request $request) { public function authenticate(Request $request) { // Global $user is deprecated, but the session system is still based on it. global $user; - require_once DRUPAL_ROOT . '/' . Settings::get('session_inc', 'core/includes/session.inc'); - drupal_session_initialize(); - if (drupal_session_started()) { + if ($this->sessionManager->initialize()->isStarted()) { return $user; } return NULL; @@ -42,7 +58,7 @@ public function authenticate(Request $request) { * {@inheritdoc} */ public function cleanup(Request $request) { - drupal_session_commit(); + $this->sessionManager->save(); } /** diff --git a/core/lib/Drupal/Core/Cron.php b/core/lib/Drupal/Core/Cron.php index 6bf463e407..5d5b2ae371 100644 --- a/core/lib/Drupal/Core/Cron.php +++ b/core/lib/Drupal/Core/Cron.php @@ -13,6 +13,7 @@ use Drupal\Core\Queue\QueueFactory; use Drupal\Core\Session\AccountProxyInterface; use Drupal\Core\Session\AnonymousUserSession; +use Drupal\Core\Session\SessionManagerInterface; /** * The Drupal core Cron service. @@ -54,6 +55,13 @@ class Cron implements CronInterface { */ protected $currentUser; + /** + * The session manager. + * + * @var \Drupal\Core\Session\SessionManagerInterface + */ + protected $sessionManager; + /** * Constructs a cron object. * @@ -67,13 +75,16 @@ class Cron implements CronInterface { * The state service. * @param \Drupal\Core\Session\AccountProxyInterface $current_user * The current user. + * @param \Drupal\Core\Session\SessionManagerInterface $session_manager + * The session manager. */ - public function __construct(ModuleHandlerInterface $module_handler, LockBackendInterface $lock, QueueFactory $queue_factory, StateInterface $state, AccountProxyInterface $current_user) { + public function __construct(ModuleHandlerInterface $module_handler, LockBackendInterface $lock, QueueFactory $queue_factory, StateInterface $state, AccountProxyInterface $current_user, SessionManagerInterface $session_manager) { $this->moduleHandler = $module_handler; $this->lock = $lock; $this->queueFactory = $queue_factory; $this->state = $state; $this->currentUser = $current_user; + $this->sessionManager = $session_manager; } /** @@ -84,8 +95,8 @@ public function run() { @ignore_user_abort(TRUE); // Prevent session information from being saved while cron is running. - $original_session_saving = drupal_save_session(); - drupal_save_session(FALSE); + $original_session_saving = $this->sessionManager->isEnabled(); + $this->sessionManager->disable(); // Force the current user to anonymous to ensure consistent permissions on // cron runs. @@ -157,7 +168,9 @@ public function run() { // Restore the user. $this->currentUser->setAccount($original_user); - drupal_save_session($original_session_saving); + if ($original_session_saving) { + $this->sessionManager->enable(); + } return $return; } diff --git a/core/lib/Drupal/Core/Session/SessionHandler.php b/core/lib/Drupal/Core/Session/SessionHandler.php index 8b51764125..1e44763597 100644 --- a/core/lib/Drupal/Core/Session/SessionHandler.php +++ b/core/lib/Drupal/Core/Session/SessionHandler.php @@ -11,7 +11,7 @@ use Drupal\Component\Utility\Settings; use Drupal\Core\Database\Connection; use Drupal\Core\Utility\Error; -use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; /** * Default session handler. @@ -19,11 +19,18 @@ class SessionHandler implements \SessionHandlerInterface { /** - * The request. + * The session manager. * - * @var \Symfony\Component\HttpFoundation\Request + * @var \Drupal\Core\Session\SessionManagerInterface */ - protected $request; + protected $sessionManager; + + /** + * The request stack. + * + * @var \Symfony\Component\HttpFoundation\RequestStack + */ + protected $requestStack; /** * The database connection. @@ -42,13 +49,16 @@ class SessionHandler implements \SessionHandlerInterface { /** * Constructs a new SessionHandler instance. * - * @param \Symfony\Component\HttpFoundation\Request $request - * The current request. + * @param \Drupal\Core\Session\SessionManagerInterface $session_manager + * The session manager. + * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack + * The request stack. * @param \Drupal\Core\Database\Connection $connection * The database connection. */ - public function __construct(Request $request, Connection $connection) { - $this->request = $request; + public function __construct(SessionManagerInterface $session_manager, RequestStack $request_stack, Connection $connection) { + $this->sessionManager = $session_manager; + $this->requestStack = $request_stack; $this->connection = $connection; } @@ -68,7 +78,7 @@ public function read($sid) { // Handle the case of first time visitors and clients that don't store // cookies (eg. web crawlers). $insecure_session_name = substr(session_name(), 1); - $cookies = $this->request->cookies; + $cookies = $this->requestStack->getCurrentRequest()->cookies; if (!$cookies->has(session_name()) && !$cookies->has($insecure_session_name)) { $user = new UserSession(); return ''; @@ -81,7 +91,7 @@ public function read($sid) { // that is in the user's cookie is hashed before being stored in the // database as a security measure. Thus, we have to hash it to match the // database. - if ($this->request->isSecure()) { + if ($this->requestStack->getCurrentRequest()->isSecure()) { // Try to load a session using the HTTPS-only secure session id. $values = $this->connection->query("SELECT u.*, s.* FROM {users} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE s.ssid = :ssid", array( ':ssid' => Crypt::hashBase64($sid), @@ -143,7 +153,7 @@ public function write($sid, $value) { // The exception handler is not active at this point, so we need to do it // manually. try { - if (!drupal_save_session()) { + if (!$this->sessionManager->isEnabled()) { // We don't have anything to do if we are not allowed to save the // session. return TRUE; @@ -160,18 +170,17 @@ public function write($sid, $value) { // Either ssid or sid or both will be added from $key below. $fields = array( 'uid' => $user->id(), - 'hostname' => $this->request->getClientIP(), + 'hostname' => $this->requestStack->getCurrentRequest()->getClientIP(), 'session' => $value, 'timestamp' => REQUEST_TIME, ); // Use the session ID as 'sid' and an empty string as 'ssid' by default. - // _drupal_session_read() does not allow empty strings so that's a safe - // default. + // read() does not allow empty strings so that's a safe default. $key = array('sid' => Crypt::hashBase64($sid), 'ssid' => ''); // On HTTPS connections, use the session ID as both 'sid' and 'ssid'. - if ($this->request->isSecure()) { + if ($this->requestStack->getCurrentRequest()->isSecure()) { $key['ssid'] = Crypt::hashBase64($sid); - $cookies = $this->request->cookies; + $cookies = $this->requestStack->getCurrentRequest()->cookies; // The "secure pages" setting allows a site to simultaneously use both // secure and insecure session cookies. If enabled and both cookies // are presented then use both keys. The session ID from the cookie is @@ -228,17 +237,17 @@ public function destroy($sid) { global $user; // Nothing to do if we are not allowed to change the session. - if (!drupal_save_session()) { + if (!$this->sessionManager->isEnabled()) { return TRUE; } - $is_https = $this->request->isSecure(); + $is_https = $this->requestStack->getCurrentRequest()->isSecure(); // Delete session data. $this->connection->delete('sessions') ->condition($is_https ? 'ssid' : 'sid', Crypt::hashBase64($sid)) ->execute(); // Reset $_SESSION and $user to prevent a new session from being started - // in drupal_session_commit(). + // in \Drupal\Core\Session\SessionManager::save(). $_SESSION = array(); $user = new AnonymousUserSession(); @@ -277,8 +286,8 @@ public function gc($lifetime) { * Force the secure value of the cookie. */ protected function deleteCookie($name, $secure = NULL) { - $cookies = $this->request->cookies; - if ($cookies->has($name) || (!$this->request->isSecure() && $secure === TRUE)) { + $cookies = $this->requestStack->getCurrentRequest()->cookies; + if ($cookies->has($name) || (!$this->requestStack->getCurrentRequest()->isSecure() && $secure === TRUE)) { $params = session_get_cookie_params(); if ($secure !== NULL) { $params['secure'] = $secure; diff --git a/core/lib/Drupal/Core/Session/SessionManager.php b/core/lib/Drupal/Core/Session/SessionManager.php new file mode 100644 index 0000000000..4de88a1dcc --- /dev/null +++ b/core/lib/Drupal/Core/Session/SessionManager.php @@ -0,0 +1,300 @@ +requestStack = $request_stack; + $this->connection = $connection; + } + + /** + * {@inheritdoc} + */ + public function initialize() { + global $user; + + // Register the default session handler. + // @todo Extract session storage from session handler into a service. + $handler = new SessionHandler($this, $this->requestStack, $this->connection); + session_set_save_handler($handler, TRUE); + + $is_https = $this->requestStack->getCurrentRequest()->isSecure(); + $cookies = $this->requestStack->getCurrentRequest()->cookies; + if (($cookies->has(session_name()) && ($session_name = $cookies->get(session_name()))) || ($is_https && Settings::get('mixed_mode_sessions', FALSE) && ($cookies->has(substr(session_name(), 1))) && ($session_name = $cookies->get(substr(session_name(), 1))))) { + // If a session cookie exists, initialize the session. Otherwise the + // session is only started on demand in save(), making + // anonymous users not use a session cookie unless something is stored in + // $_SESSION. This allows HTTP proxies to cache anonymous pageviews. + $this->start(); + if ($user->isAuthenticated() || !empty($_SESSION)) { + drupal_page_is_cacheable(FALSE); + } + } + else { + // Set a session identifier for this request. This is necessary because + // we lazily start sessions at the end of this request, and some + // processes (like drupal_get_token()) needs to know the future + // session ID in advance. + $this->lazySession = TRUE; + $user = new AnonymousUserSession(); + // Less random sessions (which are much faster to generate) are used for + // anonymous users than are generated in regenerate() when + // a user becomes authenticated. + session_id(Crypt::randomBytesBase64()); + if ($is_https && Settings::get('mixed_mode_sessions', FALSE)) { + $insecure_session_name = substr(session_name(), 1); + $session_id = Crypt::randomBytesBase64(); + $cookies->set($insecure_session_name, $session_id); + } + } + date_default_timezone_set(drupal_get_user_timezone()); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function start() { + if ($this->isCli() || $this->isStarted()) { + return; + } + // Save current session data before starting it, as PHP will destroy it. + $session_data = isset($_SESSION) ? $_SESSION : NULL; + + session_start(); + static::$started = TRUE; + + // Restore session data. + if (!empty($session_data)) { + $_SESSION += $session_data; + } + } + + /** + * {@inheritdoc} + */ + public function save() { + global $user; + + if (!$this->isEnabled()) { + // We don't have anything to do if we are not allowed to save the session. + return; + } + + if ($user->isAnonymous() && empty($_SESSION)) { + // There is no session data to store, destroy the session if it was + // previously started. + if ($this->isStarted()) { + session_destroy(); + } + } + else { + // There is session data to store. Start the session if it is not already + // started. + if (!$this->isStarted()) { + $this->start(); + if ($this->requestStack->getCurrentRequest()->isSecure() && Settings::get('mixed_mode_sessions', FALSE)) { + $insecure_session_name = substr(session_name(), 1); + $params = session_get_cookie_params(); + $expire = $params['lifetime'] ? REQUEST_TIME + $params['lifetime'] : 0; + $cookie_params = $this->requestStack->getCurrentRequest()->cookies; + setcookie($insecure_session_name, $cookie_params->get($insecure_session_name), $expire, $params['path'], $params['domain'], FALSE, $params['httponly']); + } + } + // Write the session data. + session_write_close(); + } + } + + /** + * {@inheritdoc} + */ + public function isStarted() { + return static::$started && session_status() === \PHP_SESSION_ACTIVE; + } + + /** + * {@inheritdoc} + */ + public function regenerate() { + global $user; + + // Nothing to do if we are not allowed to change the session. + if (!$this->isEnabled()) { + return; + } + + $is_https = $this->requestStack->getCurrentRequest()->isSecure(); + $cookies = $this->requestStack->getCurrentRequest()->cookies; + + if ($is_https && Settings::get('mixed_mode_sessions', FALSE)) { + $insecure_session_name = substr(session_name(), 1); + if (!isset($this->lazySession) && $cookies->has($insecure_session_name)) { + $old_insecure_session_id = $cookies->get($insecure_session_name); + } + $params = session_get_cookie_params(); + $session_id = Crypt::randomBytesBase64(); + // If a session cookie lifetime is set, the session will expire + // $params['lifetime'] seconds from the current request. If it is not set, + // it will expire when the browser is closed. + $expire = $params['lifetime'] ? REQUEST_TIME + $params['lifetime'] : 0; + setcookie($insecure_session_name, $session_id, $expire, $params['path'], $params['domain'], FALSE, $params['httponly']); + $cookies->set($insecure_session_name, $session_id); + } + + if ($this->isStarted()) { + $old_session_id = session_id(); + } + session_id(Crypt::randomBytesBase64()); + + if (isset($old_session_id)) { + $params = session_get_cookie_params(); + $expire = $params['lifetime'] ? REQUEST_TIME + $params['lifetime'] : 0; + setcookie(session_name(), session_id(), $expire, $params['path'], $params['domain'], $params['secure'], $params['httponly']); + $fields = array('sid' => Crypt::hashBase64(session_id())); + if ($is_https) { + $fields['ssid'] = Crypt::hashBase64(session_id()); + // If the "secure pages" setting is enabled, use the newly-created + // insecure session identifier as the regenerated sid. + if (Settings::get('mixed_mode_sessions', FALSE)) { + $fields['sid'] = Crypt::hashBase64($session_id); + } + } + $this->connection->update('sessions') + ->fields($fields) + ->condition($is_https ? 'ssid' : 'sid', Crypt::hashBase64($old_session_id)) + ->execute(); + } + elseif (isset($old_insecure_session_id)) { + // If logging in to the secure site, and there was no active session on + // the secure site but a session was active on the insecure site, update + // the insecure session with the new session identifiers. + $this->connection->update('sessions') + ->fields(array('sid' => Crypt::hashBase64($session_id), 'ssid' => Crypt::hashBase64(session_id()))) + ->condition('sid', Crypt::hashBase64($old_insecure_session_id)) + ->execute(); + } + else { + // Start the session when it doesn't exist yet. + // Preserve the logged in user, as it will be reset to anonymous + // by \Drupal\Core\Session\SessionHandler::read(). + $account = $user; + $this->start(); + $user = $account; + } + date_default_timezone_set(drupal_get_user_timezone()); + } + + /** + * {@inheritdoc} + */ + public function delete($uid) { + // Nothing to do if we are not allowed to change the session. + if (!$this->isEnabled()) { + return; + } + $this->connection->delete('sessions') + ->condition('uid', $uid) + ->execute(); + } + + /** + * {@inheritdoc} + */ + public function isEnabled() { + return static::$enabled; + } + + /** + * {@inheritdoc} + */ + public function disable() { + static::$enabled = FALSE; + return $this; + } + + /** + * {@inheritdoc} + */ + public function enable() { + static::$enabled = TRUE; + return $this; + } + + /** + * Returns whether the current PHP process runs on CLI. + * + * Command line clients do not support cookies nor sessions. + * + * @return bool + */ + protected function isCli() { + return PHP_SAPI === 'cli'; + } + +} diff --git a/core/lib/Drupal/Core/Session/SessionManagerInterface.php b/core/lib/Drupal/Core/Session/SessionManagerInterface.php new file mode 100644 index 0000000000..346bc0da41 --- /dev/null +++ b/core/lib/Drupal/Core/Session/SessionManagerInterface.php @@ -0,0 +1,80 @@ +kernel = new DrupalKernel('unit_testing', drupal_classloader(), FALSE); $this->kernel->boot(); + $request = Request::create('/'); + $this->container->set('request', $request); + $this->container->get('request_stack')->push($request); + // Create a minimal core.extension configuration object so that the list of // enabled modules can be maintained allowing // \Drupal\Core\Config\ConfigInstaller::installDefaultConfig() to work. diff --git a/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php index e19169f7b1..50f2eb1745 100644 --- a/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php +++ b/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php @@ -1026,8 +1026,7 @@ private function prepareEnvironment() { $this->originalUser = isset($user) ? clone $user : NULL; // Ensure that the current session is not changed by the new environment. - require_once DRUPAL_ROOT . '/' . Settings::get('session_inc', 'core/includes/session.inc'); - drupal_save_session(FALSE); + \Drupal::service('session_manager')->disable(); // Save and clean the shutdown callbacks array because it is static cached // and will be changed by the test run. Otherwise it will contain callbacks @@ -1268,7 +1267,7 @@ private function restoreEnvironment() { // Restore original user session. $this->container->set('current_user', $this->originalUser); - drupal_save_session(TRUE); + \Drupal::service('session_manager')->enable(); } /** diff --git a/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php index 33833911cd..d934cd9d86 100644 --- a/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php +++ b/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php @@ -699,8 +699,8 @@ protected function drupalUserIsLoggedIn($account) { if (!isset($account->session_id)) { return FALSE; } - // @see _drupal_session_read(). The session ID is hashed before being stored - // in the database. + // The session ID is hashed before being stored in the database. + // @see \Drupal\Core\Session\SessionHandler::read() return (bool) db_query("SELECT sid FROM {users} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE s.sid = :sid", array(':sid' => Crypt::hashBase64($account->session_id)))->fetchField(); } diff --git a/core/modules/system/core.api.php b/core/modules/system/core.api.php index 5ff42fe750..7e48c4deab 100644 --- a/core/modules/system/core.api.php +++ b/core/modules/system/core.api.php @@ -494,7 +494,6 @@ * @see mail.inc * @see php_wrappers * @see sanitization - * @see session.inc * @see transliteration * @see validation * @} diff --git a/core/modules/system/lib/Drupal/system/Tests/Common/FormatDateTest.php b/core/modules/system/lib/Drupal/system/Tests/Common/FormatDateTest.php index 1a6b758df0..171b915763 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Common/FormatDateTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Common/FormatDateTest.php @@ -128,7 +128,7 @@ function testFormatDate() { $this->drupalPostForm('user/' . $test_user->id() . '/edit', $edit, t('Save')); // Disable session saving as we are about to modify the global $user. - drupal_save_session(FALSE); + \Drupal::service('session_manager')->disable(); // Save the original user and language and then replace it with the test user and language. $real_user = $user; $user = user_load($test_user->id(), TRUE); @@ -158,6 +158,6 @@ function testFormatDate() { $language_interface->id = $real_language; // Restore default time zone. date_default_timezone_set(drupal_get_user_timezone()); - drupal_save_session(TRUE); + \Drupal::service('session_manager')->enable(); } } diff --git a/core/modules/system/lib/Drupal/system/Tests/Datetime/DrupalDateTimeTest.php b/core/modules/system/lib/Drupal/system/Tests/Datetime/DrupalDateTimeTest.php index 3707bf88a3..8484a82c3f 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Datetime/DrupalDateTimeTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Datetime/DrupalDateTimeTest.php @@ -100,7 +100,7 @@ public function testDateTimezone() { $this->drupalPostForm('user/' . $test_user->id() . '/edit', $edit, t('Save')); // Disable session saving as we are about to modify the global $user. - drupal_save_session(FALSE); + \Drupal::service('session_manager')->disable(); // Save the original user and then replace it with the test user. $real_user = $user; $user = user_load($test_user->id(), TRUE); @@ -119,7 +119,7 @@ public function testDateTimezone() { $user = $real_user; // Restore default time zone. date_default_timezone_set(drupal_get_user_timezone()); - drupal_save_session(TRUE); + \Drupal::service('session_manager')->enable(); } diff --git a/core/modules/system/lib/Drupal/system/Tests/Session/SessionTest.php b/core/modules/system/lib/Drupal/system/Tests/Session/SessionTest.php index 16d3eec8f0..634ae1d132 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Session/SessionTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Session/SessionTest.php @@ -29,14 +29,13 @@ public static function getInfo() { } /** - * Tests for drupal_save_session() and drupal_session_regenerate(). + * Tests for \Drupal\Core\Session\SessionManager::isEnabled() and ::regenerate(). */ function testSessionSaveRegenerate() { - $this->assertFalse(drupal_save_session(),'drupal_save_session() correctly returns FALSE (inside of testing framework) when initially called with no arguments.', 'Session'); - $this->assertFalse(drupal_save_session(FALSE), 'drupal_save_session() correctly returns FALSE when called with FALSE.', 'Session'); - $this->assertFalse(drupal_save_session(), 'drupal_save_session() correctly returns FALSE when saving has been disabled.', 'Session'); - $this->assertTrue(drupal_save_session(TRUE), 'drupal_save_session() correctly returns TRUE when called with TRUE.', 'Session'); - $this->assertTrue(drupal_save_session(), 'drupal_save_session() correctly returns TRUE when saving has been enabled.', 'Session'); + $session_manager = $this->container->get('session_manager'); + $this->assertFalse($session_manager->isEnabled(), 'SessionManager->isEnabled() initially returns FALSE (in testing framework).'); + $this->assertFalse($session_manager->disable()->isEnabled(), 'SessionManager->isEnabled() returns FALSE after disabling.'); + $this->assertTrue($session_manager->enable()->isEnabled(), 'SessionManager->isEnabled() returns TRUE after enabling.'); // Test session hardening code from SA-2008-044. $user = $this->drupalCreateUser(); diff --git a/core/modules/system/tests/modules/session_test/lib/Drupal/session_test/Controller/SessionTestController.php b/core/modules/system/tests/modules/session_test/lib/Drupal/session_test/Controller/SessionTestController.php index 6b00515d24..5fbd412828 100644 --- a/core/modules/system/tests/modules/session_test/lib/Drupal/session_test/Controller/SessionTestController.php +++ b/core/modules/system/tests/modules/session_test/lib/Drupal/session_test/Controller/SessionTestController.php @@ -35,11 +35,11 @@ public function get() { * A notification message with session ID. */ public function getId() { - // Set a value in $_SESSION, so that drupal_session_commit() will start + // Set a value in $_SESSION, so that SessionManager::save() will start // a session. $_SESSION['test'] = 'test'; - drupal_session_commit(); + \Drupal::service('session_manager')->save(); return 'session_id:' . session_id() . "\n"; } @@ -83,7 +83,7 @@ public function set($test_value) { * A notification message. */ public function noSet($test_value) { - drupal_save_session(FALSE); + \Drupal::service('session_manager')->disable(); $this->set($test_value); return $this->t('session saving was disabled, and then %val was set', array('%val' => $test_value)); } @@ -109,7 +109,7 @@ public function setMessage() { * A notification message. */ public function setMessageButDontSave() { - drupal_save_session(FALSE); + \Drupal::service('session_manager')->disable(); $this->setMessage(); } diff --git a/core/modules/user/lib/Drupal/user/Entity/User.php b/core/modules/user/lib/Drupal/user/Entity/User.php index 8bb2f51c92..83e21038c8 100644 --- a/core/modules/user/lib/Drupal/user/Entity/User.php +++ b/core/modules/user/lib/Drupal/user/Entity/User.php @@ -123,12 +123,13 @@ public function postSave(EntityStorageInterface $storage, $update = TRUE) { parent::postSave($storage, $update); if ($update) { + $session_manager = \Drupal::service('session_manager'); // If the password has been changed, delete all open sessions for the // user and recreate the current one. if ($this->pass->value != $this->original->pass->value) { - drupal_session_destroy_uid($this->id()); + $session_manager->delete($this->id()); if ($this->id() == \Drupal::currentUser()->id()) { - drupal_session_regenerate(); + $session_manager->regenerate(); } } @@ -140,7 +141,7 @@ public function postSave(EntityStorageInterface $storage, $update = TRUE) { // If the user was blocked, delete the user's sessions to force a logout. if ($this->original->status->value != $this->status->value && $this->status->value == 0) { - drupal_session_destroy_uid($this->id()); + $session_manager->delete($this->id()); } // Send emails after we have the new user object. diff --git a/core/modules/user/lib/Drupal/user/Tests/Views/ArgumentDefaultTest.php b/core/modules/user/lib/Drupal/user/Tests/Views/ArgumentDefaultTest.php index 8323de177b..c8de3a35af 100644 --- a/core/modules/user/lib/Drupal/user/Tests/Views/ArgumentDefaultTest.php +++ b/core/modules/user/lib/Drupal/user/Tests/Views/ArgumentDefaultTest.php @@ -37,7 +37,7 @@ public function test_plugin_argument_default_current_user() { $this->drupalLogin($account); global $user; $admin = $user; - drupal_save_session(FALSE); + $session_manager = \Drupal::service('session_manager')->disable(); $user = $account; $view = Views::getView('test_plugin_argument_default_current_user'); @@ -46,7 +46,7 @@ public function test_plugin_argument_default_current_user() { $this->assertEqual($view->argument['null']->getDefaultArgument(), $account->id(), 'Uid of the current user is used.'); // Switch back. $user = $admin; - drupal_save_session(TRUE); + $session_manager->enable(); } } diff --git a/core/modules/user/user.module b/core/modules/user/user.module index 965167eee6..191140fd00 100644 --- a/core/modules/user/user.module +++ b/core/modules/user/user.module @@ -769,7 +769,7 @@ function user_login_finalize(UserInterface $account) { // Regenerate the session ID to prevent against session fixation attacks. // This is called before hook_user in case one of those functions fails // or incorrectly does a redirect which would leave the old session in place. - drupal_session_regenerate(); + \Drupal::service('session_manager')->regenerate(); \Drupal::moduleHandler()->invokeAll('user_login', array($account)); } @@ -987,7 +987,7 @@ function _user_cancel($edit, $account, $method) { function _user_cancel_session_regenerate() { // Regenerate the users session instead of calling session_destroy() as we // want to preserve any messages that might have been set. - drupal_session_regenerate(); + \Drupal::service('session_manager')->regenerate(); } /** diff --git a/core/update.php b/core/update.php index 45904a20b7..6fb07ef0a0 100644 --- a/core/update.php +++ b/core/update.php @@ -347,8 +347,7 @@ function update_task_list($active = NULL) { // Determine if the current user has access to run update.php. drupal_bootstrap(DRUPAL_BOOTSTRAP_PAGE_CACHE); -require_once DRUPAL_ROOT . '/' . Settings::get('session_inc', 'core/includes/session.inc'); -drupal_session_initialize(); +\Drupal::service('session_manager')->initialize(); // Ensure that URLs generated for the home and admin pages don't have 'update.php' // in them. @@ -451,7 +450,7 @@ function update_task_list($active = NULL) { } if (isset($output) && $output) { // Explicitly start a session so that the update.php token will be accepted. - drupal_session_start(); + \Drupal::service('session_manager')->start(); // We defer the display of messages until all updates are done. $progress_page = ($batch = batch_get()) && isset($batch['running']); if ($output instanceof Response) { -- GitLab