Passed
Push — master ( 6151ba...17a32e )
by Chauncey
52s queued 10s
created

AbstractUser::validate()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 14
rs 9.7998
c 0
b 0
f 0
cc 3
nc 3
nop 1
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-core'
14
use Charcoal\Validator\ValidatorInterface;
15
16
// From 'charcoal-object'
17
use Charcoal\Object\Content;
18
19
// From 'charcoal-user'
20
use Charcoal\User\Access\AuthenticatableInterface;
21
use Charcoal\User\Access\AuthenticatableTrait;
22
23
/**
24
 * Full implementation, as abstract class, of the `UserInterface`.
25
 */
26
abstract class AbstractUser extends Content implements
27
    AuthenticatableInterface,
28
    UserInterface
29
{
30
    use AuthenticatableTrait;
31
32
    /**
33
     * The email address should be unique and mandatory.
34
     *
35
     * It is also used as the login name.
36
     *
37
     * @var string
38
     */
39
    private $email;
40
41
    /**
42
     * The password is stored encrypted in the (database) storage.
43
     *
44
     * @var string|null
45
     */
46
    private $password;
47
48
    /**
49
     * The display name serves as a human-readable identifier for the user.
50
     *
51
     * @var string|null
52
     */
53
    private $displayName;
54
55
    /**
56
     * Roles define a set of tasks a user is allowed or denied from performing.
57
     *
58
     * @var string[]
59
     */
60
    private $roles = [];
61
62
    /**
63
     * The timestamp of the latest (successful) login.
64
     *
65
     * @var DateTimeInterface|null
66
     */
67
    private $lastLoginDate;
68
69
    /**
70
     * The IP address during the latest (successful) login.
71
     *
72
     * @var string|null
73
     */
74
    private $lastLoginIp;
75
76
    /**
77
     * The timestamp of the latest password change.
78
     *
79
     * @var DateTimeInterface|null
80
     */
81
    private $lastPasswordDate;
82
83
    /**
84
     * The IP address during the latest password change.
85
     *
86
     * @var string|null
87
     */
88
    private $lastPasswordIp;
89
90
    /**
91
     * The token value for the "remember me" session.
92
     *
93
     * @var string|null
94
     */
95
    private $loginToken;
96
97
    /**
98
     * The user preferences.
99
     *
100
     * @var mixed
101
     */
102
    private $preferences;
103
104
    /**
105
     * @param  string $email The user email.
106
     * @throws InvalidArgumentException If the email is not a string.
107
     * @return self
108
     */
109
    public function setEmail($email)
110
    {
111
        if (!is_string($email)) {
112
            throw new InvalidArgumentException(
113
                'Set user email: Email must be a string'
114
            );
115
        }
116
117
        $this->email = $email;
118
119
        return $this;
120
    }
121
122
    /**
123
     * @return string
124
     */
125
    public function getEmail()
126
    {
127
        return $this->email;
128
    }
129
130
    /**
131
     * @param  string|null $password The user password. Encrypted in storage.
132
     * @throws InvalidArgumentException If the password is not a string (or null, to reset).
133
     * @return self
134
     */
135
    public function setPassword($password)
136
    {
137
        if ($password === null) {
138
            $this->password = $password;
139
        } elseif (is_string($password)) {
140
            $this->password = $password;
141
        } else {
142
            throw new InvalidArgumentException(
143
                'Set user password: Password must be a string'
144
            );
145
        }
146
147
        return $this;
148
    }
149
150
    /**
151
     * @return string|null
152
     */
153
    public function getPassword()
154
    {
155
        return $this->password;
156
    }
157
158
    /**
159
     * @param  string|null $name The user's display name.
160
     * @return self
161
     */
162
    public function setDisplayName($name)
163
    {
164
        $this->displayName = $name;
165
166
        return $this;
167
    }
168
169
    /**
170
     * @return string|null
171
     */
172
    public function getDisplayName()
173
    {
174
        return $this->displayName;
175
    }
176
177
    /**
178
     * @param  string|string[]|null $roles The ACL roles this user belongs to.
179
     * @throws InvalidArgumentException If the roles argument is invalid.
180
     * @return self
181
     */
182
    public function setRoles($roles)
183
    {
184
        if (empty($roles) && !is_numeric($roles)) {
185
            $this->roles = [];
186
            return $this;
187
        }
188
189
        if (is_string($roles)) {
190
            $roles = explode(',', $roles);
191
        }
192
193
        if (!is_array($roles)) {
194
            throw new InvalidArgumentException(
195
                'Roles must be a comma-separated string or an array'
196
            );
197
        }
198
199
        $this->roles = array_filter(array_map('trim', $roles), 'strlen');
200
201
        return $this;
202
    }
203
204
    /**
205
     * @return string[]
206
     */
207
    public function getRoles()
208
    {
209
        return $this->roles;
210
    }
211
212
    /**
213
     * @param  string|DateTimeInterface|null $lastLoginDate The last login date.
214
     * @throws InvalidArgumentException If the ts is not a valid date/time.
215
     * @return self
216
     */
217 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...
218
    {
219
        if ($lastLoginDate === null) {
220
            $this->lastLoginDate = null;
221
            return $this;
222
        }
223
224
        if (is_string($lastLoginDate)) {
225
            try {
226
                $lastLoginDate = new DateTime($lastLoginDate);
227
            } catch (Exception $e) {
228
                throw new InvalidArgumentException(sprintf(
229
                    'Invalid login date (%s)',
230
                    $e->getMessage()
231
                ), 0, $e);
232
            }
233
        }
234
235
        if (!($lastLoginDate instanceof DateTimeInterface)) {
236
            throw new InvalidArgumentException(
237
                'Invalid "Last Login Date" value. Must be a date/time string or a DateTime object.'
238
            );
239
        }
240
241
        $this->lastLoginDate = $lastLoginDate;
242
243
        return $this;
244
    }
245
246
    /**
247
     * @return DateTimeInterface|null
248
     */
249
    public function getLastLoginDate()
250
    {
251
        return $this->lastLoginDate;
252
    }
253
254
    /**
255
     * @param  string|integer|null $ip The last login IP address.
256
     * @throws InvalidArgumentException If the IP is not an IP string, an integer, or null.
257
     * @return self
258
     */
259 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...
260
    {
261
        if ($ip === null) {
262
            $this->lastLoginIp = null;
263
            return $this;
264
        }
265
266
        if (is_int($ip)) {
267
            $ip = long2ip($ip);
268
        }
269
270
        if (!is_string($ip)) {
271
            throw new InvalidArgumentException(
272
                'Invalid IP address'
273
            );
274
        }
275
276
        $this->lastLoginIp = $ip;
277
278
        return $this;
279
    }
280
281
    /**
282
     * Get the last login IP in x.x.x.x format
283
     *
284
     * @return string|null
285
     */
286
    public function getLastLoginIp()
287
    {
288
        return $this->lastLoginIp;
289
    }
290
291
    /**
292
     * @param  string|DateTimeInterface|null $lastPasswordDate The last password date.
293
     * @throws InvalidArgumentException If the passsword date is not a valid DateTime.
294
     * @return self
295
     */
296 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...
297
    {
298
        if ($lastPasswordDate === null) {
299
            $this->lastPasswordDate = null;
300
            return $this;
301
        }
302
303
        if (is_string($lastPasswordDate)) {
304
            try {
305
                $lastPasswordDate = new DateTime($lastPasswordDate);
306
            } catch (Exception $e) {
307
                throw new InvalidArgumentException(sprintf(
308
                    'Invalid last password date (%s)',
309
                    $e->getMessage()
310
                ), 0, $e);
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
320
        $this->lastPasswordDate = $lastPasswordDate;
321
322
        return $this;
323
    }
324
325
    /**
326
     * @return DateTimeInterface|null
327
     */
328
    public function getLastPasswordDate()
329
    {
330
        return $this->lastPasswordDate;
331
    }
332
333
    /**
334
     * @param  integer|string|null $ip The last password IP.
335
     * @throws InvalidArgumentException If the IP is not null, an integer or an IP string.
336
     * @return self
337
     */
338 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...
339
    {
340
        if ($ip === null) {
341
            $this->lastPasswordIp = null;
342
            return $this;
343
        }
344
345
        if (is_int($ip)) {
346
            $ip = long2ip($ip);
347
        }
348
349
        if (!is_string($ip)) {
350
            throw new InvalidArgumentException(
351
                'Invalid IP address'
352
            );
353
        }
354
355
        $this->lastPasswordIp = $ip;
356
357
        return $this;
358
    }
359
360
    /**
361
     * Get the last password change IP in x.x.x.x format
362
     *
363
     * @return string|null
364
     */
365
    public function getLastPasswordIp()
366
    {
367
        return $this->lastPasswordIp;
368
    }
369
370
    /**
371
     * @param  string|null $token The login token.
372
     * @throws InvalidArgumentException If the token is not a string.
373
     * @return self
374
     */
375
    public function setLoginToken($token)
376
    {
377
        if ($token === null) {
378
            $this->loginToken = null;
379
            return $this;
380
        }
381
382
        if (!is_string($token)) {
383
            throw new InvalidArgumentException(
384
                'Login Token must be a string'
385
            );
386
        }
387
388
        $this->loginToken = $token;
389
390
        return $this;
391
    }
392
393
    /**
394
     * @return string|null
395
     */
396
    public function getLoginToken()
397
    {
398
        return $this->loginToken;
399
    }
400
401
    /**
402
     * @param  mixed $preferences Structure of user preferences.
403
     * @return self
404
     */
405
    public function setPreferences($preferences)
406
    {
407
        $this->preferences = $preferences;
408
409
        return $this;
410
    }
411
412
    /**
413
     * @return mixed
414
     */
415
    public function getPreferences()
416
    {
417
        return $this->preferences;
418
    }
419
420
421
422
    // Extends Charcoal\User\Access\AuthenticatableTrait
423
    // =========================================================================
424
425
    /**
426
     * Retrieve the name of the unique ID for the user.
427
     *
428
     * @return string
429
     */
430
    public function getAuthIdKey()
431
    {
432
        return $this->key();
433
    }
434
435
    /**
436
     * Retrieve the name of the login username for the user.
437
     *
438
     * @return string
439
     */
440
    public function getAuthIdentifierKey()
441
    {
442
        return 'email';
443
    }
444
445
    /**
446
     * Retrieve the name of the login password for the user.
447
     *
448
     * @return string
449
     */
450
    public function getAuthPasswordKey()
451
    {
452
        return 'password';
453
    }
454
455
    /**
456
     * Retrieve the name of the login token for the user.
457
     *
458
     * @return string
459
     */
460
    public function getAuthLoginTokenKey()
461
    {
462
        return 'login_token';
463
    }
464
465
466
    // Extends Charcoal\Validator\ValidatableTrait
467
    // =========================================================================
468
469
    /**
470
     * Validate the user model.
471
     *
472
     * @param  ValidatorInterface $v Optional. A custom validator object to use for validation. If null, use object's.
473
     * @return boolean
474
     */
475
    public function validate(ValidatorInterface &$v = null)
476
    {
477
        $result = parent::validate($v);
478
479
        if (!$this->validateLoginRequired()) {
480
            return false;
481
        }
482
483
        if (!$this->validateLoginUnique()) {
484
            return false;
485
        }
486
487
        return $result;
488
    }
489
490
    /**
491
     * Validate the username or email address.
492
     *
493
     * @return boolean
494
     */
495
    protected function validateLoginRequired()
496
    {
497
        $userKey   = $this->getAuthIdentifierKey();
498
        $userLogin = $this->getAuthIdentifier();
499
500
        if (empty($userLogin)) {
501
            $this->validator()->error(
502
                sprintf('User Credentials: "%s" is required.', $userKey),
503
                $userKey
0 ignored issues
show
Unused Code introduced by
The call to ValidatorInterface::error() has too many arguments starting with $userKey.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
504
            );
505
            return false;
506
        }
507
508
        if (strpos($userKey, 'email') !== false && !filter_var($userLogin, FILTER_VALIDATE_EMAIL)) {
509
            $this->validator()->error(
510
                'User Credentials: Email format is incorrect.',
511
                $userKey
0 ignored issues
show
Unused Code introduced by
The call to ValidatorInterface::error() has too many arguments starting with $userKey.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
512
            );
513
            return false;
514
        }
515
516
        return true;
517
    }
518
519
    /**
520
     * Validate the username or email address is unique.
521
     *
522
     * @return boolean
523
     */
524
    protected function validateLoginUnique()
525
    {
526
        $userKey   = $this->getAuthIdentifierKey();
527
        $userLogin = $this->getAuthIdentifier();
528
529
        $objType = self::objType();
530
        $factory = $this->modelFactory();
531
532
        $originalUser = $factory->create($objType)->load($this->getAuthId());
0 ignored issues
show
Unused Code introduced by
$originalUser is not used, you could remove the assignment.

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

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

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

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

Loading history...
533
534
        if ($originalModel->getAuthIdentifier() !== $userLogin) {
0 ignored issues
show
Bug introduced by
The variable $originalModel does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
535
            $existingUser = $factory->create($objType)->loadFrom($userKey, $userLogin);
536
            /** Check for existing user with given email. */
537
            if (!empty($existingUser->getAuthId())) {
538
                $this->validator()->error(
539
                    sprintf('User Credentials: "%s" is not available.', $userKey),
540
                    $userKey
0 ignored issues
show
Unused Code introduced by
The call to ValidatorInterface::error() has too many arguments starting with $userKey.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
541
                );
542
            }
543
            return false;
544
        }
545
    }
546
}
547