Completed
Push — master ( afd267...dbe270 )
by Nazar
04:25
created

Management::init_session()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 15
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 15
rs 9.4285
cc 3
eloc 7
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
namespace cs\Session;
9
use
10
	cs\Language\Prefix as Language_prefix,
11
	cs\Config,
12
	cs\Event,
13
	cs\Page,
14
	cs\Request,
15
	cs\Response,
16
	cs\User;
17
18
/**
19
 * @method \cs\DB\_Abstract db()
20
 * @method \cs\DB\_Abstract db_prime()
21
 */
22
trait Management {
23
	/**
24
	 * Id of current session
25
	 *
26
	 * @var false|string
27
	 */
28
	protected $session_id;
29
	/**
30
	 * User id of current session
31
	 *
32
	 * @var int
33
	 */
34
	protected $user_id;
35
	/**
36
	 * @var bool
37
	 */
38
	protected $is_admin;
39
	/**
40
	 * @var bool
41
	 */
42
	protected $is_user;
43
	/**
44
	 * @var bool
45
	 */
46
	protected $is_bot;
47
	/**
48
	 * @var bool
49
	 */
50
	protected $is_guest;
51
	/**
52
	 * Use cookie as source of session id, load session
53
	 *
54
	 * Bots detection is also done here
55
	 */
56
	protected function init_session () {
57
		$Request = Request::instance();
58
		/**
59
		 * If session exists
60
		 */
61
		if ($Request->cookie('session')) {
62
			$this->user_id = $this->load();
63
		} elseif (!$Request->api_path) {
64
			/**
65
			 * Try to detect bot, not necessary for API request
66
			 */
67
			$this->bots_detection();
68
		}
69
		$this->update_user_is();
70
	}
71
	/**
72
	 * Try to determine whether visitor is a known bot, bots have no sessions
73
	 */
74
	protected function bots_detection () {
75
		$Cache   = $this->users_cache;
76
		$Request = Request::instance();
77
		/**
78
		 * For bots: login is user agent, email is IP
79
		 */
80
		$login    = $Request->header('user-agent');
81
		$email    = $Request->ip;
82
		$bot_hash = hash('sha224', $login.$email);
83
		/**
84
		 * If bot is cached
85
		 */
86
		$bot_id = $Cache->$bot_hash;
87
		/**
88
		 * If bot found in cache - exit from here
89
		 */
90
		if ($bot_id) {
91
			$this->user_id = $bot_id;
92
			return;
93
		}
94
		foreach ($this->all_bots() as $bot) {
95
			if ($this->is_this_bot($bot, $login, $email)) {
96
				$this->user_id    = $bot['id'];
97
				$Cache->$bot_hash = $bot['id'];
98
				return;
99
			}
100
		}
101
	}
102
	/**
103
	 * Get list of all bots
104
	 *
105
	 * @return array
106
	 */
107
	protected function all_bots () {
108
		return $this->users_cache->get(
109
			'bots',
110
			function () {
111
				return $this->db()->qfa(
112
					[
113
						"SELECT
114
							`u`.`id`,
115
							`u`.`login`,
116
							`u`.`email`
117
						FROM `[prefix]users` AS `u`
118
							INNER JOIN `[prefix]users_groups` AS `g`
119
						ON `u`.`id` = `g`.`id`
120
						WHERE
121
							`g`.`group`		= '%s' AND
122
							`u`.`status`	= '%s'",
123
						User::BOT_GROUP_ID,
124
						User::STATUS_ACTIVE
125
					]
126
				) ?: [];
127
			}
128
		) ?: [];
129
	}
130
	/**
131
	 * Check whether user agent and IP (login and email for bots) corresponds to passed bot data
132
	 *
133
	 * @param array  $bot
134
	 * @param string $login
135
	 * @param string $email
136
	 *
137
	 * @return bool
138
	 */
139
	protected function is_this_bot ($bot, $login, $email) {
140
		return
141
			(
142
				$bot['login'] &&
143
				(
144
					strpos($login, $bot['login']) !== false ||
145
					_preg_match($bot['login'], $login)
146
				)
147
			) ||
148
			(
149
				$bot['email'] &&
150
				(
151
					$email === $bot['email'] ||
152
					_preg_match($bot['email'], $email)
153
				)
154
			);
155
	}
156
	/**
157
	 * Updates information about who is user accessed by methods ::guest() ::bot() ::user() admin()
158
	 */
159
	protected function update_user_is () {
160
		$this->is_guest = $this->user_id == User::GUEST_ID;
161
		$this->is_bot   = false;
162
		$this->is_user  = false;
163
		$this->is_admin = false;
164
		if ($this->is_guest) {
165
			return;
166
		}
167
		/**
168
		 * Checking of user type
169
		 */
170
		$groups = User::instance()->get_groups($this->user_id) ?: [];
171
		if (in_array(User::ADMIN_GROUP_ID, $groups)) {
172
			$this->is_admin = true;
173
			$this->is_user  = true;
174
		} elseif (in_array(User::USER_GROUP_ID, $groups)) {
175
			$this->is_user = true;
176
		} elseif (in_array(User::BOT_GROUP_ID, $groups)) {
177
			$this->is_guest = true;
178
			$this->is_bot   = true;
179
		}
180
	}
181
	/**
182
	 * Is admin
183
	 *
184
	 * @return bool
185
	 */
186
	function admin () {
187
		return $this->is_admin;
188
	}
189
	/**
190
	 * Is user
191
	 *
192
	 * @return bool
193
	 */
194
	function user () {
195
		return $this->is_user;
196
	}
197
	/**
198
	 * Is guest
199
	 *
200
	 * @return bool
201
	 */
202
	function guest () {
203
		return $this->is_guest;
204
	}
205
	/**
206
	 * Is bot
207
	 *
208
	 * @return bool
209
	 */
210
	function bot () {
211
		return $this->is_bot;
212
	}
213
	/**
214
	 * Returns id of current session
215
	 *
216
	 * @return false|string
217
	 */
218
	function get_id () {
219
		if ($this->user_id == User::GUEST_ID && $this->bot()) {
220
			return false;
221
		}
222
		return $this->session_id ?: false;
223
	}
224
	/**
225
	 * Returns user id of current session
226
	 *
227
	 * @return int
228
	 */
229
	function get_user () {
230
		return $this->user_id;
231
	}
232
	/**
233
	 * Returns session details by session id
234
	 *
235
	 * @param false|null|string $session_id If `null` - loaded from `$this->session_id`, and if that also empty - from cookies
236
	 *
237
	 * @return false|array
238
	 */
239
	function get ($session_id) {
240
		$session_data = $this->get_internal($session_id);
241
		unset($session_data['data']);
242
		return $session_data;
243
	}
244
	/**
245
	 * @param false|null|string $session_id
246
	 *
247
	 * @return false|array
248
	 */
249
	protected function get_internal ($session_id) {
250
		if (!$session_id) {
251
			if (!$this->session_id) {
252
				$this->session_id = Request::instance()->cookie('session');
253
			}
254
			$session_id = $this->session_id;
255
		}
256
		if (!is_md5($session_id)) {
257
			return false;
258
		}
259
		$data = $this->cache->get(
260
			$session_id,
261
			function () use ($session_id) {
262
				$data = $this->read($session_id);
1 ignored issue
show
Bug introduced by
It seems like read() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
263
				if (!$data || $data['expire'] <= time()) {
264
					return false;
265
				}
266
				$data['data'] = $data['data'] ?: [];
267
				return $data;
268
			}
269
		);
270
		return $this->is_good_session($data) ? $data : false;
271
	}
272
	/**
273
	 * Check whether session was not expired, user agent and IP corresponds to what is expected and user is actually active
274
	 *
275
	 * @param mixed $session_data
276
	 *
277
	 * @return bool
278
	 */
279
	protected function is_good_session ($session_data) {
280
		return
281
			isset($session_data['expire'], $session_data['user']) &&
282
			$session_data['expire'] > time() &&
283
			$this->is_user_active($session_data['user']);
284
	}
285
	/**
286
	 * Whether session data belongs to current visitor (user agent, remote addr and ip check)
287
	 *
288
	 * @param string $session_id
289
	 * @param string $user_agent
290
	 * @param string $remote_addr
291
	 * @param string $ip
292
	 *
293
	 * @return bool
294
	 */
295
	function is_session_owner ($session_id, $user_agent, $remote_addr, $ip) {
296
		$session_data = $this->get($session_id);
297
		return $session_data ? $this->is_session_owner_internal($session_data, $user_agent, $remote_addr, $ip) : false;
298
	}
299
	/**
300
	 * Whether session data belongs to current visitor (user agent, remote addr and ip check)
301
	 *
302
	 * @param array       $session_data
303
	 * @param string|null $user_agent
304
	 * @param string|null $remote_addr
305
	 * @param string|null $ip
306
	 *
307
	 * @return bool
308
	 */
309
	protected function is_session_owner_internal ($session_data, $user_agent = null, $remote_addr = null, $ip = null) {
310
		/**
311
		 * md5() as protection against timing attacks
312
		 */
313
		if ($user_agent === null && $remote_addr === null && $ip === null) {
314
			$Request     = Request::instance();
315
			$user_agent  = $Request->header('user-agent');
316
			$remote_addr = $Request->remote_addr;
317
			$ip          = $Request->ip;
318
		}
319
		return
320
			md5($session_data['user_agent']) == md5($user_agent) &&
321
			(
322
				!Config::instance()->core['remember_user_ip'] ||
323
				(
324
					md5($session_data['remote_addr']) == md5(ip2hex($remote_addr)) &&
325
					md5($session_data['ip']) == md5(ip2hex($ip))
326
				)
327
			);
328
	}
329
	/**
330
	 * Load session by id and return id of session owner (user), update session expiration
331
	 *
332
	 * @param false|null|string $session_id If not specified - loaded from `$this->session_id`, and if that also empty - from cookies
333
	 *
334
	 * @return int User id
335
	 */
336
	function load ($session_id = null) {
337
		if ($this->user_id == User::GUEST_ID && $this->bot()) {
338
			return User::GUEST_ID;
339
		}
340
		$session_data = $this->get_internal($session_id);
341
		if (!$session_data || !$this->is_session_owner_internal($session_data)) {
342
			$this->add(User::GUEST_ID);
343
			return User::GUEST_ID;
344
		}
345
		/**
346
		 * Updating last online time and ip
347
		 */
348
		$Config = Config::instance();
349
		$time   = time();
350
		if ($session_data['expire'] - $time < $Config->core['session_expire'] * $Config->core['update_ratio'] / 100) {
351
			$session_data['expire'] = $time + $Config->core['session_expire'];
352
			$this->update($session_data);
1 ignored issue
show
Bug introduced by
The method update() does not exist on cs\Session\Management. Did you maybe mean update_user_is()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
353
			$this->cache->set($session_data['id'], $session_data);
354
		}
355
		unset($session_data['data']);
356
		Event::instance()->fire(
357
			'System/Session/load',
358
			[
359
				'session_data' => $session_data
360
			]
361
		);
362
		return $this->load_initialization($session_data['id'], $session_data['user']);
363
	}
364
	/**
365
	 * Initialize session (set user id, session id and update who user is)
366
	 *
367
	 * @param string $session_id
368
	 * @param int    $user_id
369
	 *
370
	 * @return int User id
371
	 */
372
	protected function load_initialization ($session_id, $user_id) {
373
		$this->session_id = $session_id;
374
		$this->user_id    = $user_id;
375
		$this->update_user_is();
376
		return $user_id;
377
	}
378
	/**
379
	 * Whether profile is activated, not disabled and not blocked
380
	 *
381
	 * @param int $user
382
	 *
383
	 * @return bool
384
	 */
385
	protected function is_user_active ($user) {
386
		/**
387
		 * Optimization, more data requested than actually used here, because data will be requested later, and it would be nice to have that data cached
388
		 */
389
		$data = User::instance()->get(
390
			[
391
				'login',
392
				'username',
393
				'language',
394
				'timezone',
395
				'status',
396
				'block_until',
397
				'avatar'
398
			],
399
			$user
400
		);
401
		if (!$data) {
402
			return false;
403
		}
404
		$L    = new Language_prefix('system_profile_sign_in_');
405
		$Page = Page::instance();
406
		switch ($data['status']) {
407
			case User::STATUS_INACTIVE:
408
				/**
409
				 * If user is disabled
410
				 */
411
				$Page->warning($L->your_account_disabled);
412
				return false;
413
			case User::STATUS_NOT_ACTIVATED:
414
				/**
415
				 * If user is not active
416
				 */
417
				$Page->warning($L->your_account_is_not_active);
418
				return false;
419
		}
420
		if ($data['block_until'] > time()) {
421
			/**
422
			 * If user if blocked
423
			 */
424
			$Page->warning($L->your_account_blocked_until(date($L->_datetime, $data['block_until'])));
425
			return false;
426
		}
427
		return true;
428
	}
429
	/**
430
	 * Create the session for the user with specified id
431
	 *
432
	 * @param int  $user
433
	 * @param bool $delete_current_session
434
	 *
435
	 * @return false|string Session id on success, `false` otherwise
436
	 */
437
	function add ($user, $delete_current_session = true) {
438
		$user = (int)$user;
439
		if (!$user) {
440
			return false;
441
		}
442
		if ($delete_current_session && is_md5($this->session_id)) {
443
			$this->del_internal($this->session_id, false);
444
		}
445
		if (!$this->is_user_active($user)) {
446
			/**
447
			 * If data was not loaded or account is not active - create guest session
448
			 */
449
			return $this->add(User::GUEST_ID);
450
		}
451
		$session_data = $this->create_unique_session($user);
452
		Response::instance()->cookie('session', $session_data['id'], $session_data['expire'], true);
453
		$this->load_initialization($session_data['id'], $session_data['user']);
454
		/**
455
		 * Delete old sessions using probability and system configuration of inserts limits and update ratio
456
		 */
457
		$Config = Config::instance();
458
		if (mt_rand(0, $Config->core['inserts_limit']) < $Config->core['inserts_limit'] / 100 * (100 - $Config->core['update_ratio']) / 5) {
459
			$this->delete_old_sessions();
460
		}
461
		Event::instance()->fire(
462
			'System/Session/add',
463
			[
464
				'session_data' => $session_data
465
			]
466
		);
467
		return $session_data['id'];
468
	}
469
	/**
470
	 * @param int $user
471
	 *
472
	 * @return array Session data
473
	 */
474
	protected function create_unique_session ($user) {
475
		$Config      = Config::instance();
476
		$Request     = Request::instance();
477
		$remote_addr = ip2hex($Request->remote_addr);
478
		$ip          = ip2hex($Request->ip);
479
		/**
480
		 * Many guests open only one page (or do not store any cookies), so create guest session only for 5 minutes max initially
481
		 */
482
		$expire_in = $user == User::GUEST_ID ? min($Config->core['session_expire'], self::INITIAL_SESSION_EXPIRATION) : $Config->core['session_expire'];
483
		$expire    = time() + $expire_in;
484
		/**
485
		 * Create unique session
486
		 */
487
		$session_data = [
488
			'id'          => null,
489
			'user'        => $user,
490
			'created'     => time(),
491
			'expire'      => $expire,
492
			'user_agent'  => $Request->header('user-agent'),
493
			'remote_addr' => $remote_addr,
494
			'ip'          => $ip,
495
			'data'        => []
496
		];
497
		do {
498
			$session_data['id'] = md5(random_bytes(1000));
499
		} while (!$this->create($session_data));
1 ignored issue
show
Bug introduced by
The method create() does not exist on cs\Session\Management. Did you maybe mean create_unique_session()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
500
		return $session_data;
501
	}
502
	/**
503
	 * Destroying of the session
504
	 *
505
	 * @param null|string $session_id
506
	 *
507
	 * @return bool
508
	 */
509
	function del ($session_id = null) {
510
		return (bool)$this->del_internal($session_id);
511
	}
512
	/**
513
	 * Deletion of the session
514
	 *
515
	 * @param string|null $session_id
516
	 * @param bool        $create_guest_session
517
	 *
518
	 * @return bool
519
	 */
520
	protected function del_internal ($session_id = null, $create_guest_session = true) {
521
		$session_id = $session_id ?: $this->session_id;
522
		if (!is_md5($session_id)) {
523
			return false;
524
		}
525
		Event::instance()->fire(
526
			'System/Session/del/before',
527
			[
528
				'id' => $session_id
529
			]
530
		);
531
		unset($this->cache->$session_id);
532
		$this->session_id = false;
533
		Response::instance()->cookie('session', '');
534
		$result = $this->delete($session_id);
1 ignored issue
show
Bug introduced by
The method delete() does not exist on cs\Session\Management. Did you maybe mean delete_old_sessions()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
535
		if ($result) {
536
			if ($create_guest_session) {
537
				return (bool)$this->add(User::GUEST_ID);
538
			}
539
			Event::instance()->fire(
540
				'System/Session/del/after',
541
				[
542
					'id' => $session_id
543
				]
544
			);
545
		}
546
		return (bool)$result;
547
	}
548
	/**
549
	 * Delete all old sessions from DB
550
	 */
551
	protected function delete_old_sessions () {
552
		$this->db_prime()->aq(
553
			"DELETE FROM `[prefix]sessions`
554
			WHERE `expire` < ".time()
555
		);
556
	}
557
	/**
558
	 * Deletion of all user sessions
559
	 *
560
	 * @param false|int $user If not specified - current user assumed
561
	 *
562
	 * @return bool
563
	 */
564
	function del_all ($user = false) {
565
		$user = $user ?: $this->user_id;
566
		Event::instance()->fire(
567
			'System/Session/del_all',
568
			[
569
				'id' => $user
570
			]
571
		);
572
		$sessions = $this->db_prime()->qfas(
573
			"SELECT `id`
574
			FROM `[prefix]sessions`
575
			WHERE `user` = '$user'"
576
		);
577
		if (is_array($sessions)) {
578
			if (!$this->delete($sessions)) {
1 ignored issue
show
Bug introduced by
The method delete() does not exist on cs\Session\Management. Did you maybe mean delete_old_sessions()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
579
				return false;
580
			}
581
			foreach ($sessions as $session) {
582
				unset($this->cache->$session);
583
			}
584
		}
585
		return true;
586
	}
587
}
588