Updated to Drupal 8.6.4, which is PHP 7.3 friendly. Also updated HTMLaw library....
[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     }
222     session_id(Crypt::randomBytesBase64());
223
224     $this->getMetadataBag()->clearCsrfTokenSeed();
225
226     if (isset($old_session_id)) {
227       $params = session_get_cookie_params();
228       $expire = $params['lifetime'] ? REQUEST_TIME + $params['lifetime'] : 0;
229       setcookie($this->getName(), $this->getId(), $expire, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
230       $this->migrateStoredSession($old_session_id);
231     }
232
233     if (!$this->isStarted()) {
234       // Start the session when it doesn't exist yet.
235       $this->startNow();
236     }
237   }
238
239   /**
240    * {@inheritdoc}
241    */
242   public function delete($uid) {
243     // Nothing to do if we are not allowed to change the session.
244     if (!$this->writeSafeHandler->isSessionWritable() || $this->isCli()) {
245       return;
246     }
247     $this->connection->delete('sessions')
248       ->condition('uid', $uid)
249       ->execute();
250   }
251
252   /**
253    * {@inheritdoc}
254    */
255   public function destroy() {
256     session_destroy();
257
258     // Unset the session cookies.
259     $session_name = $this->getName();
260     $cookies = $this->requestStack->getCurrentRequest()->cookies;
261     // setcookie() can only be called when headers are not yet sent.
262     if ($cookies->has($session_name) && !headers_sent()) {
263       $params = session_get_cookie_params();
264       setcookie($session_name, '', REQUEST_TIME - 3600, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
265       $cookies->remove($session_name);
266     }
267   }
268
269   /**
270    * {@inheritdoc}
271    */
272   public function setWriteSafeHandler(WriteSafeSessionHandlerInterface $handler) {
273     $this->writeSafeHandler = $handler;
274   }
275
276   /**
277    * Returns whether the current PHP process runs on CLI.
278    *
279    * Command line clients do not support cookies nor sessions.
280    *
281    * @return bool
282    */
283   protected function isCli() {
284     return PHP_SAPI === 'cli';
285   }
286
287   /**
288    * Determines whether the session contains user data.
289    *
290    * @return bool
291    *   TRUE when the session does not contain any values and therefore can be
292    *   destroyed.
293    */
294   protected function isSessionObsolete() {
295     $used_session_keys = array_filter($this->getSessionDataMask());
296     return empty($used_session_keys);
297   }
298
299   /**
300    * Returns a map specifying which session key is containing user data.
301    *
302    * @return array
303    *   An array where keys correspond to the session keys and the values are
304    *   booleans specifying whether the corresponding session key contains any
305    *   user data.
306    */
307   protected function getSessionDataMask() {
308     if (empty($_SESSION)) {
309       return [];
310     }
311
312     // Start out with a completely filled mask.
313     $mask = array_fill_keys(array_keys($_SESSION), TRUE);
314
315     // Ignore the metadata bag, it does not contain any user data.
316     $mask[$this->metadataBag->getStorageKey()] = FALSE;
317
318     // Ignore attribute bags when they do not contain any data.
319     foreach ($this->bags as $bag) {
320       $key = $bag->getStorageKey();
321       $mask[$key] = !empty($_SESSION[$key]);
322     }
323
324     return array_intersect_key($mask, $_SESSION);
325   }
326
327   /**
328    * Migrates the current session to a new session id.
329    *
330    * @param string $old_session_id
331    *   The old session ID. The new session ID is $this->getId().
332    */
333   protected function migrateStoredSession($old_session_id) {
334     $fields = ['sid' => Crypt::hashBase64($this->getId())];
335     $this->connection->update('sessions')
336       ->fields($fields)
337       ->condition('sid', Crypt::hashBase64($old_session_id))
338       ->execute();
339   }
340
341 }