sessionConfiguration = $session_configuration; $this->requestStack = $request_stack; $this->connection = $connection; parent::__construct($options, $handler, $metadata_bag); // @todo When not using the Symfony Session object, the list of bags in the // NativeSessionStorage will remain uninitialized. This will lead to // errors in NativeSessionHandler::loadSession. Remove this after // https://www.drupal.org/node/2229145, when we will be using the Symfony // session object (which registers an attribute bag with the // manager upon instantiation). $this->bags = []; } /** * {@inheritdoc} */ public function start() { if (($this->started || $this->startedLazy) && !$this->closed) { return $this->started; } $request = $this->requestStack->getCurrentRequest(); $this->setOptions($this->sessionConfiguration->getOptions($request)); if ($this->sessionConfiguration->hasSession($request)) { // 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. $result = $this->startNow(); } if (empty($result)) { // Randomly generate a session identifier for this request. This is // necessary because \Drupal\Core\TempStore\SharedTempStoreFactory::get() // wants to know the future session ID of a lazily started session in // advance. // // @todo: With current versions of PHP there is little reason to generate // the session id from within application code. Consider using the // default php session id instead of generating a custom one: // https://www.drupal.org/node/2238561 $this->setId(Crypt::randomBytesBase64()); // Initialize the session global and attach the Symfony session bags. $_SESSION = []; $this->loadSession(); // NativeSessionStorage::loadSession() sets started to TRUE, reset it to // FALSE here. $this->started = FALSE; $this->startedLazy = TRUE; $result = FALSE; } return $result; } /** * Forcibly start a PHP session. * * @return bool * TRUE if the session is started. */ protected function startNow() { if ($this->isCli()) { return FALSE; } if ($this->startedLazy) { // Save current session data before starting it, as PHP will destroy it. $session_data = $_SESSION; } $result = parent::start(); // Restore session data. if ($this->startedLazy) { $_SESSION = $session_data; $this->loadSession(); } return $result; } /** * {@inheritdoc} */ public function save() { if ($this->isCli()) { // We don't have anything to do if we are not allowed to save the session. return; } if ($this->isSessionObsolete()) { // There is no session data to store, destroy the session if it was // previously started. if ($this->getSaveHandler()->isActive()) { $this->destroy(); } } else { // There is session data to store. Start the session if it is not already // started. if (!$this->getSaveHandler()->isActive()) { $this->startNow(); } // Write the session data. parent::save(); } $this->startedLazy = FALSE; } /** * {@inheritdoc} */ public function regenerate($destroy = FALSE, $lifetime = NULL) { // Nothing to do if we are not allowed to change the session. if ($this->isCli()) { return; } // We do not support the optional $destroy and $lifetime parameters as long // as #2238561 remains open. if ($destroy || isset($lifetime)) { throw new \InvalidArgumentException('The optional parameters $destroy and $lifetime of SessionManager::regenerate() are not supported currently'); } if ($this->isStarted()) { $old_session_id = $this->getId(); } session_id(Crypt::randomBytesBase64()); $this->getMetadataBag()->clearCsrfTokenSeed(); if (isset($old_session_id)) { $params = session_get_cookie_params(); $expire = $params['lifetime'] ? REQUEST_TIME + $params['lifetime'] : 0; setcookie($this->getName(), $this->getId(), $expire, $params['path'], $params['domain'], $params['secure'], $params['httponly']); $this->migrateStoredSession($old_session_id); } if (!$this->isStarted()) { // Start the session when it doesn't exist yet. $this->startNow(); } } /** * {@inheritdoc} */ public function delete($uid) { // Nothing to do if we are not allowed to change the session. if (!$this->writeSafeHandler->isSessionWritable() || $this->isCli()) { return; } $this->connection->delete('sessions') ->condition('uid', $uid) ->execute(); } /** * {@inheritdoc} */ public function destroy() { session_destroy(); // Unset the session cookies. $session_name = $this->getName(); $cookies = $this->requestStack->getCurrentRequest()->cookies; // setcookie() can only be called when headers are not yet sent. if ($cookies->has($session_name) && !headers_sent()) { $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; } /** * 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'; } /** * Determines whether the session contains user data. * * @return bool * TRUE when the session does not contain any values and therefore can be * destroyed. */ protected function isSessionObsolete() { $used_session_keys = array_filter($this->getSessionDataMask()); return empty($used_session_keys); } /** * Returns a map specifying which session key is containing user data. * * @return array * An array where keys correspond to the session keys and the values are * booleans specifying whether the corresponding session key contains any * user data. */ protected function getSessionDataMask() { if (empty($_SESSION)) { return []; } // Start out with a completely filled mask. $mask = array_fill_keys(array_keys($_SESSION), TRUE); // Ignore the metadata bag, it does not contain any user data. $mask[$this->metadataBag->getStorageKey()] = FALSE; // Ignore attribute bags when they do not contain any data. foreach ($this->bags as $bag) { $key = $bag->getStorageKey(); $mask[$key] = !empty($_SESSION[$key]); } return array_intersect_key($mask, $_SESSION); } /** * Migrates the current session to a new session id. * * @param string $old_session_id * The old session ID. The new session ID is $this->getId(). */ protected function migrateStoredSession($old_session_id) { $fields = ['sid' => Crypt::hashBase64($this->getId())]; $this->connection->update('sessions') ->fields($fields) ->condition('sid', Crypt::hashBase64($old_session_id)) ->execute(); } }