Completed
Push — master ( b7b84b...78616b )
by Nazar
04:19
created

Session::initialize()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 1
Metric Value
c 2
b 0
f 1
dl 0
loc 17
rs 9.4285
cc 3
eloc 9
nc 3
nop 0
1
<?php
2
/**
3
 * @package   CleverStyle CMS
4
 * @author    Nazar Mokrynskyi <[email protected]>
5
 * @copyright Copyright (c) 2011-2016, 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\Language\Prefix as Language_prefix,
33
	cs\Cache\Prefix as Cache_prefix;
34
35
/**
36
 * Class responsible for current user session
37
 */
38
class Session {
39
	use
40
		CRUD,
41
		Singleton;
42
	const INIT_STATE_METHOD          = 'init';
43
	const INITIAL_SESSION_EXPIRATION = 300;
44
	/**
45
	 * Id of current session
46
	 *
47
	 * @var false|string
48
	 */
49
	protected $session_id;
50
	/**
51
	 * User id of current session
52
	 *
53
	 * @var int
54
	 */
55
	protected $user_id;
56
	/**
57
	 * @var bool
58
	 */
59
	protected $is_admin;
60
	/**
61
	 * @var bool
62
	 */
63
	protected $is_user;
64
	/**
65
	 * @var bool
66
	 */
67
	protected $is_bot;
68
	/**
69
	 * @var bool
70
	 */
71
	protected $is_guest;
72
	/**
73
	 * @var Cache_prefix
74
	 */
75
	protected $cache;
76
	/**
77
	 * @var Cache_prefix
78
	 */
79
	protected $users_cache;
80
	protected $data_model = [
81
		'id'          => 'text',
82
		'user'        => 'int:0',
83
		'created'     => 'int:0',
84
		'expire'      => 'int:0',
85
		'user_agent'  => 'text',
86
		'remote_addr' => 'text',
87
		'ip'          => 'text',
88
		'data'        => 'json'
89
	];
90
	protected $table      = '[prefix]sessions';
91
	/**
92
	 * Returns database index
93
	 *
94
	 * @return int
95
	 */
96
	protected function cdb () {
97
		return Config::instance()->module('System')->db('users');
98
	}
99
	/**
100
	 * Use cookie as source of session id, load session
101
	 *
102
	 * Bots detection is also done here
103
	 */
104
	protected function init () {
105
		if (!$this->cache) {
106
			$this->cache       = new Cache_prefix('sessions');
107
			$this->users_cache = new Cache_prefix('users');
108
		}
109
		$this->user_id    = User::GUEST_ID;
110
		$this->session_id = null;
111
		Event::instance()->fire('System/Session/init/before');
112
		$Request = Request::instance();
113
		/**
114
		 * If session exists
115
		 */
116
		if ($Request->cookie('session')) {
117
			$this->user_id = $this->load();
118
		} elseif (!$Request->api_path) {
119
			/**
120
			 * Try to detect bot, not necessary for API request
121
			 */
122
			$this->bots_detection();
123
		}
124
		$this->update_user_is();
125
		Event::instance()->fire('System/Session/init/after');
126
	}
127
	/**
128
	 * Try to determine whether visitor is a known bot, bots have no sessions
129
	 */
130
	protected function bots_detection () {
131
		$Cache   = $this->users_cache;
132
		$Request = Request::instance();
133
		/**
134
		 * For bots: login is user agent, email is IP
135
		 */
136
		$login    = $Request->header('user-agent');
137
		$email    = $Request->ip;
138
		$bot_hash = hash('sha224', $login.$email);
139
		/**
140
		 * If bot is cached
141
		 */
142
		$bot_id = $Cache->$bot_hash;
143
		/**
144
		 * If bot found in cache - exit from here
145
		 */
146
		if ($bot_id) {
147
			$this->user_id = $bot_id;
148
			return;
149
		}
150
		/**
151
		 * Try to find bot among known bots
152
		 */
153
		foreach ($this->all_bots() as $bot) {
154
			if ($this->is_this_bot($bot, $login, $email)) {
155
				/**
156
				 * If bot found - save it in cache
157
				 */
158
				$this->user_id    = $bot['id'];
159
				$Cache->$bot_hash = $bot['id'];
160
				return;
161
			}
162
		}
163
	}
164
	/**
165
	 * Get list of all bots
166
	 *
167
	 * @return array
168
	 */
169
	protected function all_bots () {
170
		return $this->users_cache->get(
171
			'bots',
172
			function () {
173
				return $this->db()->qfa(
174
					[
175
						"SELECT
176
							`u`.`id`,
177
							`u`.`login`,
178
							`u`.`email`
179
						FROM `[prefix]users` AS `u`
180
							INNER JOIN `[prefix]users_groups` AS `g`
181
						ON `u`.`id` = `g`.`id`
182
						WHERE
183
							`g`.`group`		= '%s' AND
184
							`u`.`status`	= '%s'",
185
						User::BOT_GROUP_ID,
186
						User::STATUS_ACTIVE
187
					]
188
				) ?: [];
189
			}
190
		) ?: [];
191
	}
192
	/**
193
	 * Check whether user agent and IP (login and email for bots) corresponds to passed bot data
194
	 *
195
	 * @param array  $bot
196
	 * @param string $login
197
	 * @param string $email
198
	 *
199
	 * @return bool
200
	 */
201
	protected function is_this_bot ($bot, $login, $email) {
202
		return
203
			(
204
				$bot['login'] &&
205
				(
206
					strpos($login, $bot['login']) !== false ||
207
					_preg_match($bot['login'], $login)
208
				)
209
			) ||
210
			(
211
				$bot['email'] &&
212
				(
213
					$email === $bot['email'] ||
214
					_preg_match($bot['email'], $email)
215
				)
216
			);
217
	}
218
	/**
219
	 * Updates information about who is user accessed by methods ::guest() ::bot() ::user() admin()
220
	 */
221
	protected function update_user_is () {
222
		$this->is_guest = false;
223
		$this->is_bot   = false;
224
		$this->is_user  = false;
225
		$this->is_admin = false;
226
		if ($this->user_id == User::GUEST_ID) {
227
			$this->is_guest = true;
228
			return;
229
		}
230
		/**
231
		 * Checking of user type
232
		 */
233
		$groups = User::instance()->get_groups($this->user_id) ?: [];
234
		if (in_array(User::ADMIN_GROUP_ID, $groups)) {
235
			$this->is_admin = true;
236
			$this->is_user  = true;
237
		} elseif (in_array(User::USER_GROUP_ID, $groups)) {
238
			$this->is_user = true;
239
		} elseif (in_array(User::BOT_GROUP_ID, $groups)) {
240
			$this->is_guest = true;
241
			$this->is_bot   = true;
242
		}
243
	}
244
	/**
245
	 * Is admin
246
	 *
247
	 * @return bool
248
	 */
249
	function admin () {
250
		return $this->is_admin;
251
	}
252
	/**
253
	 * Is user
254
	 *
255
	 * @return bool
256
	 */
257
	function user () {
258
		return $this->is_user;
259
	}
260
	/**
261
	 * Is guest
262
	 *
263
	 * @return bool
264
	 */
265
	function guest () {
266
		return $this->is_guest;
267
	}
268
	/**
269
	 * Is bot
270
	 *
271
	 * @return bool
272
	 */
273
	function bot () {
274
		return $this->is_bot;
275
	}
276
	/**
277
	 * Returns id of current session
278
	 *
279
	 * @return false|string
280
	 */
281
	function get_id () {
282
		if ($this->user_id == User::GUEST_ID && $this->bot()) {
283
			return false;
284
		}
285
		return $this->session_id ?: false;
286
	}
287
	/**
288
	 * Returns user id of current session
289
	 *
290
	 * @return int
291
	 */
292
	function get_user () {
293
		return $this->user_id;
294
	}
295
	/**
296
	 * Returns session details by session id
297
	 *
298
	 * @param false|null|string $session_id If `null` - loaded from `$this->session_id`, and if that also empty - from cookies
299
	 *
300
	 * @return false|array
301
	 */
302
	function get ($session_id) {
303
		$session_data = $this->get_internal($session_id);
304
		if ($session_data) {
305
			unset($session_data['data']);
306
		}
307
		return $session_data;
308
	}
309
	/**
310
	 * @param false|null|string $session_id
311
	 *
312
	 * @return false|array
313
	 */
314
	protected function get_internal ($session_id) {
315
		if (!$session_id) {
316
			if (!$this->session_id) {
317
				$this->session_id = Request::instance()->cookie('session');
318
			}
319
			$session_id = $this->session_id;
320
		}
321
		if (!is_md5($session_id)) {
322
			return false;
323
		}
324
		$data = $this->cache->get(
325
			$session_id,
326
			function () use ($session_id) {
327
				$data = $this->read($session_id);
328
				if (!$data || $data['expire'] <= time()) {
329
					return false;
330
				}
331
				$data['data'] = $data['data'] ?: [];
332
				return $data;
333
			}
334
		);
335
		return $this->is_good_session($data) ? $data : false;
336
	}
337
	/**
338
	 * Check whether session was not expired, user agent and IP corresponds to what is expected and user is actually active
339
	 *
340
	 * @param mixed $session_data
341
	 *
342
	 * @return bool
343
	 */
344
	protected function is_good_session ($session_data) {
345
		return
346
			isset($session_data['expire'], $session_data['user']) &&
347
			$session_data['expire'] > time() &&
348
			$this->is_user_active($session_data['user']);
349
	}
350
	/**
351
	 * Whether session data belongs to current visitor (user agent, remote addr and ip check)
352
	 *
353
	 * @param string $session_id
354
	 * @param string $user_agent
355
	 * @param string $remote_addr
356
	 * @param string $ip
357
	 *
358
	 * @return bool
359
	 */
360
	function is_session_owner ($session_id, $user_agent, $remote_addr, $ip) {
361
		$session_data = $this->get($session_id);
362
		return $session_data ? $this->is_session_owner_internal($session_data, $user_agent, $remote_addr, $ip) : false;
363
	}
364
	/**
365
	 * Whether session data belongs to current visitor (user agent, remote addr and ip check)
366
	 *
367
	 * @param array       $session_data
368
	 * @param string|null $user_agent
369
	 * @param string|null $remote_addr
370
	 * @param string|null $ip
371
	 *
372
	 * @return bool
373
	 */
374
	protected function is_session_owner_internal ($session_data, $user_agent = null, $remote_addr = null, $ip = null) {
375
		/**
376
		 * md5() as protection against timing attacks
377
		 */
378
		if ($user_agent === null && $remote_addr === null && $ip === null) {
379
			$Request     = Request::instance();
380
			$user_agent  = $Request->header('user-agent');
381
			$remote_addr = $Request->remote_addr;
382
			$ip          = $Request->ip;
383
		}
384
		return
385
			md5($session_data['user_agent']) == md5($user_agent) &&
386
			(
387
				!Config::instance()->core['remember_user_ip'] ||
388
				(
389
					md5($session_data['remote_addr']) == md5(ip2hex($remote_addr)) &&
390
					md5($session_data['ip']) == md5(ip2hex($ip))
391
				)
392
			);
393
	}
394
	/**
395
	 * Load session by id and return id of session owner (user), update session expiration
396
	 *
397
	 * @param false|null|string $session_id If not specified - loaded from `$this->session_id`, and if that also empty - from cookies
398
	 *
399
	 * @return int User id
400
	 */
401
	function load ($session_id = null) {
402
		if ($this->user_id == User::GUEST_ID && $this->bot()) {
403
			return User::GUEST_ID;
404
		}
405
		$session_data = $this->get_internal($session_id);
406
		if (!$session_data || !$this->is_session_owner_internal($session_data)) {
407
			$this->add(User::GUEST_ID);
408
			return User::GUEST_ID;
409
		}
410
		/**
411
		 * Updating last online time and ip
412
		 */
413
		$Config = Config::instance();
414
		$time   = time();
415
		if ($session_data['expire'] - $time < $Config->core['session_expire'] * $Config->core['update_ratio'] / 100) {
416
			$session_data['expire'] = $time + $Config->core['session_expire'];
417
			$this->update($session_data);
418
			$this->cache->set($session_data['id'], $session_data);
419
		}
420
		unset($session_data['data']);
421
		Event::instance()->fire(
422
			'System/Session/load',
423
			[
424
				'session_data' => $session_data
425
			]
426
		);
427
		return $this->load_initialization($session_data['id'], $session_data['user']);
428
	}
429
	/**
430
	 * Initialize session (set user id, session id and update who user is)
431
	 *
432
	 * @param string $session_id
433
	 * @param int    $user_id
434
	 *
435
	 * @return int User id
436
	 */
437
	protected function load_initialization ($session_id, $user_id) {
438
		$this->session_id = $session_id;
439
		$this->user_id    = $user_id;
440
		$this->update_user_is();
441
		return $user_id;
442
	}
443
	/**
444
	 * Whether profile is activated, not disabled and not blocked
445
	 *
446
	 * @param int $user
447
	 *
448
	 * @return bool
449
	 */
450
	protected function is_user_active ($user) {
451
		/**
452
		 * Optimization, more data requested than actually used here, because data will be requested later, and it would be nice to have that data cached
453
		 */
454
		$data = User::instance()->get(
455
			[
456
				'login',
457
				'username',
458
				'language',
459
				'timezone',
460
				'status',
461
				'block_until',
462
				'avatar'
463
			],
464
			$user
465
		);
466
		if (!$data) {
467
			return false;
468
		}
469
		$L    = new Language_prefix('system_profile_sign_in_');
470
		$Page = Page::instance();
471
		switch ($data['status']) {
472
			case User::STATUS_INACTIVE:
473
				/**
474
				 * If user is disabled
475
				 */
476
				$Page->warning($L->your_account_disabled);
477
				return false;
478
			case User::STATUS_NOT_ACTIVATED:
479
				/**
480
				 * If user is not active
481
				 */
482
				$Page->warning($L->your_account_is_not_active);
483
				return false;
484
		}
485
		if ($data['block_until'] > time()) {
486
			/**
487
			 * If user if blocked
488
			 */
489
			$Page->warning($L->your_account_blocked_until(date($L->_datetime, $data['block_until'])));
490
			return false;
491
		}
492
		return true;
493
	}
494
	/**
495
	 * Create the session for the user with specified id
496
	 *
497
	 * @param int  $user
498
	 * @param bool $delete_current_session
499
	 *
500
	 * @return false|string Session id on success, `false` otherwise
501
	 */
502
	function add ($user, $delete_current_session = true) {
503
		$user = (int)$user;
504
		if (!$user) {
505
			return false;
506
		}
507
		if ($delete_current_session && is_md5($this->session_id)) {
508
			$this->del_internal($this->session_id, false);
509
		}
510
		if (!$this->is_user_active($user)) {
511
			/**
512
			 * If data was not loaded or account is not active - create guest session
513
			 */
514
			return $this->add(User::GUEST_ID);
515
		}
516
		$session_data = $this->create_unique_session($user);
517
		Response::instance()->cookie('session', $session_data['id'], $session_data['expire'], true);
518
		$this->load_initialization($session_data['id'], $session_data['user']);
519
		/**
520
		 * Delete old sessions using probability and system configuration of inserts limits and update ratio
521
		 */
522
		$Config = Config::instance();
523
		if (mt_rand(0, $Config->core['inserts_limit']) < $Config->core['inserts_limit'] / 100 * (100 - $Config->core['update_ratio']) / 5) {
524
			$this->delete_old_sessions();
525
		}
526
		Event::instance()->fire(
527
			'System/Session/add',
528
			[
529
				'session_data' => $session_data
530
			]
531
		);
532
		return $session_data['id'];
533
	}
534
	/**
535
	 * @param int $user
536
	 *
537
	 * @return array Session data
538
	 */
539
	protected function create_unique_session ($user) {
540
		$Config      = Config::instance();
541
		$Request     = Request::instance();
542
		$remote_addr = ip2hex($Request->remote_addr);
543
		$ip          = ip2hex($Request->ip);
544
		/**
545
		 * Many guests open only one page (or do not store any cookies), so create guest session only for 5 minutes max initially
546
		 */
547
		$expire_in = $user == User::GUEST_ID ? min($Config->core['session_expire'], self::INITIAL_SESSION_EXPIRATION) : $Config->core['session_expire'];
548
		$expire    = time() + $expire_in;
549
		/**
550
		 * Create unique session
551
		 */
552
		$session_data = [
553
			'id'          => null,
554
			'user'        => $user,
555
			'created'     => time(),
556
			'expire'      => $expire,
557
			'user_agent'  => $Request->header('user-agent'),
558
			'remote_addr' => $remote_addr,
559
			'ip'          => $ip,
560
			'data'        => []
561
		];
562
		do {
563
			$session_data['id'] = md5(random_bytes(1000));
564
		} while (!$this->create($session_data));
565
		return $session_data;
566
	}
567
	/**
568
	 * Destroying of the session
569
	 *
570
	 * @param null|string $session_id
571
	 *
572
	 * @return bool
573
	 */
574
	function del ($session_id = null) {
575
		return (bool)$this->del_internal($session_id);
576
	}
577
	/**
578
	 * Deletion of the session
579
	 *
580
	 * @param string|null $session_id
581
	 * @param bool        $create_guest_session
582
	 *
583
	 * @return bool
584
	 */
585
	protected function del_internal ($session_id = null, $create_guest_session = true) {
586
		$session_id = $session_id ?: $this->session_id;
587
		if (!is_md5($session_id)) {
588
			return false;
589
		}
590
		Event::instance()->fire(
591
			'System/Session/del/before',
592
			[
593
				'id' => $session_id
594
			]
595
		);
596
		unset($this->cache->$session_id);
597
		$this->session_id = false;
598
		Response::instance()->cookie('session', '');
599
		$result = $this->delete($session_id);
600
		if ($result) {
601
			if ($create_guest_session) {
602
				return (bool)$this->add(User::GUEST_ID);
603
			}
604
			Event::instance()->fire(
605
				'System/Session/del/after',
606
				[
607
					'id' => $session_id
608
				]
609
			);
610
		}
611
		return (bool)$result;
612
	}
613
	/**
614
	 * Delete all old sessions from DB
615
	 */
616
	protected function delete_old_sessions () {
617
		$this->db_prime()->aq(
618
			"DELETE FROM `[prefix]sessions`
619
			WHERE `expire` < ".time()
620
		);
621
	}
622
	/**
623
	 * Deletion of all user sessions
624
	 *
625
	 * @param false|int $user If not specified - current user assumed
626
	 *
627
	 * @return bool
628
	 */
629
	function del_all ($user = false) {
630
		$user = $user ?: $this->user_id;
631
		Event::instance()->fire(
632
			'System/Session/del_all',
633
			[
634
				'id' => $user
635
			]
636
		);
637
		$sessions = $this->db_prime()->qfas(
638
			"SELECT `id`
639
			FROM `[prefix]sessions`
640
			WHERE `user` = '$user'"
641
		);
642
		if (is_array($sessions)) {
643
			if (!$this->delete($sessions)) {
644
				return false;
645
			}
646
			foreach ($sessions as $session) {
647
				unset($this->cache->$session);
648
			}
649
		}
650
		return true;
651
	}
652
	/**
653
	 * Get data, stored with session
654
	 *
655
	 * @param string      $item
656
	 * @param null|string $session_id
657
	 *
658
	 * @return false|mixed
659
	 *
660
	 */
661
	function get_data ($item, $session_id = null) {
662
		$session_data = $this->get_data_internal($session_id);
663
		return isset($session_data['data'][$item]) ? $session_data['data'][$item] : false;
664
	}
665
	/*
666
	 * @param null|string $session_id
667
	 *
668
	 * @return array|false
669
	 */
670
	protected function get_data_internal ($session_id) {
671
		$session_id = $session_id ?: $this->session_id;
672
		return is_md5($session_id) ? $this->get_internal($session_id) : false;
673
	}
674
	/**
675
	 * Store data with session
676
	 *
677
	 * @param string      $item
678
	 * @param mixed       $value
679
	 * @param null|string $session_id
680
	 *
681
	 * @return bool
682
	 *
683
	 */
684
	function set_data ($item, $value, $session_id = null) {
685
		$session_data = $this->get_data_internal($session_id);
686
		/**
687
		 * If there is no session yet - let's create one
688
		 */
689
		if (!$session_data) {
690
			$session_id   = $this->add(User::GUEST_ID);
691
			$session_data = $this->get_data_internal($session_id);
692
		}
693
		if (!isset($session_data['data'])) {
694
			return false;
695
		}
696
		$session_data['data'][$item] = $value;
697
		return $this->update($session_data) && $this->cache->del($session_id);
698
	}
699
	/**
700
	 * Delete data, stored with session
701
	 *
702
	 * @param string      $item
703
	 * @param null|string $session_id
704
	 *
705
	 * @return bool
706
	 *
707
	 */
708
	function del_data ($item, $session_id = null) {
709
		$session_data = $this->get_data_internal($session_id);
710
		if (!isset($session_data['data'][$item])) {
711
			return true;
712
		}
713
		return $this->update($session_data) && $this->cache->del($session_id);
714
	}
715
}
716