annotate core/lib/Drupal/Core/Session/SessionManager.php @ 13:5fb285c0d0e3

Update Drupal core to 8.4.7 via Composer. Security update; I *think* we've been lucky to get away with this so far, as we don't support self-registration which seems to be used by the so-called "drupalgeddon 2" attack that 8.4.5 was vulnerable to.
author Chris Cannam
date Mon, 23 Apr 2018 09:33:26 +0100
parents 4c8ae668cc8c
children 1fec387a4317
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@0 123 // necessary because \Drupal\user\SharedTempStoreFactory::get() wants to
Chris@0 124 // know the future session ID of a lazily started session in advance.
Chris@0 125 //
Chris@0 126 // @todo: With current versions of PHP there is little reason to generate
Chris@0 127 // the session id from within application code. Consider using the
Chris@0 128 // default php session id instead of generating a custom one:
Chris@0 129 // https://www.drupal.org/node/2238561
Chris@0 130 $this->setId(Crypt::randomBytesBase64());
Chris@0 131
Chris@0 132 // Initialize the session global and attach the Symfony session bags.
Chris@0 133 $_SESSION = [];
Chris@0 134 $this->loadSession();
Chris@0 135
Chris@0 136 // NativeSessionStorage::loadSession() sets started to TRUE, reset it to
Chris@0 137 // FALSE here.
Chris@0 138 $this->started = FALSE;
Chris@0 139 $this->startedLazy = TRUE;
Chris@0 140
Chris@0 141 $result = FALSE;
Chris@0 142 }
Chris@0 143
Chris@0 144 return $result;
Chris@0 145 }
Chris@0 146
Chris@0 147 /**
Chris@0 148 * Forcibly start a PHP session.
Chris@0 149 *
Chris@0 150 * @return bool
Chris@0 151 * TRUE if the session is started.
Chris@0 152 */
Chris@0 153 protected function startNow() {
Chris@0 154 if ($this->isCli()) {
Chris@0 155 return FALSE;
Chris@0 156 }
Chris@0 157
Chris@0 158 if ($this->startedLazy) {
Chris@0 159 // Save current session data before starting it, as PHP will destroy it.
Chris@0 160 $session_data = $_SESSION;
Chris@0 161 }
Chris@0 162
Chris@0 163 $result = parent::start();
Chris@0 164
Chris@0 165 // Restore session data.
Chris@0 166 if ($this->startedLazy) {
Chris@0 167 $_SESSION = $session_data;
Chris@0 168 $this->loadSession();
Chris@0 169 }
Chris@0 170
Chris@0 171 return $result;
Chris@0 172 }
Chris@0 173
Chris@0 174 /**
Chris@0 175 * {@inheritdoc}
Chris@0 176 */
Chris@0 177 public function save() {
Chris@0 178 if ($this->isCli()) {
Chris@0 179 // We don't have anything to do if we are not allowed to save the session.
Chris@0 180 return;
Chris@0 181 }
Chris@0 182
Chris@0 183 if ($this->isSessionObsolete()) {
Chris@0 184 // There is no session data to store, destroy the session if it was
Chris@0 185 // previously started.
Chris@0 186 if ($this->getSaveHandler()->isActive()) {
Chris@0 187 $this->destroy();
Chris@0 188 }
Chris@0 189 }
Chris@0 190 else {
Chris@0 191 // There is session data to store. Start the session if it is not already
Chris@0 192 // started.
Chris@0 193 if (!$this->getSaveHandler()->isActive()) {
Chris@0 194 $this->startNow();
Chris@0 195 }
Chris@0 196 // Write the session data.
Chris@0 197 parent::save();
Chris@0 198 }
Chris@0 199
Chris@0 200 $this->startedLazy = FALSE;
Chris@0 201 }
Chris@0 202
Chris@0 203 /**
Chris@0 204 * {@inheritdoc}
Chris@0 205 */
Chris@0 206 public function regenerate($destroy = FALSE, $lifetime = NULL) {
Chris@0 207 // Nothing to do if we are not allowed to change the session.
Chris@0 208 if ($this->isCli()) {
Chris@0 209 return;
Chris@0 210 }
Chris@0 211
Chris@0 212 // We do not support the optional $destroy and $lifetime parameters as long
Chris@0 213 // as #2238561 remains open.
Chris@0 214 if ($destroy || isset($lifetime)) {
Chris@0 215 throw new \InvalidArgumentException('The optional parameters $destroy and $lifetime of SessionManager::regenerate() are not supported currently');
Chris@0 216 }
Chris@0 217
Chris@0 218 if ($this->isStarted()) {
Chris@0 219 $old_session_id = $this->getId();
Chris@0 220 }
Chris@0 221 session_id(Crypt::randomBytesBase64());
Chris@0 222
Chris@0 223 $this->getMetadataBag()->clearCsrfTokenSeed();
Chris@0 224
Chris@0 225 if (isset($old_session_id)) {
Chris@0 226 $params = session_get_cookie_params();
Chris@0 227 $expire = $params['lifetime'] ? REQUEST_TIME + $params['lifetime'] : 0;
Chris@0 228 setcookie($this->getName(), $this->getId(), $expire, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
Chris@0 229 $this->migrateStoredSession($old_session_id);
Chris@0 230 }
Chris@0 231
Chris@0 232 if (!$this->isStarted()) {
Chris@0 233 // Start the session when it doesn't exist yet.
Chris@0 234 $this->startNow();
Chris@0 235 }
Chris@0 236 }
Chris@0 237
Chris@0 238 /**
Chris@0 239 * {@inheritdoc}
Chris@0 240 */
Chris@0 241 public function delete($uid) {
Chris@0 242 // Nothing to do if we are not allowed to change the session.
Chris@0 243 if (!$this->writeSafeHandler->isSessionWritable() || $this->isCli()) {
Chris@0 244 return;
Chris@0 245 }
Chris@0 246 $this->connection->delete('sessions')
Chris@0 247 ->condition('uid', $uid)
Chris@0 248 ->execute();
Chris@0 249 }
Chris@0 250
Chris@0 251 /**
Chris@0 252 * {@inheritdoc}
Chris@0 253 */
Chris@0 254 public function destroy() {
Chris@0 255 session_destroy();
Chris@0 256
Chris@0 257 // Unset the session cookies.
Chris@0 258 $session_name = $this->getName();
Chris@0 259 $cookies = $this->requestStack->getCurrentRequest()->cookies;
Chris@0 260 // setcookie() can only be called when headers are not yet sent.
Chris@0 261 if ($cookies->has($session_name) && !headers_sent()) {
Chris@0 262 $params = session_get_cookie_params();
Chris@0 263 setcookie($session_name, '', REQUEST_TIME - 3600, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
Chris@0 264 $cookies->remove($session_name);
Chris@0 265 }
Chris@0 266 }
Chris@0 267
Chris@0 268 /**
Chris@0 269 * {@inheritdoc}
Chris@0 270 */
Chris@0 271 public function setWriteSafeHandler(WriteSafeSessionHandlerInterface $handler) {
Chris@0 272 $this->writeSafeHandler = $handler;
Chris@0 273 }
Chris@0 274
Chris@0 275 /**
Chris@0 276 * Returns whether the current PHP process runs on CLI.
Chris@0 277 *
Chris@0 278 * Command line clients do not support cookies nor sessions.
Chris@0 279 *
Chris@0 280 * @return bool
Chris@0 281 */
Chris@0 282 protected function isCli() {
Chris@0 283 return PHP_SAPI === 'cli';
Chris@0 284 }
Chris@0 285
Chris@0 286 /**
Chris@0 287 * Determines whether the session contains user data.
Chris@0 288 *
Chris@0 289 * @return bool
Chris@0 290 * TRUE when the session does not contain any values and therefore can be
Chris@0 291 * destroyed.
Chris@0 292 */
Chris@0 293 protected function isSessionObsolete() {
Chris@0 294 $used_session_keys = array_filter($this->getSessionDataMask());
Chris@0 295 return empty($used_session_keys);
Chris@0 296 }
Chris@0 297
Chris@0 298 /**
Chris@0 299 * Returns a map specifying which session key is containing user data.
Chris@0 300 *
Chris@0 301 * @return array
Chris@0 302 * An array where keys correspond to the session keys and the values are
Chris@0 303 * booleans specifying whether the corresponding session key contains any
Chris@0 304 * user data.
Chris@0 305 */
Chris@0 306 protected function getSessionDataMask() {
Chris@0 307 if (empty($_SESSION)) {
Chris@0 308 return [];
Chris@0 309 }
Chris@0 310
Chris@0 311 // Start out with a completely filled mask.
Chris@0 312 $mask = array_fill_keys(array_keys($_SESSION), TRUE);
Chris@0 313
Chris@0 314 // Ignore the metadata bag, it does not contain any user data.
Chris@0 315 $mask[$this->metadataBag->getStorageKey()] = FALSE;
Chris@0 316
Chris@0 317 // Ignore attribute bags when they do not contain any data.
Chris@0 318 foreach ($this->bags as $bag) {
Chris@0 319 $key = $bag->getStorageKey();
Chris@0 320 $mask[$key] = !empty($_SESSION[$key]);
Chris@0 321 }
Chris@0 322
Chris@0 323 return array_intersect_key($mask, $_SESSION);
Chris@0 324 }
Chris@0 325
Chris@0 326 /**
Chris@0 327 * Migrates the current session to a new session id.
Chris@0 328 *
Chris@0 329 * @param string $old_session_id
Chris@0 330 * The old session ID. The new session ID is $this->getId().
Chris@0 331 */
Chris@0 332 protected function migrateStoredSession($old_session_id) {
Chris@0 333 $fields = ['sid' => Crypt::hashBase64($this->getId())];
Chris@0 334 $this->connection->update('sessions')
Chris@0 335 ->fields($fields)
Chris@0 336 ->condition('sid', Crypt::hashBase64($old_session_id))
Chris@0 337 ->execute();
Chris@0 338 }
Chris@0 339
Chris@0 340 }