Completed
Push — master ( c63978...2e5569 )
by Mathieu
04:42
created

AbstractUser::getAuthenticated()   B

Complexity

Conditions 6
Paths 4

Size

Total Lines 25
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 25
rs 8.439
cc 6
eloc 13
nc 4
nop 1
1
<?php
2
3
namespace Charcoal\User;
4
5
// Dependencies from `PHP`
6
use \DateTime;
7
use \DateTimeInterface;
8
use \Exception;
9
use \InvalidArgumentException;
10
11
// Module `charcoal-core` dependencies
12
use \Charcoal\Config\ConfigurableInterface;
13
use \Charcoal\Config\ConfigurableTrait;
14
15
// Module `charcoal-base` dependencies
16
use \Charcoal\Object\Content;
17
18
// Local namespace (charcoal-base) dependencies
19
use \Charcoal\User\UserConfig;
20
use \Charcoal\User\UserInterface;
21
use \Charcoal\User\AuthenticatableInterface;
22
use \Charcoal\User\AuthorizableInterface;
23
use \Charcoal\User\AuthorizableTrait;
24
use \Charcoal\User\GroupableInterface;
25
use \Charcoal\User\GroupableTrait;
26
27
/**
28
 * Full implementation, as abstract class, of the `UserInterface`.
29
 */
30
abstract class AbstractUser extends Content implements
31
    UserInterface,
32
    AuthenticatableInterface,
33
    AuthorizableInterface,
34
    GroupableInterface,
35
    ConfigurableInterface
36
{
37
    use AuthorizableTrait;
38
    use GroupableTrait;
39
    use ConfigurableTrait;
40
41
    /**
42
     * The username should be unique and mandatory.
43
     * @var string $username
44
     */
45
    private $username = '';
46
47
    /**
48
     * The password is stored encrypted in the database.
49
     * @var string $password
50
     */
51
    private $password;
52
53
    /**
54
     * @var string $email
55
     */
56
    private $email;
57
58
    /**
59
     * @var boolean $active
60
     */
61
    private $active = true;
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
62
63
    /**
64
     * The date of the latest (successful) login
65
     * @var DateTime|null $lastLoginDate
66
     */
67
    private $lastLoginDate;
68
69
    /**
70
     * @var string $lastLoginIp
71
     */
72
    private $lastLoginIp;
73
74
    /**
75
     * The date of the latest password change
76
     * @var DateTime|null
77
     */
78
    private $lastPasswordDate;
79
80
    /**
81
     * @var string $lastPasswordIp
82
     */
83
    private $lastPasswordIp;
84
85
    /**
86
     * If the login token is set (not empty), then the user should be prompted to
87
     * reset his password after login / enter the token to continue
88
     * @var string $loginToken
89
     */
90
    private $loginToken = '';
91
92
    /**
93
     * IndexableTrait > key()
94
     *
95
     * @return string
96
     */
97
    public function key()
98
    {
99
        return 'username';
100
    }
101
102
    /**
103
     * Force a lowercase username
104
     *
105
     * @param string $username The username (also the login name).
106
     * @throws InvalidArgumentException If the username is not a string.
107
     * @return User Chainable
108
     */
109
    public function setUsername($username)
110
    {
111
        if (!is_string($username)) {
112
            throw new InvalidArgumentException(
113
                'Set user username: Username must be a string'
114
            );
115
        }
116
        $this->username = mb_strtolower($username);
117
        return $this;
118
    }
119
120
    /**
121
     * @return string
122
     */
123
    public function username()
124
    {
125
        return $this->username;
126
    }
127
128
    /**
129
     * @param string $email The user email.
130
     * @throws InvalidArgumentException If the email is not a string.
131
     * @return User Chainable
132
     */
133
    public function setEmail($email)
134
    {
135
        if (!is_string($email)) {
136
            throw new InvalidArgumentException(
137
                'Set user email: Email must be a string'
138
            );
139
        }
140
        $this->email = $email;
141
        return $this;
142
    }
143
144
    /**
145
     * @return string
146
     */
147
    public function email()
148
    {
149
        return $this->email;
150
    }
151
152
    /**
153
     * @param string|null $password The user password. Encrypted in storage.
154
     * @throws InvalidArgumentException If the password is not a string (or null, to reset).
155
     * @return UserInterface Chainable
156
     */
157
    public function setPassword($password)
158
    {
159
        if ($password === null) {
160
            $this->password = $password;
161
        } elseif (is_string($password)) {
162
            $this->password = $password;
163
        } else {
164
            throw new InvalidArgumentException(
165
                'Set user password: Password must be a string'
166
            );
167
        }
168
169
        return $this;
170
    }
171
172
    /**
173
     * @return string
174
     */
175
    public function password()
176
    {
177
        return $this->password;
178
    }
179
180
    /**
181
     * @param boolean $active The active flag.
182
     * @return UserInterface Chainable
183
     */
184
    public function setActive($active)
185
    {
186
        $this->active = !!$active;
187
        return $this;
188
    }
189
    /**
190
     * @return boolean
191
     */
192
    public function active()
193
    {
194
        return $this->active;
195
    }
196
197
    /**
198
     * @param string|DateTime|null $lastLoginDate The last login date.
199
     * @throws InvalidArgumentException If the ts is not a valid date/time.
200
     * @return AbstractUser Chainable
201
     */
202 View Code Duplication
    public function setLastLoginDate($lastLoginDate)
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...
203
    {
204
        if ($lastLoginDate === null) {
205
            $this->lastLoginDate = null;
206
            return $this;
207
        }
208
        if (is_string($lastLoginDate)) {
209
            try {
210
                $lastLoginDate = new DateTime($lastLoginDate);
211
            } catch (Exception $e) {
212
                throw new InvalidArgumentException(
213
                    sprintf('Invalid login date (%s)', $e->getMessage())
214
                );
215
            }
216
        }
217
        if (!($lastLoginDate instanceof DateTimeInterface)) {
218
            throw new InvalidArgumentException(
219
                'Invalid "Last Login Date" value. Must be a date/time string or a DateTime object.'
220
            );
221
        }
222
        $this->lastLoginDate = $lastLoginDate;
223
        return $this;
224
    }
225
226
    /**
227
     * @return DateTime|null
228
     */
229
    public function lastLoginDate()
230
    {
231
        return $this->lastLoginDate;
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->lastLoginDate; of type DateTime|null adds the type DateTime to the return on line 231 which is incompatible with the return type declared by the interface Charcoal\User\UserInterface::lastLoginDate of type Charcoal\User\DateTime.
Loading history...
232
    }
233
234
    /**
235
     * @param string|integer|null $ip The last login IP address.
236
     * @throws InvalidArgumentException If the IP is not an IP string, an integer, or null.
237
     * @return UserInterface Chainable
238
     */
239 View Code Duplication
    public function setLastLoginIp($ip)
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...
240
    {
241
        if ($ip === null) {
242
            $this->lastLoginIp = null;
243
            return $this;
244
        }
245
        if (is_int($ip)) {
246
            $ip = long2ip($ip);
247
        }
248
        if (!is_string($ip)) {
249
            throw new InvalidArgumentException(
250
                'Invalid IP address'
251
            );
252
        }
253
        $this->lastLoginIp = $ip;
254
        return $this;
255
    }
256
    /**
257
     * Get the last login IP in x.x.x.x format
258
     * @return string
259
     */
260
    public function lastLoginIp()
261
    {
262
        return $this->lastLoginIp;
263
    }
264
265
    /**
266
     * @param string|DateTime|null $lastPasswordDate The last password date.
267
     * @throws InvalidArgumentException If the passsword date is not a valid DateTime.
268
     * @return UserInterface Chainable
269
     */
270 View Code Duplication
    public function setLastPasswordDate($lastPasswordDate)
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...
271
    {
272
        if ($lastPasswordDate === null) {
273
            $this->lastPasswordDate = null;
274
            return $this;
275
        }
276
        if (is_string($lastPasswordDate)) {
277
            try {
278
                $lastPasswordDate = new DateTime($lastPasswordDate);
279
            } catch (Exception $e) {
280
                throw new InvalidArgumentException(
281
                    sprintf('Invalid last password date (%s)', $e->getMessage())
282
                );
283
            }
284
        }
285
        if (!($lastPasswordDate instanceof DateTimeInterface)) {
286
            throw new InvalidArgumentException(
287
                'Invalid "Last Password Date" value. Must be a date/time string or a DateTime object.'
288
            );
289
        }
290
        $this->lastPasswordDate = $lastPasswordDate;
291
        return $this;
292
    }
293
294
    /**
295
     * @return DateTime
296
     */
297
    public function lastPasswordDate()
298
    {
299
        return $this->lastPasswordDate;
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->lastPasswordDate; of type DateTime|null adds the type DateTime to the return on line 299 which is incompatible with the return type declared by the interface Charcoal\User\UserInterface::lastPasswordDate of type Charcoal\User\DateTime.
Loading history...
300
    }
301
302
    /**
303
     * @param integer|string|null $ip The last password IP.
304
     * @throws InvalidArgumentException If the IP is not null, an integer or an IP string.
305
     * @return UserInterface Chainable
306
     */
307 View Code Duplication
    public function setLastPasswordIp($ip)
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...
308
    {
309
        if ($ip === null) {
310
            $this->lastPasswordIp = null;
311
            return $this;
312
        }
313
        if (is_int($ip)) {
314
            $ip = long2ip($ip);
315
        }
316
        if (!is_string($ip)) {
317
            throw new InvalidArgumentException(
318
                'Invalid IP address'
319
            );
320
        }
321
        $this->lastPasswordIp = $ip;
322
        return $this;
323
    }
324
    /**
325
     * Get the last password change IP in x.x.x.x format
326
     *
327
     * @return string
328
     */
329
    public function lastPasswordIp()
330
    {
331
        return $this->lastPasswordIp;
332
    }
333
334
    /**
335
     * @param string $token The login token.
336
     * @throws InvalidArgumentException If the token is not a string.
337
     * @return UserInterface Chainable
338
     */
339
    public function setLoginToken($token)
340
    {
341
        if (!is_string($token)) {
342
            throw new InvalidArgumentException(
343
                'Login Token must be a string'
344
            );
345
        }
346
        $this->loginToken = $token;
347
        return $this;
348
    }
349
350
    /**
351
     * @return string
352
     */
353
    public function loginToken()
354
    {
355
        return $this->loginToken;
356
    }
357
358
    /**
359
     * Attempt to log in a user with a username + password.
360
     *
361
     * @param string $username Username.
362
     * @param string $password Password.
363
     * @throws InvalidArgumentException If username or password is not a string.
364
     * @return boolean Login success / failure.
365
     */
366
    public function authenticate($username, $password)
367
    {
368
        if (!is_string($username) || !is_string($password)) {
369
            throw new InvalidArgumentException(
370
                'Username and password must be strings'
371
            );
372
        }
373
374
        // Force lowercase
375
        $username = mb_strtolower($username);
376
377
        // Load the user by username
378
        $this->load($username);
379
380
        if ($this->username() != $username) {
381
            $this->loginFailed($username);
382
            return false;
383
        }
384
        if ($this->active() === false) {
385
            $this->loginFailed($username);
386
            return false;
387
        }
388
389
        // Validate password
390
        if (password_verify($password, $this->password())) {
391
            if (password_needs_rehash($this->password(), PASSWORD_DEFAULT)) {
392
                $hash = password_hash($password, PASSWORD_DEFAULT);
0 ignored issues
show
Unused Code introduced by
$hash is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
393
                // @todo Update user with new hash
394
                $this->update(['password']);
395
            }
396
397
            $this->login();
398
            return true;
399
        } else {
400
            $this->loginFailed($username);
401
            return false;
402
        }
403
    }
404
405
    /**
406
     * @throws Exception If trying to save a user to session without a ID.
407
     * @return UserInterface Chainable
408
     */
409
    public function saveToSession()
0 ignored issues
show
Coding Style introduced by
saveToSession uses the super-global variable $_SESSION which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
410
    {
411
        if (!$this->id()) {
412
            throw new Exception(
413
                'Can not set auth user; no user ID'
414
            );
415
        }
416
        $_SESSION[static::sessionKey()] = $this->id();
417
        return $this;
418
    }
419
420
    /**
421
     * Log in the user (in session)
422
     *
423
     * Called when the authentication is successful.
424
     *
425
     * @return boolean Success / Failure
426
     */
427
    public function login()
0 ignored issues
show
Coding Style introduced by
login uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
428
    {
429
        if (!$this->id()) {
430
            return false;
431
        }
432
433
        $this->setLastLoginDate('now');
434
        $ip = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '';
435
        if ($ip) {
436
            $this->setLastLoginIp($ip);
437
        }
438
        $this->update(['last_login_ip', 'last_login_date']);
439
440
        $this->saveToSession();
441
442
        return true;
443
    }
444
445
    /**
446
     * @return boolean
447
     */
448
    public function logLogin()
449
    {
450
        // @todo
451
        return true;
452
    }
453
454
    /**
455
     * Failed authentication callback
456
     *
457
     * @param string $username The failed username.
458
     * @return void
459
     */
460
    public function loginFailed($username)
461
    {
462
        $this->setUsername('');
463
        $this->setPermissions([]);
464
        $this->setGroups([]);
465
466
        $this->logLoginFailed($username);
0 ignored issues
show
Unused Code introduced by
The call to the method Charcoal\User\AbstractUser::logLoginFailed() seems un-needed as the method has no side-effects.

PHP Analyzer performs a side-effects analysis of your code. A side-effect is basically anything that might be visible after the scope of the method is left.

Let’s take a look at an example:

class User
{
    private $email;

    public function getEmail()
    {
        return $this->email;
    }

    public function setEmail($email)
    {
        $this->email = $email;
    }
}

If we look at the getEmail() method, we can see that it has no side-effect. Whether you call this method or not, no future calls to other methods are affected by this. As such code as the following is useless:

$user = new User();
$user->getEmail(); // This line could safely be removed as it has no effect.

On the hand, if we look at the setEmail(), this method _has_ side-effects. In the following case, we could not remove the method call:

$user = new User();
$user->setEmail('email@domain'); // This line has a side-effect (it changes an
                                 // instance variable).
Loading history...
467
    }
468
469
    /**
470
     * @param string $username The username to log failure.
471
     * @return boolean
472
     */
473
    public function logLoginFailed($username)
0 ignored issues
show
Unused Code introduced by
The parameter $username is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
474
    {
475
        // @todo
476
        return true;
477
    }
478
479
    /**
480
     * Empties the session var associated to the session key.
481
     *
482
     * @return boolean Logged out or not.
483
     */
484
    public function logout()
0 ignored issues
show
Coding Style introduced by
logout uses the super-global variable $_SESSION which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
485
    {
486
        // Irrelevant call...
487
        if (!$this->id()) {
488
            return false;
489
        }
490
491
        $_SESSION[static::sessionKey()] = null;
492
        unset($_SESSION[static::sessionKey()]);
493
494
        return true;
495
    }
496
497
    /**
498
     * Reset the password.
499
     *
500
     * Encrypt the password and re-save the object in the database.
501
     * Also updates the last password date & ip.
502
     *
503
     * @param string $plainPassword The plain (non-encrypted) password to reset to.
504
     * @throws InvalidArgumentException If the plain password is not a string.
505
     * @return UserInterface Chainable
506
     */
507
    public function resetPassword($plainPassword)
0 ignored issues
show
Coding Style introduced by
resetPassword uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
508
    {
509
        if (!is_string($plainPassword)) {
510
            throw new InvalidArgumentException(
511
                'Can not change password: password is not a string.'
512
            );
513
        }
514
515
        $hash = password_hash($plainPassword, PASSWORD_DEFAULT);
516
        $this->setPassword($hash);
517
518
        $this->setLastPasswordDate('now');
519
        $ip = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '';
520
        if ($ip) {
521
            $this->setLastPasswordIp($ip);
522
        }
523
524
        if ($this->id()) {
525
            $this->update(['password', 'last_password_date', 'last_password_ip']);
526
        }
527
528
        return $this;
529
    }
530
531
    /**
532
     * Get the currently authenticated user (from session)
533
     *
534
     * Return null if there is no current user in logged into
535
     *
536
     * @param boolean $reinit Optional. Whether to reload user data from source.
537
     * @throws Exception If the user from session is invalid.
538
     * @return UserInterface|null
539
     */
540
    public static function getAuthenticated($reinit = true)
0 ignored issues
show
Coding Style introduced by
getAuthenticated uses the super-global variable $_SESSION which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
541
    {
542
        if (!isset($_SESSION[static::sessionKey()])) {
543
            return null;
544
        }
545
546
        $userId = $_SESSION[static::sessionKey()];
547
        if (!$userId) {
548
            return null;
549
        }
550
551
        $user_class = get_called_class();
552
        $user = new $user_class([
553
            'logger'=> new \Psr\Log\NullLogger()
554
        ]);
555
        $user->load($userId);
556
557
        // Inactive users can not authenticate
558
        if (!$user->id() || !$user->username() || !$user->active()) {
559
            // @todo log error
560
            return null;
561
        }
562
563
        return $user;
564
    }
565
566
    /**
567
     * ConfigurableInterface > create_config()
568
     *
569
     * @param array $data Optional. Configuration data.
570
     * @return UserConfig
571
     */
572
    public function createConfig(array $data = null)
573
    {
574
        $config = new UserConfig();
575
        if (is_array($data)) {
576
            $config->merge($data);
0 ignored issues
show
Unused Code introduced by
The call to the method Charcoal\User\UserConfig::merge() seems un-needed as the method has no side-effects.

PHP Analyzer performs a side-effects analysis of your code. A side-effect is basically anything that might be visible after the scope of the method is left.

Let’s take a look at an example:

class User
{
    private $email;

    public function getEmail()
    {
        return $this->email;
    }

    public function setEmail($email)
    {
        $this->email = $email;
    }
}

If we look at the getEmail() method, we can see that it has no side-effect. Whether you call this method or not, no future calls to other methods are affected by this. As such code as the following is useless:

$user = new User();
$user->getEmail(); // This line could safely be removed as it has no effect.

On the hand, if we look at the setEmail(), this method _has_ side-effects. In the following case, we could not remove the method call:

$user = new User();
$user->setEmail('email@domain'); // This line has a side-effect (it changes an
                                 // instance variable).
Loading history...
577
        }
578
        return $config;
579
    }
580
}
581