Completed
Push — master ( 7e1872...832b5f )
by Nazar
04:13
created

Data::set_data()   B

Complexity

Conditions 6
Paths 10

Size

Total Lines 31
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

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