Passed
Push — master ( a4754c...e0c6ec )
by Nazar
05:32
created

Management::load_initialization()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 4
nc 1
nop 2
dl 0
loc 5
rs 9.4285
c 0
b 0
f 0
ccs 5
cts 5
cp 1
crap 1
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 69
	protected function init_session () {
49 69
		$Request = Request::instance();
50
		/**
51
		 * If session exists
52
		 */
53 69
		if ($Request->cookie('session')) {
54 24
			$this->user_id = $this->load();
55
		}
56 69
		$this->update_user_is();
57 69
	}
58
	/**
59
	 * Updates information about who is user accessed by methods ::guest() ::user() admin()
60
	 */
61 69
	protected function update_user_is () {
62 69
		$this->is_guest = $this->user_id == User::GUEST_ID;
63 69
		$this->is_user  = false;
64 69
		$this->is_admin = false;
65 69
		if ($this->is_guest) {
66 69
			return;
67
		}
68
		/**
69
		 * Checking of user type
70
		 */
71 48
		$groups = User::instance()->get_groups($this->user_id) ?: [];
72 48
		if (in_array(User::ADMIN_GROUP_ID, $groups)) {
73 9
			$this->is_admin = true;
74 9
			$this->is_user  = true;
75 42
		} elseif (in_array(User::USER_GROUP_ID, $groups)) {
76 42
			$this->is_user = true;
77
		}
78 48
	}
79
	/**
80
	 * Is admin
81
	 *
82
	 * @return bool
83
	 */
84 15
	public function admin () {
85 15
		return $this->is_admin;
86
	}
87
	/**
88
	 * Is user
89
	 *
90
	 * @return bool
91
	 */
92 6
	public function user () {
93 6
		return $this->is_user;
94
	}
95
	/**
96
	 * Is guest
97
	 *
98
	 * @return bool
99
	 */
100 24
	public function guest () {
101 24
		return $this->is_guest;
102
	}
103
	/**
104
	 * Returns id of current session
105
	 *
106
	 * @return false|string
107
	 */
108 57
	public function get_id () {
109 57
		return $this->session_id ?: false;
110
	}
111
	/**
112
	 * Returns user id of current session
113
	 *
114
	 * @return int
115
	 */
116 69
	public function get_user () {
117 69
		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 3
	public function get ($session_id) {
127 3
		$session_data = $this->get_internal($session_id);
128 3
		unset($session_data['data']);
129 3
		return $session_data;
130
	}
131
	/**
132
	 * @param false|null|string $session_id
133
	 *
134
	 * @return false|array
135
	 */
136 69
	protected function get_internal ($session_id) {
137 69
		if (!$session_id) {
138 69
			if (!$this->session_id) {
139 69
				$this->session_id = Request::instance()->cookie('session');
140
			}
141 69
			$session_id = $this->session_id;
142
		}
143 69
		if (!is_md5($session_id)) {
144 69
			return false;
145
		}
146 27
		$data = $this->cache->get(
0 ignored issues
show
Bug Best Practice introduced by
The property cache does not exist on cs\Session\Management. Did you maybe forget to declare it?
Loading history...
147 27
			$session_id,
148 27
			function () use ($session_id) {
149 27
				$data = $this->read($session_id);
0 ignored issues
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? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

149
				/** @scrutinizer ignore-call */ 
150
    $data = $this->read($session_id);
Loading history...
150 27
				if (!$data || $data['expire'] <= time()) {
151 3
					return false;
152
				}
153 27
				$data['data'] = $data['data'] ?: [];
154 27
				return $data;
155 27
			}
156
		);
157 27
		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 27
	protected function is_good_session ($session_data) {
167
		return
168 27
			isset($session_data['expire'], $session_data['user']) &&
169 27
			$session_data['expire'] > time() &&
170 27
			$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 3
	public function is_session_owner ($session_id, $user_agent, $remote_addr, $ip) {
183 3
		$session_data = $this->get($session_id);
184 3
		return $session_data ? $this->is_session_owner_internal($session_data, $user_agent, $remote_addr, $ip) : false;
0 ignored issues
show
introduced by
The condition $session_data can never be true.
Loading history...
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 24
	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 24
		if ($user_agent === null && $remote_addr === null && $ip === null) {
201 24
			$Request     = Request::instance();
202 24
			$user_agent  = $Request->header('user-agent');
203 24
			$remote_addr = $Request->remote_addr;
204 24
			$ip          = $Request->ip;
205
		}
206
		$result =
207 24
			md5($session_data['user_agent']) == md5($user_agent) &&
208
			(
209 24
				!Config::instance()->core['remember_user_ip'] ||
210
				(
211 3
					md5($session_data['remote_addr']) == md5(ip2hex($remote_addr)) &&
212 24
					md5($session_data['ip']) == md5(ip2hex($ip))
213
				)
214
			);
215 24
		if (!$result) {
216
			// Delete session if there is a chance that it was hijacked
217 3
			$this->del($session_data['id']);
218
		}
219 24
		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 24
	public function load ($session_id = null) {
229 24
		$session_data = $this->get_internal($session_id);
230 24
		if (!$session_data || !$this->is_session_owner_internal($session_data)) {
0 ignored issues
show
introduced by
The condition ! $session_data || ! $th...internal($session_data) can never be false.
Loading history...
231 3
			$this->add(User::GUEST_ID);
232 3
			return User::GUEST_ID;
233
		}
234
		/**
235
		 * Updating last online time and ip
236
		 */
237 24
		$Config = Config::instance();
238 24
		$time   = time();
239 24
		if ($session_data['expire'] - $time < $Config->core['session_expire'] * $Config->core['update_ratio'] / 100) {
240 3
			$session_data['expire'] = $time + $Config->core['session_expire'];
241 3
			$this->update($session_data);
242 3
			$this->cache->set($session_data['id'], $session_data);
243 3
			Response::instance()->cookie('session', $session_data['id'], $session_data['expire'], true);
244
		}
245 24
		unset($session_data['data']);
246 24
		Event::instance()->fire(
247 24
			'System/Session/load',
248
			[
249 24
				'session_data' => $session_data
250
			]
251
		);
252 24
		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 48
	protected function load_initialization ($session_id, $user_id) {
263 48
		$this->session_id = $session_id;
264 48
		$this->user_id    = $user_id;
265 48
		$this->update_user_is();
266 48
		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 48
	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 48
		$data = User::instance()->get(['login', 'username', 'language', 'timezone', 'status', 'avatar'], $user);
280
		return
281 48
			$data &&
282 48
			$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 48
	public function add ($user, $delete_current_session = true) {
293 48
		$user = (int)$user ?: User::GUEST_ID;
294 48
		if ($delete_current_session && is_md5($this->session_id)) {
0 ignored issues
show
Bug introduced by
It seems like $this->session_id can also be of type false; however, parameter $string of is_md5() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

294
		if ($delete_current_session && is_md5(/** @scrutinizer ignore-type */ $this->session_id)) {
Loading history...
295 9
			$this->del($this->session_id);
0 ignored issues
show
Bug introduced by
It seems like $this->session_id can also be of type false; however, parameter $session_id of cs\Session\Management::del() does only seem to accept null|string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

295
			$this->del(/** @scrutinizer ignore-type */ $this->session_id);
Loading history...
296
		}
297 48
		if (!$this->is_user_active($user)) {
298
			/**
299
			 * If data was not loaded or account is not active - create guest session
300
			 */
301 6
			return $this->add(User::GUEST_ID);
302
		}
303 48
		$session_data = $this->create_unique_session($user);
304 48
		Response::instance()->cookie('session', $session_data['id'], $session_data['expire'], true);
0 ignored issues
show
Bug introduced by
The method cookie() does not exist on cs\False_class. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

304
		Response::instance()->/** @scrutinizer ignore-call */ cookie('session', $session_data['id'], $session_data['expire'], true);
Loading history...
305 48
		$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 48
		$Config = Config::instance();
310 48
		if (random_int(0, $Config->core['inserts_limit']) < $Config->core['inserts_limit'] / 100 * (100 - $Config->core['update_ratio']) / 5) {
311 3
			$this->delete_old_sessions();
312
		}
313 48
		Event::instance()->fire(
314 48
			'System/Session/add',
315
			[
316 48
				'session_data' => $session_data
317
			]
318
		);
319 48
		return $session_data['id'];
320
	}
321
	/**
322
	 * @param int $user
323
	 *
324
	 * @return array Session data
325
	 */
326 48
	protected function create_unique_session ($user) {
327 48
		$Config      = Config::instance();
328 48
		$Request     = Request::instance();
329 48
		$remote_addr = ip2hex($Request->remote_addr);
330 48
		$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 48
		$expire_in = $user == User::GUEST_ID ? min($Config->core['session_expire'], self::INITIAL_SESSION_EXPIRATION) : $Config->core['session_expire'];
0 ignored issues
show
Bug introduced by
The constant cs\Session\Management::INITIAL_SESSION_EXPIRATION was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
335 48
		$expire    = time() + $expire_in;
336
		/**
337
		 * Create unique session
338
		 */
339
		$session_data = [
340 48
			'id'          => null,
341 48
			'user'        => $user,
342 48
			'created'     => time(),
343 48
			'expire'      => $expire,
344 48
			'user_agent'  => $Request->header('user-agent'),
345 48
			'remote_addr' => $remote_addr,
346 48
			'ip'          => $ip,
347
			'data'        => []
348
		];
349
		do {
350 48
			$session_data['id'] = md5(random_bytes(1000));
351 48
		} while (!$this->create($session_data));
352 48
		return $session_data;
353
	}
354
	/**
355
	 * Destroying of the session
356
	 *
357
	 * @param null|string $session_id
358
	 *
359
	 * @return bool
360
	 */
361 24
	public function del ($session_id = null) {
362 24
		$session_id = $session_id ?: $this->session_id;
363 24
		if (!is_md5($session_id)) {
0 ignored issues
show
Bug introduced by
It seems like $session_id can also be of type false; however, parameter $string of is_md5() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

363
		if (!is_md5(/** @scrutinizer ignore-type */ $session_id)) {
Loading history...
364 3
			return false;
365
		}
366 24
		Event::instance()->fire(
367 24
			'System/Session/del/before',
368
			[
369 24
				'id' => $session_id
370
			]
371
		);
372 24
		$this->cache->del($session_id);
0 ignored issues
show
Bug Best Practice introduced by
The property cache does not exist on cs\Session\Management. Did you maybe forget to declare it?
Loading history...
373 24
		if ($session_id == $this->session_id) {
374 24
			$this->session_id = false;
375 24
			$this->user_id    = User::GUEST_ID;
376
		}
377 24
		if (Request::instance()->cookie('session') === $session_id) {
378 24
			Response::instance()->cookie('session', '');
379
		}
380 24
		$result = $this->delete($session_id);
0 ignored issues
show
Bug introduced by
The method delete() does not exist on cs\Session\Management. Did you maybe mean delete_old_sessions()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

380
		/** @scrutinizer ignore-call */ 
381
  $result = $this->delete($session_id);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
381 24
		if ($result) {
382 24
			Event::instance()->fire(
383 24
				'System/Session/del/after',
384
				[
385 24
					'id' => $session_id
386
				]
387
			);
388
		}
389 24
		return (bool)$result;
390
	}
391
	/**
392
	 * Delete all old sessions from DB
393
	 */
394 3
	protected function delete_old_sessions () {
395 3
		$this->db_prime()->q(
396
			'DELETE FROM `[prefix]sessions`
397 3
			WHERE `expire` < '.time()
398
		);
399 3
	}
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 42
	public function del_all ($user = false) {
408 42
		$user = (int)$user ?: $this->user_id;
409 42
		if ($user == User::GUEST_ID) {
410 3
			return false;
411
		}
412 42
		Event::instance()->fire(
413 42
			'System/Session/del_all',
414
			[
415 42
				'id' => $user
416
			]
417
		);
418 42
		$cdb   = $this->db_prime();
419
		$query =
420
			"SELECT `id`
421
			FROM `[prefix]sessions`
422 42
			WHERE `user` = '$user'";
423 42
		while ($session = $cdb->qfs($query)) {
0 ignored issues
show
Bug introduced by
$query of type string is incompatible with the type string[] expected by parameter $query of cs\DB\_Abstract::qfs(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

423
		while ($session = $cdb->qfs(/** @scrutinizer ignore-type */ $query)) {
Loading history...
424 15
			if (!$this->del($session)) {
425
				return false;
426
			}
427
		}
428 42
		return true;
429
	}
430
}
431