Passed
Push — 4.3 ( 9a8f2c...533e2f )
by Jerome
41:51 queued 12s
created

ElggSession::generateUserToken()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 5
nc 1
nop 1
dl 0
loc 7
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
use Elgg\Config;
4
use Elgg\Database;
5
use Elgg\Exceptions\LoginException;
6
use Elgg\Exceptions\SecurityException;
7
use Elgg\Http\DatabaseSessionHandler;
8
use Elgg\SystemMessagesService;
9
use Elgg\Traits\Debug\Profilable;
10
use Symfony\Component\HttpFoundation\Session\Session;
11
use Symfony\Component\HttpFoundation\Session\SessionInterface;
12
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
13
use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage;
14
15
/**
16
 * Elgg Session Management
17
 *
18
 * Reserved keys: last_forward_from, msg, sticky_forms, user, guid, id, code, name, username
19
 *
20
 * @see elgg_get_session()
21
 */
22
class ElggSession {
23
	
24
	use Profilable;
25
26
	/**
27
	 * @var SessionInterface
28
	 */
29
	protected $storage;
30
31
	/**
32
	 * @var \ElggUser|null
33
	 */
34
	protected $logged_in_user;
35
36
	/**
37
	 * @var bool
38
	 */
39
	protected $ignore_access = false;
40
41
	/**
42
	 * @var bool
43
	 */
44
	protected $show_disabled_entities = false;
45
46
	/**
47
	 * Constructor
48
	 *
49
	 * @param SessionInterface $storage The underlying Session implementation
50 7991
	 */
51 7991
	public function __construct(SessionInterface $storage) {
52
		$this->storage = $storage;
53
	}
54
	
55
	/**
56
	 * Initializes the session and checks for the remember me cookie
57
	 *
58
	 * @return void
59
	 *
60
	 * @internal
61 640
	 */
62
	public function boot(): void {
63 640
	
64
		$this->beginTimer([__METHOD__]);
65 640
	
66
		$this->start();
67
	
68 640
		// test whether we have a user session
69 1
		if ($this->has('guid')) {
70 1
			$user = _elgg_services()->entityTable->get($this->get('guid'), 'user');
71
			if (!$user instanceof ElggUser) {
72
				// OMG user has been deleted.
73
				$this->invalidate();
74
				
75
				// redirect to homepage
76 1
				$this->endTimer([__METHOD__]);
77
				_elgg_services()->responseFactory->redirect('');
78
			}
79 639
		} else {
80 639
			$user = _elgg_services()->persistentLogin->bootSession();
81
			if ($user instanceof ElggUser) {
82
				_elgg_services()->persistentLogin->updateTokenUsage($user);
83
			}
84
		}
85 640
	
86 1
		if ($user instanceof ElggUser) {
87 1
			$this->setLoggedInUser($user);
88
			$user->setLastAction();
89
	
90 1
			// logout a user with open session who has been banned
91
			if ($user->isBanned()) {
92
				$this->logout();
93
			}
94
		}
95 640
	
96
		$this->endTimer([__METHOD__]);
97
	}
98
99
	/**
100
	 * Start the session
101
	 *
102
	 * @return boolean
103
	 * @throws RuntimeException If session fails to start.
104
	 * @since 1.9
105 855
	 */
106
	public function start() {
107 855
		
108 3
		if ($this->storage->getId()) {
109
			return true;
110
		}
111 853
112 853
		$result = $this->storage->start();
113 853
		$this->generateSessionToken();
114
		return $result;
115
	}
116
117
	/**
118
	 * Migrates the session to a new session id while maintaining session attributes
119
	 *
120
	 * @param boolean $destroy Whether to delete the session or let gc handle clean up
121
	 * @return boolean
122
	 * @since 1.9
123 145
	 */
124 145
	public function migrate($destroy = false) {
125
		return $this->storage->migrate($destroy);
126
	}
127
128
	/**
129
	 * Invalidates the session
130
	 *
131
	 * Deletes session data and session persistence. Starts a new session.
132
	 *
133
	 * @return boolean
134
	 * @since 1.9
135 135
	 */
136 135
	public function invalidate() {
137 135
		$this->storage->clear();
138 135
		$this->logged_in_user = null;
139 135
		$result = $this->migrate(true);
140 135
		$this->generateSessionToken();
141 135
		_elgg_services()->sessionCache->clear();
142
		return $result;
143
	}
144
145
	/**
146
	 * Save the session data and closes the session
147
	 *
148
	 * @return void
149
	 * @since 3.0
150 127
	 */
151 127
	public function save() {
152
		$this->storage->save();
153
	}
154
155
	/**
156
	 * Has the session been started
157
	 *
158
	 * @return boolean
159
	 * @since 1.9
160 8685
	 */
161 8685
	public function isStarted() {
162
		return $this->storage->isStarted();
163
	}
164
165
	/**
166
	 * Get the session ID
167
	 *
168
	 * @return string
169
	 * @since 1.9
170 705
	 */
171 705
	public function getID() {
172
		return $this->storage->getId();
173
	}
174
175
	/**
176
	 * Set the session ID
177
	 *
178
	 * @param string $id Session ID
179
	 * @return void
180
	 * @since 1.9
181
	 */
182
	public function setId($id) {
183
		$this->storage->setId($id);
184
	}
185
186
	/**
187
	 * Get the session name
188
	 *
189
	 * @return string
190
	 * @since 1.9
191
	 */
192
	public function getName() {
193
		return $this->storage->getName();
194
	}
195
196
	/**
197
	 * Set the session name
198
	 *
199
	 * @param string $name Session name
200
	 * @return void
201
	 * @since 1.9
202
	 */
203
	public function setName($name) {
204
		$this->storage->setName($name);
205
	}
206
207
	/**
208
	 * Get an attribute of the session
209
	 *
210
	 * @param string $name    Name of the attribute to get
211
	 * @param mixed  $default Value to return if attribute is not set (default is null)
212
	 * @return mixed
213 8542
	 */
214 8542
	public function get($name, $default = null) {
215
		return $this->storage->get($name, $default);
216
	}
217
218
	/**
219
	 * Set an attribute
220
	 *
221
	 * @param string $name  Name of the attribute to set
222
	 * @param mixed  $value Value to be set
223
	 * @return void
224 8527
	 */
225 8527
	public function set($name, $value) {
226
		$this->storage->set($name, $value);
227
	}
228
229
	/**
230
	 * Remove an attribute
231
	 *
232
	 * @param string $name The name of the attribute to remove
233
	 * @return mixed The removed attribute
234
	 * @since 1.9
235 8685
	 */
236 8685
	public function remove($name) {
237
		return $this->storage->remove($name);
238
	}
239
240
	/**
241
	 * Has the attribute been defined
242
	 *
243
	 * @param string $name Name of the attribute
244
	 * @return bool
245
	 * @since 1.9
246 855
	 */
247 855
	public function has($name) {
248
		return $this->storage->has($name);
249
	}
250
	
251
	/**
252
	 * Log in a user
253
	 *
254
	 * @param \ElggUser $user       A valid Elgg user object
255
	 * @param boolean   $persistent Should this be a persistent login?
256
	 *
257
	 * @return void
258
	 * @throws LoginException
259
	 * @since 4.3
260 16
	 */
261 16
	public function login(\ElggUser $user, bool $persistent = false): void {
262 2
		if ($user->isBanned()) {
263
			throw new LoginException(elgg_echo('LoginException:BannedUser'));
264
		}
265 14
	
266 1
		// give plugins a chance to reject the login of this user (no user in session!)
267
		if (!elgg_trigger_before_event('login', 'user', $user)) {
268
			throw new LoginException(elgg_echo('LoginException:Unknown'));
269
		}
270 13
		
271 1
		if (!$user->isEnabled()) {
272
			// fallback if no plugin provided a reason
273
			throw new LoginException(elgg_echo('LoginException:DisabledUser'));
274
		}
275
		
276 12
		// #5933: set logged in user early so code in login event will be able to
277
		// use elgg_get_logged_in_user_entity().
278
		$this->setLoggedInUser($user);
279 12
		$this->setUserToken($user);
280
	
281
		// re-register at least the core language file for users with language other than site default
282 12
		_elgg_services()->translator->registerTranslations(\Elgg\Project\Paths::elgg() . 'languages/');
283 1
	
284
		// if remember me checked, set cookie with token and store hash(token) for user
285
		if ($persistent) {
286
			_elgg_services()->persistentLogin->makeLoginPersistent($user);
287 12
		}
288
	
289
		// User's privilege has been elevated, so change the session id (prevents session fixation)
290 12
		$this->migrate();
291
	
292 12
		// check before updating last login to determine first login
293 12
		$first_login = empty($user->last_login);
294
		
295 12
		$user->setLastLogin();
296
		elgg_reset_authentication_failures($user);
297 12
	
298 12
		elgg_trigger_after_event('login', 'user', $user);
299 12
		
300
		if ($first_login) {
301
			elgg_trigger_event('login:first', 'user', $user);
302
			$user->first_login = time();
303
		}
304
	}
305
	
306
	/**
307
	 * Log the current user out
308
	 *
309 4
	 * @return bool
310 4
	 * @since 4.3
311 4
	 */
312
	public function logout(): bool {
313
		$user = $this->getLoggedInUser();
314
		if (!$user) {
315 4
			return false;
316 1
		}
317
	
318
		if (!elgg_trigger_before_event('logout', 'user', $user)) {
319 3
			return false;
320
		}
321
	
322 3
		_elgg_services()->persistentLogin->removePersistentLogin();
323 3
	
324 3
		// pass along any messages into new session
325
		$old_msg = $this->get(SystemMessagesService::SESSION_KEY, []);
326 3
		$this->invalidate();
327
		$this->set(SystemMessagesService::SESSION_KEY, $old_msg);
328 3
	
329
		elgg_trigger_after_event('logout', 'user', $user);
330
	
331
		return true;
332
	}
333
334
	/**
335
	 * Sets the logged in user
336
	 *
337
	 * @param \ElggUser $user The user who is logged in
338 555
	 * @return void
339 555
	 * @since 1.9
340 555
	 */
341 555
	public function setLoggedInUser(\ElggUser $user) {
342 555
		$current_user = $this->getLoggedInUser();
343 555
		if ($current_user != $user) {
344 555
			$this->set('guid', $user->guid);
345 555
			$this->logged_in_user = $user;
346
			_elgg_services()->sessionCache->clear();
347
			_elgg_services()->entityCache->save($user);
348
			_elgg_services()->translator->setCurrentLanguage($user->language);
349
		}
350
	}
351
352
	/**
353
	 * Gets the logged in user
354
	 *
355 8685
	 * @return \ElggUser|null
356 8685
	 * @since 1.9
357
	 */
358
	public function getLoggedInUser() {
359
		return $this->logged_in_user;
360
	}
361
362
	/**
363
	 * Return the current logged in user by guid.
364
	 *
365 1996
	 * @see elgg_get_logged_in_user_entity()
366 1996
	 * @return int
367 1996
	 */
368
	public function getLoggedInUserGuid() {
369
		$user = $this->getLoggedInUser();
370
		return $user ? $user->guid : 0;
371
	}
372
	
373
	/**
374
	 * Returns whether or not the viewer is currently logged in and an admin user.
375 542
	 *
376 542
	 * @return bool
377
	 */
378 542
	public function isAdminLoggedIn() {
379
		$user = $this->getLoggedInUser();
380
	
381
		return $user && $user->isAdmin();
382
	}
383
	
384
	/**
385
	 * Returns whether or not the user is currently logged in
386 8685
	 *
387 8685
	 * @return bool
388
	 */
389
	public function isLoggedIn() {
390
		return (bool) $this->getLoggedInUser();
391
	}
392
393
	/**
394
	 * Remove the logged in user
395
	 *
396 8685
	 * @return void
397 8685
	 * @since 1.9
398 8685
	 */
399 8685
	public function removeLoggedInUser() {
400
		$this->logged_in_user = null;
401
		$this->remove('guid');
402
		_elgg_services()->sessionCache->clear();
403
	}
404
	
405
	/**
406
	 * Set a user specific token in the session for the currently logged in user
407 8685
	 *
408 8685
	 * This will invalidate the session on a password change of the logged in user
409
	 *
410
	 * @param \ElggUser $user the user to set the token for (default: logged in user)
411
	 *
412
	 * @return void
413
	 * @since 3.3.25
414
	 */
415
	public function setUserToken(\ElggUser $user = null): void {
416
		if (!$user instanceof \ElggUser) {
417
			$user = $this->getLoggedInUser();
418 8685
		}
419 8685
		if (!$user instanceof \ElggUser) {
420 8685
			return;
421
		}
422 8685
		
423
		$this->set('__user_token', $this->generateUserToken($user));
424
	}
425
	
426
	/**
427
	 * Validate the user token stored in the session
428
	 *
429
	 * @param \ElggUser $user the user to check for
430 8685
	 *
431 8685
	 * @return void
432
	 * @throws \Elgg\Exceptions\SecurityException
433
	 * @since 3.3.25
434
	 */
435
	public function validateUserToken(\ElggUser $user): void {
436
		$session_token = $this->get('__user_token');
437
		$user_token = $this->generateUserToken($user);
438
		
439
		if ($session_token !== $user_token) {
440
			throw new SecurityException(elgg_echo('session_expired'));
441 8685
		}
442 8685
	}
443 8685
	
444
	/**
445 8685
	 * Generate a token for a specific user
446
	 *
447
	 * @param \ElggUser $user the user to generate the token for
448
	 *
449
	 * @return string
450
	 * @since 3.3.25
451
	 */
452
	protected function generateUserToken(\ElggUser $user): string {
453
		$hmac = _elgg_services()->hmac->getHmac([
454
			$user->time_created,
455
			$user->guid,
456 854
		], 'sha256', $user->password_hash);
457
		
458 854
		return $hmac->getToken();
459 854
	}
460
461
	/**
462
	 * Get current ignore access setting.
463
	 *
464
	 * @return bool
465
	 */
466
	public function getIgnoreAccess() {
467
		return $this->ignore_access;
468
	}
469
470 7991
	/**
471 7991
	 * Set ignore access.
472 7991
	 *
473 7991
	 * @param bool $ignore Ignore access
474
	 *
475
	 * @return bool Previous setting
476
	 */
477
	public function setIgnoreAccess($ignore = true) {
478
		$prev = $this->ignore_access;
479
		$this->ignore_access = $ignore;
480
481
		return $prev;
482
	}
483
484
	/**
485
	 * Are disabled entities shown?
486 1
	 *
487 1
	 * @return bool
488
	 */
489
	public function getDisabledEntityVisibility() {
490
		return $this->show_disabled_entities;
491 1
	}
492
493 1
	/**
494 1
	 * Include disabled entities in queries
495 1
	 *
496 1
	 * @param bool $show Visibility status
497 1
	 *
498 1
	 * @return bool Previous setting
499
	 */
500
	public function setDisabledEntityVisibility($show = true) {
501 1
		$prev = $this->show_disabled_entities;
502 1
		$this->show_disabled_entities = $show;
503 1
504 1
		return $prev;
505
	}
506
507
	/**
508
	 * Adds a token to the session
509
	 *
510
	 * This is used in creation of CSRF token, and is passed to the client to allow validating tokens
511
	 * later, even if the PHP session was destroyed.
512
	 *
513
	 * @return void
514
	 */
515
	protected function generateSessionToken() {
516
		// Generate a simple token that we store server side
517
		if (!$this->has('__elgg_session')) {
518
			$this->set('__elgg_session', _elgg_services()->crypto->getRandomString(22));
519
		}
520
	}
521
522
	/**
523
	 * Get an isolated ElggSession that does not persist between requests
524
	 *
525
	 * @return self
526
	 *
527
	 * @internal
528
	 */
529
	public static function getMock() {
530
		$storage = new MockArraySessionStorage();
531
		$session = new Session($storage);
532
		return new self($session);
533
	}
534
535
	/**
536
	 * Create a session stored in the DB.
537
	 *
538
	 * @param Config   $config Config
539
	 * @param Database $db     Database
540
	 *
541
	 * @return ElggSession
542
	 *
543
	 * @internal
544
	 */
545
	public static function fromDatabase(Config $config, Database $db) {
546
		$params = $config->getCookieConfig()['session'];
547
		$options = [
548
			// session.cache_limiter is unfortunately set to "" by the NativeSessionStorage
549
			// constructor, so we must capture and inject it directly.
550
			'cache_limiter' => session_cache_limiter(),
551
552
			'name' => $params['name'],
553
			'cookie_path' => $params['path'],
554
			'cookie_domain' => $params['domain'],
555
			'cookie_secure' => $params['secure'],
556
			'cookie_httponly' => $params['httponly'],
557
			'cookie_lifetime' => $params['lifetime'],
558
		];
559
560
		$handler = new DatabaseSessionHandler($db);
561
		$storage = new NativeSessionStorage($options, $handler);
562
		$session = new Session($storage);
563
		return new self($session);
564
	}
565
566
	/**
567
	 * Create a session stored in files
568
	 *
569
	 * @param Config $config Config
570
	 *
571
	 * @return ElggSession
572
	 *
573
	 * @internal
574
	 */
575
	public static function fromFiles(Config $config) {
576
		$params = $config->getCookieConfig()['session'];
577
		$options = [
578
			// session.cache_limiter is unfortunately set to "" by the NativeSessionStorage
579
			// constructor, so we must capture and inject it directly.
580
			'cache_limiter' => session_cache_limiter(),
581
582
			'name' => $params['name'],
583
			'cookie_path' => $params['path'],
584
			'cookie_domain' => $params['domain'],
585
			'cookie_secure' => $params['secure'],
586
			'cookie_httponly' => $params['httponly'],
587
			'cookie_lifetime' => $params['lifetime'],
588
		];
589
590
		$storage = new NativeSessionStorage($options);
591
		$session = new Session($storage);
592
		return new self($session);
593
	}
594
}
595