1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* @package CleverStyle Framework |
4
|
|
|
* @author Nazar Mokrynskyi <[email protected]> |
5
|
|
|
* @license 0BSD |
6
|
|
|
*/ |
7
|
|
|
namespace cs\User; |
8
|
|
|
use |
9
|
|
|
cs\Config, |
10
|
|
|
cs\Event, |
11
|
|
|
cs\Language, |
12
|
|
|
cs\Session, |
13
|
|
|
cs\User, |
14
|
|
|
h; |
15
|
|
|
|
16
|
|
|
/** |
17
|
|
|
* Trait that contains all methods for `cs\User` for working with user's profile |
18
|
|
|
* |
19
|
|
|
* @property \cs\Cache\Prefix $cache |
20
|
|
|
* @property int $id |
21
|
|
|
* |
22
|
|
|
* @method \cs\DB\_Abstract db() |
23
|
|
|
* @method \cs\DB\_Abstract db_prime() |
24
|
|
|
* @method false|int[] get_groups(false|int $user) |
25
|
|
|
*/ |
26
|
|
|
trait Profile { |
27
|
|
|
/** |
28
|
|
|
* Copy of columns list of users table for internal needs without Cache usage |
29
|
|
|
* @var array |
30
|
|
|
*/ |
31
|
|
|
protected $users_columns = []; |
32
|
|
|
/** |
33
|
|
|
* Local cache of users data |
34
|
|
|
* @var array |
35
|
|
|
*/ |
36
|
|
|
protected $data = []; |
37
|
69 |
|
protected function initialize_data () { |
38
|
69 |
|
$this->users_columns = $this->cache->get( |
|
|
|
|
39
|
69 |
|
'columns', |
40
|
69 |
|
function () { |
41
|
6 |
|
return $this->db()->columns('[prefix]users'); |
42
|
69 |
|
} |
43
|
|
|
); |
44
|
69 |
|
} |
45
|
|
|
/** |
46
|
|
|
* Get data item of specified user |
47
|
|
|
* |
48
|
|
|
* @param string|string[] $item |
49
|
|
|
* @param false|int $user If not specified - current user assumed |
50
|
|
|
* |
51
|
|
|
* @return false|int|mixed[]|string|Properties If <i>$item</i> is integer - cs\User\Properties object will be returned |
52
|
|
|
*/ |
53
|
51 |
|
public function get ($item, $user = false) { |
54
|
51 |
|
if (is_scalar($item) && ctype_digit((string)$item)) { |
55
|
3 |
|
return new Properties($item); |
|
|
|
|
56
|
|
|
} |
57
|
51 |
|
return $this->get_internal($item, $user); |
58
|
|
|
} |
59
|
|
|
/** |
60
|
|
|
* Get data item of specified user |
61
|
|
|
* |
62
|
|
|
* @param string|string[] $item |
63
|
|
|
* @param false|int $user If not specified - current user assumed |
64
|
|
|
* |
65
|
|
|
* @return false|int|string|mixed[] |
66
|
|
|
*/ |
67
|
51 |
|
protected function get_internal ($item, $user = false) { |
68
|
51 |
|
$user = (int)$user ?: $this->id; |
69
|
51 |
|
if (isset($this->data[$user])) { |
70
|
48 |
|
$data = $this->data[$user]; |
71
|
|
|
} else { |
72
|
51 |
|
$data = $this->cache->get( |
73
|
51 |
|
$user, |
74
|
51 |
|
function () use ($user) { |
75
|
48 |
|
return $this->db()->qf( |
76
|
|
|
"SELECT * |
|
|
|
|
77
|
|
|
FROM `[prefix]users` |
78
|
48 |
|
WHERE `id` = $user |
79
|
|
|
LIMIT 1" |
80
|
48 |
|
) ?: false; |
81
|
51 |
|
} |
82
|
|
|
); |
83
|
51 |
|
if (!$data) { |
84
|
9 |
|
return false; |
85
|
51 |
|
} elseif ($this->memory_cache || $user == User::GUEST_ID) { |
|
|
|
|
86
|
51 |
|
$this->data[$user] = $data; |
87
|
|
|
} |
88
|
|
|
} |
89
|
|
|
/** |
90
|
|
|
* If get an array of values |
91
|
|
|
*/ |
92
|
51 |
|
if (is_array($item)) { |
93
|
48 |
|
$result = []; |
94
|
|
|
/** |
95
|
|
|
* Trying to get value from the local cache, or make up an array of missing values |
96
|
|
|
*/ |
97
|
48 |
|
foreach ($item as $i) { |
98
|
48 |
|
if (in_array($i, $this->users_columns)) { |
99
|
48 |
|
$result[$i] = $data[$i]; |
100
|
|
|
} |
101
|
|
|
} |
102
|
48 |
|
return $result; |
103
|
|
|
} |
104
|
48 |
|
return in_array($item, $this->users_columns) ? $data[$item] : false; |
105
|
|
|
} |
106
|
|
|
/** |
107
|
|
|
* Set data item of specified user |
108
|
|
|
* |
109
|
|
|
* @param array|string $item Item-value array may be specified for setting several items at once |
110
|
|
|
* @param int|null|string $value |
111
|
|
|
* @param false|int $user If not specified - current user assumed |
112
|
|
|
* |
113
|
|
|
* @return bool |
114
|
|
|
*/ |
115
|
42 |
|
public function set ($item, $value = null, $user = false) { |
116
|
42 |
|
$user = (int)$user ?: $this->id; |
117
|
42 |
|
$data_set = []; |
118
|
42 |
|
$data = is_array($item) ? $item : [$item => $value]; |
119
|
42 |
|
$result = true; |
120
|
42 |
|
foreach (xap($data) as $i => $v) { |
121
|
42 |
|
$result = $result && $this->set_internal($i, $v, $user, $data_set); |
122
|
|
|
} |
123
|
42 |
|
if (!$result) { |
124
|
6 |
|
return false; |
125
|
|
|
} |
126
|
42 |
|
if (!$data_set) { |
|
|
|
|
127
|
3 |
|
return true; |
128
|
|
|
} |
129
|
42 |
|
$this->set_internal_correct_login($data_set, $user); |
130
|
42 |
|
$update = []; |
131
|
42 |
|
foreach (array_keys($data_set) as $column) { |
132
|
42 |
|
$update[] = "`$column` = ?"; |
133
|
|
|
} |
134
|
42 |
|
$update = implode(', ', $update); |
135
|
42 |
|
$result = $this->db_prime()->q( |
136
|
|
|
"UPDATE `[prefix]users` |
137
|
42 |
|
SET $update |
138
|
42 |
|
WHERE `id` = '$user'", |
139
|
42 |
|
$data_set |
140
|
|
|
); |
141
|
42 |
|
if ($result) { |
142
|
42 |
|
unset($this->data[$user], $this->cache->$user); |
143
|
|
|
} |
144
|
42 |
|
return (bool)$result; |
145
|
|
|
} |
146
|
|
|
/** |
147
|
|
|
* Set data item of specified user |
148
|
|
|
* |
149
|
|
|
* @param string $item Item-value array may be specified for setting several items at once |
150
|
|
|
* @param int|string $value |
151
|
|
|
* @param int $user If not specified - current user assumed |
152
|
|
|
* @param array $data_set |
153
|
|
|
* |
154
|
|
|
* @return bool |
155
|
|
|
*/ |
156
|
42 |
|
protected function set_internal ($item, $value, $user, &$data_set) { |
157
|
42 |
|
if (!$this->set_internal_allowed($user, $item, $value)) { |
158
|
6 |
|
return false; |
159
|
|
|
} |
160
|
42 |
|
$old_value = $this->get($item, $user); |
161
|
42 |
|
if ($value == $old_value) { |
162
|
6 |
|
return true; |
163
|
|
|
} |
164
|
42 |
|
if ($item == 'language') { |
165
|
9 |
|
$value = $value && Language::instance()->get('clanguage', $value) == $value ? $value : ''; |
166
|
42 |
|
} elseif ($item == 'timezone') { |
167
|
6 |
|
$value = in_array($value, get_timezones_list(), true) ? $value : ''; |
168
|
42 |
|
} elseif ($item == 'avatar') { |
169
|
|
|
if ( |
170
|
6 |
|
strpos($value, 'http://') !== 0 && |
171
|
6 |
|
strpos($value, 'https://') !== 0 |
172
|
|
|
) { |
173
|
3 |
|
$value = ''; |
174
|
|
|
} |
175
|
6 |
|
$Event = Event::instance(); |
176
|
6 |
|
$Event->fire( |
177
|
6 |
|
'System/upload_files/del_tag', |
178
|
|
|
[ |
179
|
6 |
|
'url' => $old_value, |
180
|
6 |
|
'tag' => "users/$user/avatar" |
181
|
|
|
] |
182
|
|
|
); |
183
|
6 |
|
$Event->fire( |
184
|
6 |
|
'System/upload_files/add_tag', |
185
|
|
|
[ |
186
|
6 |
|
'url' => $value, |
187
|
6 |
|
'tag' => "users/$user/avatar" |
188
|
|
|
] |
189
|
|
|
); |
190
|
|
|
} |
191
|
|
|
/** |
192
|
|
|
* @var string $item |
193
|
|
|
*/ |
194
|
42 |
|
$data_set[$item] = $value; |
195
|
42 |
|
if (in_array($item, ['login', 'email'], true)) { |
196
|
6 |
|
$old_value = $this->get($item.'_hash', $user); |
197
|
6 |
|
$data_set[$item.'_hash'] = hash('sha224', $value); |
198
|
6 |
|
unset($this->cache->$old_value); |
199
|
42 |
|
} elseif ($item == 'password_hash' || ($item == 'status' && $value != User::STATUS_ACTIVE)) { |
200
|
42 |
|
Session::instance()->del_all($user); |
201
|
|
|
} |
202
|
42 |
|
return true; |
203
|
|
|
} |
204
|
|
|
/** |
205
|
|
|
* Check whether setting specified item to specified value for specified user is allowed |
206
|
|
|
* |
207
|
|
|
* @param int $user |
208
|
|
|
* @param string $item |
209
|
|
|
* @param string $value |
210
|
|
|
* |
211
|
|
|
* @return bool |
212
|
|
|
*/ |
213
|
42 |
|
protected function set_internal_allowed ($user, $item, $value) { |
214
|
|
|
if ( |
215
|
42 |
|
$user == User::GUEST_ID || |
216
|
42 |
|
$item == 'id' || |
217
|
42 |
|
!in_array($item, $this->users_columns, true) |
218
|
|
|
) { |
219
|
6 |
|
return false; |
220
|
|
|
} |
221
|
42 |
|
if (in_array($item, ['login', 'email'], true)) { |
222
|
6 |
|
$value = mb_strtolower($value); |
223
|
|
|
if ( |
224
|
6 |
|
$item == 'email' && |
225
|
6 |
|
!filter_var($value, FILTER_VALIDATE_EMAIL) |
226
|
|
|
) { |
227
|
3 |
|
return false; |
228
|
|
|
} |
229
|
|
|
if ( |
230
|
6 |
|
$item == 'login' && |
231
|
6 |
|
filter_var($value, FILTER_VALIDATE_EMAIL) && |
232
|
6 |
|
$value != $this->get('email', $user) |
233
|
|
|
) { |
234
|
3 |
|
return false; |
235
|
|
|
} |
236
|
6 |
|
if ($value == $this->get($item, $user)) { |
237
|
6 |
|
return true; |
238
|
|
|
} |
239
|
6 |
|
$existing_user = $this->get_id(hash('sha224', $value)) ?: $user; |
240
|
6 |
|
return $value && $existing_user == $user; |
241
|
|
|
} |
242
|
42 |
|
return true; |
243
|
|
|
} |
244
|
|
|
/** |
245
|
|
|
* A bit tricky here |
246
|
|
|
* |
247
|
|
|
* User is allowed to change login to own email, but not to any other email. However, when user changes email, it might happen that login will remain to |
248
|
|
|
* be the same as previous email, so we need to change login to new email as well. |
249
|
|
|
* |
250
|
|
|
* @param array $data_set |
251
|
|
|
* @param int $user |
252
|
|
|
*/ |
253
|
42 |
|
protected function set_internal_correct_login (&$data_set, $user) { |
254
|
|
|
/** |
255
|
|
|
* @var array $old_data |
256
|
|
|
*/ |
257
|
42 |
|
$old_data = $this->get(['login', 'email'], $user); |
258
|
42 |
|
$current_login = isset($data_set['login']) ? $data_set['login'] : $old_data['login']; |
259
|
|
|
if ( |
260
|
42 |
|
isset($data_set['email']) && |
261
|
42 |
|
$current_login == $old_data['email'] |
262
|
|
|
) { |
263
|
3 |
|
$data_set['login'] = $data_set['email']; |
264
|
3 |
|
$data_set['login_hash'] = $data_set['email_hash']; |
265
|
|
|
} |
266
|
42 |
|
} |
267
|
|
|
/** |
268
|
|
|
* Get user id by login or email hash (sha224) (hash from lowercase string) |
269
|
|
|
* |
270
|
|
|
* @param string $login_hash Login or email hash |
271
|
|
|
* |
272
|
|
|
* @return false|int User id if found and not guest, otherwise - boolean <i>false</i> |
273
|
|
|
*/ |
274
|
42 |
|
public function get_id ($login_hash) { |
275
|
42 |
|
if (!preg_match('/^[0-9a-z]{56}$/', $login_hash)) { |
276
|
6 |
|
return false; |
277
|
|
|
} |
278
|
42 |
|
$id = $this->cache->get( |
279
|
42 |
|
$login_hash, |
280
|
42 |
|
function () use ($login_hash) { |
281
|
42 |
|
return (int)$this->db()->qfs( |
282
|
42 |
|
"SELECT `id` |
|
|
|
|
283
|
|
|
FROM `[prefix]users` |
284
|
|
|
WHERE |
285
|
|
|
`login_hash` = '%s' OR |
286
|
|
|
`email_hash` = '%s' |
287
|
|
|
LIMIT 1", |
288
|
42 |
|
$login_hash, |
289
|
42 |
|
$login_hash |
290
|
42 |
|
) ?: false; |
291
|
42 |
|
} |
292
|
|
|
); |
293
|
42 |
|
return $id && $id != User::GUEST_ID ? $id : false; |
294
|
|
|
} |
295
|
|
|
/** |
296
|
|
|
* Get user avatar, if no one present - uses Gravatar |
297
|
|
|
* |
298
|
|
|
* @param int|null $size Avatar size, if not specified or resizing is not possible - original image is used |
299
|
|
|
* @param false|int $user If not specified - current user assumed |
300
|
|
|
* |
301
|
|
|
* @return string |
302
|
|
|
*/ |
303
|
12 |
|
public function avatar ($size = null, $user = false) { |
304
|
12 |
|
$user = (int)$user ?: $this->id; |
305
|
12 |
|
$avatar = $this->get('avatar', $user); |
306
|
12 |
|
$Config = Config::instance(); |
307
|
12 |
|
$guest_avatar = $Config->core_url().'/assets/img/guest.svg'; |
308
|
12 |
|
if (!$avatar && $this->id != User::GUEST_ID && $Config->core['gravatar_support']) { |
309
|
6 |
|
$email_hash = md5($this->get('email', $user)); |
|
|
|
|
310
|
6 |
|
$avatar = "https://www.gravatar.com/avatar/$email_hash?d=mm&s=$size&d=".urlencode($guest_avatar); |
311
|
|
|
} |
312
|
12 |
|
if (!$avatar) { |
313
|
12 |
|
$avatar = $guest_avatar; |
314
|
|
|
} |
315
|
12 |
|
return h::prepare_url($avatar, true); |
|
|
|
|
316
|
|
|
} |
317
|
|
|
/** |
318
|
|
|
* Get user name or login or email, depending on existing information |
319
|
|
|
* |
320
|
|
|
* @param false|int $user If not specified - current user assumed |
321
|
|
|
* |
322
|
|
|
* @return string |
323
|
|
|
*/ |
324
|
12 |
|
public function username ($user = false) { |
325
|
12 |
|
$user = (int)$user ?: $this->id; |
326
|
12 |
|
if ($user == User::GUEST_ID) { |
327
|
3 |
|
return Language::instance()->system_profile_guest; |
|
|
|
|
328
|
|
|
} |
329
|
9 |
|
$username = $this->get('username', $user); |
330
|
9 |
|
if (!$username) { |
331
|
9 |
|
$username = $this->get('login', $user); |
332
|
|
|
} |
333
|
9 |
|
return $username; |
334
|
|
|
} |
335
|
|
|
/** |
336
|
|
|
* Returns array of users columns, available for getting of data |
337
|
|
|
* |
338
|
|
|
* @return array |
339
|
|
|
*/ |
340
|
3 |
|
public function get_users_columns () { |
341
|
3 |
|
return $this->users_columns; |
342
|
|
|
} |
343
|
|
|
} |
344
|
|
|
|
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountId
that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theid
property of an instance of theAccount
class. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.