1 <?php
2
3 /*
4 * This file is part of the Icybee package.
5 *
6 * (c) Olivier Laviale <olivier.laviale@gmail.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12 namespace Icybee\Modules\Users;
13
14 use ICanBoogie\ActiveRecord\CreatedAtProperty;
15 use ICanBoogie\ActiveRecord\DateTimePropertySupport;
16 use ICanBoogie\ActiveRecord\RecordNotFound;
17 use ICanBoogie\DateTime;
18
19 use Icybee\Modules\Users\Roles\Role;
20
21 /**
22 * A user.
23 *
24 * @property-read string $name The formatted name of the user.
25 * @property-read boolean $is_admin true if the user is admin, false otherwise.
26 * @property-read boolean $is_guest true if the user is a guest, false otherwise.
27 * @property-read \Icybee\Modules\Users\Users\Role $role
28 *
29 * @property-read string $password_hash The password hash.
30 * @property-read bool|null $has_legacy_password_hash Whether the password hash is a legacy hash.
31 * {@link User::get_has_legacy_password_hash()}.
32 *
33 * @property \ICanBoogie\DateTime $logged_at The date and time at which the user was logged.
34 */
35 class User extends \ICanBoogie\ActiveRecord implements \Brickrouge\CSSClassNames
36 {
37 use \Brickrouge\CSSClassNamesProperty;
38
39 const UID = 'uid';
40 const EMAIL = 'email';
41 const PASSWORD = 'password';
42 const PASSWORD_HASH = 'password_hash';
43 const USERNAME = 'username';
44 const FIRSTNAME = 'firstname';
45 const LASTNAME = 'lastname';
46 const NICKNAME = 'nickname';
47 const CREATED_AT = 'created_at';
48 const LOGGED_AT = 'logged_at';
49 const CONSTRUCTOR = 'constructor';
50 const LANGUAGE = 'language';
51 const TIMEZONE = 'timezone';
52 const IS_ACTIVATED = 'is_activated';
53 const ROLES = 'roles';
54 const RESTRICTED_SITES = 'restricted_sites';
55
56 const NAME_AS = 'name_as';
57
58 /**
59 * The {@link $name} property should be created from `$username`.
60 *
61 * @var int
62 */
63 const NAME_AS_USERNAME = 0;
64
65 /**
66 * The {@link $name} property should be created from `$firstname`.
67 *
68 * @var int
69 */
70 const NAME_AS_FIRSTNAME = 1;
71
72 /**
73 * The {@link $name} property should be created from `$lastname`.
74 *
75 * @var int
76 */
77 const NAME_AS_LASTNAME = 2;
78
79 /**
80 * The {@link $name} property should be created from `$firstname $lastname`.
81 *
82 * @var int
83 */
84 const NAME_AS_FIRSTNAME_LASTNAME = 3;
85
86 /**
87 * The {@link $name} property should be created from `$lastname $firstname`.
88 *
89 * @var int
90 */
91 const NAME_AS_LASTNAME_FIRSTNAME = 4;
92
93 /**
94 * The {@link $name} property should be created from `$nickname`.
95 *
96 * @var int
97 */
98 const NAME_AS_NICKNAME = 5;
99
100 /**
101 * User identifier.
102 *
103 * @var string
104 */
105 public $uid;
106
107 /**
108 * Constructor of the user record (module id).
109 *
110 * The property MUST be defined to persist the record.
111 *
112 * @var string
113 */
114 public $constructor;
115
116 /**
117 * User email.
118 *
119 * The property MUST be defined to persist the record.
120 *
121 * @var string
122 */
123 public $email;
124
125 /**
126 * User password.
127 *
128 * The property is only used to update the {@link $password_hash} property when the
129 * record is saved.
130 *
131 * @var string
132 */
133 protected $password;
134
135 protected function get_password()
136 {
137 return $this->password;
138 }
139
140 protected function set_password($password)
141 {
142 $this->password = $password;
143
144 if ($password)
145 {
146 $this->password_hash = $this->hash_password($password);
147 }
148 }
149
150 /**
151 * User password hash.
152 *
153 * Note: The property MUST NOT be private, otherwise only instances of the class can be
154 * initialized with a value, for subclasses instances the property would be `null`.
155 *
156 * @var string
157 */
158 protected $password_hash;
159
160 /**
161 * Checks if the password hash is a legacy hash, and not a hash created by
162 * the {@link \password_hash()} function.
163 *
164 * @return bool|null `true` if the password hash is a legacy hash, `false` if the password
165 * hash was created by the {@link \password_hash()} function, and `null` if the passsword hash
166 * is empty.
167 */
168 protected function get_has_legacy_password_hash()
169 {
170 if (!$this->password_hash)
171 {
172 return;
173 }
174
175 return $this->password_hash[0] != '$';
176 }
177
178 /**
179 * Username of the user.
180 *
181 * The property MUST be defined to persist the record.
182 *
183 * @var string
184 */
185 public $username;
186
187 /**
188 * First name of the user.
189 *
190 * @var string
191 */
192 public $firstname = '';
193
194 /**
195 * Last name of the user.
196 *
197 * @var string
198 */
199 public $lastname = '';
200
201 /**
202 * Nickname of the user.
203 *
204 * @var string
205 */
206 public $nickname = '';
207
208 /**
209 * Prefered format to create the value of the {@link $name} property.
210 *
211 * @var string
212 */
213 public $name_as = self::NAME_AS_USERNAME;
214
215 use CreatedAtProperty;
216 use LoggedAtProperty;
217
218 /**
219 * Prefered language of the user.
220 *
221 * @var string
222 */
223 public $language = '';
224
225 /**
226 * Prefered timezone of the user.
227 *
228 * @var string
229 */
230 public $timezone = '';
231
232 /**
233 * State of the user account activation.
234 *
235 * @var bool
236 */
237 public $is_activated = false;
238
239 /**
240 * Defaults `$model` to "users".
241 *
242 * Initializes the {@link $constructor} property with the model identifier if it is not
243 * defined.
244 *
245 * @param string|\ICanBoogie\ActiveRecord\Model $model
246 */
247 public function __construct($model='users')
248 {
249 parent::__construct($model);
250
251 if (empty($this->constructor))
252 {
253 $this->constructor = $this->model_id;
254 }
255 }
256
257 public function __get($property)
258 {
259 $value = parent::__get($property);
260
261 if ($property === 'css_class_names')
262 {
263 new \Brickrouge\AlterCSSClassNamesEvent($this, $value);
264 }
265
266 return $value;
267 }
268
269 protected function alter_persistent_properties(array $properties, \ICanBoogie\ActiveRecord\Model $model)
270 {
271 if ($this->get_created_at()->is_empty)
272 {
273 $this->set_created_at('now');
274 }
275
276 /*
277 if ($this->get_updated_at()->is_empty)
278 {
279 $this->set_updated_at('now');
280 }
281 */
282
283 return parent::alter_persistent_properties($properties, $model);
284 }
285
286 /**
287 * Adds the {@link $logged_at} property.
288 */
289 public function to_array()
290 {
291 $array = parent::to_array();
292
293 if ($this->password)
294 {
295 $array['password'] = $this->password;
296 }
297
298 return $array;
299 }
300
301 /**
302 * Returns the formatted name of the user.
303 *
304 * The format of the name is defined by the {@link $name_as} property. The {@link $username},
305 * {@link $firstname}, {@link $lastname} and {@link $nickname} properties can be used to
306 * format the name.
307 *
308 * This is the getter for the {@link $name} magic property.
309 *
310 * @return string
311 */
312 protected function get_name()
313 {
314 $values = [
315
316 self::NAME_AS_USERNAME => $this->username,
317 self::NAME_AS_FIRSTNAME => $this->firstname,
318 self::NAME_AS_LASTNAME => $this->lastname,
319 self::NAME_AS_FIRSTNAME_LASTNAME => $this->firstname . ' ' . $this->lastname,
320 self::NAME_AS_LASTNAME_FIRSTNAME => $this->lastname . ' ' . $this->firstname,
321 self::NAME_AS_NICKNAME => $this->nickname
322
323 ];
324
325 $rc = isset($values[$this->name_as]) ? $values[$this->name_as] : null;
326
327 if (!trim($rc))
328 {
329 return $this->username;
330 }
331
332 return $rc;
333 }
334
335 /**
336 * Returns the role of the user.
337 *
338 * This is the getter for the {@link $role} magic property.
339 *
340 * @return \Icybee\Modules\Users\Users\Role
341 */
342 protected function lazy_get_role()
343 {
344 global $core;
345
346 $permissions = [];
347 $name = null;
348
349 foreach ($this->roles as $role)
350 {
351 $name .= ', ' . $role->name;
352
353 foreach ($role->perms as $access => $permission)
354 {
355 $permissions[$access] = $permission;
356 }
357 }
358
359 $role = new Role();
360 $role->perms = $permissions;
361
362 if ($name)
363 {
364 $role->name = substr($name, 2);
365 }
366
367 return $role;
368 }
369
370 /**
371 * Returns all the roles associated with the user.
372 *
373 * This is the getter for the {@link $roles} magic property.
374 *
375 * @return array
376 */
377 protected function lazy_get_roles()
378 {
379 global $core;
380
381 try
382 {
383 if (!$this->uid)
384 {
385 return [ $core->models['users.roles'][1] ];
386 }
387 }
388 catch (\Exception $e)
389 {
390 return [];
391 }
392
393 $rids = $core->models['users/has_many_roles']->select('rid')->filter_by_uid($this->uid)->all(\PDO::FETCH_COLUMN);
394
395 if (!in_array(2, $rids))
396 {
397 array_unshift($rids, 2);
398 }
399
400 try
401 {
402 return $core->models['users.roles']->find($rids);
403 }
404 catch (RecordNotFound $e)
405 {
406 trigger_error($e->getMessage());
407
408 return array_filter($e->records);
409 }
410 }
411
412 /**
413 * Checks if the user is the admin user.
414 *
415 * This is the getter for the {@link $is_admin} magic property.
416 *
417 * @return boolean `true` if the user is the admin user, `false` otherwise.
418 */
419 protected function get_is_admin()
420 {
421 return $this->uid == 1;
422 }
423
424 /**
425 * Checks if the user is a guest user.
426 *
427 * This is the getter for the {@link $is_guest} magic property.
428 *
429 * @return boolean `true` if the user is a guest user, `false` otherwise.
430 */
431 protected function get_is_guest()
432 {
433 return !$this->uid;
434 }
435
436 /**
437 * Returns the ids of the sites the user is restricted to.
438 *
439 * This is the getter for the {@link $restricted_sites_ids} magic property.
440 *
441 * @return array The array is empty if the user has no site restriction.
442 */
443 protected function lazy_get_restricted_sites_ids()
444 {
445 global $core;
446
447 return $this->is_admin ? [] : $core->models['users/has_many_sites']->select('siteid')->filter_by_uid($this->uid)->all(\PDO::FETCH_COLUMN);
448 }
449
450 /**
451 * Checks if the user has a given permission.
452 *
453 * @param string|int $permission
454 * @param mixed $target
455 *
456 * @return mixed
457 */
458 public function has_permission($permission, $target=null)
459 {
460 global $core;
461
462 if ($this->is_admin)
463 {
464 return Module::PERMISSION_ADMINISTER;
465 }
466
467 return $core->check_user_permission($this, $permission, $target);
468 }
469
470 /**
471 * Checks if the user has the ownership of an entry.
472 *
473 * If the ownership information is missing from the entry (the 'uid' property is null), the user
474 * must have the ADMINISTER level to be considered the owner.
475 *
476 * @param $module
477 * @param $record
478 *
479 * @return boolean
480 */
481 public function has_ownership($module, $record)
482 {
483 global $core;
484
485 return $core->check_user_ownership($this, $record);
486 }
487
488 /**
489 * Hashes a password.
490 *
491 * @param string $password
492 *
493 * @return string
494 */
495 static public function hash_password($password)
496 {
497 return \password_hash($password, \PASSWORD_BCRYPT);
498 }
499
500 /**
501 * Compares a password to the user's password hash.
502 *
503 * @param string $password
504 *
505 * @return bool `true` if the hashed password matches the user's password hash,
506 * `false` otherwise.
507 */
508 public function verify_password($password)
509 {
510 if (\password_verify($password, $this->password_hash))
511 {
512 return true;
513 }
514
515 #
516 # Trying old hashing
517 #
518
519 $config = \ICanBoogie\Core::get()->configs['user'];
520
521 if (empty($config['password_salt']))
522 {
523 return false;
524 }
525
526 return sha1(\ICanBoogie\pbkdf2($password, $config['password_salt'])) == $this->password_hash;
527 }
528
529 /**
530 * Logs the user in.
531 *
532 * A user is logged in by setting its id in the `application[user_agent]` session key.
533 *
534 * Note: The method does *not* checks the user authentication !
535 *
536 * The following things happen when the user is logged in:
537 *
538 * - The `$core->user` property is set to the user.
539 * - The `$core->user_id` property is set to the user id.
540 * - The session id is regenerated and the user id, ip and user agent are stored in the session.
541 *
542 * @return boolean true if the login is successful.
543 *
544 * @throws \Exception in attempt to log in a guest user.
545 *
546 * @see \Icybee\Modules\Users\Hooks\get_user_id
547 */
548 public function login()
549 {
550 global $core;
551
552 if (!$this->uid)
553 {
554 throw new \Exception('Guest users cannot login.');
555 }
556
557 $core->user = $this;
558 $core->user_id = $this->uid;
559 $core->session->regenerate_id(true);
560 $core->session->regenerate_token();
561 $core->session->users['user_id'] = $this->uid;
562
563 return true;
564 }
565
566 /**
567 * Log the user out.
568 *
569 * The following things happen when the user is logged out:
570 *
571 * - The `$core->user` property is unset.
572 * - The `$core->user_id` property is unset.
573 * - The `$core->session->users['user_id']` property is unset.
574 */
575 public function logout()
576 {
577 global $core;
578
579 unset($core->user);
580 unset($core->user_id);
581 unset($core->session->users['user_id']);
582 }
583
584 /**
585 * Returns the CSS class names of the node.
586 *
587 * @return array[string]mixed
588 */
589 protected function get_css_class_names()
590 {
591 return [
592
593 'type' => 'user',
594 'id' => ($this->uid && !$this->is_guest) ? 'user-id-' . $this->uid : null,
595 'username' => ($this->username && !$this->is_guest) ? 'user-' . $this->username : null,
596 'constructor' => 'constructor-' . \ICanBoogie\normalize($this->constructor),
597 'is-admin' => $this->is_admin,
598 'is-guest' => $this->is_guest,
599 'is-logged' => !$this->is_guest
600
601 ];
602 }
603 }
604
605 /**
606 * Implements the`logged_at` property.
607 *
608 * @property \ICanBoogie\DateTime $logged_at The date and time at which the user was logged.
609 */
610 trait LoggedAtProperty
611 {
612 /**
613 * The date and time at which the user was logged.
614 *
615 * @var mixed
616 */
617 private $logged_at;
618
619 /**
620 * Returns the date and time at which the user was logged.
621 *
622 * @return \ICanBoogie\DateTime
623 */
624 protected function get_logged_at()
625 {
626 return DateTimePropertySupport::datetime_get($this->logged_at);
627 }
628
629 /**
630 * Sets the date and time at which the user was logged.
631 *
632 * @param mixed $datetime
633 */
634 protected function set_logged_at($datetime)
635 {
636 DateTimePropertySupport::datetime_set($this->logged_at, $datetime);
637 }
638 }