Completed
Push — master ( 5e6851...19dbb1 )
by Mathieu
12:22
created

AbstractUser::authenticate()   C

Complexity

Conditions 7
Paths 6

Size

Total Lines 39
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 7
Bugs 3 Features 0
Metric Value
c 7
b 3
f 0
dl 0
loc 39
rs 6.7272
cc 7
eloc 22
nc 6
nop 2

3 Methods

Rating   Name   Duplication   Size   Complexity  
A AbstractUser::setLoginToken() 0 10 2
A AbstractUser::loginToken() 0 4 1
A AbstractUser::saveToSession() 0 10 2
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\UserInterface;
23
24
/**
25
 * Full implementation, as abstract class, of the `UserInterface`.
26
 */
27
abstract class AbstractUser extends Content implements
28
    UserInterface,
29
    ConfigurableInterface
30
{
31
    use ConfigurableTrait;
32
33
    /**
34
     * @var UserInterface $authenticatedUser
35
     */
36
    protected static $authenticatedUser;
37
38
    /**
39
     * The username should be unique and mandatory.
40
     * @var string $username
41
     */
42
    private $username = '';
43
44
    /**
45
     * The password is stored encrypted in the (database) storage.
46
     * @var string $password
47
     */
48
    private $password;
49
50
    /**
51
     * @var string $email
52
     */
53
    private $email;
54
55
    /**
56
     * @var boolean $active
57
     */
58
    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...
59
60
    /**
61
     * @var array $roles
62
     */
63
    private $roles = [];
64
65
    /**
66
     * The date of the latest (successful) login
67
     * @var DateTime|null $lastLoginDate
68
     */
69
    private $lastLoginDate;
70
71
    /**
72
     * @var string $lastLoginIp
73
     */
74
    private $lastLoginIp;
75
76
    /**
77
     * The date of the latest password change
78
     * @var DateTime|null
79
     */
80
    private $lastPasswordDate;
81
82
    /**
83
     * @var string $lastPasswordIp
84
     */
85
    private $lastPasswordIp;
86
87
    /**
88
     * If the login token is set (not empty), then the user should be prompted to
89
     * reset his password after login / enter the token to continue
90
     * @var string $loginToken
91
     */
92
    private $loginToken = '';
93
94
    /**
95
     * IndexableTrait > key()
96
     *
97
     * @return string
98
     */
99
    public function key()
100
    {
101
        return 'username';
102
    }
103
104
    /**
105
     * Force a lowercase username
106
     *
107
     * @param string $username The username (also the login name).
108
     * @throws InvalidArgumentException If the username is not a string.
109
     * @return User Chainable
110
     */
111
    public function setUsername($username)
112
    {
113
        if (!is_string($username)) {
114
            throw new InvalidArgumentException(
115
                'Set user username: Username must be a string'
116
            );
117
        }
118
        $this->username = mb_strtolower($username);
119
        return $this;
120
    }
121
122
    /**
123
     * @return string
124
     */
125
    public function username()
126
    {
127
        return $this->username;
128
    }
129
130
    /**
131
     * @param string $email The user email.
132
     * @throws InvalidArgumentException If the email is not a string.
133
     * @return User Chainable
134
     */
135
    public function setEmail($email)
136
    {
137
        if (!is_string($email)) {
138
            throw new InvalidArgumentException(
139
                'Set user email: Email must be a string'
140
            );
141
        }
142
        $this->email = $email;
143
        return $this;
144
    }
145
146
    /**
147
     * @return string
148
     */
149
    public function email()
150
    {
151
        return $this->email;
152
    }
153
154
    /**
155
     * @param string|null $password The user password. Encrypted in storage.
156
     * @throws InvalidArgumentException If the password is not a string (or null, to reset).
157
     * @return UserInterface Chainable
158
     */
159
    public function setPassword($password)
160
    {
161
        if ($password === null) {
162
            $this->password = $password;
163
        } elseif (is_string($password)) {
164
            $this->password = $password;
165
        } else {
166
            throw new InvalidArgumentException(
167
                'Set user password: Password must be a string'
168
            );
169
        }
170
171
        return $this;
172
    }
173
174
    /**
175
     * @return string
176
     */
177
    public function password()
178
    {
179
        return $this->password;
180
    }
181
182
    /**
183
     * @param boolean $active The active flag.
184
     * @return UserInterface Chainable
185
     */
186
    public function setActive($active)
187
    {
188
        $this->active = !!$active;
189
        return $this;
190
    }
191
    /**
192
     * @return boolean
193
     */
194
    public function active()
195
    {
196
        return $this->active;
197
    }
198
199
    /**
200
     * @param mixed $roles The ACL roles this user belongs to.
201
     * @throws InvalidArgumentException If the roles argument is invalid.
202
     * @return AbstractUser Chainable
203
     */
204
    public function setRoles($roles)
205
    {
206
        if (is_string($roles)) {
207
            $roles = explode(',', $roles);
208
        }
209
        if (!is_array($roles)) {
210
            throw new InvalidArgumentException(
211
                'Roles must be a comma-separated string or an array'
212
            );
213
        }
214
        $this->roles = $roles;
215
        return $this;
216
    }
217
218
    /**
219
     * @return array
220
     */
221
    public function roles()
222
    {
223
        return $this->roles;
224
    }
225
226
    /**
227
     * @param string|DateTime|null $lastLoginDate The last login date.
228
     * @throws InvalidArgumentException If the ts is not a valid date/time.
229
     * @return AbstractUser Chainable
230
     */
231 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...
232
    {
233
        if ($lastLoginDate === null) {
234
            $this->lastLoginDate = null;
235
            return $this;
236
        }
237
        if (is_string($lastLoginDate)) {
238
            try {
239
                $lastLoginDate = new DateTime($lastLoginDate);
240
            } catch (Exception $e) {
241
                throw new InvalidArgumentException(
242
                    sprintf('Invalid login date (%s)', $e->getMessage())
243
                );
244
            }
245
        }
246
        if (!($lastLoginDate instanceof DateTimeInterface)) {
247
            throw new InvalidArgumentException(
248
                'Invalid "Last Login Date" value. Must be a date/time string or a DateTime object.'
249
            );
250
        }
251
        $this->lastLoginDate = $lastLoginDate;
252
        return $this;
253
    }
254
255
    /**
256
     * @return DateTime|null
257
     */
258
    public function lastLoginDate()
259
    {
260
        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 260 which is incompatible with the return type declared by the interface Charcoal\User\UserInterface::lastLoginDate of type Charcoal\User\Datetime.
Loading history...
261
    }
262
263
    /**
264
     * @param string|integer|null $ip The last login IP address.
265
     * @throws InvalidArgumentException If the IP is not an IP string, an integer, or null.
266
     * @return UserInterface Chainable
267
     */
268 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...
269
    {
270
        if ($ip === null) {
271
            $this->lastLoginIp = null;
272
            return $this;
273
        }
274
        if (is_int($ip)) {
275
            $ip = long2ip($ip);
276
        }
277
        if (!is_string($ip)) {
278
            throw new InvalidArgumentException(
279
                'Invalid IP address'
280
            );
281
        }
282
        $this->lastLoginIp = $ip;
283
        return $this;
284
    }
285
    /**
286
     * Get the last login IP in x.x.x.x format
287
     * @return string
288
     */
289
    public function lastLoginIp()
290
    {
291
        return $this->lastLoginIp;
292
    }
293
294
    /**
295
     * @param string|DateTime|null $lastPasswordDate The last password date.
296
     * @throws InvalidArgumentException If the passsword date is not a valid DateTime.
297
     * @return UserInterface Chainable
298
     */
299 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...
300
    {
301
        if ($lastPasswordDate === null) {
302
            $this->lastPasswordDate = null;
303
            return $this;
304
        }
305
        if (is_string($lastPasswordDate)) {
306
            try {
307
                $lastPasswordDate = new DateTime($lastPasswordDate);
308
            } catch (Exception $e) {
309
                throw new InvalidArgumentException(
310
                    sprintf('Invalid last password date (%s)', $e->getMessage())
311
                );
312
            }
313
        }
314
        if (!($lastPasswordDate instanceof DateTimeInterface)) {
315
            throw new InvalidArgumentException(
316
                'Invalid "Last Password Date" value. Must be a date/time string or a DateTime object.'
317
            );
318
        }
319
        $this->lastPasswordDate = $lastPasswordDate;
320
        return $this;
321
    }
322
323
    /**
324
     * @return DateTime
325
     */
326
    public function lastPasswordDate()
327
    {
328
        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 328 which is incompatible with the return type declared by the interface Charcoal\User\UserInterface::lastPasswordDate of type Charcoal\User\Datetime.
Loading history...
329
    }
330
331
    /**
332
     * @param integer|string|null $ip The last password IP.
333
     * @throws InvalidArgumentException If the IP is not null, an integer or an IP string.
334
     * @return UserInterface Chainable
335
     */
336 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...
337
    {
338
        if ($ip === null) {
339
            $this->lastPasswordIp = null;
340
            return $this;
341
        }
342
        if (is_int($ip)) {
343
            $ip = long2ip($ip);
344
        }
345
        if (!is_string($ip)) {
346
            throw new InvalidArgumentException(
347
                'Invalid IP address'
348
            );
349
        }
350
        $this->lastPasswordIp = $ip;
351
        return $this;
352
    }
353
    /**
354
     * Get the last password change IP in x.x.x.x format
355
     *
356
     * @return string
357
     */
358
    public function lastPasswordIp()
359
    {
360
        return $this->lastPasswordIp;
361
    }
362
363
    /**
364
     * @param string $token The login token.
365
     * @throws InvalidArgumentException If the token is not a string.
366
     * @return UserInterface Chainable
367
     */
368
    public function setLoginToken($token)
369
    {
370
        if (!is_string($token)) {
371
            throw new InvalidArgumentException(
372
                'Login Token must be a string'
373
            );
374
        }
375
        $this->loginToken = $token;
376
        return $this;
377
    }
378
379
    /**
380
     * @return string
381
     */
382
    public function loginToken()
383
    {
384
        return $this->loginToken;
385
    }
386
387
    /**
388
     * @throws Exception If trying to save a user to session without a ID.
389
     * @return UserInterface Chainable
390
     */
391
    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...
392
    {
393
        if (!$this->id()) {
394
            throw new Exception(
395
                'Can not set auth user; no user ID'
396
            );
397
        }
398
        $_SESSION[static::sessionKey()] = $this->id();
399
        return $this;
400
    }
401
402
    /**
403
     * Log in the user (in session)
404
     *
405
     * Called when the authentication is successful.
406
     *
407
     * @return boolean Success / Failure
408
     */
409
    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...
410
    {
411
        if (!$this->id()) {
412
            return false;
413
        }
414
415
        $this->setLastLoginDate('now');
416
        $ip = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '';
417
        if ($ip) {
418
            $this->setLastLoginIp($ip);
419
        }
420
        $this->update(['last_login_ip', 'last_login_date']);
421
422
        $this->saveToSession();
423
424
        return true;
425
    }
426
427
    /**
428
     * @return boolean
429
     */
430
    public function logLogin()
431
    {
432
        // @todo
433
        return true;
434
    }
435
436
    /**
437
     * Failed authentication callback
438
     *
439
     * @param string $username The failed username.
440
     * @return void
441
     */
442
    public function loginFailed($username)
443
    {
444
        $this->setUsername('');
445
446
        $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...
447
    }
448
449
    /**
450
     * @param string $username The username to log failure.
451
     * @return boolean
452
     */
453
    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...
454
    {
455
        // @todo
456
        return true;
457
    }
458
459
    /**
460
     * Empties the session var associated to the session key.
461
     *
462
     * @return boolean Logged out or not.
463
     */
464
    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...
465
    {
466
        // Irrelevant call...
467
        if (!$this->id()) {
468
            return false;
469
        }
470
471
        $_SESSION[static::sessionKey()] = null;
472
        unset($_SESSION[static::sessionKey()]);
473
474
        return true;
475
    }
476
477
    /**
478
     * Reset the password.
479
     *
480
     * Encrypt the password and re-save the object in the database.
481
     * Also updates the last password date & ip.
482
     *
483
     * @param string $plainPassword The plain (non-encrypted) password to reset to.
484
     * @throws InvalidArgumentException If the plain password is not a string.
485
     * @return UserInterface Chainable
486
     */
487
    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...
488
    {
489
        if (!is_string($plainPassword)) {
490
            throw new InvalidArgumentException(
491
                'Can not change password: password is not a string.'
492
            );
493
        }
494
495
        $hash = password_hash($plainPassword, PASSWORD_DEFAULT);
496
        $this->setPassword($hash);
497
498
        $this->setLastPasswordDate('now');
499
        $ip = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '';
500
        if ($ip) {
501
            $this->setLastPasswordIp($ip);
502
        }
503
504
        if ($this->id()) {
505
            $this->update(['password', 'last_password_date', 'last_password_ip']);
506
        }
507
508
        return $this;
509
    }
510
511
    /**
512
     * Get the currently authenticated user (from session)
513
     *
514
     * Return null if there is no current user in logged into
515
     *
516
     * @param FactoryInterface $factory The factory to create the user object with.
517
     * @throws Exception If the user from session is invalid.
518
     * @return UserInterface|null
519
     */
520
    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...
521
    {
522
        if (isset(static::$authenticatedUser[static::sessionKey()])) {
523
            return static::$authenticatedUser[static::sessionKey()];
524
        }
525
526
        if (!isset($_SESSION[static::sessionKey()])) {
527
            return null;
528
        }
529
530
        $userId = $_SESSION[static::sessionKey()];
531
        if (!$userId) {
532
            return null;
533
        }
534
535
        $userClass = get_called_class();
536
        $user = $factory->create($userClass);
537
        $user->load($userId);
538
539
        // Inactive users can not authenticate
540
        if (!$user->id() || !$user->username() || !$user->active()) {
541
            // @todo log error
542
            return null;
543
        }
544
545
        static::$authenticatedUser[static::sessionKey()] = $user;
546
        return $user;
547
    }
548
}
549