Completed
Push — master ( d634b5...adc68b )
by
unknown
03:23
created

AbstractUser::preferences()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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