Completed
Push — master ( 424862...431faa )
by Nazar
04:28
created

Data::get_id()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 23
Code Lines 12

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 23
rs 8.5906
cc 5
eloc 12
nc 5
nop 1
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\Event,
12
	cs\Language,
13
	cs\Session,
14
	cs\User,
15
	h;
16
17
/**
18
 * Trait that contains all methods from <i>>cs\User</i> for working with user data
19
 *
20
 * @property \cs\Cache\Prefix $cache
21
 * @property int              $id
22
 *
23
 * @method \cs\DB\_Abstract db()
24
 * @method \cs\DB\_Abstract db_prime()
25
 * @method false|int[]        get_groups(false|int $user)
26
 */
27
trait Data {
28
	/**
29
	 * Copy of columns list of users table for internal needs without Cache usage
30
	 * @var array
31
	 */
32
	protected $users_columns = [];
33
	/**
34
	 * Local cache of users data
35
	 * @var array
36
	 */
37
	protected $data = [];
38
	/**
39
	 * Changed users data, at the finish, data in db must be replaced by this data
40
	 * @var array
41
	 */
42
	protected $data_set = [];
43
	/**
44
	 * Whether to use memory cache (locally, inside object, may require a lot of memory if working with many users together)
45
	 * @var bool
46
	 */
47
	protected $memory_cache = true;
48
	protected function initialize_data () {
49
		$this->users_columns = $this->cache->get(
50
			'columns',
51
			function () {
52
				return $this->db()->columns('[prefix]users');
53
			}
54
		);
55
	}
56
	/**
57
	 * Get data item of specified user
58
	 *
59
	 * @param string|string[] $item
60
	 * @param false|int       $user If not specified - current user assumed
61
	 *
62
	 * @return false|int|mixed[]|string|Properties If <i>$item</i> is integer - cs\User\Properties object will be returned
63
	 */
64
	function get ($item, $user = false) {
65
		if (is_scalar($item) && ctype_digit((string)$item)) {
66
			return new Properties($item);
67
		}
68
		return $this->get_internal($item, $user);
69
	}
70
	/**
71
	 * Get data item of specified user
72
	 *
73
	 * @todo Refactor this to select all or nothing; this selection of only necessary stuff is tricky and should be simplified
74
	 *
75
	 * @param string|string[] $item
76
	 * @param false|int       $user If not specified - current user assumed
77
	 *
78
	 * @return false|int|string|mixed[]
79
	 */
80
	protected function get_internal ($item, $user = false) {
81
		$user = (int)$user ?: $this->id;
82
		if (!$user) {
83
			return false;
84
		}
85
		/** @noinspection NestedTernaryOperatorInspection */
86
		if (isset($this->data[$user])) {
87
			$data = $this->data[$user];
88
		} else {
89
			$data = $this->cache->get(
90
				$user,
91
				function () use ($user) {
92
					return $this->db()->qf(
93
						"SELECT *
94
						FROM `[prefix]users`
95
						WHERE `id` = $user
96
						LIMIT 1"
97
					) ?: false;
98
				}
99
			);
100
			if (!$data) {
101
				return false;
102
			} elseif ($this->memory_cache) {
103
				$this->data[$user] = $data;
104
			}
105
		}
106
		/**
107
		 * If get an array of values
108
		 */
109
		if (is_array($item)) {
110
			$result = [];
111
			/**
112
			 * Trying to get value from the local cache, or make up an array of missing values
113
			 */
114
			foreach ($item as $i) {
115
				if (in_array($i, $this->users_columns)) {
116
					$result[$i] = $data[$i];
117
				}
118
			}
119
			return $result;
120
		}
121
		return in_array($item, $this->users_columns) ? $data[$item] : false;
122
	}
123
	/**
124
	 * Set data item of specified user
125
	 *
126
	 * @param array|string    $item Item-value array may be specified for setting several items at once
127
	 * @param int|null|string $value
128
	 * @param false|int       $user If not specified - current user assumed
129
	 *
130
	 * @return bool
131
	 */
132
	function set ($item, $value = null, $user = false) {
133
		$result = $this->set_internal($item, $value, $user);
134
		$this->persist_data();
135
		return $result;
136
	}
137
	/**
138
	 * Set data item of specified user
139
	 *
140
	 * @param array|string    $item Item-value array may be specified for setting several items at once
141
	 * @param int|null|string $value
142
	 * @param false|int       $user If not specified - current user assumed
143
	 *
144
	 * @return bool
145
	 */
146
	protected function set_internal ($item, $value = null, $user = false) {
147
		$user = (int)$user ?: $this->id;
148
		if (!$user) {
149
			return false;
150
		}
151
		if (is_array($item)) {
152
			$result = true;
153
			foreach ($item as $i => $v) {
154
				$result = $result && $this->set($i, $v, $user);
155
			}
156
			return $result;
157
		}
158
		if (!$this->set_internal_allowed($user, $item, $value)) {
159
			return false;
160
		}
161
		if ($item === 'language') {
162
			$value = $value ? Language::instance()->get('clanguage', $value) : '';
163
		} elseif ($item === 'timezone') {
164
			$value = in_array($value, get_timezones_list(), true) ? $value : '';
165
		} elseif ($item == 'avatar') {
166
			if (
167
				strpos($value, 'http://') !== 0 &&
168
				strpos($value, 'https://') !== 0
169
			) {
170
				$value = '';
171
			} else {
172
				$old_value = $this->get($item, $user);
173
				if ($value !== $old_value) {
174
					$Event = Event::instance();
175
					$Event->fire(
176
						'System/upload_files/del_tag',
177
						[
178
							'url' => $old_value,
179
							'tag' => "users/$user/avatar"
180
						]
181
					);
182
					$Event->fire(
183
						'System/upload_files/add_tag',
184
						[
185
							'url' => $value,
186
							'tag' => "users/$user/avatar"
187
						]
188
					);
189
				}
190
			}
191
		}
192
		$this->data_set[$user][$item] = $value;
193
		if (in_array($item, ['login', 'email'], true)) {
194
			$old_value                            = $this->get($item.'_hash', $user);
195
			$this->data_set[$user][$item.'_hash'] = hash('sha224', $value);
196
			unset($this->cache->$old_value);
197
		} elseif ($item === 'password_hash' || ($item === 'status' && $value == 0)) {
198
			Session::instance()->del_all($user);
199
		}
200
		return true;
201
	}
202
	/**
203
	 * Check whether setting specified item to specified value for specified user is allowed
204
	 *
205
	 * @param int    $user
206
	 * @param string $item
207
	 * @param string $value
208
	 *
209
	 * @return bool
210
	 */
211
	protected function set_internal_allowed ($user, $item, $value) {
212
		if (
213
			$user === User::GUEST_ID ||
214
			$item === 'id' ||
215
			!in_array($item, $this->users_columns, true)
216
		) {
217
			return false;
218
		}
219
		if (in_array($item, ['login', 'email'], true)) {
220
			$value = mb_strtolower($value);
221
			if (
222
				$item === 'email' &&
223
				!filter_var($value, FILTER_VALIDATE_EMAIL) &&
224
				!in_array(User::BOT_GROUP_ID, $this->get_groups($user))
225
			) {
226
				return false;
227
			}
228
			if ($value === $this->get($item, $user)) {
229
				return true;
230
			}
231
			return !$this->get_id(hash('sha224', $value));
232
		}
233
		return true;
234
	}
235
	/**
236
	 * Getting additional data item(s) of specified user
237
	 *
238
	 * @param string|string[] $item
239
	 * @param false|int       $user If not specified - current user assumed
240
	 *
241
	 * @return false|string|mixed[]
242
	 */
243
	function get_data ($item, $user = false) {
244
		$user = (int)$user ?: $this->id;
245
		if (!$user || !$item || $user == User::GUEST_ID) {
246
			return false;
247
		}
248
		$Cache = $this->cache;
249
		$data  = $Cache->{"data/$user"} ?: [];
250
		if (is_array($item)) {
251
			$result = [];
252
			$absent = [];
253
			foreach ($item as $i) {
254
				if (isset($data[$i])) {
255
					$result[$i] = $data[$i];
256
				} else {
257
					$absent[] = $i;
258
				}
259
			}
260
			if ($absent) {
261
				$absent = implode(
262
					',',
263
					$this->db()->s($absent)
264
				);
265
				$absent = array_column(
266
					$this->db()->qfa(
267
						[
268
							"SELECT `item`, `value`
269
							FROM `[prefix]users_data`
270
							WHERE
271
								`id`	= '$user' AND
272
								`item`	IN($absent)"
273
						]
274
					),
275
					'value',
276
					'item'
277
				);
278
				foreach ($absent as &$a) {
279
					$a = _json_decode($a);
280
					if ($a === null) {
281
						$a = false;
282
					}
283
				}
284
				unset($a);
285
				$result += $absent;
286
				$data += $absent;
287
				$Cache->{"data/$user"} = $data;
288
			}
289
			return $result;
290
		}
291
		if ($data === false || !isset($data[$item])) {
292
			if (!is_array($data)) {
293
				$data = [];
294
			}
295
			$data[$item] = _json_decode(
296
				$this->db()->qfs(
297
					[
298
						"SELECT `value`
299
						FROM `[prefix]users_data`
300
						WHERE
301
							`id`	= '$user' AND
302
							`item`	= '%s'",
303
						$item
304
					]
305
				)
306
			);
307
			if ($data[$item] === null) {
308
				$data[$item] = false;
309
			}
310
			$Cache->{"data/$user"} = $data;
311
		}
312
		return $data[$item];
313
	}
314
	/**
315
	 * Setting additional data item(s) of specified user
316
	 *
317
	 * @param array|string $item Item-value array may be specified for setting several items at once
318
	 * @param mixed|null   $value
319
	 * @param false|int    $user If not specified - current user assumed
320
	 *
321
	 * @return bool
322
	 */
323
	function set_data ($item, $value = null, $user = false) {
324
		$user = (int)$user ?: $this->id;
325
		if (!$user || !$item || $user == User::GUEST_ID) {
326
			return false;
327
		}
328
		if (is_array($item)) {
329
			$params = [];
330
			foreach ($item as $i => $v) {
331
				$params[] = [
332
					$i,
333
					_json_encode($v)
334
				];
335
			}
336
			unset($i, $v);
337
			$result = $this->db_prime()->insert(
338
				"REPLACE INTO `[prefix]users_data`
339
					(
340
						`id`,
341
						`item`,
342
						`value`
343
					) VALUES (
344
						$user,
345
						'%s',
346
						'%s'
347
					)",
348
				$params
349
			);
350
		} else {
351
			$result = $this->db_prime()->q(
352
				"REPLACE INTO `[prefix]users_data`
353
					(
354
						`id`,
355
						`item`,
356
						`value`
357
					) VALUES (
358
						'$user',
359
						'%s',
360
						'%s'
361
					)",
362
				$item,
363
				_json_encode($value)
364
			);
365
		}
366
		unset($this->cache->{"data/$user"});
367
		return (bool)$result;
368
	}
369
	/**
370
	 * Deletion of additional data item(s) of specified user
371
	 *
372
	 * @param string|string[] $item
373
	 * @param false|int       $user If not specified - current user assumed
374
	 *
375
	 * @return bool
376
	 */
377
	function del_data ($item, $user = false) {
378
		$user = (int)$user ?: $this->id;
379
		if (!$user || !$item || $user == User::GUEST_ID) {
380
			return false;
381
		}
382
		$item   = implode(
383
			',',
384
			$this->db_prime()->s((array)$item)
385
		);
386
		$result = $this->db_prime()->q(
387
			"DELETE FROM `[prefix]users_data`
388
			WHERE
389
				`id`	= '$user' AND
390
				`item`	IN($item)"
391
		);
392
		unset($this->cache->{"data/$user"});
393
		return (bool)$result;
394
	}
395
	/**
396
	 * Get user id by login or email hash (sha224) (hash from lowercase string)
397
	 *
398
	 * @param  string $login_hash Login or email hash
399
	 *
400
	 * @return false|int User id if found and not guest, otherwise - boolean <i>false</i>
401
	 */
402
	function get_id ($login_hash) {
403
		if (!preg_match('/^[0-9a-z]{56}$/', $login_hash)) {
404
			return false;
405
		}
406
		$id = $this->cache->get(
407
			$login_hash,
408
			function () use ($login_hash) {
409
				return $this->db()->qfs(
410
					[
411
						"SELECT `id`
412
						FROM `[prefix]users`
413
						WHERE
414
							`login_hash`	= '%s' OR
415
							`email_hash`	= '%s'
416
						LIMIT 1",
417
						$login_hash,
418
						$login_hash
419
					]
420
				) ?: false;
421
			}
422
		);
423
		return $id && $id != User::GUEST_ID ? $id : false;
424
	}
425
	/**
426
	 * Get user avatar, if no one present - uses Gravatar
427
	 *
428
	 * @param int|null  $size Avatar size, if not specified or resizing is not possible - original image is used
429
	 * @param false|int $user If not specified - current user assumed
430
	 *
431
	 * @return string
432
	 */
433
	function avatar ($size = null, $user = false) {
434
		$user   = (int)$user ?: $this->id;
435
		$avatar = $this->get('avatar', $user);
436
		$Config = Config::instance();
437
		if (!$avatar && $this->id != User::GUEST_ID && $Config->core['gravatar_support']) {
438
			$email_hash     = md5($this->get('email', $user));
439
			$default_avatar = urlencode($Config->core_url().'/includes/img/guest.svg');
440
			$avatar         = "https://www.gravatar.com/avatar/$email_hash?d=mm&s=$size&d=$default_avatar";
441
		}
442
		if (!$avatar) {
443
			$avatar = '/includes/img/guest.svg';
444
		}
445
		return h::prepare_url($avatar, true);
446
	}
447
	/**
448
	 * Get user name or login or email, depending on existing information
449
	 *
450
	 * @param false|int $user If not specified - current user assumed
451
	 *
452
	 * @return string
453
	 */
454
	function username ($user = false) {
455
		$user = (int)$user ?: $this->id;
456
		if ($user === User::GUEST_ID) {
457
			return Language::instance()->system_profile_guest;
458
		}
459
		$username = $this->get('username', $user);
460
		if (!$username) {
461
			$username = $this->get('login', $user);
462
		}
463
		if (!$username) {
464
			$username = $this->get('email', $user);
465
		}
466
		return $username;
467
	}
468
	/**
469
	 * Disable memory cache
470
	 *
471
	 * Memory cache stores users data inside User class in order to get data faster next time.
472
	 * But in case of working with large amount of users this cache can be too large. Disabling will cause some performance drop, but save a lot of RAM.
473
	 */
474
	function disable_memory_cache () {
475
		$this->memory_cache = false;
476
	}
477
	/**
478
	 * Returns array of users columns, available for getting of data
479
	 *
480
	 * @return array
481
	 */
482
	function get_users_columns () {
483
		return $this->users_columns;
484
	}
485
	/**
486
	 * Saving changes of cache and users data
487
	 */
488
	protected function persist_data () {
489
		foreach ($this->data_set as $user => $data_set) {
490
			$update = [];
491
			foreach ($data_set as $item => $value) {
492
				if ($item != 'id' && in_array($item, $this->users_columns)) {
493
					$value    = xap($value, false);
494
					$update[] = "`$item` = ".$this->db_prime()->s($value);
495
				}
496
			}
497
			if ($update) {
498
				$update = implode(', ', $update);
499
				$this->db_prime()->q(
500
					"UPDATE `[prefix]users`
501
					SET $update
502
					WHERE `id` = '$user'"
503
				);
504
				unset($this->data[$user], $this->cache->$user);
505
			}
506
		}
507
		$this->data_set = [];
508
	}
509
}
510