annotate core/lib/Drupal/Core/Session/SessionManager.php @ 19:fa3358dc1485 tip

Add ndrum files
author Chris Cannam
date Wed, 28 Aug 2019 13:14:47 +0100
parents 129ea1e6d783
children
rev   line source
Chris@0 1 <?php
Chris@0 2
Chris@0 3 namespace Drupal\Core\Session;
Chris@0 4
Chris@0 5 use Drupal\Component\Utility\Crypt;
Chris@0 6 use Drupal\Core\Database\Connection;
Chris@0 7 use Drupal\Core\DependencyInjection\DependencySerializationTrait;
Chris@0 8 use Symfony\Component\HttpFoundation\RequestStack;
Chris@0 9 use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage;
Chris@0 10
Chris@0 11 /**
Chris@0 12 * Manages user sessions.
Chris@0 13 *
Chris@0 14 * This class implements the custom session management code inherited from
Chris@0 15 * Drupal 7 on top of the corresponding Symfony component. Regrettably the name
Chris@0 16 * NativeSessionStorage is not quite accurate. In fact the responsibility for
Chris@0 17 * storing and retrieving session data has been extracted from it in Symfony 2.1
Chris@0 18 * but the class name was not changed.
Chris@0 19 *
Chris@0 20 * @todo
Chris@0 21 * In fact the NativeSessionStorage class already implements all of the
Chris@0 22 * functionality required by a typical Symfony application. Normally it is not
Chris@0 23 * necessary to subclass it at all. In order to reach the point where Drupal
Chris@0 24 * can use the Symfony session management unmodified, the code implemented
Chris@0 25 * here needs to be extracted either into a dedicated session handler proxy
Chris@0 26 * (e.g. sid-hashing) or relocated to the authentication subsystem.
Chris@0 27 */
Chris@0 28 class SessionManager extends NativeSessionStorage implements SessionManagerInterface {
Chris@0 29
Chris@0 30 use DependencySerializationTrait;
Chris@0 31
Chris@0 32 /**
Chris@0 33 * The request stack.
Chris@0 34 *
Chris@0 35 * @var \Symfony\Component\HttpFoundation\RequestStack
Chris@0 36 */
Chris@0 37 protected $requestStack;
Chris@0 38
Chris@0 39 /**
Chris@0 40 * The database connection to use.
Chris@0 41 *
Chris@0 42 * @var \Drupal\Core\Database\Connection
Chris@0 43 */
Chris@0 44 protected $connection;
Chris@0 45
Chris@0 46 /**
Chris@0 47 * The session configuration.
Chris@0 48 *
Chris@0 49 * @var \Drupal\Core\Session\SessionConfigurationInterface
Chris@0 50 */
Chris@0 51 protected $sessionConfiguration;
Chris@0 52
Chris@0 53 /**
Chris@0 54 * Whether a lazy session has been started.
Chris@0 55 *
Chris@0 56 * @var bool
Chris@0 57 */
Chris@0 58 protected $startedLazy;
Chris@0 59
Chris@0 60 /**
Chris@0 61 * The write safe session handler.
Chris@0 62 *
Chris@0 63 * @todo: This reference should be removed once all database queries
Chris@0 64 * are removed from the session manager class.
Chris@0 65 *
Chris@0 66 * @var \Drupal\Core\Session\WriteSafeSessionHandlerInterface
Chris@0 67 */
Chris@0 68 protected $writeSafeHandler;
Chris@0 69
Chris@0 70 /**
Chris@0 71 * Constructs a new session manager instance.
Chris@0 72 *
Chris@0 73 * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
Chris@0 74 * The request stack.
Chris@0 75 * @param \Drupal\Core\Database\Connection $connection
Chris@0 76 * The database connection.
Chris@0 77 * @param \Drupal\Core\Session\MetadataBag $metadata_bag
Chris@0 78 * The session metadata bag.
Chris@0 79 * @param \Drupal\Core\Session\SessionConfigurationInterface $session_configuration
Chris@0 80 * The session configuration interface.
Chris@0 81 * @param \Symfony\Component\HttpFoundation\Session\Storage\Proxy\AbstractProxy|Symfony\Component\HttpFoundation\Session\Storage\Handler\NativeSessionHandler|\SessionHandlerInterface|null $handler
Chris@0 82 * The object to register as a PHP session handler.
Chris@0 83 * @see \Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage::setSaveHandler()
Chris@0 84 */
Chris@0 85 public function __construct(RequestStack $request_stack, Connection $connection, MetadataBag $metadata_bag, SessionConfigurationInterface $session_configuration, $handler = NULL) {
Chris@0 86 $options = [];
Chris@0 87 $this->sessionConfiguration = $session_configuration;
Chris@0 88 $this->requestStack = $request_stack;
Chris@0 89 $this->connection = $connection;
Chris@0 90
Chris@0 91 parent::__construct($options, $handler, $metadata_bag);
Chris@0 92
Chris@0 93 // @todo When not using the Symfony Session object, the list of bags in the
Chris@0 94 // NativeSessionStorage will remain uninitialized. This will lead to
Chris@0 95 // errors in NativeSessionHandler::loadSession. Remove this after
Chris@0 96 // https://www.drupal.org/node/2229145, when we will be using the Symfony
Chris@0 97 // session object (which registers an attribute bag with the
Chris@0 98 // manager upon instantiation).
Chris@0 99 $this->bags = [];
Chris@0 100 }
Chris@0 101
Chris@0 102 /**
Chris@0 103 * {@inheritdoc}
Chris@0 104 */
Chris@0 105 public function start() {
Chris@0 106 if (($this->started || $this->startedLazy) && !$this->closed) {
Chris@0 107 return $this->started;
Chris@0 108 }
Chris@0 109
Chris@0 110 $request = $this->requestStack->getCurrentRequest();
Chris@0 111 $this->setOptions($this->sessionConfiguration->getOptions($request));
Chris@0 112
Chris@0 113 if ($this->sessionConfiguration->hasSession($request)) {
Chris@0 114 // If a session cookie exists, initialize the session. Otherwise the
Chris@0 115 // session is only started on demand in save(), making
Chris@0 116 // anonymous users not use a session cookie unless something is stored in
Chris@0 117 // $_SESSION. This allows HTTP proxies to cache anonymous pageviews.
Chris@0 118 $result = $this->startNow();
Chris@0 119 }
Chris@0 120
Chris@0 121 if (empty($result)) {
Chris@0 122 // Randomly generate a session identifier for this request. This is
Chris@14 123 // necessary because \Drupal\Core\TempStore\SharedTempStoreFactory::get()
Chris@14 124 // wants to know the future session ID of a lazily started session in
Chris@14 125 // advance.
Chris@0 126 //
Chris@0 127 // @todo: With current versions of PHP there is little reason to generate
Chris@0 128 // the session id from within application code. Consider using the
Chris@0 129 // default php session id instead of generating a custom one:
Chris@0 130 // https://www.drupal.org/node/2238561
Chris@0 131 $this->setId(Crypt::randomBytesBase64());
Chris@0 132
Chris@0 133 // Initialize the session global and attach the Symfony session bags.
Chris@0 134 $_SESSION = [];
Chris@0 135 $this->loadSession();
Chris@0 136
Chris@0 137 // NativeSessionStorage::loadSession() sets started to TRUE, reset it to
Chris@0 138 // FALSE here.
Chris@0 139 $this->started = FALSE;
Chris@0 140 $this->startedLazy = TRUE;
Chris@0 141
Chris@0 142 $result = FALSE;
Chris@0 143 }
Chris@0 144
Chris@0 145 return $result;
Chris@0 146 }
Chris@0 147
Chris@0 148 /**
Chris@0 149 * Forcibly start a PHP session.
Chris@0 150 *
Chris@0 151 * @return bool
Chris@0 152 * TRUE if the session is started.
Chris@0 153 */
Chris@0 154 protected function startNow() {
Chris@0 155 if ($this->isCli()) {
Chris@0 156 return FALSE;
Chris@0 157 }
Chris@0 158
Chris@0 159 if ($this->startedLazy) {
Chris@0 160 // Save current session data before starting it, as PHP will destroy it.
Chris@0 161 $session_data = $_SESSION;
Chris@0 162 }
Chris@0 163
Chris@0 164 $result = parent::start();
Chris@0 165
Chris@0 166 // Restore session data.
Chris@0 167 if ($this->startedLazy) {
Chris@0 168 $_SESSION = $session_data;
Chris@0 169 $this->loadSession();
Chris@0 170 }
Chris@0 171
Chris@0 172 return $result;
Chris@0 173 }
Chris@0 174
Chris@0 175 /**
Chris@0 176 * {@inheritdoc}
Chris@0 177 */
Chris@0 178 public function save() {
Chris@0 179 if ($this->isCli()) {
Chris@0 180 // We don't have anything to do if we are not allowed to save the session.
Chris@0 181 return;
Chris@0 182 }
Chris@0 183
Chris@0 184 if ($this->isSessionObsolete()) {
Chris@0 185 // There is no session data to store, destroy the session if it was
Chris@0 186 // previously started.
Chris@0 187 if ($this->getSaveHandler()->isActive()) {
Chris@0 188 $this->destroy();
Chris@0 189 }
Chris@0 190 }
Chris@0 191 else {
Chris@0 192 // There is session data to store. Start the session if it is not already
Chris@0 193 // started.
Chris@0 194 if (!$this->getSaveHandler()->isActive()) {
Chris@0 195 $this->startNow();
Chris@0 196 }
Chris@0 197 // Write the session data.
Chris@0 198 parent::save();
Chris@0 199 }
Chris@0 200
Chris@0 201 $this->startedLazy = FALSE;
Chris@0 202 }
Chris@0 203
Chris@0 204 /**
Chris@0 205 * {@inheritdoc}
Chris@0 206 */
Chris@0 207 public function regenerate($destroy = FALSE, $lifetime = NULL) {
Chris@0 208 // Nothing to do if we are not allowed to change the session.
Chris@0 209 if ($this->isCli()) {
Chris@0 210 return;
Chris@0 211 }
Chris@0 212
Chris@0 213 // We do not support the optional $destroy and $lifetime parameters as long
Chris@0 214 // as #2238561 remains open.
Chris@0 215 if ($destroy || isset($lifetime)) {
Chris@0 216 throw new \InvalidArgumentException('The optional parameters $destroy and $lifetime of SessionManager::regenerate() are not supported currently');
Chris@0 217 }
Chris@0 218
Chris@0 219 if ($this->isStarted()) {
Chris@0 220 $old_session_id = $this->getId();
Chris@17 221 // Save and close the old session. Call the parent method to avoid issue
Chris@17 222 // with session destruction due to the session being considered obsolete.
Chris@17 223 parent::save();
Chris@17 224 // Ensure the session is reloaded correctly.
Chris@17 225 $this->startedLazy = TRUE;
Chris@0 226 }
Chris@0 227 session_id(Crypt::randomBytesBase64());
Chris@0 228
Chris@0 229 $this->getMetadataBag()->clearCsrfTokenSeed();
Chris@0 230
Chris@0 231 if (isset($old_session_id)) {
Chris@0 232 $params = session_get_cookie_params();
Chris@0 233 $expire = $params['lifetime'] ? REQUEST_TIME + $params['lifetime'] : 0;
Chris@0 234 setcookie($this->getName(), $this->getId(), $expire, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
Chris@0 235 $this->migrateStoredSession($old_session_id);
Chris@0 236 }
Chris@0 237
Chris@17 238 $this->startNow();
Chris@0 239 }
Chris@0 240
Chris@0 241 /**
Chris@0 242 * {@inheritdoc}
Chris@0 243 */
Chris@0 244 public function delete($uid) {
Chris@0 245 // Nothing to do if we are not allowed to change the session.
Chris@0 246 if (!$this->writeSafeHandler->isSessionWritable() || $this->isCli()) {
Chris@0 247 return;
Chris@0 248 }
Chris@0 249 $this->connection->delete('sessions')
Chris@0 250 ->condition('uid', $uid)
Chris@0 251 ->execute();
Chris@0 252 }
Chris@0 253
Chris@0 254 /**
Chris@0 255 * {@inheritdoc}
Chris@0 256 */
Chris@0 257 public function destroy() {
Chris@0 258 session_destroy();
Chris@0 259
Chris@0 260 // Unset the session cookies.
Chris@0 261 $session_name = $this->getName();
Chris@0 262 $cookies = $this->requestStack->getCurrentRequest()->cookies;
Chris@0 263 // setcookie() can only be called when headers are not yet sent.
Chris@0 264 if ($cookies->has($session_name) && !headers_sent()) {
Chris@0 265 $params = session_get_cookie_params();
Chris@0 266 setcookie($session_name, '', REQUEST_TIME - 3600, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
Chris@0 267 $cookies->remove($session_name);
Chris@0 268 }
Chris@0 269 }
Chris@0 270
Chris@0 271 /**
Chris@0 272 * {@inheritdoc}
Chris@0 273 */
Chris@0 274 public function setWriteSafeHandler(WriteSafeSessionHandlerInterface $handler) {
Chris@0 275 $this->writeSafeHandler = $handler;
Chris@0 276 }
Chris@0 277
Chris@0 278 /**
Chris@0 279 * Returns whether the current PHP process runs on CLI.
Chris@0 280 *
Chris@0 281 * Command line clients do not support cookies nor sessions.
Chris@0 282 *
Chris@0 283 * @return bool
Chris@0 284 */
Chris@0 285 protected function isCli() {
Chris@0 286 return PHP_SAPI === 'cli';
Chris@0 287 }
Chris@0 288
Chris@0 289 /**
Chris@0 290 * Determines whether the session contains user data.
Chris@0 291 *
Chris@0 292 * @return bool
Chris@0 293 * TRUE when the session does not contain any values and therefore can be
Chris@0 294 * destroyed.
Chris@0 295 */
Chris@0 296 protected function isSessionObsolete() {
Chris@0 297 $used_session_keys = array_filter($this->getSessionDataMask());
Chris@0 298 return empty($used_session_keys);
Chris@0 299 }
Chris@0 300
Chris@0 301 /**
Chris@0 302 * Returns a map specifying which session key is containing user data.
Chris@0 303 *
Chris@0 304 * @return array
Chris@0 305 * An array where keys correspond to the session keys and the values are
Chris@0 306 * booleans specifying whether the corresponding session key contains any
Chris@0 307 * user data.
Chris@0 308 */
Chris@0 309 protected function getSessionDataMask() {
Chris@0 310 if (empty($_SESSION)) {
Chris@0 311 return [];
Chris@0 312 }
Chris@0 313
Chris@0 314 // Start out with a completely filled mask.
Chris@0 315 $mask = array_fill_keys(array_keys($_SESSION), TRUE);
Chris@0 316
Chris@0 317 // Ignore the metadata bag, it does not contain any user data.
Chris@0 318 $mask[$this->metadataBag->getStorageKey()] = FALSE;
Chris@0 319
Chris@0 320 // Ignore attribute bags when they do not contain any data.
Chris@0 321 foreach ($this->bags as $bag) {
Chris@0 322 $key = $bag->getStorageKey();
Chris@0 323 $mask[$key] = !empty($_SESSION[$key]);
Chris@0 324 }
Chris@0 325
Chris@0 326 return array_intersect_key($mask, $_SESSION);
Chris@0 327 }
Chris@0 328
Chris@0 329 /**
Chris@0 330 * Migrates the current session to a new session id.
Chris@0 331 *
Chris@0 332 * @param string $old_session_id
Chris@0 333 * The old session ID. The new session ID is $this->getId().
Chris@0 334 */
Chris@0 335 protected function migrateStoredSession($old_session_id) {
Chris@0 336 $fields = ['sid' => Crypt::hashBase64($this->getId())];
Chris@0 337 $this->connection->update('sessions')
Chris@0 338 ->fields($fields)
Chris@0 339 ->condition('sid', Crypt::hashBase64($old_session_id))
Chris@0 340 ->execute();
Chris@0 341 }
Chris@0 342
Chris@0 343 }