Completed
Push — master ( 5c0dbc...42c9a9 )
by Nazar
04:27
created

Management::add()   B

Complexity

Conditions 6
Paths 12

Size

Total Lines 29
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 6

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 6
eloc 16
nc 12
nop 2
dl 0
loc 29
ccs 16
cts 16
cp 1
crap 6
rs 8.439
c 1
b 0
f 1
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 30
	protected function init_session () {
51 30
		$Request = Request::instance();
52
		/**
53
		 * If session exists
54
		 */
55 30
		if ($Request->cookie('session')) {
56 2
			$this->user_id = $this->load();
57
		}
58 30
		$this->update_user_is();
59 30
	}
60
	/**
61
	 * Updates information about who is user accessed by methods ::guest() ::user() admin()
62
	 */
63 30
	protected function update_user_is () {
64 30
		$this->is_guest = $this->user_id == User::GUEST_ID;
65 30
		$this->is_user  = false;
66 30
		$this->is_admin = false;
67 30
		if ($this->is_guest) {
68 30
			return;
69
		}
70
		/**
71
		 * Checking of user type
72
		 */
73 22
		$groups = User::instance()->get_groups($this->user_id) ?: [];
74 22
		if (in_array(User::ADMIN_GROUP_ID, $groups)) {
75 8
			$this->is_admin = true;
76 8
			$this->is_user  = true;
77 16
		} elseif (in_array(User::USER_GROUP_ID, $groups)) {
78 16
			$this->is_user = true;
79
		}
80 22
	}
81
	/**
82
	 * Is admin
83
	 *
84
	 * @return bool
85
	 */
86 10
	function admin () {
87 10
		return $this->is_admin;
88
	}
89
	/**
90
	 * Is user
91
	 *
92
	 * @return bool
93
	 */
94 2
	function user () {
95 2
		return $this->is_user;
96
	}
97
	/**
98
	 * Is guest
99
	 *
100
	 * @return bool
101
	 */
102 8
	function guest () {
103 8
		return $this->is_guest;
104
	}
105
	/**
106
	 * Returns id of current session
107
	 *
108
	 * @return false|string
109
	 */
110 26
	function get_id () {
111 26
		return $this->session_id ?: false;
112
	}
113
	/**
114
	 * Returns user id of current session
115
	 *
116
	 * @return int
117
	 */
118 30
	function get_user () {
119 30
		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 2
	function get ($session_id) {
129 2
		$session_data = $this->get_internal($session_id);
130 2
		unset($session_data['data']);
131 2
		return $session_data;
132
	}
133
	/**
134
	 * @param false|null|string $session_id
135
	 *
136
	 * @return false|array
137
	 */
138 4
	protected function get_internal ($session_id) {
139 4
		if (!$session_id) {
140 2
			if (!$this->session_id) {
141 2
				$this->session_id = Request::instance()->cookie('session');
142
			}
143 2
			$session_id = $this->session_id;
144
		}
145 4
		if (!is_md5($session_id)) {
146 2
			return false;
147
		}
148 4
		$data = $this->cache->get(
149
			$session_id,
150 4
			function () use ($session_id) {
151 4
				$data = $this->read($session_id);
152 4
				if (!$data || $data['expire'] <= time()) {
153 2
					return false;
154
				}
155 4
				$data['data'] = $data['data'] ?: [];
156 4
				return $data;
157 4
			}
158
		);
159 4
		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 4
	protected function is_good_session ($session_data) {
169
		return
170 4
			isset($session_data['expire'], $session_data['user']) &&
171 4
			$session_data['expire'] > time() &&
172 4
			$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 2
	function is_session_owner ($session_id, $user_agent, $remote_addr, $ip) {
185 2
		$session_data = $this->get($session_id);
186 2
		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 2
	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 2
		if ($user_agent === null && $remote_addr === null && $ip === null) {
203 2
			$Request     = Request::instance();
204 2
			$user_agent  = $Request->header('user-agent');
205 2
			$remote_addr = $Request->remote_addr;
206 2
			$ip          = $Request->ip;
207
		}
208
		return
209 2
			md5($session_data['user_agent']) == md5($user_agent) &&
210
			(
211 2
				!Config::instance()->core['remember_user_ip'] ||
212
				(
213 2
					md5($session_data['remote_addr']) == md5(ip2hex($remote_addr)) &&
214 2
					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 2
	function load ($session_id = null) {
226 2
		$session_data = $this->get_internal($session_id);
227 2
		if (!$session_data || !$this->is_session_owner_internal($session_data)) {
228 2
			$this->add(User::GUEST_ID);
229 2
			return User::GUEST_ID;
230
		}
231
		/**
232
		 * Updating last online time and ip
233
		 */
234 2
		$Config = Config::instance();
235 2
		$time   = time();
236 2
		if ($session_data['expire'] - $time < $Config->core['session_expire'] * $Config->core['update_ratio'] / 100) {
237 2
			$session_data['expire'] = $time + $Config->core['session_expire'];
238 2
			$this->update($session_data);
239 2
			$this->cache->set($session_data['id'], $session_data);
240 2
			Response::instance()->cookie('session', $session_data['id'], $session_data['expire'], true);
241
		}
242 2
		unset($session_data['data']);
243 2
		Event::instance()->fire(
244 2
			'System/Session/load',
245
			[
246 2
				'session_data' => $session_data
247
			]
248
		);
249 2
		return $this->load_initialization($session_data['id'], $session_data['user']);
250
	}
251
	/**
252
	 * Initialize session (set user id, session id and update who user is)
253
	 *
254
	 * @param string $session_id
255
	 * @param int    $user_id
256
	 *
257
	 * @return int User id
258
	 */
259 22
	protected function load_initialization ($session_id, $user_id) {
260 22
		$this->session_id = $session_id;
261 22
		$this->user_id    = $user_id;
262 22
		$this->update_user_is();
263 22
		return $user_id;
264
	}
265
	/**
266
	 * Whether profile is activated, not disabled and not blocked
267
	 *
268
	 * @param int $user
269
	 *
270
	 * @return bool
271
	 */
272 22
	protected function is_user_active ($user) {
273
		/**
274
		 * Optimization, more data requested than actually used here, because data will be requested later, and it would be nice to have that data cached
275
		 */
276 22
		$data = User::instance()->get(
277
			[
278 22
				'login',
279
				'username',
280
				'language',
281
				'timezone',
282
				'status',
283
				'block_until',
284
				'avatar'
285
			],
286
			$user
287
		);
288 22
		if (!$data) {
289 2
			return false;
290
		}
291 22
		$L    = Language::prefix('system_profile_sign_in_');
292 22
		$Page = Page::instance();
293 22
		switch ($data['status']) {
294 22
			case User::STATUS_INACTIVE:
295
				/**
296
				 * If user is disabled
297
				 */
298 4
				$Page->warning($L->your_account_disabled);
299 4
				return false;
300 22
			case User::STATUS_NOT_ACTIVATED:
301
				/**
302
				 * If user is not active
303
				 */
304 2
				$Page->warning($L->your_account_is_not_active);
305 2
				return false;
306
		}
307 22
		if ($data['block_until'] > time()) {
308
			/**
309
			 * If user if blocked
310
			 */
311 2
			$Page->warning($L->your_account_blocked_until(date($L->_datetime, $data['block_until'])));
312 2
			return false;
313
		}
314 22
		return true;
315
	}
316
	/**
317
	 * Create the session for the user with specified id
318
	 *
319
	 * @param int  $user
320
	 * @param bool $delete_current_session
321
	 *
322
	 * @return false|string Session id on success, `false` otherwise
323
	 */
324 22
	function add ($user, $delete_current_session = true) {
325 22
		$user = (int)$user ?: User::GUEST_ID;
326 22
		if ($delete_current_session && is_md5($this->session_id)) {
327 8
			$this->del_internal($this->session_id, false);
328
		}
329 22
		if (!$this->is_user_active($user)) {
330
			/**
331
			 * If data was not loaded or account is not active - create guest session
332
			 */
333 4
			return $this->add(User::GUEST_ID);
334
		}
335 22
		$session_data = $this->create_unique_session($user);
336 22
		Response::instance()->cookie('session', $session_data['id'], $session_data['expire'], true);
337 22
		$this->load_initialization($session_data['id'], $session_data['user']);
338
		/**
339
		 * Delete old sessions using probability and system configuration of inserts limits and update ratio
340
		 */
341 22
		$Config = Config::instance();
342 22
		if (mt_rand(0, $Config->core['inserts_limit']) < $Config->core['inserts_limit'] / 100 * (100 - $Config->core['update_ratio']) / 5) {
343 2
			$this->delete_old_sessions();
344
		}
345 22
		Event::instance()->fire(
346 22
			'System/Session/add',
347
			[
348 22
				'session_data' => $session_data
349
			]
350
		);
351 22
		return $session_data['id'];
352
	}
353
	/**
354
	 * @param int $user
355
	 *
356
	 * @return array Session data
357
	 */
358 22
	protected function create_unique_session ($user) {
359 22
		$Config      = Config::instance();
360 22
		$Request     = Request::instance();
361 22
		$remote_addr = ip2hex($Request->remote_addr);
362 22
		$ip          = ip2hex($Request->ip);
363
		/**
364
		 * Many guests open only one page (or do not store any cookies), so create guest session only for 5 minutes max initially
365
		 */
366 22
		$expire_in = $user == User::GUEST_ID ? min($Config->core['session_expire'], self::INITIAL_SESSION_EXPIRATION) : $Config->core['session_expire'];
367 22
		$expire    = time() + $expire_in;
368
		/**
369
		 * Create unique session
370
		 */
371
		$session_data = [
372 22
			'id'          => null,
373 22
			'user'        => $user,
374 22
			'created'     => time(),
375 22
			'expire'      => $expire,
376 22
			'user_agent'  => $Request->header('user-agent'),
377 22
			'remote_addr' => $remote_addr,
378 22
			'ip'          => $ip,
379
			'data'        => []
380
		];
381
		do {
382 22
			$session_data['id'] = md5(random_bytes(1000));
383 22
		} while (!$this->create($session_data));
384 22
		return $session_data;
385
	}
386
	/**
387
	 * Destroying of the session
388
	 *
389
	 * @param null|string $session_id
390
	 *
391
	 * @return bool
392
	 */
393 8
	function del ($session_id = null) {
394 8
		return (bool)$this->del_internal($session_id);
395
	}
396
	/**
397
	 * Deletion of the session
398
	 *
399
	 * @param string|null $session_id
400
	 * @param bool        $create_guest_session
401
	 *
402
	 * @return bool
403
	 */
404 10
	protected function del_internal ($session_id = null, $create_guest_session = true) {
405 10
		$session_id = $session_id ?: $this->session_id;
406 10
		if (!is_md5($session_id)) {
407 2
			return false;
408
		}
409 10
		Event::instance()->fire(
410 10
			'System/Session/del/before',
411
			[
412 10
				'id' => $session_id
413
			]
414
		);
415 10
		unset($this->cache->$session_id);
416 10
		if ($session_id == $this->session_id) {
417 10
			$this->session_id = false;
418 10
			$this->user_id    = User::GUEST_ID;
419
		}
420 10
		Response::instance()->cookie('session', '');
421 10
		$result = $this->delete($session_id);
422 10
		if ($result) {
423 10
			if ($create_guest_session) {
424 8
				return (bool)$this->add(User::GUEST_ID);
425
			}
426 8
			Event::instance()->fire(
427 8
				'System/Session/del/after',
428
				[
429 8
					'id' => $session_id
430
				]
431
			);
432
		}
433 8
		return (bool)$result;
434
	}
435
	/**
436
	 * Delete all old sessions from DB
437
	 */
438 2
	protected function delete_old_sessions () {
439 2
		$this->db_prime()->q(
440
			"DELETE FROM `[prefix]sessions`
0 ignored issues
show
Coding Style Comprehensibility introduced by
The string literal DELETE FROM `[prefix]ses...`\n WHERE `expire` < does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
441 2
			WHERE `expire` < ".time()
442
		);
443 2
	}
444
	/**
445
	 * Deletion of all user sessions
446
	 *
447
	 * @param false|int $user If not specified - current user assumed
448
	 *
449
	 * @return bool
450
	 */
451 16
	function del_all ($user = false) {
452 16
		$user = $user ?: $this->user_id;
453 16
		if ($user == User::GUEST_ID) {
454 2
			return false;
455
		}
456 16
		Event::instance()->fire(
457 16
			'System/Session/del_all',
458
			[
459 16
				'id' => $user
460
			]
461
		);
462 16
		$sessions = $this->db_prime()->qfas(
463
			"SELECT `id`
464
			FROM `[prefix]sessions`
465 16
			WHERE `user` = '$user'"
466
		);
467 16
		foreach ($sessions ?: [] as $session) {
468 6
			if (!$this->del($session)) {
469 6
				return false;
470
			}
471
		}
472 16
		return true;
473
	}
474
}
475