Management   F
last analyzed

Complexity

Total Complexity 63

Size/Duplication

Total Lines 409
Duplicated Lines 0 %

Test Coverage

Coverage 99.39%

Importance

Changes 0
Metric Value
dl 0
loc 409
rs 3.6585
c 0
b 0
f 0
ccs 164
cts 165
cp 0.9939
wmc 63

20 Methods

Rating   Name   Duplication   Size   Complexity  
B create_unique_session() 0 27 3
B update_user_is() 0 16 5
A get() 0 4 1
A delete_old_sessions() 0 4 1
A is_good_session() 0 5 3
A guest() 0 2 1
B load() 0 25 4
C is_session_owner_internal() 0 24 8
A admin() 0 2 1
A init_session() 0 9 2
A get_user() 0 2 1
A is_session_owner() 0 3 2
B add() 0 28 6
A get_id() 0 2 2
A is_user_active() 0 8 2
B del() 0 29 6
A user() 0 2 1
C get_internal() 0 22 8
B del_all() 0 22 5
A load_initialization() 0 5 1

How to fix   Complexity   

Complex Class

Complex classes like Management often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Management, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @package CleverStyle Framework
4
 * @author  Nazar Mokrynskyi <[email protected]>
5
 * @license 0BSD
6
 */
7
namespace cs\Session;
8
use
9
	cs\Config,
10
	cs\Event,
11
	cs\Request,
12
	cs\Response,
13
	cs\User;
14
15
/**
16
 * @method \cs\DB\_Abstract db()
17
 * @method \cs\DB\_Abstract db_prime()
18
 */
19
trait Management {
20
	/**
21
	 * Id of current session
22
	 *
23
	 * @var false|string
24
	 */
25
	protected $session_id;
26
	/**
27
	 * User id of current session
28
	 *
29
	 * @var int
30
	 */
31
	protected $user_id;
32
	/**
33
	 * @var bool
34
	 */
35
	protected $is_admin;
36
	/**
37
	 * @var bool
38
	 */
39
	protected $is_user;
40
	/**
41
	 * @var bool
42
	 */
43
	protected $is_guest;
44
	/**
45
	 * Use cookie as source of session id, load session
46
	 */
47 69
	protected function init_session () {
48 69
		$Request = Request::instance();
49
		/**
50
		 * If session exists
51
		 */
52 69
		if ($Request->cookie('session')) {
53 24
			$this->user_id = $this->load();
54
		}
55 69
		$this->update_user_is();
56 69
	}
57
	/**
58
	 * Updates information about who is user accessed by methods ::guest() ::user() admin()
59
	 */
60 69
	protected function update_user_is () {
61 69
		$this->is_guest = $this->user_id == User::GUEST_ID;
62 69
		$this->is_user  = false;
63 69
		$this->is_admin = false;
64 69
		if ($this->is_guest) {
65 69
			return;
66
		}
67
		/**
68
		 * Checking of user type
69
		 */
70 48
		$groups = User::instance()->get_groups($this->user_id) ?: [];
71 48
		if (in_array(User::ADMIN_GROUP_ID, $groups)) {
72 9
			$this->is_admin = true;
73 9
			$this->is_user  = true;
74 42
		} elseif (in_array(User::USER_GROUP_ID, $groups)) {
75 42
			$this->is_user = true;
76
		}
77 48
	}
78
	/**
79
	 * Is admin
80
	 *
81
	 * @return bool
82
	 */
83 15
	public function admin () {
84 15
		return $this->is_admin;
85
	}
86
	/**
87
	 * Is user
88
	 *
89
	 * @return bool
90
	 */
91 6
	public function user () {
92 6
		return $this->is_user;
93
	}
94
	/**
95
	 * Is guest
96
	 *
97
	 * @return bool
98
	 */
99 24
	public function guest () {
100 24
		return $this->is_guest;
101
	}
102
	/**
103
	 * Returns id of current session
104
	 *
105
	 * @return false|string
106
	 */
107 57
	public function get_id () {
108 57
		return $this->session_id ?: false;
109
	}
110
	/**
111
	 * Returns user id of current session
112
	 *
113
	 * @return int
114
	 */
115 69
	public function get_user () {
116 69
		return $this->user_id;
117
	}
118
	/**
119
	 * Returns session details by session id
120
	 *
121
	 * @param false|null|string $session_id If `null` - loaded from `$this->session_id`, and if that also empty - from cookies
122
	 *
123
	 * @return false|array
124
	 */
125 3
	public function get ($session_id) {
126 3
		$session_data = $this->get_internal($session_id);
127 3
		unset($session_data['data']);
128 3
		return $session_data;
129
	}
130
	/**
131
	 * @param false|null|string $session_id
132
	 *
133
	 * @return false|array
134
	 */
135 69
	protected function get_internal ($session_id) {
136 69
		if (!$session_id) {
137 69
			if (!$this->session_id) {
138 69
				$this->session_id = Request::instance()->cookie('session');
139
			}
140 69
			$session_id = $this->session_id;
141
		}
142 69
		if (!is_md5($session_id)) {
143 69
			return false;
144
		}
145 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...
146 27
			$session_id,
147 27
			function () use ($session_id) {
148 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

148
				/** @scrutinizer ignore-call */ 
149
    $data = $this->read($session_id);
Loading history...
149 27
				if (!$data || $data['expire'] <= time()) {
150 3
					return false;
151
				}
152 27
				$data['data'] = $data['data'] ?: [];
153 27
				return $data;
154 27
			}
155
		);
156 27
		return $this->is_good_session($data) ? $data : false;
157
	}
158
	/**
159
	 * Check whether session was not expired, user agent and IP corresponds to what is expected and user is actually active
160
	 *
161
	 * @param mixed $session_data
162
	 *
163
	 * @return bool
164
	 */
165 27
	protected function is_good_session ($session_data) {
166
		return
167 27
			isset($session_data['expire'], $session_data['user']) &&
168 27
			$session_data['expire'] > time() &&
169 27
			$this->is_user_active($session_data['user']);
170
	}
171
	/**
172
	 * Whether session data belongs to current visitor (user agent, remote addr and ip check)
173
	 *
174
	 * @param string $session_id
175
	 * @param string $user_agent
176
	 * @param string $remote_addr
177
	 * @param string $ip
178
	 *
179
	 * @return bool
180
	 */
181 3
	public function is_session_owner ($session_id, $user_agent, $remote_addr, $ip) {
182 3
		$session_data = $this->get($session_id);
183 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 is always false.
Loading history...
184
	}
185
	/**
186
	 * Whether session data belongs to current visitor (user agent, remote addr and ip check)
187
	 *
188
	 * @param array       $session_data
189
	 * @param string|null $user_agent
190
	 * @param string|null $remote_addr
191
	 * @param string|null $ip
192
	 *
193
	 * @return bool
194
	 */
195 24
	protected function is_session_owner_internal ($session_data, $user_agent = null, $remote_addr = null, $ip = null) {
196
		/**
197
		 * md5() as protection against timing attacks
198
		 */
199 24
		if ($user_agent === null && $remote_addr === null && $ip === null) {
200 24
			$Request     = Request::instance();
201 24
			$user_agent  = $Request->header('user-agent');
202 24
			$remote_addr = $Request->remote_addr;
203 24
			$ip          = $Request->ip;
204
		}
205
		$result =
206 24
			md5($session_data['user_agent']) == md5($user_agent) &&
207
			(
208 24
				!Config::instance()->core['remember_user_ip'] ||
209
				(
210 3
					md5($session_data['remote_addr']) == md5(ip2hex($remote_addr)) &&
0 ignored issues
show
Bug introduced by
It seems like ip2hex($remote_addr) can also be of type false; however, parameter $str of 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

210
					md5($session_data['remote_addr']) == md5(/** @scrutinizer ignore-type */ ip2hex($remote_addr)) &&
Loading history...
211 24
					md5($session_data['ip']) == md5(ip2hex($ip))
212
				)
213
			);
214 24
		if (!$result) {
215
			// Delete session if there is a chance that it was hijacked
216 3
			$this->del($session_data['id']);
217
		}
218 24
		return $result;
219
	}
220
	/**
221
	 * Load session by id and return id of session owner (user), update session expiration
222
	 *
223
	 * @param false|null|string $session_id If not specified - loaded from `$this->session_id`, and if that also empty - from cookies
224
	 *
225
	 * @return int User id
226
	 */
227 24
	public function load ($session_id = null) {
228 24
		$session_data = $this->get_internal($session_id);
229 24
		if (!$session_data || !$this->is_session_owner_internal($session_data)) {
0 ignored issues
show
introduced by
The condition $session_data is always false.
Loading history...
230 3
			$this->add(User::GUEST_ID);
231 3
			return User::GUEST_ID;
232
		}
233
		/**
234
		 * Updating last online time and ip
235
		 */
236 24
		$Config = Config::instance();
237 24
		$time   = time();
238 24
		if ($session_data['expire'] - $time < $Config->core['session_expire'] * $Config->core['update_ratio'] / 100) {
239 3
			$session_data['expire'] = $time + $Config->core['session_expire'];
240 3
			$this->update($session_data);
241 3
			$this->cache->set($session_data['id'], $session_data);
242 3
			Response::instance()->cookie('session', $session_data['id'], $session_data['expire'], true);
243
		}
244 24
		unset($session_data['data']);
245 24
		Event::instance()->fire(
246 24
			'System/Session/load',
247
			[
248 24
				'session_data' => $session_data
249
			]
250
		);
251 24
		return $this->load_initialization($session_data['id'], $session_data['user']);
252
	}
253
	/**
254
	 * Initialize session (set user id, session id and update who user is)
255
	 *
256
	 * @param string $session_id
257
	 * @param int    $user_id
258
	 *
259
	 * @return int User id
260
	 */
261 48
	protected function load_initialization ($session_id, $user_id) {
262 48
		$this->session_id = $session_id;
263 48
		$this->user_id    = $user_id;
264 48
		$this->update_user_is();
265 48
		return $user_id;
266
	}
267
	/**
268
	 * Whether profile is activated, not disabled and not blocked
269
	 *
270
	 * @param int $user
271
	 *
272
	 * @return bool
273
	 */
274 48
	protected function is_user_active ($user) {
275
		/**
276
		 * Optimization, more data requested than actually used here because data will be requested later, and it would be nice to have that data cached
277
		 */
278 48
		$data = User::instance()->get(['login', 'username', 'language', 'timezone', 'status', 'avatar'], $user);
279
		return
280 48
			$data &&
281 48
			$data['status'] == User::STATUS_ACTIVE;
282
	}
283
	/**
284
	 * Create the session for the user with specified id
285
	 *
286
	 * @param int  $user
287
	 * @param bool $delete_current_session
288
	 *
289
	 * @return false|string Session id on success, `false` otherwise
290
	 */
291 48
	public function add ($user, $delete_current_session = true) {
292 48
		$user = (int)$user ?: User::GUEST_ID;
293 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

293
		if ($delete_current_session && is_md5(/** @scrutinizer ignore-type */ $this->session_id)) {
Loading history...
294 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

294
			$this->del(/** @scrutinizer ignore-type */ $this->session_id);
Loading history...
295
		}
296 48
		if (!$this->is_user_active($user)) {
297
			/**
298
			 * If data was not loaded or account is not active - create guest session
299
			 */
300 6
			return $this->add(User::GUEST_ID);
301
		}
302 48
		$session_data = $this->create_unique_session($user);
303 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

303
		Response::instance()->/** @scrutinizer ignore-call */ cookie('session', $session_data['id'], $session_data['expire'], true);
Loading history...
304 48
		$this->load_initialization($session_data['id'], $session_data['user']);
305
		/**
306
		 * Delete old sessions using probability and system configuration of inserts limits and update ratio
307
		 */
308 48
		$Config = Config::instance();
309 48
		if (random_int(0, $Config->core['inserts_limit']) < $Config->core['inserts_limit'] / 100 * (100 - $Config->core['update_ratio']) / 5) {
310 8
			$this->delete_old_sessions();
311
		}
312 48
		Event::instance()->fire(
313 48
			'System/Session/add',
314
			[
315 48
				'session_data' => $session_data
316
			]
317
		);
318 48
		return $session_data['id'];
319
	}
320
	/**
321
	 * @param int $user
322
	 *
323
	 * @return array Session data
324
	 */
325 48
	protected function create_unique_session ($user) {
326 48
		$Config      = Config::instance();
327 48
		$Request     = Request::instance();
328 48
		$remote_addr = ip2hex($Request->remote_addr);
329 48
		$ip          = ip2hex($Request->ip);
330
		/**
331
		 * Many guests open only one page (or do not store any cookies), so create guest session only for 5 minutes max initially
332
		 */
333 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...
334 48
		$expire    = time() + $expire_in;
335
		/**
336
		 * Create unique session
337
		 */
338
		$session_data = [
339 48
			'id'          => null,
340 48
			'user'        => $user,
341 48
			'created'     => time(),
342 48
			'expire'      => $expire,
343 48
			'user_agent'  => $Request->header('user-agent'),
344 48
			'remote_addr' => $remote_addr,
345 48
			'ip'          => $ip,
346
			'data'        => []
347
		];
348
		do {
349 48
			$session_data['id'] = md5(random_bytes(1000));
350 48
		} while (!$this->create($session_data));
351 48
		return $session_data;
352
	}
353
	/**
354
	 * Destroying of the session
355
	 *
356
	 * @param null|string $session_id
357
	 *
358
	 * @return bool
359
	 */
360 24
	public function del ($session_id = null) {
361 24
		$session_id = $session_id ?: $this->session_id;
362 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

362
		if (!is_md5(/** @scrutinizer ignore-type */ $session_id)) {
Loading history...
363 3
			return false;
364
		}
365 24
		Event::instance()->fire(
366 24
			'System/Session/del/before',
367
			[
368 24
				'id' => $session_id
369
			]
370
		);
371 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...
372 24
		if ($session_id == $this->session_id) {
373 24
			$this->session_id = false;
374 24
			$this->user_id    = User::GUEST_ID;
375
		}
376 24
		if (Request::instance()->cookie('session') === $session_id) {
377 24
			Response::instance()->cookie('session', '');
378
		}
379 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

379
		/** @scrutinizer ignore-call */ 
380
  $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...
380 24
		if ($result) {
381 24
			Event::instance()->fire(
382 24
				'System/Session/del/after',
383
				[
384 24
					'id' => $session_id
385
				]
386
			);
387
		}
388 24
		return (bool)$result;
389
	}
390
	/**
391
	 * Delete all old sessions from DB
392
	 */
393 8
	protected function delete_old_sessions () {
394 8
		$this->db_prime()->q(
395
			'DELETE FROM `[prefix]sessions`
396 8
			WHERE `expire` < '.time()
397
		);
398 8
	}
399
	/**
400
	 * Deletion of all user sessions
401
	 *
402
	 * @param false|int $user If not specified - current user assumed
403
	 *
404
	 * @return bool
405
	 */
406 42
	public function del_all ($user = false) {
407 42
		$user = (int)$user ?: $this->user_id;
408 42
		if ($user == User::GUEST_ID) {
409 3
			return false;
410
		}
411 42
		Event::instance()->fire(
412 42
			'System/Session/del_all',
413
			[
414 42
				'id' => $user
415
			]
416
		);
417 42
		$cdb   = $this->db_prime();
418
		$query =
419
			"SELECT `id`
420
			FROM `[prefix]sessions`
421 42
			WHERE `user` = '$user'";
422 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

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