Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
Total | |
0.00% |
0 / 1 |
|
90.91% |
10 / 11 |
CRAP | |
85.71% |
30 / 35 |
LocalUser | |
0.00% |
0 / 1 |
|
90.91% |
10 / 11 |
67.81 | |
85.71% |
30 / 35 |
setId | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
getId | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
setNickname | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
getNickname | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
setPassword | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
getPassword | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
setOutgoingEmail | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
getOutgoingEmail | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
setIncomingEmail | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
getIncomingEmail | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
setIsEmailVerified | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
getIsEmailVerified | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
setLanguage | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
getLanguage | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
setTimezone | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
getTimezone | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
setPhoneNumber | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
getPhoneNumber | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
setSmsCarrier | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
getSmsCarrier | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
setSmsEmail | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
getSmsEmail | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
setUri | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
getUri | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
setAutoFollowBack | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
getAutoFollowBack | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
setFollowPolicy | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
getFollowPolicy | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
setIsStreamPrivate | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
getIsStreamPrivate | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
setCreated | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
getCreated | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
setModified | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
getModified | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
getActor | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
getRoles | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
getSalt | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
getUsername | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
eraseCredentials | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
findByNicknameOrEmail | |
100.00% |
1 / 1 |
4 | |
100.00% |
6 / 6 |
|||
checkPassword | |
100.00% |
1 / 1 |
3 | |
100.00% |
6 / 6 |
|||
changePassword | |
0.00% |
0 / 1 |
12 | |
0.00% |
0 / 5 |
|||
hashPassword | |
100.00% |
1 / 1 |
1 | |
100.00% |
3 / 3 |
|||
algoNameToConstant | |
100.00% |
1 / 1 |
7 | |
100.00% |
9 / 9 |
|||
schemaDef | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
1 | <?php |
2 | |
3 | // {{{ License |
4 | |
5 | // This file is part of GNU social - https://www.gnu.org/software/social |
6 | // |
7 | // GNU social is free software: you can redistribute it and/or modify |
8 | // it under the terms of the GNU Affero General Public License as published by |
9 | // the Free Software Foundation, either version 3 of the License, or |
10 | // (at your option) any later version. |
11 | // |
12 | // GNU social is distributed in the hope that it will be useful, |
13 | // but WITHOUT ANY WARRANTY; without even the implied warranty of |
14 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
15 | // GNU Affero General Public License for more details. |
16 | // |
17 | // You should have received a copy of the GNU Affero General Public License |
18 | // along with GNU social. If not, see <http://www.gnu.org/licenses/>. |
19 | |
20 | // }}} |
21 | |
22 | namespace App\Entity; |
23 | |
24 | use App\Core\DB\DB; |
25 | use App\Core\Entity; |
26 | use App\Core\UserRoles; |
27 | use App\Util\Common; |
28 | use App\Util\Exception\DuplicateFoundException; |
29 | use DateTimeInterface; |
30 | use Exception; |
31 | use libphonenumber\PhoneNumber; |
32 | use Symfony\Component\Security\Core\User\UserInterface; |
33 | |
34 | /** |
35 | * Entity for users |
36 | * |
37 | * @category DB |
38 | * @package GNUsocial |
39 | * |
40 | * @author Zach Copley <zach@status.net> |
41 | * @copyright 2010 StatusNet Inc. |
42 | * @author Mikael Nordfeldth <mmn@hethane.se> |
43 | * @copyright 2009-2014 Free Software Foundation, Inc http://www.fsf.org |
44 | * @author Hugo Sales <hugo@hsal.es> |
45 | * @copyright 2020-2021 Free Software Foundation, Inc http://www.fsf.org |
46 | * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later |
47 | */ |
48 | class LocalUser extends Entity implements UserInterface |
49 | { |
50 | // {{{ Autocode |
51 | // @codeCoverageIgnoreStart |
52 | private int $id; |
53 | private string $nickname; |
54 | private ?string $password; |
55 | private ?string $outgoing_email; |
56 | private ?string $incoming_email; |
57 | private ?bool $is_email_verified; |
58 | private ?string $language; |
59 | private ?string $timezone; |
60 | private ?PhoneNumber $phone_number; |
61 | private ?int $sms_carrier; |
62 | private ?string $sms_email; |
63 | private ?string $uri; |
64 | private ?bool $auto_follow_back; |
65 | private ?int $follow_policy; |
66 | private ?bool $is_stream_private; |
67 | private \DateTimeInterface $created; |
68 | private \DateTimeInterface $modified; |
69 | |
70 | public function setId(int $id): self |
71 | { |
72 | $this->id = $id; |
73 | return $this; |
74 | } |
75 | |
76 | public function getId(): int |
77 | { |
78 | return $this->id; |
79 | } |
80 | |
81 | public function setNickname(string $nickname): self |
82 | { |
83 | $this->nickname = $nickname; |
84 | return $this; |
85 | } |
86 | |
87 | public function getNickname(): string |
88 | { |
89 | return $this->nickname; |
90 | } |
91 | |
92 | public function setPassword(?string $password): self |
93 | { |
94 | $this->password = $password; |
95 | return $this; |
96 | } |
97 | |
98 | public function getPassword(): ?string |
99 | { |
100 | return $this->password; |
101 | } |
102 | |
103 | public function setOutgoingEmail(?string $outgoing_email): self |
104 | { |
105 | $this->outgoing_email = $outgoing_email; |
106 | return $this; |
107 | } |
108 | |
109 | public function getOutgoingEmail(): ?string |
110 | { |
111 | return $this->outgoing_email; |
112 | } |
113 | |
114 | public function setIncomingEmail(?string $incoming_email): self |
115 | { |
116 | $this->incoming_email = $incoming_email; |
117 | return $this; |
118 | } |
119 | |
120 | public function getIncomingEmail(): ?string |
121 | { |
122 | return $this->incoming_email; |
123 | } |
124 | |
125 | public function setIsEmailVerified(?bool $is_email_verified): self |
126 | { |
127 | $this->is_email_verified = $is_email_verified; |
128 | return $this; |
129 | } |
130 | |
131 | public function getIsEmailVerified(): ?bool |
132 | { |
133 | return $this->is_email_verified; |
134 | } |
135 | |
136 | public function setLanguage(?string $language): self |
137 | { |
138 | $this->language = $language; |
139 | return $this; |
140 | } |
141 | |
142 | public function getLanguage(): ?string |
143 | { |
144 | return $this->language; |
145 | } |
146 | |
147 | public function setTimezone(?string $timezone): self |
148 | { |
149 | $this->timezone = $timezone; |
150 | return $this; |
151 | } |
152 | |
153 | public function getTimezone(): ?string |
154 | { |
155 | return $this->timezone; |
156 | } |
157 | |
158 | public function setPhoneNumber(?PhoneNumber $phone_number): self |
159 | { |
160 | $this->phone_number = $phone_number; |
161 | return $this; |
162 | } |
163 | |
164 | public function getPhoneNumber(): ?PhoneNumber |
165 | { |
166 | return $this->phone_number; |
167 | } |
168 | |
169 | public function setSmsCarrier(?int $sms_carrier): self |
170 | { |
171 | $this->sms_carrier = $sms_carrier; |
172 | return $this; |
173 | } |
174 | |
175 | public function getSmsCarrier(): ?int |
176 | { |
177 | return $this->sms_carrier; |
178 | } |
179 | |
180 | public function setSmsEmail(?string $sms_email): self |
181 | { |
182 | $this->sms_email = $sms_email; |
183 | return $this; |
184 | } |
185 | |
186 | public function getSmsEmail(): ?string |
187 | { |
188 | return $this->sms_email; |
189 | } |
190 | |
191 | public function setUri(?string $uri): self |
192 | { |
193 | $this->uri = $uri; |
194 | return $this; |
195 | } |
196 | |
197 | public function getUri(): ?string |
198 | { |
199 | return $this->uri; |
200 | } |
201 | |
202 | public function setAutoFollowBack(?bool $auto_follow_back): self |
203 | { |
204 | $this->auto_follow_back = $auto_follow_back; |
205 | return $this; |
206 | } |
207 | |
208 | public function getAutoFollowBack(): ?bool |
209 | { |
210 | return $this->auto_follow_back; |
211 | } |
212 | |
213 | public function setFollowPolicy(?int $follow_policy): self |
214 | { |
215 | $this->follow_policy = $follow_policy; |
216 | return $this; |
217 | } |
218 | |
219 | public function getFollowPolicy(): ?int |
220 | { |
221 | return $this->follow_policy; |
222 | } |
223 | |
224 | public function setIsStreamPrivate(?bool $is_stream_private): self |
225 | { |
226 | $this->is_stream_private = $is_stream_private; |
227 | return $this; |
228 | } |
229 | |
230 | public function getIsStreamPrivate(): ?bool |
231 | { |
232 | return $this->is_stream_private; |
233 | } |
234 | |
235 | public function setCreated(DateTimeInterface $created): self |
236 | { |
237 | $this->created = $created; |
238 | return $this; |
239 | } |
240 | |
241 | public function getCreated(): DateTimeInterface |
242 | { |
243 | return $this->created; |
244 | } |
245 | |
246 | public function setModified(DateTimeInterface $modified): self |
247 | { |
248 | $this->modified = $modified; |
249 | return $this; |
250 | } |
251 | |
252 | public function getModified(): DateTimeInterface |
253 | { |
254 | return $this->modified; |
255 | } |
256 | |
257 | // @codeCoverageIgnoreEnd |
258 | // }}} Autocode |
259 | |
260 | public function getActor() |
261 | { |
262 | return DB::find('gsactor', ['id' => $this->id]); |
263 | } |
264 | |
265 | /** |
266 | * Returns the roles granted to the user |
267 | */ |
268 | public function getRoles() |
269 | { |
270 | return UserRoles::toArray($this->getActor()->getRoles()); |
271 | } |
272 | |
273 | /** |
274 | * Returns the password used to authenticate the user. |
275 | * |
276 | * Implemented in the auto code |
277 | */ |
278 | |
279 | /** |
280 | * Returns the salt that was originally used to encode the password. |
281 | * BCrypt and Argon2 generate their own salts |
282 | */ |
283 | public function getSalt() |
284 | { |
285 | return null; |
286 | } |
287 | |
288 | /** |
289 | * Returns the username used to authenticate the user. |
290 | */ |
291 | public function getUsername() |
292 | { |
293 | return $this->nickname; |
294 | } |
295 | |
296 | /** |
297 | * Removes sensitive data from the user. |
298 | * |
299 | * This is important if, at any given point, sensitive information like |
300 | * the plain-text password is stored on this object. |
301 | */ |
302 | public function eraseCredentials() |
303 | { |
304 | } |
305 | |
306 | /** |
307 | * Is the nickname or email already in use locally? |
308 | * |
309 | * @return self Returns self if nickname or email found |
310 | */ |
311 | public static function findByNicknameOrEmail(string $nickname, string $email): ?self |
312 | { |
313 | $users = DB::findBy('local_user', ['or' => ['nickname' => $nickname, 'outgoing_email' => $email, 'incoming_email' => $email]]); |
314 | switch (count($users)) { |
315 | case 0: |
316 | return null; |
317 | case 1: |
318 | return $users[0]; |
319 | default: |
320 | // @codeCoverageIgnoreStart |
321 | throw new DuplicateFoundException('Multiple values in table local_user match the requested criteria'); |
322 | // @codeCoverageIgnoreEnd |
323 | } |
324 | } |
325 | |
326 | /** |
327 | * When authenticating, check a user's password in a timing safe |
328 | * way. Will update the password by rehashing if deemed necessary |
329 | */ |
330 | public function checkPassword(string $password_plain_text): bool |
331 | { |
332 | // Timing safe password verification |
333 | if (password_verify($password_plain_text, $this->password)) { |
334 | // Update old formats |
335 | if (password_needs_rehash($this->password, |
336 | self::algoNameToConstant(Common::config('security', 'algorithm')), |
337 | Common::config('security', 'options')) |
338 | ) { |
339 | // @codeCoverageIgnoreStart |
340 | $this->changePassword(null, $password_plain_text, override: true); |
341 | // @codeCoverageIgnoreEnd |
342 | } |
343 | return true; |
344 | } |
345 | return false; |
346 | } |
347 | |
348 | public function changePassword(?string $old_password_plain_text, string $new_password_plain_text, bool $override = false): bool |
349 | { |
350 | if ($override || $this->checkPassword($old_password_plain_text)) { |
351 | $this->setPassword(self::hashPassword($new_password_plain_text)); |
352 | DB::flush(); |
353 | return true; |
354 | } |
355 | return false; |
356 | } |
357 | |
358 | public static function hashPassword(string $password) |
359 | { |
360 | $algorithm = self::algoNameToConstant(Common::config('security', 'algorithm')); |
361 | $options = Common::config('security', 'options'); |
362 | |
363 | return password_hash($password, $algorithm, $options); |
364 | } |
365 | |
366 | /** |
367 | * Public for testing |
368 | */ |
369 | public static function algoNameToConstant(string $algo) |
370 | { |
371 | switch ($algo) { |
372 | case 'bcrypt': |
373 | case 'argon2i': |
374 | case 'argon2d': |
375 | case 'argon2id': |
376 | $c = 'PASSWORD_' . strtoupper($algo); |
377 | if (defined($c)) { |
378 | return constant($c); |
379 | } |
380 | // fallthrough |
381 | // no break |
382 | default: |
383 | throw new Exception('Unsupported or unsafe hashing algorithm requested'); |
384 | } |
385 | } |
386 | |
387 | public static function schemaDef(): array |
388 | { |
389 | return [ |
390 | 'name' => 'local_user', |
391 | 'description' => 'local users, bots, etc', |
392 | 'fields' => [ |
393 | 'id' => ['type' => 'int', 'foreign key' => true, 'target' => 'GSActor.id', 'multiplicity' => 'one to one', 'not null' => true, 'description' => 'foreign key to gsactor table'], |
394 | 'nickname' => ['type' => 'varchar', 'not null' => true, 'length' => 64, 'description' => 'nickname or username, foreign key to gsactor'], |
395 | 'password' => ['type' => 'varchar', 'length' => 191, 'description' => 'salted password, can be null for users with federated authentication'], |
396 | 'outgoing_email' => ['type' => 'varchar', 'length' => 191, 'description' => 'email address for password recovery, notifications, etc.'], |
397 | 'incoming_email' => ['type' => 'varchar', 'length' => 191, 'description' => 'email address for post-by-email'], |
398 | 'is_email_verified' => ['type' => 'bool', 'default' => false, 'description' => 'Whether the user opened the comfirmation email'], |
399 | 'language' => ['type' => 'varchar', 'length' => 50, 'description' => 'preferred language'], |
400 | 'timezone' => ['type' => 'varchar', 'length' => 50, 'description' => 'timezone'], |
401 | 'phone_number' => ['type' => 'phone_number', 'description' => 'phone number'], |
402 | 'sms_carrier' => ['type' => 'int', 'foreign key' => true, 'target' => 'SmsCarrier.id', 'multiplicity' => 'one to one', 'description' => 'foreign key to sms_carrier'], |
403 | 'sms_email' => ['type' => 'varchar', 'length' => 191, 'description' => 'built from sms and carrier (see sms_carrier)'], |
404 | 'uri' => ['type' => 'varchar', 'length' => 191, 'description' => 'universally unique identifier, usually a tag URI'], |
405 | 'auto_follow_back' => ['type' => 'bool', 'default' => false, 'description' => 'automatically follow users who follow us'], |
406 | 'follow_policy' => ['type' => 'int', 'size' => 'tiny', 'default' => 0, 'description' => '0 = anybody can follow; 1 = require approval'], |
407 | 'is_stream_private' => ['type' => 'bool', 'default' => false, 'description' => 'whether to limit all notices to followers only'], |
408 | 'created' => ['type' => 'datetime', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was created'], |
409 | 'modified' => ['type' => 'timestamp', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was modified'], |
410 | ], |
411 | 'primary key' => ['id'], |
412 | 'unique keys' => [ |
413 | 'user_nickname_key' => ['nickname'], |
414 | 'user_outgoing_email_key' => ['outgoing_email'], |
415 | 'user_incoming_email_key' => ['incoming_email'], |
416 | 'user_phone_number_key' => ['phone_number'], |
417 | 'user_uri_key' => ['uri'], |
418 | ], |
419 | 'indexes' => [ |
420 | 'user_nickname_idx' => ['nickname'], |
421 | 'user_created_idx' => ['created'], |
422 | 'user_sms_email_idx' => ['sms_email'], |
423 | ], |
424 | ]; |
425 | } |
426 | } |