Completed
Push — master ( 7beef5...6d2624 )
by Nazar
04:17
created

Data::save_cache_and_user_data()   C

Complexity

Conditions 13
Paths 9

Size

Total Lines 44
Code Lines 29

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 44
rs 5.1234
cc 13
eloc 29
nc 9
nop 0

How to fix   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\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
 * @method                  __finish()
27
 */
28
trait Data {
29
	/**
30
	 * Copy of columns list of users table for internal needs without Cache usage
31
	 * @var array
32
	 */
33
	protected $users_columns = [];
34
	/**
35
	 * Do we need to update users cache, if so - array will not be empty
36
	 * @var array
37
	 */
38
	protected $update_cache = [];
39
	/**
40
	 * Local cache of users data
41
	 * @var array
42
	 */
43
	protected $data = [];
44
	/**
45
	 * Changed users data, at the finish, data in db must be replaced by this data
46
	 * @var array
47
	 */
48
	protected $data_set = [];
49
	/**
50
	 * Whether to use memory cache (locally, inside object, may require a lot of memory if working with many users together)
51
	 * @var bool
52
	 */
53
	protected $memory_cache = true;
54
	protected function initialize_data () {
55
		$this->users_columns = $this->cache->get(
56
			'columns',
57
			function () {
58
				return $this->db()->columns('[prefix]users');
59
			}
60
		);
61
	}
62
	/**
63
	 * Get data item of specified user
64
	 *
65
	 * @param string|string[] $item
66
	 * @param false|int       $user If not specified - current user assumed
67
	 *
68
	 * @return false|int|mixed[]|string|Properties If <i>$item</i> is integer - cs\User\Properties object will be returned
0 ignored issues
show
Documentation introduced by
Should the return type not be Properties|false|array|integer|string? Also, consider making the array more specific, something like array<String>, or 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.

If the return type contains the type array, this check recommends the use of a more specific type like String[] or array<String>.

Loading history...
69
	 */
70
	function get ($item, $user = false) {
71
		if (is_scalar($item) && preg_match('/^[0-9]+$/', $item)) {
72
			return new Properties($item);
73
		}
74
		$result = $this->get_internal($item, $user);
75
		if (!$this->memory_cache) {
76
			$this->__finish();
77
		}
78
		return $result;
79
	}
80
	/**
81
	 * Get data item of specified user
82
	 *
83
	 * @param string|string[] $item
84
	 * @param false|int       $user If not specified - current user assumed
85
	 * @param bool            $cache_only
86
	 *
87
	 * @return false|int|string|mixed[]
0 ignored issues
show
Documentation introduced by
Should the return type not be false|array|integer|string? Also, consider making the array more specific, something like array<String>, or 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.

If the return type contains the type array, this check recommends the use of a more specific type like String[] or array<String>.

Loading history...
88
	 */
89
	protected function get_internal ($item, $user = false, $cache_only = false) {
90
		$user = (int)$user ?: $this->id;
91
		if (!$user) {
92
			return false;
93
		}
94
		/**
95
		 * Reference for simpler usage
96
		 */
97
		$data = &$this->data[$user];
98
		/**
99
		 * If get an array of values
100
		 */
101
		if (is_array($item)) {
102
			$result = $new_items = [];
103
			/**
104
			 * Trying to get value from the local cache, or make up an array of missing values
105
			 */
106
			foreach ($item as $i) {
107
				if (in_array($i, $this->users_columns)) {
108
					if (($res = $this->get_internal($i, $user, true)) !== false) {
109
						$result[$i] = $res;
110
					} else {
111
						$new_items[] = $i;
112
					}
113
				}
114
			}
115
			if (empty($new_items)) {
116
				return $result;
117
			}
118
			/**
119
			 * If there are missing values - get them from the database
120
			 */
121
			$new_items = '`'.implode('`, `', $new_items).'`';
122
			$res       = $this->db()->qf(
123
				"SELECT $new_items
124
				FROM `[prefix]users`
125
				WHERE `id` = '$user'
126
				LIMIT 1"
127
			);
128
			unset($new_items);
129
			if (is_array($res)) {
130
				$this->update_cache[$user] = true;
131
				$data                      = array_merge($res, $data ?: []);
132
				$result                    = array_merge($result, $res);
133
				/**
134
				 * Sorting the resulting array in the same manner as the input array
135
				 */
136
				$res = [];
137
				foreach ($item as $i) {
138
					$res[$i] = &$result[$i];
139
				}
140
				return $res;
141
			} else {
142
				return false;
143
			}
144
		}
145
		/**
146
		 * If get one value
147
		 */
148
		return $this->get_internal_one_item($item, $user, $data, $cache_only);
149
	}
150
	/**
151
	 * @param string  $item
152
	 * @param int     $user
153
	 * @param mixed[] $data
154
	 * @param bool    $cache_only
155
	 *
156
	 * @return false|int|string
157
	 */
158
	protected function get_internal_one_item ($item, $user, &$data, $cache_only) {
159
		if (!in_array($item, $this->users_columns)) {
160
			return false;
161
		}
162
		/**
163
		 * If data in local cache - return them
164
		 */
165
		if (isset($data[$item])) {
166
			return $data[$item];
167
		}
168
		/**
169
		 * Try to get data from the cache
170
		 */
171
		$data_from_cache = $this->cache->$user;
172
		if (is_array($data_from_cache)) {
173
			/**
174
			 * Update the local cache
175
			 */
176
			$data = array_merge($data_from_cache, $data ?: []);
177
			/**
178
			 * New attempt of getting the data from cache
179
			 */
180
			if (isset($data[$item])) {
181
				return $data[$item];
182
			}
183
		}
184
		if (!$cache_only) {
185
			$data_from_db = $this->db()->qfs(
186
				"SELECT `$item`
187
				FROM `[prefix]users`
188
				WHERE `id` = '$user'
189
				LIMIT 1"
190
			);
191
			if ($data_from_db !== false) {
192
				$this->update_cache[$user] = true;
193
				return $data[$item] = $data_from_db;
194
			}
195
		}
196
		return false;
197
	}
198
	/**
199
	 * Set data item of specified user
200
	 *
201
	 * @param array|string    $item Item-value array may be specified for setting several items at once
202
	 * @param int|null|string $value
203
	 * @param false|int       $user If not specified - current user assumed
204
	 *
205
	 * @return bool
206
	 */
207
	function set ($item, $value = null, $user = false) {
208
		$result = $this->set_internal($item, $value, $user);
209
		if (!$this->memory_cache) {
210
			$this->__finish();
211
		}
212
		return $result;
213
	}
214
	/**
215
	 * Set data item of specified user
216
	 *
217
	 * @param array|string    $item Item-value array may be specified for setting several items at once
218
	 * @param int|null|string $value
219
	 * @param false|int       $user If not specified - current user assumed
220
	 *
221
	 * @return bool
222
	 */
223
	protected function set_internal ($item, $value = null, $user = false) {
224
		$user = (int)$user ?: $this->id;
225
		if (!$user) {
226
			return false;
227
		}
228
		if (is_array($item)) {
229
			$result = true;
230
			foreach ($item as $i => $v) {
231
				$result = $result && $this->set($i, $v, $user);
232
			}
233
			return $result;
234
		}
235
		if (!$this->set_internal_allowed($user, $item, $value)) {
236
			return false;
237
		}
238
		if ($item === 'language') {
239
			$value = $value ? Language::instance()->get('clanguage', $value) : '';
240
		} elseif ($item == 'avatar') {
241
			if (
242
				strpos($value, 'http://') !== 0 &&
243
				strpos($value, 'https://') !== 0
244
			) {
245
				$value = '';
246
			} else {
247
				$old_value = $this->get($item, $user);
248
				if ($value !== $old_value) {
249
					$Event = Event::instance();
250
					$Event->fire(
251
						'System/upload_files/del_tag',
252
						[
253
							'url' => $old_value,
254
							'tag' => "users/$user/avatar"
255
						]
256
					);
257
					$Event->fire(
258
						'System/upload_files/add_tag',
259
						[
260
							'url' => $value,
261
							'tag' => "users/$user/avatar"
262
						]
263
					);
264
				}
265
			}
266
		}
267
		$this->update_cache[$user]    = true;
268
		$this->data[$user][$item]     = $value;
269
		$this->data_set[$user][$item] = $value;
270
		if (in_array($item, ['login', 'email'], true)) {
271
			$old_value                            = $this->get($item.'_hash', $user);
272
			$this->data[$user][$item.'_hash']     = hash('sha224', $value);
273
			$this->data_set[$user][$item.'_hash'] = hash('sha224', $value);
274
			unset($this->cache->$old_value);
275
		} elseif ($item === 'password_hash' || ($item === 'status' && $value == 0)) {
276
			Session::instance()->del_all($user);
277
		}
278
		return true;
279
	}
280
	/**
281
	 * Check whether setting specified item to specified value for specified user is allowed
282
	 *
283
	 * @param int    $user
284
	 * @param string $item
285
	 * @param string $value
286
	 *
287
	 * @return bool
288
	 */
289
	protected function set_internal_allowed ($user, $item, $value) {
290
		if (
291
			$user === User::GUEST_ID ||
292
			$item === 'id' ||
293
			!in_array($item, $this->users_columns, true)
294
		) {
295
			return false;
296
		}
297
		if (in_array($item, ['login', 'email'], true)) {
298
			$value = mb_strtolower($value);
299
			if (
300
				$item === 'email' &&
301
				!filter_var($value, FILTER_VALIDATE_EMAIL) &&
302
				!in_array(User::BOT_GROUP_ID, $this->get_groups($user))
303
			) {
304
				return false;
305
			}
306
			if ($value === $this->get($item, $user)) {
307
				return true;
308
			}
309
			return !$this->get_id(hash('sha224', $value));
310
		}
311
		return true;
312
	}
313
	/**
314
	 * Getting additional data item(s) of specified user
315
	 *
316
	 * @param string|string[] $item
317
	 * @param false|int       $user If not specified - current user assumed
318
	 *
319
	 * @return false|string|mixed[]
320
	 */
321
	function get_data ($item, $user = false) {
322
		$user = (int)$user ?: $this->id;
323
		if (!$user || !$item || $user == User::GUEST_ID) {
324
			return false;
325
		}
326
		$Cache = $this->cache;
327
		$data  = $Cache->{"data/$user"} ?: [];
328
		if (is_array($item)) {
329
			$result = [];
330
			$absent = [];
331
			foreach ($item as $i) {
332
				if (isset($data[$i])) {
333
					$result[$i] = $data[$i];
334
				} else {
335
					$absent[] = $i;
336
				}
337
			}
338
			if ($absent) {
339
				$absent = implode(
340
					',',
341
					$this->db()->s($absent)
342
				);
343
				$absent = array_column(
344
					$this->db()->qfa(
345
						[
346
							"SELECT `item`, `value`
347
							FROM `[prefix]users_data`
348
							WHERE
349
								`id`	= '$user' AND
350
								`item`	IN($absent)"
351
						]
352
					),
353
					'value',
354
					'item'
355
				);
356
				foreach ($absent as &$a) {
357
					$a = _json_decode($a);
358
					if ($a === null) {
359
						$a = false;
360
					}
361
				}
362
				unset($a);
363
				$result += $absent;
364
				$data += $absent;
365
				$Cache->{"data/$user"} = $data;
366
			}
367
			return $result;
368
		}
369
		if ($data === false || !isset($data[$item])) {
370
			if (!is_array($data)) {
371
				$data = [];
372
			}
373
			$data[$item] = _json_decode(
374
				$this->db()->qfs(
375
					[
376
						"SELECT `value`
377
						FROM `[prefix]users_data`
378
						WHERE
379
							`id`	= '$user' AND
380
							`item`	= '%s'",
381
						$item
382
					]
383
				)
384
			);
385
			if ($data[$item] === null) {
386
				$data[$item] = false;
387
			}
388
			$Cache->{"data/$user"} = $data;
389
		}
390
		return $data[$item];
391
	}
392
	/**
393
	 * Setting additional data item(s) of specified user
394
	 *
395
	 * @param array|string $item Item-value array may be specified for setting several items at once
396
	 * @param mixed|null   $value
397
	 * @param false|int    $user If not specified - current user assumed
398
	 *
399
	 * @return bool
400
	 */
401
	function set_data ($item, $value = null, $user = false) {
402
		$user = (int)$user ?: $this->id;
403
		if (!$user || !$item || $user == User::GUEST_ID) {
404
			return false;
405
		}
406
		if (is_array($item)) {
407
			$params = [];
408
			foreach ($item as $i => $v) {
409
				$params[] = [
410
					$i,
411
					_json_encode($v)
412
				];
413
			}
414
			unset($i, $v);
415
			$result = $this->db_prime()->insert(
416
				"INSERT INTO `[prefix]users_data`
417
					(
418
						`id`,
419
						`item`,
420
						`value`
421
					) VALUES (
422
						$user,
423
						'%s',
424
						'%s'
425
					)
426
				ON DUPLICATE KEY UPDATE `value` = VALUES(`value`)",
427
				$params
428
			);
429
		} else {
430
			$result = $this->db_prime()->q(
431
				"INSERT INTO `[prefix]users_data`
432
					(
433
						`id`,
434
						`item`,
435
						`value`
436
					) VALUES (
437
						'$user',
438
						'%s',
439
						'%s'
440
					)
441
				ON DUPLICATE KEY UPDATE `value` = VALUES(`value`)",
442
				$item,
443
				_json_encode($value)
444
			);
445
		}
446
		unset($this->cache->{"data/$user"});
447
		return (bool)$result;
448
	}
449
	/**
450
	 * Deletion of additional data item(s) of specified user
451
	 *
452
	 * @param string|string[] $item
453
	 * @param false|int       $user If not specified - current user assumed
454
	 *
455
	 * @return bool
456
	 */
457
	function del_data ($item, $user = false) {
458
		$user = (int)$user ?: $this->id;
459
		if (!$user || !$item || $user == User::GUEST_ID) {
460
			return false;
461
		}
462
		$item   = implode(
463
			',',
464
			$this->db_prime()->s((array)$item)
465
		);
466
		$result = $this->db_prime()->q(
467
			"DELETE FROM `[prefix]users_data`
468
			WHERE
469
				`id`	= '$user' AND
470
				`item`	IN($item)"
471
		);
472
		unset($this->cache->{"data/$user"});
473
		return (bool)$result;
474
	}
475
	/**
476
	 * Get user id by login or email hash (sha224) (hash from lowercase string)
477
	 *
478
	 * @param  string $login_hash Login or email hash
479
	 *
480
	 * @return false|int User id if found and not guest, otherwise - boolean <i>false</i>
481
	 */
482
	function get_id ($login_hash) {
483
		if (!preg_match('/^[0-9a-z]{56}$/', $login_hash)) {
484
			return false;
485
		}
486
		$id = $this->cache->get(
487
			$login_hash,
488
			function () use ($login_hash) {
489
				return $this->db()->qfs(
490
					[
491
						"SELECT `id`
492
						FROM `[prefix]users`
493
						WHERE
494
							`login_hash`	= '%s' OR
495
							`email_hash`	= '%s'
496
						LIMIT 1",
497
						$login_hash,
498
						$login_hash
499
					]
500
				) ?: false;
501
			}
502
		);
503
		return $id && $id != User::GUEST_ID ? $id : false;
504
	}
505
	/**
506
	 * Get user avatar, if no one present - uses Gravatar
507
	 *
508
	 * @param int|null  $size Avatar size, if not specified or resizing is not possible - original image is used
509
	 * @param false|int $user If not specified - current user assumed
510
	 *
511
	 * @return string
512
	 */
513
	function avatar ($size = null, $user = false) {
514
		$user   = (int)$user ?: $this->id;
515
		$avatar = $this->get('avatar', $user);
516
		$Config = Config::instance();
517
		if (!$avatar && $this->id != User::GUEST_ID && $Config->core['gravatar_support']) {
518
			$email_hash     = md5($this->get('email', $user));
519
			$default_avatar = urlencode($Config->core_url().'/includes/img/guest.svg');
520
			$avatar         = "https://www.gravatar.com/avatar/$email_hash?d=mm&s=$size&d=$default_avatar";
521
		}
522
		if (!$avatar) {
523
			$avatar = '/includes/img/guest.svg';
524
		}
525
		return h::prepare_url($avatar, true);
526
	}
527
	/**
528
	 * Get user name or login or email, depending on existing information
529
	 *
530
	 * @param false|int $user If not specified - current user assumed
531
	 *
532
	 * @return string
533
	 */
534
	function username ($user = false) {
535
		$user = (int)$user ?: $this->id;
536
		if ($user === User::GUEST_ID) {
537
			return Language::instance()->system_profile_guest;
538
		}
539
		$username = $this->get('username', $user);
540
		if (!$username) {
541
			$username = $this->get('login', $user);
542
		}
543
		if (!$username) {
544
			$username = $this->get('email', $user);
545
		}
546
		return $username;
547
	}
548
	/**
549
	 * Disable memory cache
550
	 *
551
	 * Memory cache stores users data inside User class in order to get data faster next time.
552
	 * 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.
553
	 */
554
	function disable_memory_cache () {
555
		$this->memory_cache = false;
556
	}
557
	/**
558
	 * Returns array of users columns, available for getting of data
559
	 *
560
	 * @return array
561
	 */
562
	function get_users_columns () {
563
		return $this->users_columns;
564
	}
565
	/**
566
	 * Saving changes of cache and users data
567
	 */
568
	protected function save_cache_and_user_data () {
569
		/**
570
		 * Updating users data
571
		 */
572
		if (is_array($this->data_set) && !empty($this->data_set)) {
573
			$update = [];
574
			foreach ($this->data_set as $id => &$data_set) {
575
				$data = [];
576
				foreach ($data_set as $i => $value) {
577
					if ($i != 'id' && in_array($i, $this->users_columns)) {
578
						$value  = xap($value, false);
579
						$data[] = "`$i` = ".$this->db_prime()->s($value);
580
					} elseif ($i != 'id') {
581
						unset($data_set[$i]);
582
					}
583
				}
584
				unset($i, $value);
585
				if (!empty($data)) {
586
					$data     = implode(', ', $data);
587
					$update[] = "UPDATE `[prefix]users`
588
						SET $data
589
						WHERE `id` = '$id'";
590
				}
591
				unset($data);
592
			}
593
			unset($id, $data_set);
594
			if (!empty($update)) {
595
				$this->db_prime()->q($update);
596
			}
597
			unset($update);
598
		}
599
		/**
600
		 * Updating users cache
601
		 */
602
		foreach ($this->data as $id => $data) {
603
			if (isset($this->update_cache[$id]) && $this->update_cache[$id]) {
604
				$data['id']       = $id;
605
				$this->cache->$id = $data;
606
			}
607
		}
608
		unset($id, $data);
609
		$this->update_cache = [];
610
		$this->data_set     = [];
611
	}
612
}
613