Completed
Push — master ( 0b6105...cc4c1e )
by Nazar
04:59
created

Management::is_user_active()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 44
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 5.5359

Importance

Changes 0
Metric Value
cc 5
eloc 25
nc 5
nop 1
dl 0
loc 44
ccs 13
cts 18
cp 0.7221
crap 5.5359
rs 8.439
c 0
b 0
f 0
1
<?php
2
/**
3
 * @package   CleverStyle Framework
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\Config,
11
	cs\Event,
12
	cs\Language,
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_guest;
47
	/**
48
	 * Use cookie as source of session id, load session
49
	 */
50 26
	protected function init_session () {
51 26
		$Request = Request::instance();
52
		/**
53
		 * If session exists
54
		 */
55 26
		if ($Request->cookie('session')) {
56
			$this->user_id = $this->load();
57
		}
58 26
		$this->update_user_is();
59 26
	}
60
	/**
61
	 * Updates information about who is user accessed by methods ::guest() ::user() admin()
62
	 */
63 26
	protected function update_user_is () {
64 26
		$this->is_guest = $this->user_id == User::GUEST_ID;
65 26
		$this->is_user  = false;
66 26
		$this->is_admin = false;
67 26
		if ($this->is_guest) {
68 26
			return;
69
		}
70
		/**
71
		 * Checking of user type
72
		 */
73 18
		$groups = User::instance()->get_groups($this->user_id) ?: [];
74 18
		if (in_array(User::ADMIN_GROUP_ID, $groups)) {
75 4
			$this->is_admin = true;
76 4
			$this->is_user  = true;
77 14
		} elseif (in_array(User::USER_GROUP_ID, $groups)) {
78 14
			$this->is_user = true;
79
		}
80 18
	}
81
	/**
82
	 * Is admin
83
	 *
84
	 * @return bool
85
	 */
86 8
	function admin () {
87 8
		return $this->is_admin;
88
	}
89
	/**
90
	 * Is user
91
	 *
92
	 * @return bool
93
	 */
94
	function user () {
95
		return $this->is_user;
96
	}
97
	/**
98
	 * Is guest
99
	 *
100
	 * @return bool
101
	 */
102 6
	function guest () {
103 6
		return $this->is_guest;
104
	}
105
	/**
106
	 * Returns id of current session
107
	 *
108
	 * @return false|string
109
	 */
110 22
	function get_id () {
111 22
		return $this->session_id ?: false;
112
	}
113
	/**
114
	 * Returns user id of current session
115
	 *
116
	 * @return int
117
	 */
118 26
	function get_user () {
119 26
		return $this->user_id;
120
	}
121
	/**
122
	 * Returns session details by session id
123
	 *
124
	 * @param false|null|string $session_id If `null` - loaded from `$this->session_id`, and if that also empty - from cookies
125
	 *
126
	 * @return false|array
127
	 */
128
	function get ($session_id) {
129
		$session_data = $this->get_internal($session_id);
130
		unset($session_data['data']);
131
		return $session_data;
132
	}
133
	/**
134
	 * @param false|null|string $session_id
135
	 *
136
	 * @return false|array
137
	 */
138
	protected function get_internal ($session_id) {
139
		if (!$session_id) {
140
			if (!$this->session_id) {
141
				$this->session_id = Request::instance()->cookie('session');
142
			}
143
			$session_id = $this->session_id;
144
		}
145
		if (!is_md5($session_id)) {
146
			return false;
147
		}
148
		$data = $this->cache->get(
149
			$session_id,
150
			function () use ($session_id) {
151
				$data = $this->read($session_id);
152
				if (!$data || $data['expire'] <= time()) {
153
					return false;
154
				}
155
				$data['data'] = $data['data'] ?: [];
156
				return $data;
157
			}
158
		);
159
		return $this->is_good_session($data) ? $data : false;
160
	}
161
	/**
162
	 * Check whether session was not expired, user agent and IP corresponds to what is expected and user is actually active
163
	 *
164
	 * @param mixed $session_data
165
	 *
166
	 * @return bool
167
	 */
168
	protected function is_good_session ($session_data) {
169
		return
170
			isset($session_data['expire'], $session_data['user']) &&
171
			$session_data['expire'] > time() &&
172
			$this->is_user_active($session_data['user']);
173
	}
174
	/**
175
	 * Whether session data belongs to current visitor (user agent, remote addr and ip check)
176
	 *
177
	 * @param string $session_id
178
	 * @param string $user_agent
179
	 * @param string $remote_addr
180
	 * @param string $ip
181
	 *
182
	 * @return bool
183
	 */
184
	function is_session_owner ($session_id, $user_agent, $remote_addr, $ip) {
185
		$session_data = $this->get($session_id);
186
		return $session_data ? $this->is_session_owner_internal($session_data, $user_agent, $remote_addr, $ip) : false;
187
	}
188
	/**
189
	 * Whether session data belongs to current visitor (user agent, remote addr and ip check)
190
	 *
191
	 * @param array       $session_data
192
	 * @param string|null $user_agent
193
	 * @param string|null $remote_addr
194
	 * @param string|null $ip
195
	 *
196
	 * @return bool
197
	 */
198
	protected function is_session_owner_internal ($session_data, $user_agent = null, $remote_addr = null, $ip = null) {
199
		/**
200
		 * md5() as protection against timing attacks
201
		 */
202
		if ($user_agent === null && $remote_addr === null && $ip === null) {
203
			$Request     = Request::instance();
204
			$user_agent  = $Request->header('user-agent');
205
			$remote_addr = $Request->remote_addr;
206
			$ip          = $Request->ip;
207
		}
208
		return
209
			md5($session_data['user_agent']) == md5($user_agent) &&
210
			(
211
				!Config::instance()->core['remember_user_ip'] ||
212
				(
213
					md5($session_data['remote_addr']) == md5(ip2hex($remote_addr)) &&
214
					md5($session_data['ip']) == md5(ip2hex($ip))
215
				)
216
			);
217
	}
218
	/**
219
	 * Load session by id and return id of session owner (user), update session expiration
220
	 *
221
	 * @param false|null|string $session_id If not specified - loaded from `$this->session_id`, and if that also empty - from cookies
222
	 *
223
	 * @return int User id
224
	 */
225
	function load ($session_id = null) {
226
		$session_data = $this->get_internal($session_id);
227
		if (!$session_data || !$this->is_session_owner_internal($session_data)) {
228
			$this->add(User::GUEST_ID);
229
			return User::GUEST_ID;
230
		}
231
		/**
232
		 * Updating last online time and ip
233
		 */
234
		$Config = Config::instance();
235
		$time   = time();
236
		if ($session_data['expire'] - $time < $Config->core['session_expire'] * $Config->core['update_ratio'] / 100) {
237
			$session_data['expire'] = $time + $Config->core['session_expire'];
238
			$this->update($session_data);
239
			$this->cache->set($session_data['id'], $session_data);
240
		}
241
		unset($session_data['data']);
242
		Event::instance()->fire(
243
			'System/Session/load',
244
			[
245
				'session_data' => $session_data
246
			]
247
		);
248
		return $this->load_initialization($session_data['id'], $session_data['user']);
249
	}
250
	/**
251
	 * Initialize session (set user id, session id and update who user is)
252
	 *
253
	 * @param string $session_id
254
	 * @param int    $user_id
255
	 *
256
	 * @return int User id
257
	 */
258 18
	protected function load_initialization ($session_id, $user_id) {
259 18
		$this->session_id = $session_id;
260 18
		$this->user_id    = $user_id;
261 18
		$this->update_user_is();
262 18
		return $user_id;
263
	}
264
	/**
265
	 * Whether profile is activated, not disabled and not blocked
266
	 *
267
	 * @param int $user
268
	 *
269
	 * @return bool
270
	 */
271 18
	protected function is_user_active ($user) {
272
		/**
273
		 * Optimization, more data requested than actually used here, because data will be requested later, and it would be nice to have that data cached
274
		 */
275 18
		$data = User::instance()->get(
276
			[
277 18
				'login',
278
				'username',
279
				'language',
280
				'timezone',
281
				'status',
282
				'block_until',
283
				'avatar'
284
			],
285
			$user
286
		);
287 18
		if (!$data) {
288
			return false;
289
		}
290 18
		$L    = Language::prefix('system_profile_sign_in_');
291 18
		$Page = Page::instance();
292 18
		switch ($data['status']) {
293 18
			case User::STATUS_INACTIVE:
294
				/**
295
				 * If user is disabled
296
				 */
297 2
				$Page->warning($L->your_account_disabled);
298 2
				return false;
299 18
			case User::STATUS_NOT_ACTIVATED:
300
				/**
301
				 * If user is not active
302
				 */
303
				$Page->warning($L->your_account_is_not_active);
304
				return false;
305
		}
306 18
		if ($data['block_until'] > time()) {
307
			/**
308
			 * If user if blocked
309
			 */
310
			$Page->warning($L->your_account_blocked_until(date($L->_datetime, $data['block_until'])));
311
			return false;
312
		}
313 18
		return true;
314
	}
315
	/**
316
	 * Create the session for the user with specified id
317
	 *
318
	 * @param int  $user
319
	 * @param bool $delete_current_session
320
	 *
321
	 * @return false|string Session id on success, `false` otherwise
322
	 */
323 18
	function add ($user, $delete_current_session = true) {
324 18
		$user = (int)$user;
325 18
		if (!$user) {
326
			return false;
327
		}
328 18
		if ($delete_current_session && is_md5($this->session_id)) {
329 6
			$this->del_internal($this->session_id, false);
330
		}
331 18
		if (!$this->is_user_active($user)) {
332
			/**
333
			 * If data was not loaded or account is not active - create guest session
334
			 */
335 2
			return $this->add(User::GUEST_ID);
336
		}
337 18
		$session_data = $this->create_unique_session($user);
338 18
		Response::instance()->cookie('session', $session_data['id'], $session_data['expire'], true);
339 18
		$this->load_initialization($session_data['id'], $session_data['user']);
340
		/**
341
		 * Delete old sessions using probability and system configuration of inserts limits and update ratio
342
		 */
343 18
		$Config = Config::instance();
344 18
		if (mt_rand(0, $Config->core['inserts_limit']) < $Config->core['inserts_limit'] / 100 * (100 - $Config->core['update_ratio']) / 5) {
345 1
			$this->delete_old_sessions();
346
		}
347 18
		Event::instance()->fire(
348 18
			'System/Session/add',
349
			[
350 18
				'session_data' => $session_data
351
			]
352
		);
353 18
		return $session_data['id'];
354
	}
355
	/**
356
	 * @param int $user
357
	 *
358
	 * @return array Session data
359
	 */
360 18
	protected function create_unique_session ($user) {
361 18
		$Config      = Config::instance();
362 18
		$Request     = Request::instance();
363 18
		$remote_addr = ip2hex($Request->remote_addr);
364 18
		$ip          = ip2hex($Request->ip);
365
		/**
366
		 * Many guests open only one page (or do not store any cookies), so create guest session only for 5 minutes max initially
367
		 */
368 18
		$expire_in = $user == User::GUEST_ID ? min($Config->core['session_expire'], self::INITIAL_SESSION_EXPIRATION) : $Config->core['session_expire'];
369 18
		$expire    = time() + $expire_in;
370
		/**
371
		 * Create unique session
372
		 */
373
		$session_data = [
374 18
			'id'          => null,
375 18
			'user'        => $user,
376 18
			'created'     => time(),
377 18
			'expire'      => $expire,
378 18
			'user_agent'  => $Request->header('user-agent'),
379 18
			'remote_addr' => $remote_addr,
380 18
			'ip'          => $ip,
381
			'data'        => []
382
		];
383
		do {
384 18
			$session_data['id'] = md5(random_bytes(1000));
385 18
		} while (!$this->create($session_data));
386 18
		return $session_data;
387
	}
388
	/**
389
	 * Destroying of the session
390
	 *
391
	 * @param null|string $session_id
392
	 *
393
	 * @return bool
394
	 */
395 6
	function del ($session_id = null) {
396 6
		return (bool)$this->del_internal($session_id);
397
	}
398
	/**
399
	 * Deletion of the session
400
	 *
401
	 * @param string|null $session_id
402
	 * @param bool        $create_guest_session
403
	 *
404
	 * @return bool
405
	 */
406 8
	protected function del_internal ($session_id = null, $create_guest_session = true) {
407 8
		$session_id = $session_id ?: $this->session_id;
408 8
		if (!is_md5($session_id)) {
409
			return false;
410
		}
411 8
		Event::instance()->fire(
412 8
			'System/Session/del/before',
413
			[
414 8
				'id' => $session_id
415
			]
416
		);
417 8
		unset($this->cache->$session_id);
418 8
		if ($session_id == $this->session_id) {
419 8
			$this->session_id = false;
420 8
			$this->user_id    = User::GUEST_ID;
421
		}
422 8
		Response::instance()->cookie('session', '');
423 8
		$result = $this->delete($session_id);
424 8
		if ($result) {
425 8
			if ($create_guest_session) {
426 6
				return (bool)$this->add(User::GUEST_ID);
427
			}
428 6
			Event::instance()->fire(
429 6
				'System/Session/del/after',
430
				[
431 6
					'id' => $session_id
432
				]
433
			);
434
		}
435 6
		return (bool)$result;
436
	}
437
	/**
438
	 * Delete all old sessions from DB
439
	 */
440 1
	protected function delete_old_sessions () {
441 1
		$this->db_prime()->q(
442
			"DELETE FROM `[prefix]sessions`
443 1
			WHERE `expire` < ".time()
444
		);
445 1
	}
446
	/**
447
	 * Deletion of all user sessions
448
	 *
449
	 * @param false|int $user If not specified - current user assumed
450
	 *
451
	 * @return bool
452
	 */
453 14
	function del_all ($user = false) {
454 14
		$user = $user ?: $this->user_id;
455 14
		if ($user == User::GUEST_ID) {
456
			return false;
457
		}
458 14
		Event::instance()->fire(
459 14
			'System/Session/del_all',
460
			[
461 14
				'id' => $user
462
			]
463
		);
464 14
		$sessions = $this->db_prime()->qfas(
465
			"SELECT `id`
466
			FROM `[prefix]sessions`
467 14
			WHERE `user` = '$user'"
468
		);
469 14
		foreach ($sessions ?: [] as $session) {
1 ignored issue
show
Bug introduced by
The expression $sessions ?: array() of type integer|string|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
470 4
			if (!$this->del($session)) {
471 4
				return false;
472
			}
473
		}
474 14
		return true;
475
	}
476
}
477