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) |
|
|
|
|
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) |
|
|
|
|
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) |
|
|
|
|
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) |
|
|
|
|
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 |
|
|
|
|
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 |
|
|
|
|
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()); |
|
|
|
|
533
|
|
|
|
534
|
|
|
if ($originalModel->getAuthIdentifier() !== $userLogin) { |
|
|
|
|
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 |
|
|
|
|
541
|
|
|
); |
542
|
|
|
} |
543
|
|
|
return false; |
544
|
|
|
} |
545
|
|
|
} |
546
|
|
|
} |
547
|
|
|
|
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.