GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.

User::getAllUserOptions()   A
last analyzed

Complexity

Conditions 4
Paths 3

Size

Total Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
nc 3
nop 2
dl 0
loc 20
rs 9.6
c 0
b 0
f 0
1
<?php
2
3
namespace phpMyFAQ;
4
5
/**
6
 * Creates a new user object.
7
 *
8
 * A user are recognized by the session-id using getUserBySessionId(), by his
9
 * using getUserById() or by his nickname (login) using getUserByLogin(). New
10
 * are created using createNewUser().
11
 *
12
 * 
13
 *
14
 * This Source Code Form is subject to the terms of the Mozilla Public License,
15
 * v. 2.0. If a copy of the MPL was not distributed with this file, You can
16
 * obtain one at http://mozilla.org/MPL/2.0/.
17
 *
18
 * @package phpMyFAQ
19
 *
20
 * @author Lars Tiedemann <[email protected]>
21
 * @author Thorsten Rinne <[email protected]>
22
 * @author Sarah Hermann <[email protected]>
23
 * @copyright 2005-2019 phpMyFAQ Team
24
 * @license http://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
25
 * @link https://www.phpmyfaq.de
26
 * @since 2005-09-17
27
 */
28
29
use phpMyFAQ\Auth\Driver;
30
use phpMyFAQ\Permission\BasicPermission;
31
use phpMyFAQ\Permission\MediumPermission;
32
use phpMyFAQ\User\UserData;
33
34
if (!defined('IS_VALID_PHPMYFAQ')) {
35
    exit();
36
}
37
38
if (!defined('PMF_ENCRYPTION_TYPE')) {
39
    define('PMF_ENCRYPTION_TYPE', 'md5'); // Fallback to md5()
40
}
41
42
/**
43
 * User.
44
 *
45
 * @package phpMyFAQ
46
 * @author Lars Tiedemann <[email protected]>
47
 * @author Thorsten Rinne <[email protected]>
48
 * @author Sarah Hermann <[email protected]>
49
 * @copyright 2005-2019 phpMyFAQ Team
50
 * @license http://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
51
 * @link https://www.phpmyfaq.de
52
 * @since 2005-09-17
53
 */
54
class User
55
{
56
    const ERROR_UNDEFINED_PARAMETER = 'Following parameter must to be defined: ';
57
    const ERROR_USER_ADD = 'Account could not be created. ';
58
    const ERROR_USER_CANNOT_CREATE_USER = 'User account could not be created. ';
59
    const ERROR_USER_CANNOT_CREATE_USERDATA = 'Entry for user data could not be created. ';
60
    const ERROR_USER_CANNOT_DELETE_USER = 'User account could not be deleted. ';
61
    const ERROR_USER_CANNOT_DELETE_USERDATA = 'Entry for user data could not be deleted. ';
62
    const ERROR_USER_CANNOT_UPDATE_USERDATA = 'Entry for user data could not be updated. ';
63
    const ERROR_USER_CHANGE = 'Account could not be updated. ';
64
    const ERROR_USER_DELETE = 'Account could not be deleted. ';
65
    const ERROR_USER_INCORRECT_LOGIN = 'Specified login could not be found. ';
66
    const ERROR_USER_INCORRECT_PASSWORD = 'Specified password is not correct.';
67
    const ERROR_USER_INVALID_STATUS = 'Undefined user status.';
68
    const ERROR_USER_LOGINNAME_TOO_SHORT = 'The chosen loginname is too short.';
69
    const ERROR_USER_LOGIN_NOT_UNIQUE = 'Specified login name already exists. ';
70
    const ERROR_USER_LOGIN_INVALID = 'The chosen login is invalid. A valid login has at least four characters. Only letters, numbers and underscore _ are allowed. The first letter must be a letter. ';
71
    const ERROR_USER_NO_AUTH = 'No authentication method specified. ';
72
    const ERROR_USER_NO_DB = 'No database specified.';
73
    const ERROR_USER_NO_PERM = 'No permission container specified.';
74
    const ERROR_USER_NO_USERID = 'No user-ID found. ';
75
    const ERROR_USER_NO_USERLOGINDATA = 'No user login data found. ';
76
    const ERROR_USER_NOT_FOUND = 'User account could not be found. ';
77
    const ERROR_USER_NOWRITABLE = 'No authentication object is writable. ';
78
    const ERROR_USER_NO_LOGIN_DATA = 'A username and password must be provided. ';
79
    const ERROR_USER_TOO_MANY_FAILED_LOGINS = 'You exceeded the maximum amounts of login attempts and are temporarily blocked. Please try again later.';
80
81
    const STATUS_USER_PROTECTED = 'User account is protected. ';
82
    const STATUS_USER_BLOCKED = 'User account is blocked. ';
83
    const STATUS_USER_ACTIVE = 'User account is active. ';
84
85
    // --- ATTRIBUTES ---
86
87
    /**
88
     * Permission container.
89
     *
90
     * @var BasicPermission|MediumPermission
91
     */
92
    public $perm = null;
93
94
    /**
95
     * User-data storage container.
96
     *
97
     * @var UserData
98
     */
99
    public $userdata = null;
100
101
    /**
102
     * Default Authentication properties.
103
     *
104
     * @var array
105
     */
106
    private $authData = [
107
        'authSource' => [
108
            'name' => 'database',
109
            'type' => 'local',
110
        ],
111
        'encType' => PMF_ENCRYPTION_TYPE,
112
        'readOnly' => false,
113
    ];
114
115
    /**
116
     * Public array that contains error messages.
117
     *
118
     * @var array
119
     */
120
    public $errors = [];
121
122
    /**
123
     * authentication container.
124
     *
125
     * @var Driver[]
126
     */
127
    protected $authContainer = [];
128
129
    /**
130
     * login string.
131
     *
132
     * @var string
133
     */
134
    private $login = '';
135
136
    /**
137
     * minimum length of login string (default: 2).
138
     *
139
     * @var int
140
     */
141
    private $loginMinLength = 2;
142
143
    /**
144
     * regular expression to find invalid login strings
145
     * (default: /^[a-z0-9][\w\.\-@]+/i ).
146
     *
147
     * @var string
148
     */
149
    private $validUsername = '/^[a-z0-9][\w\.\-@]+/i';
150
151
    /**
152
     * user ID.
153
     *
154
     * @var int
155
     */
156
    private $userId = -1;
157
158
    /**
159
     * Status of user.
160
     *
161
     * @var string
162
     */
163
    private $status = '';
164
165
    /**
166
     * IS the user a super admin?
167
     * @var bool
168
     */
169
    private $isSuperAdmin = false;
170
171
    /**
172
     * array of allowed values for status.
173
     *
174
     * @var array
175
     */
176
    private $allowedStatus = [
177
        'active' => self::STATUS_USER_ACTIVE,
178
        'blocked' => self::STATUS_USER_BLOCKED,
179
        'protected' => self::STATUS_USER_PROTECTED,
180
    ];
181
182
    /**
183
     * Configuration.
184
     *
185
     * @var Configuration
186
     */
187
    protected $config = null;
188
189
    /**
190
     * Constructor.
191
     *
192
     * @param Configuration $config
193
     */
194
    public function __construct(Configuration $config)
195
    {
196
        $this->config = $config;
197
198
        $perm = Permission::selectPerm($this->config->get('security.permLevel'), $this->config);
199
        if (!$this->addPerm($perm)) {
200
            return;
201
        }
202
203
        // authentication objects
204
        // always make a 'local' $auth object (see: $authData)
205
        $this->authContainer = [];
206
        $auth = new Auth($this->config);
207
        $authLocal = $auth->selectAuth($this->getAuthSource('name'));
208
        $authLocal->selectEncType($this->getAuthData('encType'));
209
        $authLocal->setReadOnly($this->getAuthData('readOnly'));
0 ignored issues
show
Bug introduced by
It seems like $this->getAuthData('readOnly') targeting phpMyFAQ\User::getAuthData() can also be of type string; however, phpMyFAQ\Auth::setReadOnly() does only seem to accept boolean|null, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
210
211
        if (!$this->addAuth($authLocal, $this->getAuthSource('type'))) {
0 ignored issues
show
Documentation introduced by
$authLocal is of type object<phpMyFAQ\Auth>, but the function expects a object<phpMyFAQ\Auth\Driver>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
212
            return;
213
        }
214
215
        // additionally, set given $auth objects
216
        if (count($this->authContainer) > 0) {
217
            foreach ($auth as $name => $authObject) {
0 ignored issues
show
Bug introduced by
The expression $auth of type object<phpMyFAQ\Auth> is not traversable.
Loading history...
218
                if (!$authObject instanceof Driver && !$this->addAuth($authObject, $name)) {
219
                    break;
220
                }
221
            }
222
        }
223
224
        // user data object
225
        $this->userdata = new UserData($this->config);
226
    }
227
228
    /**
229
     * adds a permission object to the user.
230
     *
231
     * @param Permission $perm Permission object
232
     *
233
     * @return bool
234
     */
235
    public function addPerm(Permission $perm)
236
    {
237
        if ($this->checkPerm($perm)) {
238
            $this->perm = $perm;
0 ignored issues
show
Documentation Bug introduced by
It seems like $perm of type object<phpMyFAQ\Permission> is incompatible with the declared type object<phpMyFAQ\Permissi...ssion\MediumPermission> of property $perm.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
239
            return true;
240
        }
241
242
        $this->perm = null;
243
        return false;
244
    }
245
246
    /**
247
     * Returns the User ID of the user.
248
     *
249
     * @return int
250
     */
251
    public function getUserId()
252
    {
253
        if (isset($this->userId) && is_int($this->userId)) {
254
            return (int)$this->userId;
255
        }
256
        $this->userId = -1;
257
        $this->errors[] = self::ERROR_USER_NO_USERID;
258
259
        return -1;
260
    }
261
262
    /**
263
     * Loads basic user information from the database selecting the user with
264
     * specified user-ID.
265
     *
266
     * @param int  $userId            User ID
267
     * @param bool $allowBlockedUsers Allow blocked users as well, e.g. in admin
268
     *
269
     * @return bool
270
     */
271
    public function getUserById($userId, $allowBlockedUsers = false)
272
    {
273
        $select = sprintf('
274
            SELECT
275
                user_id,
276
                login,
277
                account_status,
278
                is_superadmin
279
            FROM
280
                %sfaquser
281
            WHERE
282
                user_id = %d '.($allowBlockedUsers ? '' : "AND account_status != 'blocked'"),
283
            Db::getTablePrefix(),
284
            (int)$userId
285
        );
286
287
        $res = $this->config->getDb()->query($select);
288 View Code Duplication
        if ($this->config->getDb()->numRows($res) != 1) {
289
            $this->errors[] = self::ERROR_USER_NO_USERID.'error(): '.$this->config->getDb()->error();
290
291
            return false;
292
        }
293
        $user = $this->config->getDb()->fetchArray($res);
294
        $this->userId = (int)$user['user_id'];
295
        $this->login = (string)$user['login'];
296
        $this->status = (string)$user['account_status'];
297
        $this->isSuperAdmin = (bool)$user['is_superadmin'];
298
299
        // get encrypted password
300
        // @todo: Add a getEncPassword method to the Auth* classes for the (local and remote) Auth Sources.
301
        if ('db' === $this->getAuthSource('name')) {
302
            $select = sprintf("
303
                SELECT
304
                    pass
305
                FROM
306
                    %sfaquserlogin
307
                WHERE
308
                    login = '%s'",
309
                Db::getTablePrefix(),
310
                $this->login
311
            );
312
313
            $res = $this->config->getDb()->query($select);
314 View Code Duplication
            if ($this->config->getDb()->numRows($res) != 1) {
315
                $this->errors[] = self::ERROR_USER_NO_USERLOGINDATA.'error(): '.$this->config->getDb()->error();
316
317
                return false;
318
            }
319
        }
320
        // get user-data
321
        if (!$this->userdata instanceof UserData) {
322
            $this->userdata = new UserData($this->config);
323
        }
324
        $this->userdata->load($this->getUserId());
325
326
        return true;
327
    }
328
329
    /**
330
     * loads basic user information from the database selecting the user with
331
     * specified login.
332
     *
333
     * @param string $login      Login name
334
     * @param bool   $raiseError Raise error?
335
     *
336
     * @return bool
337
     */
338 View Code Duplication
    public function getUserByLogin($login, $raiseError = true)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
339
    {
340
        $select = sprintf("
341
            SELECT
342
                user_id,
343
                login,
344
                account_status
345
            FROM
346
                %sfaquser
347
            WHERE
348
                login = '%s'",
349
            Db::getTablePrefix(),
350
            $this->config->getDb()->escape($login)
351
        );
352
353
        $res = $this->config->getDb()->query($select);
354
        if ($this->config->getDb()->numRows($res) !== 1) {
355
            if ($raiseError) {
356
                $this->errors[] = self::ERROR_USER_INCORRECT_LOGIN;
357
            }
358
359
            return false;
360
        }
361
        $user = $this->config->getDb()->fetchArray($res);
362
        $this->userId = (int)$user['user_id'];
363
        $this->login = (string)$user['login'];
364
        $this->status = (string)$user['account_status'];
365
366
        // get user-data
367
        if (!$this->userdata instanceof UserData) {
368
            $this->userdata = new UserData($this->config);
369
        }
370
        $this->userdata->load($this->getUserId());
371
372
        return true;
373
    }
374
375
    /**
376
     * loads basic user information from the database selecting the user with
377
     * specified cookie information.
378
     *
379
     * @param string $cookie
380
     *
381
     * @return bool
382
     */
383 View Code Duplication
    public function getUserByCookie($cookie)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
384
    {
385
        $select = sprintf("
386
            SELECT
387
                user_id,
388
                login,
389
                account_status
390
            FROM
391
                %sfaquser
392
            WHERE
393
                remember_me = '%s' AND account_status != 'blocked'",
394
            Db::getTablePrefix(),
395
            $this->config->getDb()->escape($cookie)
396
        );
397
398
        $res = $this->config->getDb()->query($select);
399
        if ($this->config->getDb()->numRows($res) !== 1) {
400
            $this->errors[] = self::ERROR_USER_INCORRECT_LOGIN;
401
402
            return false;
403
        }
404
        $user = $this->config->getDb()->fetchArray($res);
405
406
        // Don't ever login via anonymous user
407
        if (-1 === $user['user_id']) {
408
            return false;
409
        }
410
411
        $this->userId = (int)$user['user_id'];
412
        $this->login = (string)$user['login'];
413
        $this->status = (string)$user['account_status'];
414
415
        // get user-data
416
        if (!$this->userdata instanceof UserData) {
417
            $this->userdata = new UserData($this->config);
418
        }
419
        $this->userdata->load($this->getUserId());
420
421
        return true;
422
    }
423
424
    /**
425
     * Checks if display name is already used. Returns true, if already in use.
426
     *
427
     * @param string $name
428
     *
429
     * @return bool
430
     */
431 View Code Duplication
    public function checkDisplayName($name)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
432
    {
433
        if (!$this->userdata instanceof UserData) {
434
            $this->userdata = new UserData($this->config);
435
        }
436
437
        if ($name === $this->userdata->fetch('display_name', $name)) {
438
            return true;
439
        } else {
440
            return false;
441
        }
442
    }
443
444
    /**
445
     * Checks if email address is already used. Returns true, if already in use.
446
     *
447
     * @param string $name
448
     *
449
     * @return bool
450
     */
451 View Code Duplication
    public function checkMailAddress($name)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
452
    {
453
        if (!$this->userdata instanceof UserData) {
454
            $this->userdata = new UserData($this->config);
455
        }
456
457
        if ($name === $this->userdata->fetch('email', $name)) {
458
            return true;
459
        } else {
460
            return false;
461
        }
462
    }
463
464
    /**
465
     * search users by login.
466
     *
467
     * @param string $search Login name
468
     *
469
     * @return array
470
     */
471
    public function searchUsers($search)
472
    {
473
        $select = sprintf("
474
            SELECT
475
                login, 
476
                user_id,
477
                account_status
478
            FROM
479
                %sfaquser
480
            WHERE 
481
                login LIKE '%s'",
482
            Db::getTablePrefix(),
483
            $this->config->getDb()->escape($search.'%')
484
        );
485
486
        $res = $this->config->getDb()->query($select);
487
        if (!$res) {
488
            return [];
489
        }
490
491
        $result = [];
492
        while ($row = $this->config->getDb()->fetchArray($res)) {
493
            $result[] = $row;
494
        }
495
496
        return $result;
497
    }
498
499
    /**
500
     * Creates a new user and stores basic data in the database.
501
     *
502
     * @param string $login
503
     * @param string $pass
504
     * @param string $domain
505
     * @param int $userId
506
     *
507
     * @return boolean
508
     */
509
    public function createUser($login, $pass = '', $domain = '', $userId = 0)
510
    {
511
        foreach ($this->authContainer as $auth) {
512
            if (!$this->checkAuth($auth)) {
513
                return false;
514
            }
515
        }
516
517
        // is $login valid?
518
        $login = (string)$login;
519
        if (!$this->isValidLogin($login)) {
520
            $this->errors[] = self::ERROR_USER_LOGINNAME_TOO_SHORT;
521
522
            return false;
523
        }
524
525
        // does $login already exist?
526
        if ($this->getUserByLogin($login, false)) {
527
            $this->errors[] = self::ERROR_USER_LOGIN_NOT_UNIQUE;
528
529
            return false;
530
        }
531
532
        // set user-ID
533
        if (0 == $userId) {
534
            $this->userId = (int)$this->config->getDb()->nextId(Db::getTablePrefix().'faquser', 'user_id');
535
        } else {
536
            $this->userId = $userId;
537
        }
538
539
        // create user entry
540
        $insert = sprintf("
541
            INSERT INTO
542
                %sfaquser
543
            (user_id, login, session_timestamp, member_since)
544
                VALUES
545
            (%d, '%s', %d, '%s')",
546
            Db::getTablePrefix(),
547
            $this->getUserId(),
548
            $this->config->getDb()->escape($login),
549
            $_SERVER['REQUEST_TIME'],
550
            date('YmdHis', $_SERVER['REQUEST_TIME'])
551
        );
552
553
        $this->config->getDb()->query($insert);
554
        if (!$this->userdata instanceof UserData) {
555
            $this->userdata = new UserData($this->config);
556
        }
557
        $data = $this->userdata->add($this->getUserId());
558
        if (!$data) {
559
            $this->errors[] = self::ERROR_USER_CANNOT_CREATE_USERDATA;
560
561
            return false;
562
        }
563
564
        // create authentication entry
565
        if ($pass == '') {
566
            $pass = $this->createPassword();
567
        }
568
        $success = false;
569
570
        foreach ($this->authContainer as $name => $auth) {
571
            if ($auth->setReadOnly()) {
572
                continue;
573
            }
574
            if (!$auth->add($login, $pass, $domain)) {
575
                $this->errors[] = self::ERROR_USER_CANNOT_CREATE_USER.'in Auth '.$name;
576
            } else {
577
                $success = true;
578
            }
579
        }
580
        if (!$success) {
581
            return false;
582
        }
583
584
        if ($this->perm instanceof MediumPermission) {
585
            $this->perm->autoJoin($this->userId);
586
        }
587
588
        return $this->getUserByLogin($login, false);
589
    }
590
591
    /**
592
     * deletes the user from the database.
593
     *
594
     * @return bool
595
     */
596
    public function deleteUser()
597
    {
598
        if (!isset($this->userId) || $this->userId == 0) {
599
            $this->errors[] = self::ERROR_USER_NO_USERID;
600
601
            return false;
602
        }
603
604
        if (!isset($this->login) || strlen($this->login) == 0) {
605
            $this->errors[] = self::ERROR_USER_LOGIN_INVALID;
606
607
            return false;
608
        }
609
610
        if (isset($this->allowedStatus[$this->status]) && $this->allowedStatus[$this->status] == self::STATUS_USER_PROTECTED) {
611
            $this->errors[] = self::ERROR_USER_CANNOT_DELETE_USER.self::STATUS_USER_PROTECTED;
612
613
            return false;
614
        }
615
616
        $this->perm->refuseAllUserRights($this->userId);
617
618
        $delete = sprintf('
619
            DELETE FROM
620
                %sfaquser
621
            WHERE
622
                user_id = %d',
623
            Db::getTablePrefix(),
624
            $this->userId
625
        );
626
627
        $res = $this->config->getDb()->query($delete);
628 View Code Duplication
        if (!$res) {
629
            $this->errors[] = self::ERROR_USER_CANNOT_DELETE_USER.'error(): '.$this->config->getDb()->error();
630
631
            return false;
632
        }
633
634
        if (!$this->userdata instanceof UserData) {
635
            $this->userdata = new UserData($this->config);
636
        }
637
        $data = $this->userdata->delete($this->getUserId());
638
        if (!$data) {
639
            $this->errors[] = self::ERROR_USER_CANNOT_DELETE_USERDATA;
640
641
            return false;
642
        }
643
644
        $readOnly = 0;
645
        $authCount = 0;
646
        $delete = [];
647
        foreach ($this->authContainer as $auth) {
648
            ++$authCount;
649
            if ($auth->setReadOnly()) {
650
                ++$readOnly;
651
                continue;
652
            }
653
            $delete[] = $auth->delete($this->login);
654
        }
655
656
        if ($readOnly == $authCount) {
657
            $this->errors[] = self::ERROR_USER_NO_AUTH_WRITABLE;
658
        }
659
        if (!in_array(true, $delete)) {
660
            return false;
661
        }
662
663
        return true;
664
    }
665
666
    /**
667
     * changes the user's password. If $pass is omitted, a new
668
     * password is generated using the createPassword() method.
669
     *
670
     * @param string $pass Password
671
     *
672
     * @return bool
673
     */
674
    public function changePassword($pass = '')
675
    {
676
        foreach ($this->authContainer as $auth) {
677
            if (!$this->checkAuth($auth)) {
678
                return false;
679
            }
680
        }
681
682
        $login = $this->getLogin();
683
        if ($pass == '') {
684
            $pass = $this->createPassword();
685
        }
686
687
        $success = false;
688
        foreach ($this->authContainer as $auth) {
689
            if ($auth->setReadOnly()) {
690
                continue;
691
            }
692
            if (!$auth->changePassword($login, $pass)) {
693
                continue;
694
            } else {
695
                $success = true;
696
            }
697
        }
698
699
        return $success;
700
    }
701
702
    /**
703
     * returns the user's status.
704
     *
705
     * @return string
706
     */
707
    public function getStatus()
708
    {
709
        if (isset($this->status) && strlen($this->status) > 0) {
710
            return $this->status;
711
        }
712
713
        return false;
714
    }
715
716
    /**
717
     * Sets the user's status and updates the database entry.
718
     *
719
     * @param string $status Status
720
     * @return bool
721
     */
722
    public function setStatus($status)
723
    {
724
        // is status allowed?
725
        $status = strtolower($status);
726
        if (!in_array($status, array_keys($this->allowedStatus))) {
727
            $this->errors[] = self::ERROR_USER_INVALID_STATUS;
728
729
            return false;
730
        }
731
732
        // update status
733
        $this->status = $status;
734
        $update = sprintf("
735
            UPDATE
736
                %sfaquser
737
            SET
738
                account_status = '%s'
739
            WHERE
740
                user_id = %d",
741
            Db::getTablePrefix(),
742
            $this->config->getDb()->escape($status),
743
            $this->userId
744
        );
745
746
        $res = $this->config->getDb()->query($update);
747
748
        if ($res) {
749
            return true;
750
        }
751
752
        return false;
753
    }
754
755
    /**
756
     * Returns a string with error messages.
757
     *
758
     * The string returned by error() contains messages for all errors that
759
     * during object procesing. Messages are separated by new lines.
760
     *
761
     * Error messages are stored in the public array errors.
762
     *
763
     * @return string
764
     */
765
    public function error()
766
    {
767
        $message = '';
768
        foreach ($this->errors as $error) {
769
            $message .= $error."<br>\n";
770
        }
771
        $this->errors = [];
772
773
        return $message;
774
    }
775
776
    /**
777
     * returns true if login is a valid login string.
778
     *
779
     * $this->loginMinLength defines the minimum length the
780
     * login string. If login has more characters than allowed,
781
     * false is returned.
782
     * $this->login_invalidRegExp is a regular expression.
783
     * If login matches this false is returned.
784
     *
785
     * @param string $login Login name
786
     *
787
     * @return bool
788
     */
789
    public function isValidLogin($login)
790
    {
791
        $login = (string)$login;
792
793
        if (strlen($login) < $this->loginMinLength || !preg_match($this->validUsername, $login)) {
794
            $this->errors[] = self::ERROR_USER_LOGIN_INVALID;
795
796
            return false;
797
        }
798
799
        return true;
800
    }
801
802
    /**
803
     * adds a new authentication object to the user object.
804
     *
805
     * @param Driver $auth Driver object
806
     * @param string          $name Auth name
807
     *
808
     * @return bool
809
     */
810
    public function addAuth($auth, $name)
811
    {
812
        if ($this->checkAuth($auth)) {
813
            $this->authContainer[$name] = $auth;
814
815
            return true;
816
        }
817
818
        return false;
819
    }
820
821
    /**
822
     * Returns true if auth is a valid authentication object.
823
     *
824
     * @param Driver $auth Auth object
825
     *
826
     * @return bool
827
     */
828
    protected function checkAuth($auth)
829
    {
830
        $methods = ['checkPassword'];
831
        foreach ($methods as $method) {
832
            if (!method_exists($auth, $method)) {
833
                return false;
834
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
835
            }
836
        }
837
838
        return true;
839
    }
840
841
    /**
842
     * Returns the data aof the auth container.
843
     *
844
     * @return Driver[]
845
     */
846
    public function getAuthContainer()
847
    {
848
        return $this->authContainer;
849
    }
850
851
    /**
852
     * Returns a specific entry from the auth data source array.
853
     *
854
     * @param string $key
855
     *
856
     * @return string|null
857
     */
858
    public function getAuthSource($key)
859
    {
860
        if (isset($this->authData['authSource'][$key])) {
861
            return $this->authData['authSource'][$key];
862
        } else {
863
            return;
864
        }
865
    }
866
867
    /**
868
     * Returns a specific entry from the auth data array.
869
     *
870
     * @param string $key
871
     *
872
     * @return string|null
873
     */
874
    public function getAuthData($key)
875
    {
876
        if (isset($this->authData[$key])) {
877
            return $this->authData[$key];
878
        } else {
879
            return null;
880
        }
881
    }
882
883
    /**
884
     * returns true if perm is a valid permission object.
885
     *
886
     * @param Permission $perm Permission object
887
     *
888
     * @return bool
889
     */
890
    private function checkPerm($perm)
891
    {
892
        if ($perm instanceof Permission) {
893
            return true;
894
        }
895
        $this->errors[] = self::ERROR_USER_NO_PERM;
896
897
        return false;
898
    }
899
900
    /**
901
     * returns the user's login.
902
     *
903
     * @return string
904
     */
905
    public function getLogin()
906
    {
907
        return $this->login;
908
    }
909
910
    /**
911
     * Returns the data of the current user.
912
     *
913
     * @param string $field Field
914
     *
915
     * @return array|string|int
916
     */
917
    public function getUserData($field = '*')
918
    {
919
        if (!($this->userdata instanceof UserData)) {
920
            $this->userdata = new UserData($this->config);
921
        }
922
923
        return $this->userdata->get($field);
924
    }
925
926
    /**
927
     * Adds user data.
928
     *
929
     * @param array $data Array with user data
930
     *
931
     * @return bool
932
     */
933
    public function setUserData(Array $data)
934
    {
935
        if (!($this->userdata instanceof UserData)) {
936
            $this->userdata = new UserData($this->config);
937
        }
938
        $this->userdata->load($this->getUserId());
939
940
        return $this->userdata->set(array_keys($data), array_values($data));
941
    }
942
943
    /**
944
     * Returns an array with the user-IDs of all users found in
945
     * the database. By default, the Anonymous User will not be returned.
946
     *
947
     * @param bool $withoutAnonymous  Without anonymous?
948
     * @param bool $allowBlockedUsers Allow blocked users as well, e.g. in admin
949
     *
950
     * @return array
951
     */
952 View Code Duplication
    public function getAllUsers($withoutAnonymous = true, $allowBlockedUsers = true)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
953
    {
954
        $select = sprintf('
955
            SELECT
956
                user_id
957
            FROM
958
                %sfaquser
959
            WHERE
960
                1 = 1
961
            %s
962
            %s
963
            ORDER BY
964
                user_id ASC',
965
            Db::getTablePrefix(),
966
            ($withoutAnonymous ? 'AND user_id <> -1' : ''),
967
            ($allowBlockedUsers ? '' : "AND account_status != 'blocked'")
968
        );
969
970
        $res = $this->config->getDb()->query($select);
971
        if (!$res) {
972
            return [];
973
        }
974
975
        $result = [];
976
        while ($row = $this->config->getDb()->fetchArray($res)) {
977
            $result[] = $row['user_id'];
978
        }
979
980
        return $result;
981
    }
982
983
    /**
984
     * Returns an array of all users found in the database. By default, the 
985
     * anonymous User will not be returned. The returned array contains the
986
     * user ID as key, the values are login name, account status, authentication
987
     * source and the user creation date.
988
     *
989
     * @param bool $withoutAnonymous Without anonymous?
990
     *
991
     * @return array
992
     */
993 View Code Duplication
    public function getAllUserData($withoutAnonymous = true)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
994
    {
995
        $select = sprintf('
996
            SELECT
997
                user_id, login, account_status, auth_source, member_since
998
            FROM
999
                %sfaquser
1000
            %s
1001
            ORDER BY
1002
               login ASC',
1003
            Db::getTablePrefix(),
1004
            ($withoutAnonymous ? 'WHERE user_id <> -1' : ''));
1005
1006
        $res = $this->config->getDb()->query($select);
1007
        if (!$res) {
1008
            return [];
1009
        }
1010
1011
        $result = [];
1012
        while ($row = $this->config->getDb()->fetchArray($res)) {
1013
            $result[$row['user_id']] = $row;
1014
        }
1015
1016
        return $result;
1017
    }
1018
1019
    /**
1020
     * Get all users in <option> tags.
1021
     *
1022
     * @param int  $id                Selected user ID
1023
     * @param bool $allowBlockedUsers Allow blocked users as well, e.g. in admin
1024
     *
1025
     * @return string
1026
     */
1027
    public function getAllUserOptions($id = 1, $allowBlockedUsers = false)
1028
    {
1029
        $options = '';
1030
        $allUsers = $this->getAllUsers(true, $allowBlockedUsers);
1031
1032
        foreach ($allUsers as $userId) {
1033
            if (-1 !== $userId) {
1034
                $this->getUserById($userId);
1035
                $options .= sprintf(
1036
                    '<option value="%d"%s>%s (%s)</option>',
1037
                    $userId,
1038
                    (($userId === $id) ? ' selected' : ''),
1039
                    $this->getUserData('display_name'),
1040
                    $this->getLogin()
1041
                );
1042
            }
1043
        }
1044
1045
        return $options;
1046
    }
1047
1048
    /**
1049
     * sets the minimum login string length.
1050
     *
1051
     * @param int $loginMinLength Minimum length of login name
1052
     */
1053
    public function setLoginMinLength($loginMinLength)
1054
    {
1055
        if (is_int($loginMinLength)) {
1056
            $this->loginMinLength = $loginMinLength;
1057
        }
1058
    }
1059
1060
    /**
1061
     * Returns a new password.
1062
     *
1063
     * @param int  $minimumLength
1064
     * @param bool $allowUnderscore
1065
     *
1066
     * @return string
1067
     */
1068
    public function createPassword($minimumLength = 8, $allowUnderscore = true)
1069
    {
1070
        // To make passwords harder to get wrong, a few letters & numbers have been omitted.
1071
        // This will ensure safety with browsers using fonts with confusable letters.
1072
        // Removed: o,O,0,1,l,L
1073
        $consonants = ['b', 'c', 'd', 'f', 'g', 'h', 'j', 'k', 'm', 'n', 'p', 'r', 's', 't', 'v', 'w', 'x', 'y', 'z'];
1074
        $vowels = ['a', 'e', 'i', 'u'];
1075
        $newPassword = '';
1076
        $skipped = false;
1077
1078
        while (strlen($newPassword) < $minimumLength) {
1079
            if (Utils::createRandomNumber(0, 1)) {
1080
                $caseFunc = 'strtoupper';
1081
            } else {
1082
                $caseFunc = 'strtolower';
1083
            }
1084
1085
            switch (Utils::createRandomNumber(0, $skipped ? 3 : ($allowUnderscore ? 5 : 4))) {
1086
                case 0 : case 1 : $nextChar = $caseFunc($consonants[rand(0, 18)]);
1087
                    break;
1088
                case 2:
1089
                case 3:
1090
                    $nextChar = $caseFunc($vowels[rand(0, 3)]);
1091
                    break;
1092
                case 4:
1093
                    $nextChar = rand(2, 9); // No 0 to avoid confusion with O, same for 1 and l.
1094
                    break;
1095
                case 5:
1096
                    $newPassword .= '_';
1097
                    continue 2;
1098
                    break;
0 ignored issues
show
Unused Code introduced by
break; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
1099
            }
1100
1101
            $skipped = false;
1102
1103
            // Ensure letters and numbers only occur once.
1104
            if (strpos($newPassword, $nextChar) === false) {
1105
                $newPassword .= $nextChar;
0 ignored issues
show
Bug introduced by
The variable $nextChar does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
1106
            } else {
1107
                $skipped = true;
1108
            }
1109
        }
1110
1111
        return $newPassword;
1112
    }
1113
1114
    /**
1115
     * Sends mail to the current user.
1116
     *
1117
     * @param string $subject
1118
     * @param string $message
1119
     * @return bool
1120
     */
1121
    public function mailUser($subject, $message)
1122
    {
1123
        $mail = new Mail($this->config);
1124
        $mail->addTo($this->getUserData('email'));
1125
        $mail->subject = $subject;
1126
        $mail->message = $message;
1127
        $result = $mail->send();
1128
        unset($mail);
1129
1130
        return $result;
1131
    }
1132
1133
    /**
1134
     * Returns true on success.
1135
     *
1136
     * This will change a users status to active, and send an email with a new password.
1137
     *
1138
     * @return bool
1139
     */
1140
    public function activateUser()
1141
    {
1142
        if ($this->getStatus() == 'blocked') {
1143
1144
            // Generate and change user password.
1145
            $newPassword = $this->createPassword();
1146
            $this->changePassword($newPassword);
1147
            // Send activation email.
1148
            $subject = '[%sitename%] Login name / activation';
1149
            $message = sprintf(
1150
                "\nName: %s\nLogin name: %s\nNew password: %s\n\n",
1151
                $this->getUserData('display_name'),
1152
                $this->getLogin(),
1153
                $newPassword
1154
            );
1155
            // Only set to active if the activation mail sent correctly.
1156
            if ($this->mailUser($subject, $message)) {
1157
                return $this->setStatus('active');
1158
            }
1159
            return true;
1160
        }
1161
1162
        return false;
1163
    }
1164
1165
    /**
1166
     * Returns true, if a user is a super admin.
1167
     * @return bool
1168
     */
1169
    public function isSuperAdmin()
1170
    {
1171
        return $this->isSuperAdmin;
1172
    }
1173
1174
    /**
1175
     * Sets the users "is_superadmin" flag and updates the database entry.
1176
     * @param $isSuperAdmin
1177
     * @return bool
1178
     */
1179
    public function setSuperAdmin($isSuperAdmin)
1180
    {
1181
        $this->isSuperAdmin = $isSuperAdmin;
1182
        $update = sprintf("
1183
            UPDATE
1184
                %sfaquser
1185
            SET
1186
                is_superadmin = %d
1187
            WHERE
1188
                user_id = %d",
1189
            Db::getTablePrefix(),
1190
            (int)$this->isSuperAdmin,
1191
            $this->userId
1192
        );
1193
1194
        $res = $this->config->getDb()->query($update);
1195
1196
        if ($res) {
1197
            return true;
1198
        }
1199
1200
        return false;
1201
    }
1202
}
1203