Completed
Push — master ( 701c1d...a14c1c )
by Nazar
04:37
created

Management::delete_unconfirmed_users()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2.0078

Importance

Changes 0
Metric Value
cc 2
eloc 8
nc 2
nop 0
dl 0
loc 14
ccs 7
cts 8
cp 0.875
crap 2.0078
rs 9.4285
c 0
b 0
f 0
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 now needed and
80
	 *                             needs to be specified separately)<br>
81
	 *                             &nbsp;&nbsp;&nbsp;&nbsp;<b>'id'       => *</b>  //Id of registered user in DB<br>
82
	 *                             <b>]</b>
83
	 */
84 2
	function registration ($email, $confirmation = true, $auto_sign_in = true) {
85 2
		if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
86 2
			return false;
87
		}
88 2
		$email      = mb_strtolower($email);
89 2
		$email_hash = hash('sha224', $email);
90 2
		$login      = strstr($email, '@', true);
91 2
		$login_hash = hash('sha224', $login);
92
		if (
93 2
			$this->get_id($login_hash) !== false ||
94
			(
95 2
				$login &&
96 2
				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 2
		if ($this->db_prime()->qf(
103
			"SELECT `id`
104
			FROM `[prefix]users`
105
			WHERE `email_hash` = '%s'
106 2
			LIMIT 1",
107
			$email_hash
108
		)
109
		) {
110 2
			return 'exists';
111
		}
112 2
		$this->delete_unconfirmed_users();
113 2
		if (!Event::instance()->fire(
114 2
			'System/User/registration/before',
115
			[
116 2
				'email' => $email
117
			]
118
		)
119
		) {
120 2
			return false;
121
		}
122 2
		$Config       = Config::instance();
123 2
		$confirmation = $confirmation && $Config->core['require_registration_confirmation'];
124 2
		$reg_key      = md5(random_bytes(1000));
125 2
		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 2
			)",
145
			$login,
146
			$login_hash,
147
			$email,
148
			$email_hash,
149
			time(),
150 2
			ip2hex(Request::instance()->ip),
151
			$reg_key,
152 2
			!$confirmation ? 1 : -1
153
		)
154
		) {
155 2
			$this->reg_id = $this->db_prime()->id();
156 2
			$password     = '';
157 2
			if ($confirmation) {
158
				$password = password_generate($Config->core['password_min_length'], $Config->core['password_min_strength']);
159
				$this->set_password($password, $this->reg_id);
160
			}
161 2
			if (!$confirmation) {
162 2
				$this->set_groups([User::USER_GROUP_ID], $this->reg_id);
163
			}
164 2
			if (!$confirmation && $auto_sign_in && $Config->core['auto_sign_in_after_registration']) {
165 2
				Session::instance()->add($this->reg_id);
166
			}
167 2
			if (!Event::instance()->fire(
168 2
				'System/User/registration/after',
169
				[
170 2
					'id' => $this->reg_id
171
				]
172
			)
173
			) {
174 2
				$this->registration_cancel();
175 2
				return false;
176
			}
177 2
			if (!$confirmation) {
178 2
				$this->set_groups([User::USER_GROUP_ID], $this->reg_id);
179
			}
180 2
			unset($this->cache->$login_hash);
181
			return [
182 2
				'reg_key'  => !$confirmation ? true : $reg_key,
183 2
				'password' => $password,
184 2
				'id'       => $this->reg_id
185
			];
186
		} else {
187
			return 'error';
188
		}
189
	}
190
	/**
191
	 * Confirmation of registration process
192
	 *
193
	 * @param string $reg_key
194
	 *
195
	 * @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...
196
	 */
197
	function registration_confirmation ($reg_key) {
198
		if (!is_md5($reg_key)) {
199
			return false;
200
		}
201
		if (!Event::instance()->fire(
202
			'System/User/registration/confirmation/before',
203
			[
204
				'reg_key' => $reg_key
205
			]
206
		)
207
		) {
208
			$this->registration_cancel();
209
			return false;
210
		}
211
		$this->delete_unconfirmed_users();
212
		$data = $this->db_prime()->qf(
213
			"SELECT
214
				`id`,
215
				`login_hash`,
216
				`email`
217
			FROM `[prefix]users`
218
			WHERE
219
				`reg_key`	= '%s' AND
220
				`status`	= '%s'
221
			LIMIT 1",
222
			$reg_key,
223
			User::STATUS_NOT_ACTIVATED
224
		);
225
		if (!$data) {
226
			return false;
227
		}
228
		$this->reg_id = $data['id'];
229
		$Config       = Config::instance();
230
		$password     = '';
231
		if (!$this->get('password_hash', $data['id'])) {
232
			$password = password_generate($Config->core['password_min_length'], $Config->core['password_min_strength']);
233
			$this->set_password($password, $this->reg_id);
234
		}
235
		$this->set('status', User::STATUS_ACTIVE, $this->reg_id);
236
		$this->set_groups([User::USER_GROUP_ID], $this->reg_id);
237
		Session::instance()->add($this->reg_id);
238
		if (!Event::instance()->fire(
239
			'System/User/registration/confirmation/after',
240
			[
241
				'id' => $this->reg_id
242
			]
243
		)
244
		) {
245
			$this->registration_cancel();
246
			return false;
247
		}
248
		unset($this->cache->{$data['login_hash']});
249
		return [
250
			'id'       => $this->reg_id,
251
			'email'    => $data['email'],
252
			'password' => $password
253
		];
254
	}
255
	/**
256
	 * Canceling of bad/failed registration
257
	 */
258 2
	function registration_cancel () {
259 2
		if ($this->reg_id == 0) {
260
			return;
261
		}
262 2
		Session::instance()->add(User::GUEST_ID);
263 2
		$this->del_user($this->reg_id);
264 2
		$this->reg_id = 0;
265 2
	}
266
	/**
267
	 * Checks for unconfirmed registrations and deletes expired
268
	 */
269 2
	protected function delete_unconfirmed_users () {
270 2
		$reg_date = time() - Config::instance()->core['registration_confirmation_time'] * 86400;    //1 day = 86400 seconds
271 2
		$ids      = $this->db_prime()->qfas(
272
			"SELECT `id`
273
			FROM `[prefix]users`
274
			WHERE
275
				`status`	= '%s' AND
276 2
				`reg_date`	< $reg_date",
277 2
			User::STATUS_NOT_ACTIVATED
278
		);
279 2
		if ($ids) {
280
			$this->del_user($ids);
281
		}
282 2
	}
283
	/**
284
	 * Proper password setting without any need to deal with low-level implementation
285
	 *
286
	 * @param string    $new_password
287
	 * @param false|int $user
288
	 * @param bool      $already_prepared If true - assumed that `sha512(sha512(password) + public_key)` was applied to password
289
	 *
290
	 * @return bool
291
	 */
292
	function set_password ($new_password, $user = false, $already_prepared = false) {
293
		$public_key = Core::instance()->public_key;
294
		if (!$already_prepared) {
295
			$new_password = hash('sha512', hash('sha512', $new_password).$public_key);
296
		}
297
		/**
298
		 * Do not allow to set password to empty
299
		 */
300
		if ($new_password == hash('sha512', hash('sha512', '').$public_key)) {
301
			return false;
302
		}
303
		return $this->set('password_hash', password_hash($new_password, PASSWORD_DEFAULT), $user);
304
	}
305
	/**
306
	 * Proper password validation without any need to deal with low-level implementation
307
	 *
308
	 * @param string    $password
309
	 * @param false|int $user
310
	 * @param bool      $already_prepared If true - assumed that `sha512(sha512(password) + public_key)` was applied to password
311
	 *
312
	 * @return bool
313
	 */
314 2
	function validate_password ($password, $user = false, $already_prepared = false) {
315 2
		if (!$already_prepared) {
316
			$password = hash('sha512', hash('sha512', $password).Core::instance()->public_key);
317
		}
318 2
		$user          = (int)$user ?: $this->id;
319 2
		$password_hash = $this->get('password_hash', $user);
320 2
		if (!password_verify($password, $password_hash)) {
321
			return false;
322
		}
323
		/**
324
		 * Rehash password if needed
325
		 */
326 2
		if (password_needs_rehash($password_hash, PASSWORD_DEFAULT)) {
327
			$current_user_id = $this->id;
328
			$this->set_password($password, $user, true);
329
			if ($current_user_id == $user) {
330
				Session::instance()->add($current_user_id);
331
			}
332
		}
333 2
		return true;
334
	}
335
	/**
336
	 * Restoring of password
337
	 *
338
	 * @param int $user
339
	 *
340
	 * @return false|string Key for confirmation or <b>false</b> on failure
341
	 */
342
	function restore_password ($user) {
343
		if ($user && $user != User::GUEST_ID) {
344
			$reg_key = md5(random_bytes(1000));
345
			if ($this->set('reg_key', $reg_key, $user)) {
346
				$data                  = $this->get('data', $user);
347
				$data['restore_until'] = time() + Config::instance()->core['registration_confirmation_time'] * 86400;
348
				if ($this->set('data', $data, $user)) {
349
					return $reg_key;
350
				}
351
			}
352
		}
353
		return false;
354
	}
355
	/**
356
	 * Confirmation of password restoring process
357
	 *
358
	 * @param string $key
359
	 *
360
	 * @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...
361
	 */
362
	function restore_password_confirmation ($key) {
363
		if (!is_md5($key)) {
364
			return false;
365
		}
366
		$id = $this->db_prime()->qfs(
367
			"SELECT `id`
368
			FROM `[prefix]users`
369
			WHERE
370
				`reg_key`	= '%s' AND
371
				`status`	= '%s'
372
			LIMIT 1",
373
			$key,
374
			User::STATUS_ACTIVE
375
		);
376
		if (!$id) {
377
			return false;
378
		}
379
		$data = $this->get('data', $id);
380
		if (!isset($data['restore_until'])) {
381
			return false;
382
		} elseif ($data['restore_until'] < time()) {
383
			unset($data['restore_until']);
384
			$this->set('data', $data, $id);
385
			return false;
386
		}
387
		unset($data['restore_until']);
388
		$Config   = Config::instance();
389
		$password = password_generate($Config->core['password_min_length'], $Config->core['password_min_strength']);
390
		$this->set_password($password, $id);
391
		$this->set('data', $data, $id);
392
		Session::instance()->add($id);
393
		return [
394
			'id'       => $id,
395
			'password' => $password
396
		];
397
	}
398
	/**
399
	 * Delete specified user or array of users
400
	 *
401
	 * @param int|int[] $user User id or array of users ids
402
	 */
403 2
	function del_user ($user) {
404 2
		if (is_array($user)) {
405
			foreach ($user as $id) {
406
				$this->del_user($id);
407
			}
408
			return;
409
		}
410 2
		$user = (int)$user;
411 2
		if (!$user) {
412
			return;
413
		}
414 2
		Event::instance()->fire(
415 2
			'System/User/del/before',
416
			[
417 2
				'id' => $user
418
			]
419
		);
420 2
		$this->set_groups([], $user);
421 2
		$this->del_permissions_all($user);
422 2
		$Cache      = $this->cache;
423 2
		$login_hash = hash('sha224', $this->get('login', $user));
424 2
		$this->db_prime()->q(
425
			"DELETE FROM `[prefix]users`
426 2
			WHERE `id` = $user"
427
		);
428
		unset(
429 2
			$Cache->$login_hash,
430 2
			$Cache->$user
431
		);
432 2
		Event::instance()->fire(
433 2
			'System/User/del/after',
434
			[
435 2
				'id' => $user
436
			]
437
		);
438 2
	}
439
	/**
440
	 * Returns array of user id, that are associated as contacts
441
	 *
442
	 * @param false|int $user If not specified - current user assumed
443
	 *
444
	 * @return int[] Array of user id
445
	 */
446
	function get_contacts ($user = false) {
447
		$user = (int)$user ?: $this->id;
448
		if (!$user || $user == User::GUEST_ID) {
449
			return [];
450
		}
451
		$contacts = [];
452
		Event::instance()->fire(
453
			'System/User/get_contacts',
454
			[
455
				'id'       => $user,
456
				'contacts' => &$contacts
457
			]
458
		);
459
		return array_unique($contacts);
460
	}
461
}
462