Completed
Branch master (c03353)
by Mathieu
03:43
created

AbstractUser::setActive()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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