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