Completed
Push — master ( b6640d...1255af )
by Mathieu
04:15
created

AbstractUser::getAuthenticated()   C

Complexity

Conditions 7
Paths 5

Size

Total Lines 28
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
c 4
b 0
f 0
dl 0
loc 28
rs 6.7272
cc 7
eloc 15
nc 5
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-factory` dependencies
12
use \Charcoal\Factory\FactoryInterface;
13
14
// Module `charcoal-core` dependencies
15
use \Charcoal\Config\ConfigurableInterface;
16
use \Charcoal\Config\ConfigurableTrait;
17
18
// Module `charcoal-base` dependencies
19
use \Charcoal\Object\Content;
20
21
// Local namespace (charcoal-base) dependencies
22
use \Charcoal\User\UserConfig;
23
use \Charcoal\User\UserInterface;
24
use \Charcoal\User\AuthenticatableInterface;
25
26
/**
27
 * Full implementation, as abstract class, of the `UserInterface`.
28
 */
29
abstract class AbstractUser extends Content implements
30
    UserInterface,
31
    AuthenticatableInterface,
32
    ConfigurableInterface
33
{
34
    use ConfigurableTrait;
35
36
    /**
37
     * @var UserInterface $authenticatedUser
38
     */
39
    protected static $authenticatedUser;
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) storage.
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
     * @return string
104
     */
105
    abstract public static function sessionKey();
106
107
    /**
108
     * Force a lowercase username
109
     *
110
     * @param string $username The username (also the login name).
111
     * @throws InvalidArgumentException If the username is not a string.
112
     * @return User Chainable
113
     */
114 View Code Duplication
    public function setUsername($username)
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...
115
    {
116
        if (!is_string($username)) {
117
            throw new InvalidArgumentException(
118
                'Set user username: Username must be a string'
119
            );
120
        }
121
        $this->username = mb_strtolower($username);
122
        return $this;
123
    }
124
125
    /**
126
     * @return string
127
     */
128
    public function username()
129
    {
130
        return $this->username;
131
    }
132
133
    /**
134
     * @param string $email The user email.
135
     * @throws InvalidArgumentException If the email is not a string.
136
     * @return User Chainable
137
     */
138
    public function setEmail($email)
139
    {
140
        if (!is_string($email)) {
141
            throw new InvalidArgumentException(
142
                'Set user email: Email must be a string'
143
            );
144
        }
145
        $this->email = $email;
146
        return $this;
147
    }
148
149
    /**
150
     * @return string
151
     */
152
    public function email()
153
    {
154
        return $this->email;
155
    }
156
157
    /**
158
     * @param string|null $password The user password. Encrypted in storage.
159
     * @throws InvalidArgumentException If the password is not a string (or null, to reset).
160
     * @return UserInterface Chainable
161
     */
162
    public function setPassword($password)
163
    {
164
        if ($password === null) {
165
            $this->password = $password;
166
        } elseif (is_string($password)) {
167
            $this->password = $password;
168
        } else {
169
            throw new InvalidArgumentException(
170
                'Set user password: Password must be a string'
171
            );
172
        }
173
174
        return $this;
175
    }
176
177
    /**
178
     * @return string
179
     */
180
    public function password()
181
    {
182
        return $this->password;
183
    }
184
185
    /**
186
     * @param boolean $active The active flag.
187
     * @return UserInterface Chainable
188
     */
189
    public function setActive($active)
190
    {
191
        $this->active = !!$active;
192
        return $this;
193
    }
194
    /**
195
     * @return boolean
196
     */
197
    public function active()
198
    {
199
        return $this->active;
200
    }
201
202
    /**
203
     * @param string|DateTime|null $lastLoginDate The last login date.
204
     * @throws InvalidArgumentException If the ts is not a valid date/time.
205
     * @return AbstractUser Chainable
206
     */
207 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...
208
    {
209
        if ($lastLoginDate === null) {
210
            $this->lastLoginDate = null;
211
            return $this;
212
        }
213
        if (is_string($lastLoginDate)) {
214
            try {
215
                $lastLoginDate = new DateTime($lastLoginDate);
216
            } catch (Exception $e) {
217
                throw new InvalidArgumentException(
218
                    sprintf('Invalid login date (%s)', $e->getMessage())
219
                );
220
            }
221
        }
222
        if (!($lastLoginDate instanceof DateTimeInterface)) {
223
            throw new InvalidArgumentException(
224
                'Invalid "Last Login Date" value. Must be a date/time string or a DateTime object.'
225
            );
226
        }
227
        $this->lastLoginDate = $lastLoginDate;
228
        return $this;
229
    }
230
231
    /**
232
     * @return DateTime|null
233
     */
234
    public function lastLoginDate()
235
    {
236
        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 236 which is incompatible with the return type declared by the interface Charcoal\User\UserInterface::lastLoginDate of type Charcoal\User\Datetime.
Loading history...
237
    }
238
239
    /**
240
     * @param string|integer|null $ip The last login IP address.
241
     * @throws InvalidArgumentException If the IP is not an IP string, an integer, or null.
242
     * @return UserInterface Chainable
243
     */
244 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...
245
    {
246
        if ($ip === null) {
247
            $this->lastLoginIp = null;
248
            return $this;
249
        }
250
        if (is_int($ip)) {
251
            $ip = long2ip($ip);
252
        }
253
        if (!is_string($ip)) {
254
            throw new InvalidArgumentException(
255
                'Invalid IP address'
256
            );
257
        }
258
        $this->lastLoginIp = $ip;
259
        return $this;
260
    }
261
    /**
262
     * Get the last login IP in x.x.x.x format
263
     * @return string
264
     */
265
    public function lastLoginIp()
266
    {
267
        return $this->lastLoginIp;
268
    }
269
270
    /**
271
     * @param string|DateTime|null $lastPasswordDate The last password date.
272
     * @throws InvalidArgumentException If the passsword date is not a valid DateTime.
273
     * @return UserInterface Chainable
274
     */
275 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...
276
    {
277
        if ($lastPasswordDate === null) {
278
            $this->lastPasswordDate = null;
279
            return $this;
280
        }
281
        if (is_string($lastPasswordDate)) {
282
            try {
283
                $lastPasswordDate = new DateTime($lastPasswordDate);
284
            } catch (Exception $e) {
285
                throw new InvalidArgumentException(
286
                    sprintf('Invalid last password date (%s)', $e->getMessage())
287
                );
288
            }
289
        }
290
        if (!($lastPasswordDate instanceof DateTimeInterface)) {
291
            throw new InvalidArgumentException(
292
                'Invalid "Last Password Date" value. Must be a date/time string or a DateTime object.'
293
            );
294
        }
295
        $this->lastPasswordDate = $lastPasswordDate;
296
        return $this;
297
    }
298
299
    /**
300
     * @return DateTime
301
     */
302
    public function lastPasswordDate()
303
    {
304
        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 304 which is incompatible with the return type declared by the interface Charcoal\User\UserInterface::lastPasswordDate of type Charcoal\User\Datetime.
Loading history...
305
    }
306
307
    /**
308
     * @param integer|string|null $ip The last password IP.
309
     * @throws InvalidArgumentException If the IP is not null, an integer or an IP string.
310
     * @return UserInterface Chainable
311
     */
312 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...
313
    {
314
        if ($ip === null) {
315
            $this->lastPasswordIp = null;
316
            return $this;
317
        }
318
        if (is_int($ip)) {
319
            $ip = long2ip($ip);
320
        }
321
        if (!is_string($ip)) {
322
            throw new InvalidArgumentException(
323
                'Invalid IP address'
324
            );
325
        }
326
        $this->lastPasswordIp = $ip;
327
        return $this;
328
    }
329
    /**
330
     * Get the last password change IP in x.x.x.x format
331
     *
332
     * @return string
333
     */
334
    public function lastPasswordIp()
335
    {
336
        return $this->lastPasswordIp;
337
    }
338
339
    /**
340
     * @param string $token The login token.
341
     * @throws InvalidArgumentException If the token is not a string.
342
     * @return UserInterface Chainable
343
     */
344
    public function setLoginToken($token)
345
    {
346
        if (!is_string($token)) {
347
            throw new InvalidArgumentException(
348
                'Login Token must be a string'
349
            );
350
        }
351
        $this->loginToken = $token;
352
        return $this;
353
    }
354
355
    /**
356
     * @return string
357
     */
358
    public function loginToken()
359
    {
360
        return $this->loginToken;
361
    }
362
363
    /**
364
     * Attempt to log in a user with a username + password.
365
     *
366
     * @param string $username Username.
367
     * @param string $password Password.
368
     * @throws InvalidArgumentException If username or password is not a string.
369
     * @return boolean Login success / failure.
370
     */
371
    public function authenticate($username, $password)
372
    {
373
        if (!is_string($username) || !is_string($password)) {
374
            throw new InvalidArgumentException(
375
                'Username and password must be strings'
376
            );
377
        }
378
379
        // Force lowercase
380
        $username = mb_strtolower($username);
381
382
        // Load the user by username
383
        $this->load($username);
384
385
        if ($this->username() != $username) {
386
            $this->loginFailed($username);
387
            return false;
388
        }
389
        if ($this->active() === false) {
390
            $this->loginFailed($username);
391
            return false;
392
        }
393
394
        // Validate password
395
        if (password_verify($password, $this->password())) {
396
            if (password_needs_rehash($this->password(), PASSWORD_DEFAULT)) {
397
                $hash = password_hash($password, PASSWORD_DEFAULT);
398
                // @todo Update user with new hash
399
                $this->setPassword($hash);
400
                $this->update(['password']);
401
            }
402
403
            $this->login();
404
            return true;
405
        } else {
406
            $this->loginFailed($username);
407
            return false;
408
        }
409
    }
410
411
    /**
412
     * @throws Exception If trying to save a user to session without a ID.
413
     * @return UserInterface Chainable
414
     */
415
    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...
416
    {
417
        if (!$this->id()) {
418
            throw new Exception(
419
                'Can not set auth user; no user ID'
420
            );
421
        }
422
        $_SESSION[static::sessionKey()] = $this->id();
423
        return $this;
424
    }
425
426
    /**
427
     * Log in the user (in session)
428
     *
429
     * Called when the authentication is successful.
430
     *
431
     * @return boolean Success / Failure
432
     */
433
    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...
434
    {
435
        if (!$this->id()) {
436
            return false;
437
        }
438
439
        $this->setLastLoginDate('now');
440
        $ip = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '';
441
        if ($ip) {
442
            $this->setLastLoginIp($ip);
443
        }
444
        $this->update(['last_login_ip', 'last_login_date']);
445
446
        $this->saveToSession();
447
448
        return true;
449
    }
450
451
    /**
452
     * @return boolean
453
     */
454
    public function logLogin()
455
    {
456
        // @todo
457
        return true;
458
    }
459
460
    /**
461
     * Failed authentication callback
462
     *
463
     * @param string $username The failed username.
464
     * @return void
465
     */
466
    public function loginFailed($username)
467
    {
468
        $this->setUsername('');
469
470
        $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...
471
    }
472
473
    /**
474
     * @param string $username The username to log failure.
475
     * @return boolean
476
     */
477
    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...
478
    {
479
        // @todo
480
        return true;
481
    }
482
483
    /**
484
     * Empties the session var associated to the session key.
485
     *
486
     * @return boolean Logged out or not.
487
     */
488
    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...
489
    {
490
        // Irrelevant call...
491
        if (!$this->id()) {
492
            return false;
493
        }
494
495
        $_SESSION[static::sessionKey()] = null;
496
        unset($_SESSION[static::sessionKey()]);
497
498
        return true;
499
    }
500
501
    /**
502
     * Reset the password.
503
     *
504
     * Encrypt the password and re-save the object in the database.
505
     * Also updates the last password date & ip.
506
     *
507
     * @param string $plainPassword The plain (non-encrypted) password to reset to.
508
     * @throws InvalidArgumentException If the plain password is not a string.
509
     * @return UserInterface Chainable
510
     */
511
    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...
512
    {
513
        if (!is_string($plainPassword)) {
514
            throw new InvalidArgumentException(
515
                'Can not change password: password is not a string.'
516
            );
517
        }
518
519
        $hash = password_hash($plainPassword, PASSWORD_DEFAULT);
520
        $this->setPassword($hash);
521
522
        $this->setLastPasswordDate('now');
523
        $ip = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '';
524
        if ($ip) {
525
            $this->setLastPasswordIp($ip);
526
        }
527
528
        if ($this->id()) {
529
            $this->update(['password', 'last_password_date', 'last_password_ip']);
530
        }
531
532
        return $this;
533
    }
534
535
    /**
536
     * Get the currently authenticated user (from session)
537
     *
538
     * Return null if there is no current user in logged into
539
     *
540
     * @param FactoryInterface $factory The factory to create the user object with.
541
     * @throws Exception If the user from session is invalid.
542
     * @return UserInterface|null
543
     */
544
    public static function getAuthenticated(FactoryInterface $factory)
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...
545
    {
546
        if (isset(static::$authenticatedUser[static::sessionKey()])) {
547
            return static::$authenticatedUser[static::sessionKey()];
548
        }
549
550
        if (!isset($_SESSION[static::sessionKey()])) {
551
            return null;
552
        }
553
554
        $userId = $_SESSION[static::sessionKey()];
555
        if (!$userId) {
556
            return null;
557
        }
558
559
        $userClass = get_called_class();
560
        $user = $factory->create($userClass);
561
        $user->load($userId);
562
563
        // Inactive users can not authenticate
564
        if (!$user->id() || !$user->username() || !$user->active()) {
565
            // @todo log error
566
            return null;
567
        }
568
569
        static::$authenticatedUser[static::sessionKey()] = $user;
570
        return $user;
571
    }
572
573
    /**
574
     * ConfigurableInterface > create_config()
575
     *
576
     * @param array $data Optional. Configuration data.
577
     * @return UserConfig
578
     */
579
    public function createConfig(array $data = null)
580
    {
581
        $config = new UserConfig();
582
        if (is_array($data)) {
583
            $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...
584
        }
585
        return $config;
586
    }
587
}
588