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