Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
90.91% covered (success)
90.91%
10 / 11
CRAP
85.71% covered (warning)
85.71%
30 / 35
LocalUser
0.00% covered (danger)
0.00%
0 / 1
90.91% covered (success)
90.91%
10 / 11
67.81
85.71% covered (warning)
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% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 getRoles
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 getSalt
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 getUsername
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 eraseCredentials
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 findByNicknameOrEmail
100.00% covered (success)
100.00%
1 / 1
4
100.00% covered (success)
100.00%
6 / 6
 checkPassword
100.00% covered (success)
100.00%
1 / 1
3
100.00% covered (success)
100.00%
6 / 6
 changePassword
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 5
 hashPassword
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
3 / 3
 algoNameToConstant
100.00% covered (success)
100.00%
1 / 1
7
100.00% covered (success)
100.00%
9 / 9
 schemaDef
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
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
22namespace App\Entity;
23
24use App\Core\DB\DB;
25use App\Core\Entity;
26use App\Core\UserRoles;
27use App\Util\Common;
28use App\Util\Exception\DuplicateFoundException;
29use DateTimeInterface;
30use Exception;
31use libphonenumber\PhoneNumber;
32use 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 */
48class 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}