SessionManager.php 10.4 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
<?php

/**
 * @file
 * Contains \Drupal\Core\Session\SessionManager.
 */

namespace Drupal\Core\Session;

use Drupal\Component\Utility\Crypt;
use Drupal\Core\Database\Connection;
12
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
13
use Symfony\Component\HttpFoundation\RequestStack;
14
use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage;
15
16
17

/**
 * Manages user sessions.
18
19
20
21
22
23
24
25
26
27
28
29
30
 *
 * 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
31
 *   (e.g. sid-hashing) or relocated to the authentication subsystem.
32
 */
33
34
class SessionManager extends NativeSessionStorage implements SessionManagerInterface {

35
36
  use DependencySerializationTrait;

37
38
39
40
41
42
43
44
45
46
47
48
49
50
  /**
   * The request stack.
   *
   * @var \Symfony\Component\HttpFoundation\RequestStack
   */
  protected $requestStack;

  /**
   * The database connection to use.
   *
   * @var \Drupal\Core\Database\Connection
   */
  protected $connection;

51
52
53
54
55
56
57
  /**
   * The session configuration.
   *
   * @var \Drupal\Core\Session\SessionConfigurationInterface
   */
  protected $sessionConfiguration;

58
59
60
61
62
  /**
   * Whether a lazy session has been started.
   *
   * @var bool
   */
63
  protected $startedLazy;
64
65

  /**
66
   * The write safe session handler.
67
   *
68
   * @todo: This reference should be removed once all database queries
69
   *   are removed from the session manager class.
70
   *
71
   * @var \Drupal\Core\Session\WriteSafeSessionHandlerInterface
72
   */
73
  protected $writeSafeHandler;
74
75
76
77

  /**
   * Constructs a new session manager instance.
   *
78
   * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
79
80
81
   *   The request stack.
   * @param \Drupal\Core\Database\Connection $connection
   *   The database connection.
82
   * @param \Drupal\Core\Session\MetadataBag $metadata_bag
83
   *   The session metadata bag.
84
85
   * @param \Drupal\Core\Session\SessionConfigurationInterface $session_configuration
   *   The session configuration interface.
86
87
88
   * @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()
89
   */
90
  public function __construct(RequestStack $request_stack, Connection $connection, MetadataBag $metadata_bag, SessionConfigurationInterface $session_configuration, $handler = NULL) {
91
    $options = array();
92
    $this->sessionConfiguration = $session_configuration;
93
94
    $this->requestStack = $request_stack;
    $this->connection = $connection;
95

96
    parent::__construct($options, $handler, $metadata_bag);
97
98
99
100

    // @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
101
    //   https://www.drupal.org/node/2229145, when we will be using the Symfony
102
103
104
    //   session object (which registers an attribute bag with the
    //   manager upon instantiation).
    $this->bags = array();
105
106
107
108
109
  }

  /**
   * {@inheritdoc}
   */
110
  public function start() {
111
112
113
    if (($this->started || $this->startedLazy) && !$this->closed) {
      return $this->started;
    }
114

115
116
117
118
    $request = $this->requestStack->getCurrentRequest();
    $this->setOptions($this->sessionConfiguration->getOptions($request));

    if ($this->sessionConfiguration->hasSession($request)) {
119
120
121
122
      // 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.
123
      $result = $this->startNow();
124
    }
125
126
127

    if (empty($result)) {
      // Randomly generate a session identifier for this request. This is
128
129
      // necessary because \Drupal\user\SharedTempStoreFactory::get() wants to
      // know the future session ID of a lazily started session in advance.
130
131
132
133
134
      //
      // @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
135
      $this->setId(Crypt::randomBytesBase64());
136
137
138
139
140
141
142
143
144
145

      // Initialize the session global and attach the Symfony session bags.
      $_SESSION = array();
      $this->loadSession();

      // NativeSessionStorage::loadSession() sets started to TRUE, reset it to
      // FALSE here.
      $this->started = FALSE;
      $this->startedLazy = TRUE;

146
      $result = FALSE;
147
148
    }

149
150
151
152
    return $result;
  }

  /**
153
154
   * Forcibly start a PHP session.
   *
155
   * @return bool
156
   *   TRUE if the session is started.
157
   */
158
  protected function startNow() {
159
    if ($this->isCli()) {
160
161
162
163
164
165
      return FALSE;
    }

    if ($this->startedLazy) {
      // Save current session data before starting it, as PHP will destroy it.
      $session_data = $_SESSION;
166
167
    }

168
    $result = parent::start();
169
170

    // Restore session data.
171
172
    if ($this->startedLazy) {
      $_SESSION = $session_data;
173
      $this->loadSession();
174
    }
175
176

    return $result;
177
178
179
180
181
182
  }

  /**
   * {@inheritdoc}
   */
  public function save() {
183
    if ($this->isCli()) {
184
185
186
187
      // We don't have anything to do if we are not allowed to save the session.
      return;
    }

188
    if ($this->isSessionObsolete()) {
189
190
      // There is no session data to store, destroy the session if it was
      // previously started.
191
      if ($this->getSaveHandler()->isActive()) {
192
        $this->destroy();
193
194
195
196
197
      }
    }
    else {
      // There is session data to store. Start the session if it is not already
      // started.
198
      if (!$this->getSaveHandler()->isActive()) {
199
        $this->startNow();
200
201
      }
      // Write the session data.
202
      parent::save();
203
    }
204
205

    $this->startedLazy = FALSE;
206
207
208
209
210
  }

  /**
   * {@inheritdoc}
   */
211
  public function regenerate($destroy = FALSE, $lifetime = NULL) {
212
    // Nothing to do if we are not allowed to change the session.
213
    if ($this->isCli()) {
214
215
216
      return;
    }

217
218
219
220
221
222
    // 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');
    }

223
    if ($this->isStarted()) {
224
      $old_session_id = $this->getId();
225
226
227
    }
    session_id(Crypt::randomBytesBase64());

228
    $this->getMetadataBag()->clearCsrfTokenSeed();
229

230
231
232
    if (isset($old_session_id)) {
      $params = session_get_cookie_params();
      $expire = $params['lifetime'] ? REQUEST_TIME + $params['lifetime'] : 0;
233
      setcookie($this->getName(), $this->getId(), $expire, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
234
      $this->migrateStoredSession($old_session_id);
235
    }
236
237

    if (!$this->isStarted()) {
238
      // Start the session when it doesn't exist yet.
239
      $this->startNow();
240
241
242
243
244
245
246
247
    }
  }

  /**
   * {@inheritdoc}
   */
  public function delete($uid) {
    // Nothing to do if we are not allowed to change the session.
248
    if (!$this->writeSafeHandler->isSessionWritable() || $this->isCli()) {
249
250
251
252
253
254
255
      return;
    }
    $this->connection->delete('sessions')
      ->condition('uid', $uid)
      ->execute();
  }

256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
  /**
   * {@inheritdoc}
   */
  public function destroy() {
    session_destroy();

    // Unset the session cookies.
    $session_name = $this->getName();
    $cookies = $this->requestStack->getCurrentRequest()->cookies;
    if ($cookies->has($session_name)) {
      $params = session_get_cookie_params();
      setcookie($session_name, '', REQUEST_TIME - 3600, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
      $cookies->remove($session_name);
    }
  }

272
273
274
275
276
277
278
  /**
   * {@inheritdoc}
   */
  public function setWriteSafeHandler(WriteSafeSessionHandlerInterface $handler) {
    $this->writeSafeHandler = $handler;
  }

279
280
281
282
283
284
285
286
287
288
289
  /**
   * 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';
  }

290
291
292
293
294
295
296
297
  /**
   * 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() {
298
299
300
301
302
303
304
305
306
307
308
309
310
    $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() {
311
    if (empty($_SESSION)) {
312
      return array();
313
314
    }

315
316
317
318
319
320
321
322
323
    // 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();
324
      $mask[$key] = !empty($_SESSION[$key]);
325
326
327
    }

    return array_intersect_key($mask, $_SESSION);
328
329
  }

330
331
332
333
  /**
   * Migrates the current session to a new session id.
   *
   * @param string $old_session_id
334
   *   The old session ID. The new session ID is $this->getId().
335
   */
336
  protected function migrateStoredSession($old_session_id) {
337
338
339
    $fields = array('sid' => Crypt::hashBase64($this->getId()));
    $this->connection->update('sessions')
      ->fields($fields)
340
      ->condition('sid', Crypt::hashBase64($old_session_id))
341
342
343
      ->execute();
  }

344
}