123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341 |
- <?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\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();
- }
- }
|