diff core/lib/Drupal/Core/Session/SessionManager.php @ 0:4c8ae668cc8c

Initial import (non-working)
author Chris Cannam
date Wed, 29 Nov 2017 16:09:58 +0000
parents
children 1fec387a4317
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/core/lib/Drupal/Core/Session/SessionManager.php	Wed Nov 29 16:09:58 2017 +0000
@@ -0,0 +1,340 @@
+<?php
+
+namespace Drupal\Core\Session;
+
+use Drupal\Component\Utility\Crypt;
+use Drupal\Core\Database\Connection;
+use Drupal\Core\DependencyInjection\DependencySerializationTrait;
+use Symfony\Component\HttpFoundation\RequestStack;
+use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage;
+
+/**
+ * Manages user sessions.
+ *
+ * This class implements the custom session management code inherited from
+ * Drupal 7 on top of the corresponding Symfony component. Regrettably the name
+ * NativeSessionStorage is not quite accurate. In fact the responsibility for
+ * storing and retrieving session data has been extracted from it in Symfony 2.1
+ * but the class name was not changed.
+ *
+ * @todo
+ *   In fact the NativeSessionStorage class already implements all of the
+ *   functionality required by a typical Symfony application. Normally it is not
+ *   necessary to subclass it at all. In order to reach the point where Drupal
+ *   can use the Symfony session management unmodified, the code implemented
+ *   here needs to be extracted either into a dedicated session handler proxy
+ *   (e.g. sid-hashing) or relocated to the authentication subsystem.
+ */
+class SessionManager extends NativeSessionStorage implements SessionManagerInterface {
+
+  use DependencySerializationTrait;
+
+  /**
+   * The request stack.
+   *
+   * @var \Symfony\Component\HttpFoundation\RequestStack
+   */
+  protected $requestStack;
+
+  /**
+   * The database connection to use.
+   *
+   * @var \Drupal\Core\Database\Connection
+   */
+  protected $connection;
+
+  /**
+   * The session configuration.
+   *
+   * @var \Drupal\Core\Session\SessionConfigurationInterface
+   */
+  protected $sessionConfiguration;
+
+  /**
+   * Whether a lazy session has been started.
+   *
+   * @var bool
+   */
+  protected $startedLazy;
+
+  /**
+   * The write safe session handler.
+   *
+   * @todo: This reference should be removed once all database queries
+   *   are removed from the session manager class.
+   *
+   * @var \Drupal\Core\Session\WriteSafeSessionHandlerInterface
+   */
+  protected $writeSafeHandler;
+
+  /**
+   * Constructs a new session manager instance.
+   *
+   * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
+   *   The request stack.
+   * @param \Drupal\Core\Database\Connection $connection
+   *   The database connection.
+   * @param \Drupal\Core\Session\MetadataBag $metadata_bag
+   *   The session metadata bag.
+   * @param \Drupal\Core\Session\SessionConfigurationInterface $session_configuration
+   *   The session configuration interface.
+   * @param \Symfony\Component\HttpFoundation\Session\Storage\Proxy\AbstractProxy|Symfony\Component\HttpFoundation\Session\Storage\Handler\NativeSessionHandler|\SessionHandlerInterface|null $handler
+   *   The object to register as a PHP session handler.
+   *   @see \Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage::setSaveHandler()
+   */
+  public function __construct(RequestStack $request_stack, Connection $connection, MetadataBag $metadata_bag, SessionConfigurationInterface $session_configuration, $handler = NULL) {
+    $options = [];
+    $this->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\user\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();
+  }
+
+}