Completed
Push — master ( ff9168...5fb3fd )
by Nazar
06:20
created

Management::registration()   D

Complexity

Conditions 17
Paths 88

Size

Total Lines 108
Code Lines 56

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 108
rs 4.8361
cc 17
eloc 56
nc 88
nop 3

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * @package   CleverStyle CMS
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\Session,
14
	cs\User;
15
16
/**
17
 * Trait that contains all methods from <i>>cs\User</i> for working with user management (creation, modification, deletion)
18
 *
19
 * @property int              $id
20
 * @property \cs\Cache\Prefix $cache
21
 * @property string           $ip
22
 *
23
 * @method \cs\DB\_Abstract db()
24
 * @method \cs\DB\_Abstract db_prime()
25
 * @method false|int        get_id(string $login_hash)
26
 * @method bool             set_groups(int[] $groups, false|int $user = false)
27
 * @method false|string     get(array|string $item, false|int $user = false)
28
 * @method bool             set(array|string $item, mixed|null $value = null, false|int $user = false)
29
 * @method bool             del_permissions_all(false|int $user = false)
30
 */
31
trait Management {
32
	/**
33
	 * User id after registration
34
	 * @var int
35
	 */
36
	protected $reg_id = 0;
37
	/**
38
	 * Search keyword in login, username and email
39
	 *
40
	 * @param string $search_phrase
41
	 *
42
	 * @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...
43
	 */
44
	function search_users ($search_phrase) {
45
		$search_phrase = trim($search_phrase, "%\n");
46
		$found_users   = $this->db()->qfas(
47
			[
48
				"SELECT `id`
49
				FROM `[prefix]users`
50
				WHERE
51
					(
52
						`login`		LIKE '%s' OR
53
						`username`	LIKE '%s' OR
54
						`email`		LIKE '%s'
55
					) AND
56
					`status` != '%s'",
57
				$search_phrase,
58
				$search_phrase,
59
				$search_phrase,
60
				User::STATUS_NOT_ACTIVATED
61
			]
62
		);
63
		if (!$found_users) {
64
			return false;
65
		}
66
		return $found_users;
67
	}
68
	/**
69
	 * User registration
70
	 *
71
	 * @param string $email
72
	 * @param bool   $confirmation If <b>true</b> - default system option is used, if <b>false</b> - registration will be finished without necessity of
73
	 *                             confirmation, independently from default system option (is used for manual registration).
74
	 * @param bool   $auto_sign_in If <b>false</b> - no auto sign in, if <b>true</b> - according to system configuration
75
	 *
76
	 * @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...
77
	 *                             <b>error</b> - if error occurred<br>
78
	 *                             <b>false</b> - if email is incorrect<br>
79
	 *                             <b>[<br>
80
	 *                             &nbsp;'reg_key'     => *,</b> //Registration confirmation key, or <b>true</b> if confirmation is not required<br>
81
	 *                             &nbsp;<b>'password' => *,</b> //Automatically generated password<br>
82
	 *                             &nbsp;<b>'id'       => *</b> //Id of registered user in DB<br>
83
	 *                             <b>]</b>
84
	 */
85
	function registration ($email, $confirmation = true, $auto_sign_in = true) {
86
		$email = mb_strtolower($email);
87
		if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
88
			return false;
89
		}
90
		$this->delete_unconfirmed_users();
91
		if (!Event::instance()->fire(
92
			'System/User/registration/before',
93
			[
94
				'email' => $email
95
			]
96
		)
97
		) {
98
			return false;
99
		}
100
		$email_hash = hash('sha224', $email);
101
		$login      = strstr($email, '@', true);
102
		$login_hash = hash('sha224', $login);
103
		if (
104
			$this->get_id($login_hash) !== false ||
105
			(
106
				$login &&
107
				in_array($login, file_get_json(MODULES.'/System/index.json')['profile'])
108
			)
109
		) {
110
			$login      = $email;
111
			$login_hash = $email_hash;
112
		}
113
		if ($this->db_prime()->qf(
114
			[
115
				"SELECT `id`
116
				FROM `[prefix]users`
117
				WHERE `email_hash` = '%s'
118
				LIMIT 1",
119
				$email_hash
120
			]
121
		)
122
		) {
123
			return 'exists';
124
		}
125
		$Config   = Config::instance();
126
		$password = password_generate($Config->core['password_min_length'], $Config->core['password_min_strength']);
127
		/**
128
		 * @var \cs\_SERVER $_SERVER
129
		 */
130
		$reg_key      = md5(random_bytes(1000));
131
		$confirmation = $confirmation && $Config->core['require_registration_confirmation'];
132
		if ($this->db_prime()->q(
133
			"INSERT INTO `[prefix]users` (
134
				`login`,
135
				`login_hash`,
136
				`email`,
137
				`email_hash`,
138
				`reg_date`,
139
				`reg_ip`,
140
				`reg_key`,
141
				`status`
142
			) VALUES (
143
				'%s',
144
				'%s',
145
				'%s',
146
				'%s',
147
				'%s',
148
				'%s',
149
				'%s',
150
				'%s'
151
			)",
152
			$login,
153
			$login_hash,
154
			$email,
155
			$email_hash,
156
			time(),
157
			ip2hex($_SERVER->ip),
158
			$reg_key,
159
			!$confirmation ? 1 : -1
160
		)
161
		) {
162
			$this->reg_id = $this->db_prime()->id();
163
			$this->set_password($password, $this->reg_id);
164
			if (!$confirmation) {
165
				$this->set_groups([User::USER_GROUP_ID], $this->reg_id);
166
			}
167
			if (!$confirmation && $auto_sign_in && $Config->core['auto_sign_in_after_registration']) {
168
				Session::instance()->add($this->reg_id);
169
			}
170
			if (!Event::instance()->fire(
171
				'System/User/registration/after',
172
				[
173
					'id' => $this->reg_id
174
				]
175
			)
176
			) {
177
				$this->registration_cancel();
178
				return false;
179
			}
180
			if (!$confirmation) {
181
				$this->set_groups([User::USER_GROUP_ID], $this->reg_id);
182
			}
183
			unset($this->cache->$login_hash);
184
			return [
185
				'reg_key'  => !$confirmation ? true : $reg_key,
186
				'password' => $password,
187
				'id'       => $this->reg_id
188
			];
189
		} else {
190
			return 'error';
191
		}
192
	}
193
	/**
194
	 * Confirmation of registration process
195
	 *
196
	 * @param string $reg_key
197
	 *
198
	 * @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...
199
	 */
200
	function registration_confirmation ($reg_key) {
201
		if (!is_md5($reg_key)) {
202
			return false;
203
		}
204
		if (!Event::instance()->fire(
205
			'System/User/registration/confirmation/before',
206
			[
207
				'reg_key' => $reg_key
208
			]
209
		)
210
		) {
211
			$this->registration_cancel();
212
			return false;
213
		}
214
		$this->delete_unconfirmed_users();
215
		$data = $this->db_prime()->qf(
216
			[
217
				"SELECT
218
					`id`,
219
					`login_hash`,
220
					`email`
221
				FROM `[prefix]users`
222
				WHERE
223
					`reg_key`	= '%s' AND
224
					`status`	= '%s'
225
				LIMIT 1",
226
				$reg_key,
227
				User::STATUS_NOT_ACTIVATED
228
			]
229
		);
230
		if (!$data) {
231
			return false;
232
		}
233
		$this->reg_id = $data['id'];
234
		$Config       = Config::instance();
235
		$password     = password_generate($Config->core['password_min_length'], $Config->core['password_min_strength']);
236
		$this->set_password($password, $this->reg_id);
237
		$this->set('status', User::STATUS_ACTIVE, $this->reg_id);
238
		$this->set_groups([User::USER_GROUP_ID], $this->reg_id);
239
		Session::instance()->add($this->reg_id);
240
		if (!Event::instance()->fire(
241
			'System/User/registration/confirmation/after',
242
			[
243
				'id' => $this->reg_id
244
			]
245
		)
246
		) {
247
			$this->registration_cancel();
248
			return false;
249
		}
250
		unset($this->cache->{$data['login_hash']});
251
		return [
252
			'id'       => $this->reg_id,
253
			'email'    => $data['email'],
254
			'password' => $password
255
		];
256
	}
257
	/**
258
	 * Canceling of bad/failed registration
259
	 */
260
	function registration_cancel () {
261
		if ($this->reg_id == 0) {
262
			return;
263
		}
264
		Session::instance()->add(User::GUEST_ID);
265
		$this->del_user($this->reg_id);
266
		$this->reg_id = 0;
267
	}
268
	/**
269
	 * Checks for unconfirmed registrations and deletes expired
270
	 */
271
	protected function delete_unconfirmed_users () {
272
		$reg_date = time() - Config::instance()->core['registration_confirmation_time'] * 86400;    //1 day = 86400 seconds
273
		$ids      = $this->db_prime()->qfas(
274
			[
275
				"SELECT `id`
276
				FROM `[prefix]users`
277
				WHERE
278
					`status`	= '%s' AND
279
					`reg_date`	< $reg_date",
280
				User::STATUS_NOT_ACTIVATED
281
			]
282
		);
283
		if ($ids) {
284
			$this->del_user($ids);
285
		}
286
	}
287
	/**
288
	 * Proper password setting without any need to deal with low-level implementation
289
	 *
290
	 * @param string    $new_password
291
	 * @param false|int $user
292
	 * @param bool      $already_prepared If true - assumed that `sha512(sha512(password) + public_key)` was applied to password
293
	 *
294
	 * @return bool
295
	 */
296
	function set_password ($new_password, $user = false, $already_prepared = false) {
297
		$public_key = Core::instance()->public_key;
298
		if (!$already_prepared) {
299
			$new_password = hash('sha512', hash('sha512', $new_password).$public_key);
300
		}
301
		/**
302
		 * Do not allow to set password to empty
303
		 */
304
		if ($new_password == hash('sha512', hash('sha512', '').$public_key)) {
305
			return false;
306
		}
307
		return $this->set('password_hash', password_hash($new_password, PASSWORD_DEFAULT), $user);
308
	}
309
	/**
310
	 * Proper password validation without any need to deal with low-level implementation
311
	 *
312
	 * @param string    $password
313
	 * @param false|int $user
314
	 * @param bool      $already_prepared If true - assumed that `sha512(sha512(password) + public_key)` was applied to password
315
	 *
316
	 * @return bool
317
	 */
318
	function validate_password ($password, $user = false, $already_prepared = false) {
319
		if (!$already_prepared) {
320
			$password = hash('sha512', hash('sha512', $password).Core::instance()->public_key);
321
		}
322
		$user          = (int)$user ?: $this->id;
323
		$password_hash = $this->get('password_hash', $user);
324
		if (!password_verify($password, $password_hash)) {
325
			return false;
326
		}
327
		/**
328
		 * Rehash password if needed
329
		 */
330
		if (password_needs_rehash($password_hash, PASSWORD_DEFAULT)) {
331
			$current_user_id = $this->id;
332
			$this->set_password($password, $user, true);
333
			if ($current_user_id == $user) {
334
				Session::instance()->add($current_user_id);
335
			}
336
		}
337
		return true;
338
	}
339
	/**
340
	 * Restoring of password
341
	 *
342
	 * @param int $user
343
	 *
344
	 * @return false|string Key for confirmation or <b>false</b> on failure
345
	 */
346
	function restore_password ($user) {
347
		if ($user && $user != User::GUEST_ID) {
348
			$reg_key = md5(random_bytes(1000));
349
			if ($this->set('reg_key', $reg_key, $user)) {
350
				$data                  = $this->get('data', $user);
351
				$data['restore_until'] = time() + Config::instance()->core['registration_confirmation_time'] * 86400;
352
				if ($this->set('data', $data, $user)) {
353
					return $reg_key;
354
				}
355
			}
356
		}
357
		return false;
358
	}
359
	/**
360
	 * Confirmation of password restoring process
361
	 *
362
	 * @param string $key
363
	 *
364
	 * @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...
365
	 */
366
	function restore_password_confirmation ($key) {
367
		if (!is_md5($key)) {
368
			return false;
369
		}
370
		$id = $this->db_prime()->qfs(
371
			[
372
				"SELECT `id`
373
				FROM `[prefix]users`
374
				WHERE
375
					`reg_key`	= '%s' AND
376
					`status`	= '%s'
377
				LIMIT 1",
378
				$key,
379
				User::STATUS_ACTIVE
380
			]
381
		);
382
		if (!$id) {
383
			return false;
384
		}
385
		$data = $this->get('data', $id);
386
		if (!isset($data['restore_until'])) {
387
			return false;
388
		} elseif ($data['restore_until'] < time()) {
389
			unset($data['restore_until']);
390
			$this->set('data', $data, $id);
391
			return false;
392
		}
393
		unset($data['restore_until']);
394
		$Config   = Config::instance();
395
		$password = password_generate($Config->core['password_min_length'], $Config->core['password_min_strength']);
396
		$this->set_password($password, $id);
397
		$this->set('data', $data, $id);
398
		Session::instance()->add($id);
399
		return [
400
			'id'       => $id,
401
			'password' => $password
402
		];
403
	}
404
	/**
405
	 * Delete specified user or array of users
406
	 *
407
	 * @param int|int[] $user User id or array of users ids
408
	 */
409
	function del_user ($user) {
410
		if (is_array($user)) {
411
			foreach ($user as $id) {
412
				$this->del_user($id);
413
			}
414
			return;
415
		}
416
		$user = (int)$user;
417
		if (!$user) {
418
			return;
419
		}
420
		Event::instance()->fire(
421
			'System/User/del/before',
422
			[
423
				'id' => $user
424
			]
425
		);
426
		$this->set_groups([], $user);
427
		$this->del_permissions_all($user);
428
		$Cache      = $this->cache;
429
		$login_hash = hash('sha224', $this->get('login', $user));
430
		$this->db_prime()->q(
431
			"DELETE FROM `[prefix]users`
432
			WHERE `id` = $user
433
			LIMIT 1"
434
		);
435
		unset(
436
			$Cache->$login_hash,
437
			$Cache->$user
438
		);
439
		Event::instance()->fire(
440
			'System/User/del/after',
441
			[
442
				'id' => $user
443
			]
444
		);
445
	}
446
	/**
447
	 * Add bot
448
	 *
449
	 * @param string $name       Bot name
450
	 * @param string $user_agent User Agent string or regular expression
451
	 * @param string $ip         IP string or regular expression
452
	 *
453
	 * @return false|int Bot <b>id</b> in DB or <b>false</b> on failure
454
	 */
455
	function add_bot ($name, $user_agent, $ip) {
456
		if ($this->db_prime()->q(
457
			"INSERT INTO `[prefix]users`
458
				(
459
					`username`,
460
					`login`,
461
					`email`,
462
					`status`
463
				) VALUES (
464
					'%s',
465
					'%s',
466
					'%s',
467
					'%s'
468
				)",
469
			xap($name),
470
			xap($user_agent),
471
			xap($ip),
472
			User::STATUS_ACTIVE
473
		)
474
		) {
475
			$id = $this->db_prime()->id();
476
			$this->set_groups([User::BOT_GROUP_ID], $id);
477
			Event::instance()->fire(
478
				'System/User/add_bot',
479
				[
480
					'id' => $id
481
				]
482
			);
483
			return $id;
484
		}
485
		return false;
486
	}
487
	/**
488
	 * Set bot
489
	 *
490
	 * @param int    $id         Bot id
491
	 * @param string $name       Bot name
492
	 * @param string $user_agent User Agent string or regular expression
493
	 * @param string $ip         IP string or regular expression
494
	 *
495
	 * @return bool
496
	 */
497
	function set_bot ($id, $name, $user_agent, $ip) {
498
		$result = $this->set(
499
			[
500
				'username' => $name,
501
				'login'    => $user_agent,
502
				'email'    => $ip
503
			],
504
			null,
505
			$id
506
		);
507
		unset($this->cache->bots);
508
		return $result;
509
	}
510
	/**
511
	 * Delete specified bot or array of bots
512
	 *
513
	 * @param int|int[] $bot Bot id or array of bots ids
514
	 */
515
	function del_bot ($bot) {
516
		$this->del_user($bot);
517
		unset($this->cache->bots);
518
	}
519
	/**
520
	 * Returns array of user id, that are associated as contacts
521
	 *
522
	 * @param false|int $user If not specified - current user assumed
523
	 *
524
	 * @return int[] Array of user id
525
	 */
526
	function get_contacts ($user = false) {
527
		$user = (int)$user ?: $this->id;
528
		if (!$user || $user == User::GUEST_ID) {
529
			return [];
530
		}
531
		$contacts = [];
532
		Event::instance()->fire(
533
			'System/User/get_contacts',
534
			[
535
				'id'       => $user,
536
				'contacts' => &$contacts
537
			]
538
		);
539
		return array_unique($contacts);
540
	}
541
}
542