Completed
Push — master ( 90a9f3...e6817d )
by Nazar
06:03
created

Management::is_session_owner_internal()   C

Complexity

Conditions 8
Paths 16

Size

Total Lines 25
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 8

Importance

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