Completed
Push — master ( a14c1c...b0672a )
by Nazar
04:30
created

Management::del_user()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 37
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 23
CRAP Score 4.0011

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 4
eloc 25
nc 4
nop 1
dl 0
loc 37
ccs 23
cts 24
cp 0.9583
crap 4.0011
rs 8.5806
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\User;
9
use
10
	cs\Config,
11
	cs\Core,
12
	cs\Event,
13
	cs\Request,
14
	cs\Session,
15
	cs\User;
16
17
/**
18
 * Trait that contains all methods from <i>>cs\User</i> for working with user management (creation, modification, deletion)
19
 *
20
 * @property int              $id
21
 * @property \cs\Cache\Prefix $cache
22
 * @property string           $ip
23
 *
24
 * @method \cs\DB\_Abstract db()
25
 * @method \cs\DB\_Abstract db_prime()
26
 * @method false|int        get_id(string $login_hash)
27
 * @method bool             set_groups(int[] $groups, false|int $user = false)
28
 * @method false|string     get(array|string $item, false|int $user = false)
29
 * @method bool             set(array|string $item, mixed|null $value = null, false|int $user = false)
30
 * @method bool             del_permissions_all(false|int $user = false)
31
 */
32
trait Management {
33
	/**
34
	 * User id after registration
35
	 * @var int
36
	 */
37
	protected $reg_id = 0;
38
	/**
39
	 * Search keyword in login, username and email
40
	 *
41
	 * @param string $search_phrase
42
	 *
43
	 * @return false|int[]
0 ignored issues
show
Documentation introduced by
Should the return type not be false|array[]|integer|integer[]|string|string[]?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
44
	 */
45
	function search_users ($search_phrase) {
46
		$search_phrase = trim($search_phrase, "%\n");
47
		$found_users   = $this->db()->qfas(
48
			"SELECT `id`
49
			FROM `[prefix]users`
50
			WHERE
51
				(
52
					`login`		LIKE '%1\$s' OR
53
					`username`	LIKE '%1\$s' OR
54
					`email`		LIKE '%1\$s'
55
				) AND
56
				`status` != '%2\$s'",
57
			$search_phrase,
58
			User::STATUS_NOT_ACTIVATED
59
		);
60
		if (!$found_users) {
61
			return false;
62
		}
63
		return $found_users;
64
	}
65
	/**
66
	 * User registration
67
	 *
68
	 * @param string $email
69
	 * @param bool   $confirmation If <b>true</b> - default system option is used, if <b>false</b> - registration will be finished without necessity of
70
	 *                             confirmation, independently from default system option (is used for manual registration).
71
	 * @param bool   $auto_sign_in If <b>false</b> - no auto sign in, if <b>true</b> - according to system configuration
72
	 *
73
	 * @return array|false|string  <b>exists</b> - if user with such email is already registered<br>
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use false|string|array.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
74
	 *                             <b>error</b> - if error occurred<br>
75
	 *                             <b>false</b> - if email is incorrect<br>
76
	 *                             <b>[<br>
77
	 *                             &nbsp;&nbsp;&nbsp;&nbsp;'reg_key'     => *,</b> //Registration confirmation key, or <b>true</b> if confirmation is not
78
	 *                             required<br>
79
	 *                             &nbsp;&nbsp;&nbsp;&nbsp;<b>'password' => *,</b> //Automatically generated password (empty if confirmation is needed and
80
	 *                             will be set during registration confirmation)<br>
81
	 *                             &nbsp;&nbsp;&nbsp;&nbsp;<b>'id'       => *</b>  //Id of registered user in DB<br>
82
	 *                             <b>]</b>
83
	 */
84 4
	function registration ($email, $confirmation = true, $auto_sign_in = true) {
85 4
		if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
86 2
			return false;
87
		}
88 4
		$email      = mb_strtolower($email);
89 4
		$email_hash = hash('sha224', $email);
90 4
		$login      = strstr($email, '@', true);
91 4
		$login_hash = hash('sha224', $login);
92
		if (
93 4
			$this->get_id($login_hash) !== false ||
94
			(
95 4
				$login &&
96 4
				in_array($login, file_get_json(MODULES.'/System/index.json')['profile'])
97
			)
98
		) {
99 2
			$login      = $email;
100 2
			$login_hash = $email_hash;
101
		}
102 4
		if ($this->db_prime()->qf(
103
			"SELECT `id`
104
			FROM `[prefix]users`
105
			WHERE `email_hash` = '%s'
106 4
			LIMIT 1",
107
			$email_hash
108
		)
109
		) {
110 2
			return 'exists';
111
		}
112 4
		$this->delete_unconfirmed_users();
113 4
		if (!Event::instance()->fire(
114 4
			'System/User/registration/before',
115
			[
116 4
				'email' => $email
117
			]
118
		)
119
		) {
120 2
			return false;
121
		}
122 4
		$Config       = Config::instance();
123 4
		$confirmation = $confirmation && $Config->core['require_registration_confirmation'];
124 4
		$reg_key      = md5(random_bytes(1000));
125 4
		if ($this->db_prime()->q(
126
			"INSERT INTO `[prefix]users` (
127
				`login`,
128
				`login_hash`,
129
				`email`,
130
				`email_hash`,
131
				`reg_date`,
132
				`reg_ip`,
133
				`reg_key`,
134
				`status`
135
			) VALUES (
136
				'%s',
137
				'%s',
138
				'%s',
139
				'%s',
140
				'%s',
141
				'%s',
142
				'%s',
143
				'%s'
144 4
			)",
145
			$login,
146
			$login_hash,
147
			$email,
148
			$email_hash,
149
			time(),
150 4
			ip2hex(Request::instance()->ip),
151
			$reg_key,
152 4
			!$confirmation ? 1 : -1
153
		)
154
		) {
155 4
			$this->reg_id = $this->db_prime()->id();
156 4
			$password     = '';
157 4
			if (!$confirmation) {
158 2
				$password = password_generate($Config->core['password_min_length'], $Config->core['password_min_strength']);
159 2
				$this->set_password($password, $this->reg_id);
160 2
				$this->set_groups([User::USER_GROUP_ID], $this->reg_id);
161 2
				if ($auto_sign_in && $Config->core['auto_sign_in_after_registration']) {
162 2
					Session::instance()->add($this->reg_id);
163
				}
164
			}
165 4
			if (!Event::instance()->fire(
166 4
				'System/User/registration/after',
167
				[
168 4
					'id' => $this->reg_id
169
				]
170
			)
171
			) {
172 2
				$this->registration_cancel();
173 2
				return false;
174
			}
175 4
			if (!$confirmation) {
176 2
				$this->set_groups([User::USER_GROUP_ID], $this->reg_id);
177
			}
178 4
			unset($this->cache->$login_hash);
179
			return [
180 4
				'reg_key'  => !$confirmation ? true : $reg_key,
181 4
				'password' => $password,
182 4
				'id'       => $this->reg_id
183
			];
184
		} else {
185
			return 'error';
186
		}
187
	}
188
	/**
189
	 * Confirmation of registration process
190
	 *
191
	 * @param string $reg_key
192
	 *
193
	 * @return array|false ['id' => <i>id</i>, 'email' => <i>email</i>, 'password' => <i>password</i>] or <b>false</b> on failure
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use false|array.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
194
	 */
195 2
	function registration_confirmation ($reg_key) {
196 2
		if (!is_md5($reg_key)) {
197 2
			return false;
198
		}
199 2
		if (!Event::instance()->fire(
200 2
			'System/User/registration/confirmation/before',
201
			[
202 2
				'reg_key' => $reg_key
203
			]
204
		)
205
		) {
206 2
			$this->registration_cancel();
207 2
			return false;
208
		}
209 2
		$this->delete_unconfirmed_users();
210 2
		$data = $this->db_prime()->qf(
211
			"SELECT
212
				`id`,
213
				`login_hash`,
214
				`email`
215
			FROM `[prefix]users`
216
			WHERE
217
				`reg_key`	= '%s' AND
218
				`status`	= '%s'
219 2
			LIMIT 1",
220
			$reg_key,
221 2
			User::STATUS_NOT_ACTIVATED
222
		);
223 2
		if (!$data) {
224
			return false;
225
		}
226 2
		$this->reg_id = $data['id'];
227 2
		$Config       = Config::instance();
228 2
		$password     = '';
229 2
		if (!$this->get('password_hash', $data['id'])) {
230 2
			$password = password_generate($Config->core['password_min_length'], $Config->core['password_min_strength']);
231 2
			$this->set_password($password, $this->reg_id);
232
		}
233 2
		$this->set('status', User::STATUS_ACTIVE, $this->reg_id);
234 2
		$this->set_groups([User::USER_GROUP_ID], $this->reg_id);
235 2
		Session::instance()->add($this->reg_id);
236 2
		if (!Event::instance()->fire(
237 2
			'System/User/registration/confirmation/after',
238
			[
239 2
				'id' => $this->reg_id
240
			]
241
		)
242
		) {
243 2
			$this->registration_cancel();
244 2
			return false;
245
		}
246 2
		unset($this->cache->{$data['login_hash']});
247
		return [
248 2
			'id'       => $this->reg_id,
249 2
			'email'    => $data['email'],
250 2
			'password' => $password
251
		];
252
	}
253
	/**
254
	 * Canceling of bad/failed registration
255
	 */
256 4
	function registration_cancel () {
257 4
		if ($this->reg_id == 0) {
258
			return;
259
		}
260 4
		Session::instance()->add(User::GUEST_ID);
261 4
		$this->del_user($this->reg_id);
262 4
		$this->reg_id = 0;
263 4
	}
264
	/**
265
	 * Checks for unconfirmed registrations and deletes expired
266
	 */
267 4
	protected function delete_unconfirmed_users () {
268 4
		$reg_date = time() - Config::instance()->core['registration_confirmation_time'] * 86400;    //1 day = 86400 seconds
269 4
		$ids      = $this->db_prime()->qfas(
270
			"SELECT `id`
271
			FROM `[prefix]users`
272
			WHERE
273
				`status`	= '%s' AND
274 4
				`reg_date`	< $reg_date",
275 4
			User::STATUS_NOT_ACTIVATED
276
		);
277 4
		if ($ids) {
278 2
			$this->del_user($ids);
279
		}
280 4
	}
281
	/**
282
	 * Proper password setting without any need to deal with low-level implementation
283
	 *
284
	 * @param string    $new_password
285
	 * @param false|int $user
286
	 * @param bool      $already_prepared If true - assumed that `sha512(sha512(password) + public_key)` was applied to password
287
	 *
288
	 * @return bool
289
	 */
290 4
	function set_password ($new_password, $user = false, $already_prepared = false) {
291 4
		$public_key = Core::instance()->public_key;
292 4
		if (!$already_prepared) {
293 4
			$new_password = hash('sha512', hash('sha512', $new_password).$public_key);
294
		}
295
		/**
296
		 * Do not allow to set password to empty
297
		 */
298 4
		if ($new_password == hash('sha512', hash('sha512', '').$public_key)) {
299
			return false;
300
		}
301 4
		return $this->set('password_hash', password_hash($new_password, PASSWORD_DEFAULT), $user);
302
	}
303
	/**
304
	 * Proper password validation without any need to deal with low-level implementation
305
	 *
306
	 * @param string    $password
307
	 * @param false|int $user
308
	 * @param bool      $already_prepared If true - assumed that `sha512(sha512(password) + public_key)` was applied to password
309
	 *
310
	 * @return bool
311
	 */
312 2
	function validate_password ($password, $user = false, $already_prepared = false) {
313 2
		if (!$already_prepared) {
314
			$password = hash('sha512', hash('sha512', $password).Core::instance()->public_key);
315
		}
316 2
		$user          = (int)$user ?: $this->id;
317 2
		$password_hash = $this->get('password_hash', $user);
318 2
		if (!password_verify($password, $password_hash)) {
319
			return false;
320
		}
321
		/**
322
		 * Rehash password if needed
323
		 */
324 2
		if (password_needs_rehash($password_hash, PASSWORD_DEFAULT)) {
325
			$current_user_id = $this->id;
326
			$this->set_password($password, $user, true);
327
			if ($current_user_id == $user) {
328
				Session::instance()->add($current_user_id);
329
			}
330
		}
331 2
		return true;
332
	}
333
	/**
334
	 * Restoring of password
335
	 *
336
	 * @param int $user
337
	 *
338
	 * @return false|string Key for confirmation or <b>false</b> on failure
339
	 */
340
	function restore_password ($user) {
341
		if ($user && $user != User::GUEST_ID) {
342
			$reg_key = md5(random_bytes(1000));
343
			if ($this->set('reg_key', $reg_key, $user)) {
344
				$data                  = $this->get('data', $user);
345
				$data['restore_until'] = time() + Config::instance()->core['registration_confirmation_time'] * 86400;
346
				if ($this->set('data', $data, $user)) {
347
					return $reg_key;
348
				}
349
			}
350
		}
351
		return false;
352
	}
353
	/**
354
	 * Confirmation of password restoring process
355
	 *
356
	 * @param string $key
357
	 *
358
	 * @return array|false ['id' => <i>id</i>, 'password' => <i>password</i>] or <b>false</b> on failure
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use false|array.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
359
	 */
360
	function restore_password_confirmation ($key) {
361
		if (!is_md5($key)) {
362
			return false;
363
		}
364
		$id = $this->db_prime()->qfs(
365
			"SELECT `id`
366
			FROM `[prefix]users`
367
			WHERE
368
				`reg_key`	= '%s' AND
369
				`status`	= '%s'
370
			LIMIT 1",
371
			$key,
372
			User::STATUS_ACTIVE
373
		);
374
		if (!$id) {
375
			return false;
376
		}
377
		$data = $this->get('data', $id);
378
		if (!isset($data['restore_until'])) {
379
			return false;
380
		} elseif ($data['restore_until'] < time()) {
381
			unset($data['restore_until']);
382
			$this->set('data', $data, $id);
383
			return false;
384
		}
385
		unset($data['restore_until']);
386
		$Config   = Config::instance();
387
		$password = password_generate($Config->core['password_min_length'], $Config->core['password_min_strength']);
388
		$this->set_password($password, $id);
389
		$this->set('data', $data, $id);
390
		Session::instance()->add($id);
391
		return [
392
			'id'       => $id,
393
			'password' => $password
394
		];
395
	}
396
	/**
397
	 * Delete specified user or array of users
398
	 *
399
	 * @param int|int[] $user User id or array of users ids
400
	 */
401 4
	function del_user ($user) {
402 4
		$this->disable_memory_cache();
1 ignored issue
show
Bug introduced by
It seems like disable_memory_cache() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
403 4
		if (is_array($user)) {
404 2
			foreach ($user as $id) {
405 2
				$this->del_user($id);
406
			}
407 2
			return;
408
		}
409 4
		$user = (int)$user;
410 4
		if (!$user) {
411
			return;
412
		}
413 4
		Event::instance()->fire(
414 4
			'System/User/del/before',
415
			[
416 4
				'id' => $user
417
			]
418
		);
419 4
		$this->set_groups([], $user);
420 4
		$this->del_permissions_all($user);
421 4
		$Cache      = $this->cache;
422 4
		$login_hash = hash('sha224', $this->get('login', $user));
423 4
		$this->db_prime()->q(
424
			"DELETE FROM `[prefix]users`
425 4
			WHERE `id` = $user"
426
		);
427
		unset(
428 4
			$Cache->$login_hash,
429 4
			$Cache->$user
430
		);
431 4
		Event::instance()->fire(
432 4
			'System/User/del/after',
433
			[
434 4
				'id' => $user
435
			]
436
		);
437 4
	}
438
	/**
439
	 * Returns array of user id, that are associated as contacts
440
	 *
441
	 * @param false|int $user If not specified - current user assumed
442
	 *
443
	 * @return int[] Array of user id
444
	 */
445
	function get_contacts ($user = false) {
446
		$user = (int)$user ?: $this->id;
447
		if (!$user || $user == User::GUEST_ID) {
448
			return [];
449
		}
450
		$contacts = [];
451
		Event::instance()->fire(
452
			'System/User/get_contacts',
453
			[
454
				'id'       => $user,
455
				'contacts' => &$contacts
456
			]
457
		);
458
		return array_unique($contacts);
459
	}
460
}
461