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