Completed
Push — master ( a7fa28...21fd07 )
by Nazar
04:35
created

Management::init_session()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 0
dl 0
loc 10
ccs 6
cts 6
cp 1
crap 2
rs 9.4285
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 28
	protected function init_session () {
51 28
		$Request = Request::instance();
52
		/**
53
		 * If session exists
54
		 */
55 28
		if ($Request->cookie('session')) {
56 2
			$this->user_id = $this->load();
57
		}
58 28
		$this->update_user_is();
59 28
	}
60
	/**
61
	 * Updates information about who is user accessed by methods ::guest() ::user() admin()
62
	 */
63 28
	protected function update_user_is () {
64 28
		$this->is_guest = $this->user_id == User::GUEST_ID;
65 28
		$this->is_user  = false;
66 28
		$this->is_admin = false;
67 28
		if ($this->is_guest) {
68 28
			return;
69
		}
70
		/**
71
		 * Checking of user type
72
		 */
73 20
		$groups = User::instance()->get_groups($this->user_id) ?: [];
74 20
		if (in_array(User::ADMIN_GROUP_ID, $groups)) {
75 6
			$this->is_admin = true;
76 6
			$this->is_user  = true;
77 16
		} elseif (in_array(User::USER_GROUP_ID, $groups)) {
78 16
			$this->is_user = true;
79
		}
80 20
	}
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 24
	function get_id () {
111 24
		return $this->session_id ?: false;
112
	}
113
	/**
114
	 * Returns user id of current session
115
	 *
116
	 * @return int
117
	 */
118 28
	function get_user () {
119 28
		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 2
	protected function get_internal ($session_id) {
139 2
		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 2
		if (!is_md5($session_id)) {
146 2
			return false;
147
		}
148 2
		$data = $this->cache->get(
149
			$session_id,
150 2
			function () use ($session_id) {
151 2
				$data = $this->read($session_id);
152 2
				if (!$data || $data['expire'] <= time()) {
153 2
					return false;
154
				}
155 2
				$data['data'] = $data['data'] ?: [];
156 2
				return $data;
157 2
			}
158
		);
159 2
		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 2
	protected function is_good_session ($session_data) {
169
		return
170 2
			isset($session_data['expire'], $session_data['user']) &&
171 2
			$session_data['expire'] > time() &&
172 2
			$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 20
	protected function load_initialization ($session_id, $user_id) {
260 20
		$this->session_id = $session_id;
261 20
		$this->user_id    = $user_id;
262 20
		$this->update_user_is();
263 20
		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 20
	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 20
		$data = User::instance()->get(
277
			[
278 20
				'login',
279
				'username',
280
				'language',
281
				'timezone',
282
				'status',
283
				'block_until',
284
				'avatar'
285
			],
286
			$user
287
		);
288 20
		if (!$data) {
289 2
			return false;
290
		}
291 20
		$L    = Language::prefix('system_profile_sign_in_');
292 20
		$Page = Page::instance();
293 20
		switch ($data['status']) {
294 20
			case User::STATUS_INACTIVE:
295
				/**
296
				 * If user is disabled
297
				 */
298 4
				$Page->warning($L->your_account_disabled);
299 4
				return false;
300 20
			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 20
		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 20
		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 20
	function add ($user, $delete_current_session = true) {
325 20
		$user = (int)$user ?: User::GUEST_ID;
326 20
		if ($delete_current_session && is_md5($this->session_id)) {
327 8
			$this->del_internal($this->session_id, false);
328
		}
329 20
		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 20
		$session_data = $this->create_unique_session($user);
336 20
		Response::instance()->cookie('session', $session_data['id'], $session_data['expire'], true);
337 20
		$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 20
		$Config = Config::instance();
342 20
		if (mt_rand(0, $Config->core['inserts_limit']) < $Config->core['inserts_limit'] / 100 * (100 - $Config->core['update_ratio']) / 5) {
343 3
			$this->delete_old_sessions();
344
		}
345 20
		Event::instance()->fire(
346 20
			'System/Session/add',
347
			[
348 20
				'session_data' => $session_data
349
			]
350
		);
351 20
		return $session_data['id'];
352
	}
353
	/**
354
	 * @param int $user
355
	 *
356
	 * @return array Session data
357
	 */
358 20
	protected function create_unique_session ($user) {
359 20
		$Config      = Config::instance();
360 20
		$Request     = Request::instance();
361 20
		$remote_addr = ip2hex($Request->remote_addr);
362 20
		$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 20
		$expire_in = $user == User::GUEST_ID ? min($Config->core['session_expire'], self::INITIAL_SESSION_EXPIRATION) : $Config->core['session_expire'];
367 20
		$expire    = time() + $expire_in;
368
		/**
369
		 * Create unique session
370
		 */
371
		$session_data = [
372 20
			'id'          => null,
373 20
			'user'        => $user,
374 20
			'created'     => time(),
375 20
			'expire'      => $expire,
376 20
			'user_agent'  => $Request->header('user-agent'),
377 20
			'remote_addr' => $remote_addr,
378 20
			'ip'          => $ip,
379
			'data'        => []
380
		];
381
		do {
382 20
			$session_data['id'] = md5(random_bytes(1000));
383 20
		} while (!$this->create($session_data));
384 20
		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 3
	protected function delete_old_sessions () {
439 3
		$this->db_prime()->q(
440
			"DELETE FROM `[prefix]sessions`
441 3
			WHERE `expire` < ".time()
442
		);
443 3
	}
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