Completed
Push — master ( 2b9739...8f8085 )
by Nazar
04:19
created

Session::del_data()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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