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