Completed
Push — master ( cdd7a4...83eeb5 )
by Nazar
04:24
created

Management::restore_password_confirmation()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 31
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
eloc 20
nc 4
nop 1
dl 0
loc 31
ccs 18
cts 18
cp 1
crap 4
rs 8.5806
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[]
44
	 */
45 2
	public function search_users ($search_phrase) {
46 2
		$search_phrase = trim($search_phrase, "%\n");
47 2
		$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 2
				`status` != '%2\$s'",
57
			$search_phrase,
58 2
			User::STATUS_NOT_ACTIVATED
59
		);
60 2
		if (!$found_users) {
61 2
			return false;
62
		}
63 2
		return _int($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 28
	public function registration ($email, $confirmation = true, $auto_sign_in = true) {
85 28
		if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
86 6
			return false;
87
		}
88 28
		$email      = mb_strtolower($email);
89 28
		$email_hash = hash('sha224', $email);
90 28
		$login      = strstr($email, '@', true);
91 28
		$login_hash = hash('sha224', $login);
92 28
		if ($this->registration_login_already_occupied($login, $login_hash)) {
93 4
			$login      = $email;
94 4
			$login_hash = $email_hash;
95
		}
96 28
		if ($this->get_id($email_hash)) {
97 4
			return 'exists';
98
		}
99 28
		$this->delete_unconfirmed_users();
100 28
		if (!Event::instance()->fire(
101 28
			'System/User/registration/before',
102
			[
103 28
				'email' => $email
104
			]
105
		)
106
		) {
107 2
			return false;
108
		}
109 28
		$Config       = Config::instance();
110 28
		$confirmation = $confirmation && $Config->core['require_registration_confirmation'];
111 28
		$reg_key      = md5(random_bytes(1000));
112 28
		if ($this->db_prime()->q(
113
			"INSERT INTO `[prefix]users` (
114
				`login`,
115
				`login_hash`,
116
				`email`,
117
				`email_hash`,
118
				`reg_date`,
119
				`reg_ip`,
120
				`reg_key`,
121
				`status`
122
			) VALUES (
123
				'%s',
124
				'%s',
125
				'%s',
126
				'%s',
127
				'%s',
128
				'%s',
129
				'%s',
130
				'%s'
131 28
			)",
132
			$login,
133
			$login_hash,
134
			$email,
135
			$email_hash,
136
			time(),
137 28
			ip2hex(Request::instance()->ip),
138
			$reg_key,
139 28
			!$confirmation ? 1 : -1
140
		)
141
		) {
142 28
			$this->reg_id = (int)$this->db_prime()->id();
143 28
			$password     = '';
144 28
			if (!$confirmation) {
145 26
				$password = password_generate($Config->core['password_min_length'], $Config->core['password_min_strength']);
146 26
				$this->set_password($password, $this->reg_id);
147 26
				$this->set_groups([User::USER_GROUP_ID], $this->reg_id);
148 26
				if ($auto_sign_in && $Config->core['auto_sign_in_after_registration']) {
149 12
					Session::instance()->add($this->reg_id);
150
				}
151
			}
152 28
			if (!Event::instance()->fire(
153 28
				'System/User/registration/after',
154
				[
155 28
					'id' => $this->reg_id
156
				]
157
			)
158
			) {
159 2
				$this->registration_cancel();
160 2
				return false;
161
			}
162 28
			if (!$confirmation) {
163 26
				$this->set_groups([User::USER_GROUP_ID], $this->reg_id);
164
			}
165 28
			unset($this->cache->$login_hash);
166
			return [
167 28
				'reg_key'  => !$confirmation ? true : $reg_key,
168 28
				'password' => $password,
169 28
				'id'       => $this->reg_id
170
			];
171
		} else {
172
			return 'error';
173
		}
174
	}
175
	/**
176
	 * @param string $login
177
	 * @param string $login_hash
178
	 *
179
	 * @return bool
180
	 */
181 28
	protected function registration_login_already_occupied ($login, $login_hash) {
182
		return
183 28
			$this->get_id($login_hash) !== false ||
184
			(
185 28
				$login &&
186 28
				in_array($login, file_get_json(MODULES.'/System/index.json')['profile'])
187
			);
188
	}
189
	/**
190
	 * Confirmation of registration process
191
	 *
192
	 * @param string $reg_key
193
	 *
194
	 * @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...
195
	 */
196 2
	public function registration_confirmation ($reg_key) {
197 2
		if (!is_md5($reg_key)) {
198 2
			return false;
199
		}
200 2
		if (!Event::instance()->fire(
201 2
			'System/User/registration/confirmation/before',
202
			[
203 2
				'reg_key' => $reg_key
204
			]
205
		)
206
		) {
207 2
			$this->registration_cancel();
208 2
			return false;
209
		}
210 2
		$this->delete_unconfirmed_users();
211 2
		$data = $this->db_prime()->qf(
212
			"SELECT
213
				`id`,
214
				`login_hash`,
215
				`email`
216
			FROM `[prefix]users`
217
			WHERE
218
				`reg_key`	= '%s' AND
219
				`status`	= '%s'
220 2
			LIMIT 1",
221
			$reg_key,
222 2
			User::STATUS_NOT_ACTIVATED
223
		);
224 2
		if (!$data) {
225 2
			return false;
226
		}
227 2
		$this->reg_id = (int)$data['id'];
228 2
		$Config       = Config::instance();
229 2
		$password     = '';
230 2
		if (!$this->get('password_hash', $this->reg_id)) {
231 2
			$password = password_generate($Config->core['password_min_length'], $Config->core['password_min_strength']);
232 2
			$this->set_password($password, $this->reg_id);
233
		}
234 2
		$this->set('status', User::STATUS_ACTIVE, $this->reg_id);
235 2
		$this->set_groups([User::USER_GROUP_ID], $this->reg_id);
236 2
		Session::instance()->add($this->reg_id);
237 2
		if (!Event::instance()->fire(
238 2
			'System/User/registration/confirmation/after',
239
			[
240 2
				'id' => $this->reg_id
241
			]
242
		)
243
		) {
244 2
			$this->registration_cancel();
245 2
			return false;
246
		}
247 2
		unset($this->cache->{$data['login_hash']});
248
		return [
249 2
			'id'       => $this->reg_id,
250 2
			'email'    => $data['email'],
251 2
			'password' => $password
252
		];
253
	}
254
	/**
255
	 * Canceling of bad/failed registration
256
	 */
257 6
	public function registration_cancel () {
258 6
		if ($this->reg_id) {
259 6
			Session::instance()->add(User::GUEST_ID);
260 6
			$this->del_user($this->reg_id);
261 6
			$this->reg_id = 0;
262
		}
263 6
	}
264
	/**
265
	 * Checks for unconfirmed registrations and deletes expired
266
	 */
267 28
	protected function delete_unconfirmed_users () {
268 28
		$reg_date = time() - Config::instance()->core['registration_confirmation_time'] * 86400;    //1 day = 86400 seconds
269 28
		$ids      = $this->db_prime()->qfas(
270
			"SELECT `id`
271
			FROM `[prefix]users`
272
			WHERE
273
				`status`	= '%s' AND
274 28
				`reg_date`	< $reg_date",
275 28
			User::STATUS_NOT_ACTIVATED
276
		);
277 28
		if ($ids) {
278 2
			$this->del_user($ids);
279
		}
280 28
	}
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 28
	public function set_password ($new_password, $user = false, $already_prepared = false) {
291 28
		$public_key = Core::instance()->public_key;
292 28
		if (!$already_prepared) {
293 28
			$new_password = hash('sha512', hash('sha512', $new_password).$public_key);
294
		}
295
		/**
296
		 * Do not allow to set password to empty
297
		 */
298 28
		if ($new_password == hash('sha512', hash('sha512', '').$public_key)) {
299 4
			return false;
300
		}
301 28
		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 8
	public function validate_password ($password, $user = false, $already_prepared = false) {
313 8
		if (!$already_prepared) {
314 4
			$password = hash('sha512', hash('sha512', $password).Core::instance()->public_key);
315
		}
316 8
		$user          = (int)$user ?: $this->id;
317 8
		$password_hash = $this->get('password_hash', $user);
318 8
		if (!password_verify($password, $password_hash)) {
319 6
			return false;
320
		}
321
		/**
322
		 * Rehash password if needed
323
		 */
324 8
		if (password_needs_rehash($password_hash, PASSWORD_DEFAULT)) {
325 2
			$current_user_id = $this->id;
326 2
			$this->set_password($password, $user, true);
327 2
			if ($current_user_id == $user) {
328 2
				Session::instance()->add($current_user_id);
329
			}
330
		}
331 8
		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 4
	public function restore_password ($user) {
341 4
		if ($user && $user != User::GUEST_ID) {
342 4
			$reg_key = md5(random_bytes(1000));
343 4
			if ($this->set('reg_key', $reg_key, $user)) {
344 4
				$restore_until = time() + Config::instance()->core['registration_confirmation_time'] * 86400;
345 4
				if ($this->set_data('restore_until', $restore_until, $user)) {
346 4
					return $reg_key;
347
				}
348
			}
349
		}
350 2
		return false;
351
	}
352
	/**
353
	 * Confirmation of password restoring process
354
	 *
355
	 * @param string $key
356
	 *
357
	 * @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...
358
	 */
359 2
	public function restore_password_confirmation ($key) {
360 2
		if (!is_md5($key)) {
361 2
			return false;
362
		}
363 2
		$id = (int)$this->db_prime()->qfs(
364
			"SELECT `id`
365
			FROM `[prefix]users`
366
			WHERE
367
				`reg_key`	= '%s' AND
368
				`status`	= '%s'
369 2
			LIMIT 1",
370
			$key,
371 2
			User::STATUS_ACTIVE
372
		);
373 2
		if (!$id) {
374 2
			return false;
375
		}
376 2
		$restore_until = $this->get_data('restore_until', $id);
377 2
		$this->del_data('restore_until', $id);
378 2
		if ($restore_until < time()) {
379 2
			return false;
380
		}
381 2
		$Config   = Config::instance();
382 2
		$password = password_generate($Config->core['password_min_length'], $Config->core['password_min_strength']);
383 2
		$this->set_password($password, $id);
384 2
		Session::instance()->add($id);
385
		return [
386 2
			'id'       => $id,
387 2
			'password' => $password
388
		];
389
	}
390
	/**
391
	 * Delete specified user or array of users
392
	 *
393
	 * @param int|int[] $user User id or array of users ids
394
	 */
395 16
	public function del_user ($user) {
396 16
		$this->disable_memory_cache();
397 16
		if (is_array($user)) {
398 2
			foreach ($user as $id) {
399 2
				$this->del_user($id);
400
			}
401 2
			return;
402
		}
403 16
		$user = (int)$user;
404 16
		if (in_array($user, [0, User::GUEST_ID, User::ROOT_ID]) || !$this->get('id', $user)) {
405 2
			return;
406
		}
407 16
		Event::instance()->fire(
408 16
			'System/User/del/before',
409
			[
410 16
				'id' => $user
411
			]
412
		);
413 16
		$this->set_groups([], $user);
414 16
		$this->del_permissions_all($user);
415 16
		$Cache      = $this->cache;
416 16
		$login_hash = $this->get('login_hash', $user);
417 16
		$email_hash = $this->get('email_hash', $user);
418 16
		$this->db_prime()->q(
419
			"DELETE FROM `[prefix]users`
420 16
			WHERE `id` = $user"
421
		);
422
		unset(
423 16
			$Cache->$login_hash,
424 16
			$Cache->$email_hash,
425 16
			$Cache->$user
426
		);
427 16
		Event::instance()->fire(
428 16
			'System/User/del/after',
429
			[
430 16
				'id' => $user
431
			]
432
		);
433 16
	}
434
}
435