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