Overview

Namespaces

  • Charcoal
    • Object
    • User
      • Acl

Classes

  • Charcoal\Object\Content
  • Charcoal\Object\ObjectRevision
  • Charcoal\Object\ObjectRoute
  • Charcoal\Object\ObjectSchedule
  • Charcoal\Object\UserData
  • Charcoal\User\AbstractUser
  • Charcoal\User\Acl\Manager
  • Charcoal\User\Acl\Permission
  • Charcoal\User\Acl\PermissionCategory
  • Charcoal\User\Acl\Role
  • Charcoal\User\Authenticator
  • Charcoal\User\Authorizer
  • Charcoal\User\AuthToken
  • Charcoal\User\AuthTokenMetadata
  • Charcoal\User\GenericUser

Interfaces

  • Charcoal\Object\ArchivableInterface
  • Charcoal\Object\CategorizableInterface
  • Charcoal\Object\CategorizableMultipleInterface
  • Charcoal\Object\CategoryInterface
  • Charcoal\Object\ContentInterface
  • Charcoal\Object\HierarchicalInterface
  • Charcoal\Object\ObjectRevisionInterface
  • Charcoal\Object\ObjectRouteInterface
  • Charcoal\Object\ObjectScheduleInterface
  • Charcoal\Object\PublishableInterface
  • Charcoal\Object\RevisionableInterface
  • Charcoal\Object\RoutableInterface
  • Charcoal\Object\UserDataInterface
  • Charcoal\User\UserInterface

Traits

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