Completed
Push — master ( c7c099...bc3d45 )
by Nazar
04:15
created

Management::set_bot()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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