Completed
Push — master ( b80779...c4c0b1 )
by Nazar
06:16
created

Session::admin()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 3
rs 10
cc 1
eloc 2
nc 1
nop 0
1
<?php
2
/**
3
 * @package   CleverStyle CMS
4
 * @author    Nazar Mokrynskyi <[email protected]>
5
 * @copyright Copyright (c) 2011-2015, Nazar Mokrynskyi
6
 * @license   MIT License, see license.txt
7
 */
8
/**
9
 * Provides next events:
10
 *
11
 *  System/Session/init/before
12
 *
13
 *  System/Session/init/after
14
 *
15
 *  System/Session/load
16
 *  ['session_data' => $session_data]
17
 *
18
 *  System/Session/add
19
 *  ['session_data' => $session_data]
20
 *
21
 *  System/Session/del/before
22
 *  ['id' => $session_id]
23
 *
24
 *  System/Session/del/after
25
 *  ['id' => $session_id]
26
 *
27
 *  System/Session/del_all
28
 *  ['id' => $user_id]
29
 */
30
namespace cs;
31
use
32
	cs\Cache\Prefix;
33
/**
34
 * Class responsible for current user session
35
 *
36
 * @method static Session instance($check = false)
37
 */
38
class Session {
39
	use
40
		CRUD,
41
		Singleton;
42
	const INITIAL_SESSION_EXPIRATION = 300;
43
	/**
44
	 * Id of current session
45
	 *
46
	 * @var string
47
	 */
48
	protected $session_id;
49
	/**
50
	 * User id of current session
51
	 *
52
	 * @var false|int
53
	 */
54
	protected $user_id  = false;
55
	protected $is_admin = false;
56
	protected $is_user  = false;
57
	protected $is_bot   = false;
58
	protected $is_guest = false;
59
	/**
60
	 * @var Prefix
61
	 */
62
	protected $cache;
63
	/**
64
	 * @var Prefix
65
	 */
66
	protected $users_cache;
67
	protected $data_model = [
68
		'id'          => 'text',
69
		'user'        => 'int:0',
70
		'created'     => 'int:0',
71
		'expire'      => 'int:0',
72
		'user_agent'  => 'text',
73
		'remote_addr' => 'text',
74
		'ip'          => 'text',
75
		'data'        => 'json'
76
	];
77
	protected $table      = '[prefix]sessions';
78
	protected function construct () {
79
		$this->cache       = new Prefix('sessions');
80
		$this->users_cache = new Prefix('users');
81
		$this->initialize();
82
	}
83
	/**
84
	 * Returns database index
85
	 *
86
	 * @return int
87
	 */
88
	protected function cdb () {
89
		return Config::instance()->module('System')->db('users');
90
	}
91
	/**
92
	 * Use cookie as source of session id, load session
93
	 *
94
	 * Bots detection is also done here
95
	 */
96
	protected function initialize () {
97
		Event::instance()->fire('System/Session/init/before');
98
		/**
99
		 * If session exists
100
		 */
101
		if (_getcookie('session')) {
102
			$this->user_id = $this->load();
103
		} elseif (!api_path()) {
104
			/**
105
			 * Try to detect bot, not necessary for API request
106
			 */
107
			$this->bots_detection();
108
		}
109
		/**
110
		 * If session not found and visitor is not bot - create new session
111
		 */
112
		if (!$this->user_id) {
113
			$this->user_id = User::GUEST_ID;
114
			/**
115
			 * Do not create session for API requests
116
			 */
117
			if (!api_path()) {
118
				$this->add();
119
			}
120
		}
121
		$this->update_user_is();
122
		Event::instance()->fire('System/Session/init/after');
123
	}
124
	/**
125
	 * Try to determine whether visitor is a known bot, bots have no sessions
126
	 */
127
	protected function bots_detection () {
128
		$Cache = $this->users_cache;
129
		/**
130
		 * @var \cs\_SERVER $_SERVER
131
		 */
132
		/**
133
		 * For bots: login is user agent, email is IP
134
		 */
135
		$login    = $_SERVER->user_agent;
136
		$email    = $_SERVER->ip;
137
		$bot_hash = hash('sha224', $login.$email);
138
		/**
139
		 * If bot is cached
140
		 */
141
		$this->user_id = $Cache->$bot_hash;
142
		/**
143
		 * If bot found in cache - exit from here
144
		 */
145
		if ($this->user_id) {
146
			return;
147
		}
148
		/**
149
		 * Try to find bot among known bots
150
		 */
151
		foreach ($this->all_bots() as $bot) {
152
			if ($this->is_this_bot($bot, $login, $email)) {
153
				/**
154
				 * If bot found - save it in cache
155
				 */
156
				$this->user_id    = $bot['id'];
157
				$Cache->$bot_hash = $bot['id'];
158
				return;
159
			}
160
		}
161
	}
162
	/**
163
	 * Get list of all bots
164
	 *
165
	 * @return array
166
	 */
167
	protected function all_bots () {
168
		return $this->users_cache->get(
169
			'bots',
170
			function () {
171
				return $this->db()->qfa(
172
					[
173
						"SELECT
174
							`u`.`id`,
175
							`u`.`login`,
176
							`u`.`email`
177
						FROM `[prefix]users` AS `u`
178
							INNER JOIN `[prefix]users_groups` AS `g`
179
						ON `u`.`id` = `g`.`id`
180
						WHERE
181
							`g`.`group`		= '%s' AND
182
							`u`.`status`	= '%s'",
183
						User::BOT_GROUP_ID,
184
						User::STATUS_ACTIVE
185
					]
186
				) ?: [];
187
			}
188
		) ?: [];
189
	}
190
	/**
191
	 * Check whether user agent and IP (login and email for bots) corresponds to passed bot data
192
	 *
193
	 * @param array  $bot
194
	 * @param string $login
195
	 * @param string $email
196
	 *
197
	 * @return bool
198
	 */
199
	protected function is_this_bot ($bot, $login, $email) {
200
		return
201
			(
202
				$bot['login'] &&
203
				(
204
					strpos($login, $bot['login']) !== false ||
205
					_preg_match($bot['login'], $login)
206
				)
207
			) ||
208
			(
209
				$bot['email'] &&
210
				(
211
					$email === $bot['email'] ||
212
					_preg_match($bot['email'], $email)
213
				)
214
			);
215
	}
216
	/**
217
	 * Updates information about who is user accessed by methods ::guest() ::bot() ::user() admin()
218
	 */
219
	protected function update_user_is () {
220
		$this->is_guest = false;
221
		$this->is_bot   = false;
222
		$this->is_user  = false;
223
		$this->is_admin = false;
224
		if ($this->user_id == User::GUEST_ID) {
225
			$this->is_guest = true;
226
			return;
227
		}
228
		/**
229
		 * Checking of user type
230
		 */
231
		$groups = User::instance()->get_groups($this->user_id) ?: [];
232
		if (in_array(User::ADMIN_GROUP_ID, $groups)) {
233
			$this->is_admin = true;
234
			$this->is_user  = true;
235
		} elseif (in_array(User::USER_GROUP_ID, $groups)) {
236
			$this->is_user = true;
237
		} elseif (in_array(User::BOT_GROUP_ID, $groups)) {
238
			$this->is_guest = true;
239
			$this->is_bot   = true;
240
		}
241
	}
242
	/**
243
	 * Is admin
244
	 *
245
	 * @return bool
246
	 */
247
	function admin () {
248
		return $this->is_admin;
249
	}
250
	/**
251
	 * Is user
252
	 *
253
	 * @return bool
254
	 */
255
	function user () {
256
		return $this->is_user;
257
	}
258
	/**
259
	 * Is guest
260
	 *
261
	 * @return bool
262
	 */
263
	function guest () {
264
		return $this->is_guest;
265
	}
266
	/**
267
	 * Is bot
268
	 *
269
	 * @return bool
270
	 */
271
	function bot () {
272
		return $this->is_bot;
273
	}
274
	/**
275
	 * Returns id of current session
276
	 *
277
	 * @return false|string
278
	 */
279
	function get_id () {
280
		if ($this->user_id == User::GUEST_ID && $this->bot()) {
281
			return false;
282
		}
283
		return $this->session_id ?: false;
284
	}
285
	/**
286
	 * Returns user id of current session
287
	 *
288
	 * @return false|int
289
	 */
290
	function get_user () {
291
		return $this->user_id;
292
	}
293
	/**
294
	 * Returns session details by session id
295
	 *
296
	 * @param false|null|string $session_id If `null` - loaded from `$this->session_id`, and if that also empty - from cookies
297
	 *
298
	 * @return false|array
299
	 */
300
	function get ($session_id) {
301
		$session_data = $this->get_internal($session_id);
302
		if ($session_data) {
303
			unset($session_data['data']);
304
		}
305
		return $session_data;
306
	}
307
	/**
308
	 * @param false|null|string $session_id
309
	 *
310
	 * @return false|array
311
	 */
312
	protected function get_internal ($session_id) {
313
		if (!$session_id) {
314
			if (!$this->session_id) {
315
				$this->session_id = _getcookie('session');
0 ignored issues
show
Documentation Bug introduced by
It seems like _getcookie('session') can also be of type false. However, the property $session_id is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
316
			}
317
			$session_id = $this->session_id;
318
		}
319
		if (!is_md5($session_id)) {
320
			return false;
321
		}
322
		$data = $this->cache->get(
323
			$session_id,
324
			function () use ($session_id) {
325
				$data = $this->read($session_id);
326
				if (!$data || $data['expire'] <= time()) {
327
					return false;
328
				}
329
				$data['data'] = $data['data'] ?: [];
330
				return $data;
331
			}
332
		);
333
		return $this->is_good_session($data) ? $data : false;
334
	}
335
	/**
336
	 * Check whether session was not expired, user agent and IP corresponds to what is expected and user is actually active
337
	 *
338
	 * @param mixed $session_data
339
	 *
340
	 * @return bool
341
	 */
342
	protected function is_good_session ($session_data) {
343
		return
344
			is_array($session_data) &&
345
			$session_data['expire'] > time() &&
346
			$this->is_user_active($session_data['user']);
347
	}
348
	/**
349
	 * Whether session data belongs to current visitor (user agent, remote addr and ip check)
350
	 *
351
	 * @param string $session_id
352
	 * @param string $user_agent
353
	 * @param string $remote_addr
354
	 * @param string $ip
355
	 *
356
	 * @return bool
357
	 */
358
	function is_session_owner ($session_id, $user_agent, $remote_addr, $ip) {
359
		return $this->is_session_owner_internal(
360
			$this->get($session_id),
0 ignored issues
show
Security Bug introduced by
It seems like $this->get($session_id) targeting cs\Session::get() can also be of type false; however, cs\Session::is_session_owner_internal() does only seem to accept array, did you maybe forget to handle an error condition?
Loading history...
361
			$user_agent,
362
			$remote_addr,
363
			$ip
364
		);
365
	}
366
	/**
367
	 * Whether session data belongs to current visitor (user agent, remote addr and ip check)
368
	 *
369
	 * @param array       $session_data
370
	 * @param string|null $user_agent
371
	 * @param string|null $remote_addr
372
	 * @param string|null $ip
373
	 *
374
	 * @return bool
375
	 */
376
	protected function is_session_owner_internal ($session_data, $user_agent = null, $remote_addr = null, $ip = null) {
377
		/**
378
		 * md5() as protection against timing attacks
379
		 *
380
		 * @var \cs\_SERVER $_SERVER
381
		 */
382
		if ($user_agent === null) {
383
			$user_agent = $_SERVER->user_agent;
384
		}
385
		if ($remote_addr === null) {
386
			$remote_addr = $_SERVER->remote_addr;
387
		}
388
		if ($ip === null) {
389
			$ip = $_SERVER->ip;
390
		}
391
		return
392
			md5($session_data['user_agent']) == md5($user_agent) &&
393
			(
394
				!Config::instance()->core['remember_user_ip'] ||
395
				(
396
					md5($session_data['remote_addr']) == md5(ip2hex($remote_addr)) &&
397
					md5($session_data['ip']) == md5(ip2hex($ip))
398
				)
399
			);
400
	}
401
	/**
402
	 * Load session by id and return id of session owner (user), update session expiration
403
	 *
404
	 * @param false|null|string $session_id If not specified - loaded from `$this->session_id`, and if that also empty - from cookies
405
	 *
406
	 * @return int User id
407
	 */
408
	function load ($session_id = null) {
409
		if ($this->user_id == User::GUEST_ID && $this->bot()) {
410
			return User::GUEST_ID;
411
		}
412
		$session_data = $this->get_internal($session_id);
413
		if (!$session_data || !$this->is_session_owner_internal($session_data)) {
414
			$this->add(User::GUEST_ID);
415
			return User::GUEST_ID;
416
		}
417
		/**
418
		 * Updating last online time and ip
419
		 */
420
		$Config = Config::instance();
421
		$time   = time();
422
		if ($session_data['expire'] - $time < $Config->core['session_expire'] * $Config->core['update_ratio'] / 100) {
423
			$session_data['expire'] = $time + $Config->core['session_expire'];
424
			$this->update($session_data);
425
			$this->cache->set($session_data['id'], $session_data);
426
		}
427
		unset($session_data['data']);
428
		Event::instance()->fire(
429
			'System/Session/load',
430
			[
431
				'session_data' => $session_data
432
			]
433
		);
434
		return $this->load_initialization($session_data['id'], $session_data['user']);
435
	}
436
	/**
437
	 * Initialize session (set user id, session id and update who user is)
438
	 *
439
	 * @param string $session_id
440
	 * @param int    $user_id
441
	 *
442
	 * @return int User id
443
	 */
444
	protected function load_initialization ($session_id, $user_id) {
445
		$this->session_id = $session_id;
446
		$this->user_id    = $user_id;
447
		$this->update_user_is();
448
		return $user_id;
449
	}
450
	/**
451
	 * Whether profile is activated, not disabled and not blocked
452
	 *
453
	 * @param int $user
454
	 *
455
	 * @return bool
456
	 */
457
	protected function is_user_active ($user) {
458
		/**
459
		 * Optimization, more data requested than actually used here, because data will be requested later, and it would be nice to have that data cached
460
		 */
461
		$data = User::instance()->get(
462
			[
463
				'login',
464
				'username',
465
				'language',
466
				'timezone',
467
				'status',
468
				'block_until',
469
				'avatar'
470
			],
471
			$user
472
		);
473
		if (!$data) {
474
			return false;
475
		}
476
		$L    = Language::instance();
477
		$Page = Page::instance();
478
		switch ($data['status']) {
479
			case User::STATUS_INACTIVE:
480
				/**
481
				 * If user is disabled
482
				 */
483
				$Page->warning($L->your_account_disabled);
484
				return false;
485
			case User::STATUS_NOT_ACTIVATED:
486
				/**
487
				 * If user is not active
488
				 */
489
				$Page->warning($L->your_account_is_not_active);
490
				return false;
491
		}
492
		if ($data['block_until'] > time()) {
493
			/**
494
			 * If user if blocked
495
			 */
496
			$Page->warning($L->your_account_blocked_until.' '.date($L->_datetime, $data['block_until']));
497
			return false;
498
		}
499
		return true;
500
	}
501
	/**
502
	 * Create the session for the user with specified id
503
	 *
504
	 * @param false|int $user
505
	 * @param bool      $delete_current_session
506
	 *
507
	 * @return false|string Session id on success, `false` otherwise
508
	 */
509
	function add ($user = false, $delete_current_session = true) {
510
		$user = (int)$user ?: User::GUEST_ID;
511
		if ($delete_current_session && is_md5($this->session_id)) {
512
			$this->del_internal($this->session_id, false);
513
		}
514
		if (!$this->is_user_active($user)) {
515
			/**
516
			 * If data was not loaded or account is not active - create guest session
517
			 */
518
			return $this->add(User::GUEST_ID);
519
		}
520
		$session_data = $this->create_unique_session($user);
521
		_setcookie('session', $session_data['id'], $session_data['expire'], true);
522
		$this->load_initialization($session_data['id'], $session_data['user']);
523
		/**
524
		 * Delete old sessions using probability and system configuration of inserts limits and update ratio
525
		 */
526
		$Config = Config::instance();
527
		if (mt_rand(0, $Config->core['inserts_limit']) < $Config->core['inserts_limit'] / 100 * (100 - $Config->core['update_ratio']) / 5) {
528
			$this->delete_old_sessions();
529
		}
530
		Event::instance()->fire(
531
			'System/Session/add',
532
			[
533
				'session_data' => $session_data
534
			]
535
		);
536
		return $session_data['id'];
537
	}
538
	/**
539
	 * @param int $user
540
	 *
541
	 * @return array Session data
542
	 */
543
	protected function create_unique_session ($user) {
544
		$Config = Config::instance();
545
		/**
546
		 * @var \cs\_SERVER $_SERVER
547
		 */
548
		$remote_addr = ip2hex($_SERVER->remote_addr);
549
		$ip          = ip2hex($_SERVER->ip);
550
		/**
551
		 * Many guests open only one page (or do not store any cookies), so create guest session only for 5 minutes max initially
552
		 */
553
		$expire_in = $user == User::GUEST_ID ? min($Config->core['session_expire'], self::INITIAL_SESSION_EXPIRATION) : $Config->core['session_expire'];
554
		$expire    = time() + $expire_in;
555
		/**
556
		 * Create unique session
557
		 */
558
		$session_data = [
559
			'id'          => null,
560
			'user'        => $user,
561
			'created'     => time(),
562
			'expire'      => $expire,
563
			'user_agent'  => $_SERVER->user_agent,
564
			'remote_addr' => $remote_addr,
565
			'ip'          => $ip,
566
			'data'        => []
567
		];
568
		do {
569
			$session_data['id'] = md5(random_bytes(1000));
570
		} while (!$this->create($session_data));
571
		return $session_data;
572
	}
573
	/**
574
	 * Destroying of the session
575
	 *
576
	 * @param null|string $session_id
577
	 *
578
	 * @return bool
579
	 */
580
	function del ($session_id = null) {
581
		return (bool)$this->del_internal($session_id);
582
	}
583
	/**
584
	 * Deletion of the session
585
	 *
586
	 * @param string|null $session_id
587
	 * @param bool        $create_guest_session
588
	 *
589
	 * @return bool
590
	 */
591
	protected function del_internal ($session_id = null, $create_guest_session = true) {
592
		$session_id = $session_id ?: $this->session_id;
593
		if (!is_md5($session_id)) {
594
			return false;
595
		}
596
		Event::instance()->fire(
597
			'System/Session/del/before',
598
			[
599
				'id' => $session_id
600
			]
601
		);
602
		unset($this->cache->$session_id);
603
		$this->session_id = false;
0 ignored issues
show
Documentation Bug introduced by
The property $session_id was declared of type string, but false is of type false. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
604
		_setcookie('session', '');
605
		$result = $this->delete($session_id);
606
		if ($result) {
607
			if ($create_guest_session) {
608
				return (bool)$this->add(User::GUEST_ID);
609
			}
610
			Event::instance()->fire(
611
				'System/Session/del/after',
612
				[
613
					'id' => $session_id
614
				]
615
			);
616
		}
617
		return (bool)$result;
618
	}
619
	/**
620
	 * Delete all old sessions from DB
621
	 */
622
	protected function delete_old_sessions () {
623
		$this->db_prime()->aq(
624
			"DELETE FROM `[prefix]sessions`
625
			WHERE `expire` < ".time()
626
		);
627
	}
628
	/**
629
	 * Deletion of all user sessions
630
	 *
631
	 * @param false|int $user If not specified - current user assumed
632
	 *
633
	 * @return bool
634
	 */
635
	function del_all ($user = false) {
636
		$user = $user ?: $this->user_id;
637
		Event::instance()->fire(
638
			'System/Session/del_all',
639
			[
640
				'id' => $user
641
			]
642
		);
643
		$sessions = $this->db_prime()->qfas(
644
			"SELECT `id`
645
			FROM `[prefix]sessions`
646
			WHERE `user` = '$user'"
647
		);
648
		if (is_array($sessions)) {
649
			if (!$this->delete($sessions)) {
650
				return false;
651
			}
652
			foreach ($sessions as $session) {
653
				unset($this->cache->$session);
654
			}
655
		}
656
		return true;
657
	}
658
	/**
659
	 * Get data, stored with session
660
	 *
661
	 * @param string      $item
662
	 * @param null|string $session_id
663
	 *
664
	 * @return false|mixed
665
	 *
666
	 */
667
	function get_data ($item, $session_id = null) {
668
		$session_id = $session_id ?: $this->session_id;
669
		if (!is_md5($session_id)) {
670
			return false;
671
		}
672
		$session_data = $this->get_internal($session_id);
673
		return $session_data && isset($session_data['data'][$item]) ? $session_data['data'][$item] : false;
674
	}
675
	/**
676
	 * Store data with session
677
	 *
678
	 * @param string      $item
679
	 * @param mixed       $value
680
	 * @param null|string $session_id
681
	 *
682
	 * @return bool
683
	 *
684
	 */
685
	function set_data ($item, $value, $session_id = null) {
686
		$session_id = $session_id ?: $this->session_id;
687
		if (!is_md5($session_id)) {
688
			return false;
689
		}
690
		$session_data = $this->get_internal($session_id);
691
		if (!$session_data) {
692
			return false;
693
		}
694
		$session_data['data'][$item] = $value;
695
		return $this->update($session_data) && $this->cache->del($session_id);
696
	}
697
	/**
698
	 * Delete data, stored with session
699
	 *
700
	 * @param string      $item
701
	 * @param null|string $session_id
702
	 *
703
	 * @return bool
704
	 *
705
	 */
706
	function del_data ($item, $session_id = null) {
707
		$session_id = $session_id ?: $this->session_id;
708
		if (!is_md5($session_id)) {
709
			return false;
710
		}
711
		$session_data = $this->get_internal($session_id);
712
		if (!$session_data) {
713
			return false;
714
		}
715
		if (!isset($session_data['data'][$item])) {
716
			return true;
717
		}
718
		return $this->update($session_data) && $this->cache->del($session_id);
719
	}
720
}
721