Completed
Push — 3.0 ( ad4164...1cd324 )
by Olivier
03:03
created

User::save()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
c 4
b 0
f 0
dl 0
loc 9
rs 9.6666
cc 2
eloc 4
nc 2
nop 1
1
<?php
2
3
/*
4
 * This file is part of the Icybee package.
5
 *
6
 * (c) Olivier Laviale <[email protected]>
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;
15
use ICanBoogie\ActiveRecord\CreatedAtProperty;
16
use ICanBoogie\ActiveRecord\RecordNotFound;
17
18
use Brickrouge\AlterCSSClassNamesEvent;
19
use Brickrouge\CSSClassNames;
20
use Brickrouge\CSSClassNamesProperty;
21
22
use Icybee\Modules\Users\Roles\Role;
23
use Icybee\Binding\Core\PrototypedBindings as IcybeeBindings;
24
use Icybee\Modules\Registry\Binding\UserBindings as RegistryBindings;
25
26
/**
27
 * A user.
28
 *
29
 * @property-read UserModel $model
30
 * @property-read \ICanBoogie\Core|Binding\CoreBindings $app
31
 *
32
 * @property-read string $name The formatted name of the user.
33
 * @property-read boolean $is_admin true if the user is admin, false otherwise.
34
 * @property-read boolean $is_guest true if the user is a guest, false otherwise.
35
 * @property-read Role $role
36
 * @property Role[] $roles
37
 *
38
 * @property-read string $password_hash The password hash.
39
 * @property-read bool|null $has_legacy_password_hash Whether the password hash is a legacy hash.
40
 * {@link User::get_has_legacy_password_hash()}.
41
 */
42
class User extends ActiveRecord implements CSSClassNames
43
{
44
	use IcybeeBindings, RegistryBindings;
45
	use CreatedAtProperty, LoggedAtProperty, CSSClassNamesProperty;
46
	use PasswordTrait;
47
48
	const MODEL_ID = 'users';
49
50
	const UID = 'uid';
51
	const EMAIL = 'email';
52
	const PASSWORD = 'password';
53
	const PASSWORD_HASH = 'password_hash';
54
	const PASSWORD_VERIFY = 'password-verify';
55
	const USERNAME = 'username';
56
	const FIRSTNAME = 'firstname';
57
	const LASTNAME = 'lastname';
58
	const NICKNAME = 'nickname';
59
	const CREATED_AT = 'created_at';
60
	const LOGGED_AT = 'logged_at';
61
	const CONSTRUCTOR = 'constructor';
62
	const LANGUAGE = 'language';
63
	const TIMEZONE = 'timezone';
64
	const IS_ACTIVATED = 'is_activated';
65
	const ROLES = 'roles';
66
	const RESTRICTED_SITES = 'restricted_sites';
67
68
	const NAME_AS = 'name_as';
69
70
	/**
71
	 * The {@link $name} property should be created from `$username`.
72
	 *
73
	 * @var int
74
	 */
75
	const NAME_AS_USERNAME = 0;
76
77
	/**
78
	 * The {@link $name} property should be created from `$firstname`.
79
	 *
80
	 * @var int
81
	 */
82
	const NAME_AS_FIRSTNAME = 1;
83
84
	/**
85
	 * The {@link $name} property should be created from `$lastname`.
86
	 *
87
	 * @var int
88
	 */
89
	const NAME_AS_LASTNAME = 2;
90
91
	/**
92
	 * The {@link $name} property should be created from `$firstname $lastname`.
93
	 *
94
	 * @var int
95
	 */
96
	const NAME_AS_FIRSTNAME_LASTNAME = 3;
97
98
	/**
99
	 * The {@link $name} property should be created from `$lastname $firstname`.
100
	 *
101
	 * @var int
102
	 */
103
	const NAME_AS_LASTNAME_FIRSTNAME = 4;
104
105
	/**
106
	 * The {@link $name} property should be created from `$nickname`.
107
	 *
108
	 * @var int
109
	 */
110
	const NAME_AS_NICKNAME = 5;
111
112
	/**
113
	 * @inheritdoc
114
	 *
115
	 * The method takes care of setting the {@link password_hash} property which is not
116
	 * settable otherwise.
117
	 *
118
	 * @return static
119
	 */
120
	static public function from($properties = null, array $construct_args = [], $class_name = null)
121
	{
122
		if (!is_array($properties) || !array_key_exists('password_hash', $properties))
123
		{
124
			return parent::from($properties, $construct_args, $class_name);
125
		}
126
127
		$password_hash = $properties['password_hash'];
128
		unset($properties['password_hash']);
129
		$instance = parent::from($properties, $construct_args);
130
		$instance->password_hash = $password_hash;
131
132
		return $instance;
133
	}
134
135
	/**
136
	 * User identifier.
137
	 *
138
	 * @var string
139
	 */
140
	public $uid;
141
142
	/**
143
	 * Constructor of the user record (module id).
144
	 *
145
	 * The property MUST be defined to persist the record.
146
	 *
147
	 * @var string
148
	 */
149
	public $constructor;
150
151
	/**
152
	 * User email.
153
	 *
154
	 * The property MUST be defined to persist the record.
155
	 *
156
	 * @var string
157
	 */
158
	public $email;
159
160
	/**
161
	 * Username of the user.
162
	 *
163
	 * The property MUST be defined to persist the record.
164
	 *
165
	 * @var string
166
	 */
167
	public $username;
168
169
	/**
170
	 * First name of the user.
171
	 *
172
	 * @var string
173
	 */
174
	public $firstname = '';
175
176
	/**
177
	 * Last name of the user.
178
	 *
179
	 * @var string
180
	 */
181
	public $lastname = '';
182
183
	/**
184
	 * Nickname of the user.
185
	 *
186
	 * @var string
187
	 */
188
	public $nickname = '';
189
190
	/**
191
	 * Preferred format to create the value of the {@link $name} property.
192
	 *
193
	 * @var string
194
	 */
195
	public $name_as = self::NAME_AS_USERNAME;
196
197
	/**
198
	 * Preferred language of the user.
199
	 *
200
	 * @var string
201
	 */
202
	public $language = '';
203
204
	/**
205
	 * Preferred timezone of the user.
206
	 *
207
	 * @var string
208
	 */
209
	public $timezone = '';
210
211
	/**
212
	 * State of the user account activation.
213
	 *
214
	 * @var bool
215
	 */
216
	public $is_activated = false;
217
218
	/**
219
	 * If empty, the {@link $constructor} property is initialized with the model identifier.
220
	 *
221
	 * @inheritdoc
222
	 */
223
	public function __construct($model = null)
224
	{
225
		parent::__construct($model);
226
227
		if (empty($this->constructor))
228
		{
229
			$this->constructor = $this->model_id;
230
		}
231
	}
232
233
	/**
234
	 * @inheritdoc
235
	 */
236
	public function __get($property)
237
	{
238
		$value = parent::__get($property);
239
240
		if ($property === 'css_class_names')
241
		{
242
			new AlterCSSClassNamesEvent($this, $value);
243
		}
244
245
		return $value;
246
	}
247
248
	/**
249
	 * @inheritdoc
250
	 */
251
	public function create_validation_rules()
252
	{
253
		return [
254
255
			'username' => 'required',
256
			'email' => 'required|email|unique',
257
			'timezone' => 'timezone'
258
259
		];
260
	}
261
262
	/**
263
	 * @inheritdoc
264
	 */
265
	public function save(array $options = [])
266
	{
267
		if ($this->get_created_at()->is_empty)
268
		{
269
			$this->set_created_at('now');
270
		}
271
272
		return parent::save($options);
273
	}
274
275
	/**
276
	 * Adds the {@link $password_hash} property.
277
	 */
278
	public function to_array()
279
	{
280
		$array = parent::to_array();
281
282
		if ($this->password_hash)
283
		{
284
			$array['password_hash'] = $this->password_hash;
285
		}
286
287
		return $array;
288
	}
289
290
	/**
291
	 * Returns the formatted name of the user.
292
	 *
293
	 * The format of the name is defined by the {@link $name_as} property. The {@link $username},
294
	 * {@link $firstname}, {@link $lastname} and {@link $nickname} properties can be used to
295
	 * format the name.
296
	 *
297
	 * This is the getter for the {@link $name} magic property.
298
	 *
299
	 * @return string
300
	 */
301
	protected function get_name()
302
	{
303
		$values = [
304
305
			self::NAME_AS_USERNAME => $this->username,
306
			self::NAME_AS_FIRSTNAME => $this->firstname,
307
			self::NAME_AS_LASTNAME => $this->lastname,
308
			self::NAME_AS_FIRSTNAME_LASTNAME => $this->firstname . ' ' . $this->lastname,
309
			self::NAME_AS_LASTNAME_FIRSTNAME => $this->lastname . ' ' . $this->firstname,
310
			self::NAME_AS_NICKNAME => $this->nickname
311
312
		];
313
314
		$rc = isset($values[$this->name_as]) ? $values[$this->name_as] : null;
315
316
		if (!trim($rc))
317
		{
318
			return $this->username;
319
		}
320
321
		return $rc;
322
	}
323
324
	/**
325
	 * Returns the role of the user.
326
	 *
327
	 * This is the getter for the {@link $role} magic property.
328
	 *
329
	 * @return Role
330
	 */
331
	protected function lazy_get_role()
332
	{
333
		$permissions = [];
334
		$name = null;
335
336
		foreach ($this->roles as $role)
337
		{
338
			$name .= ', ' . $role->name;
339
340
			foreach ($role->perms as $access => $permission)
341
			{
342
				$permissions[$access] = $permission;
343
			}
344
		}
345
346
		$role = new Role;
347
		$role->perms = $permissions;
348
349
		if ($name)
350
		{
351
			$role->name = substr($name, 2);
352
		}
353
354
		return $role;
355
	}
356
357
	/**
358
	 * Returns all the roles associated with the user.
359
	 *
360
	 * This is the getter for the {@link $roles} magic property.
361
	 *
362
	 * @return array
363
	 */
364
	protected function lazy_get_roles()
365
	{
366
		$models = $this->model->models;
367
368
		try
369
		{
370
			if (!$this->uid)
371
			{
372
				return [ $models['users.roles'][1] ];
373
			}
374
		}
375
		catch (\Exception $e)
376
		{
377
			return [];
378
		}
379
380
		$rids = $models['users/has_many_roles']
381
		->select('rid')
382
		->filter_by_uid($this->uid)
383
		->all(\PDO::FETCH_COLUMN);
384
385
		if (!in_array(2, $rids))
386
		{
387
			array_unshift($rids, 2);
388
		}
389
390
		try
391
		{
392
			return $models['users.roles']->find($rids);
393
		}
394
		catch (RecordNotFound $e)
395
		{
396
			trigger_error($e->getMessage());
397
398
			return array_filter($e->records);
399
		}
400
	}
401
402
	/**
403
	 * Checks if the user is the admin user.
404
	 *
405
	 * This is the getter for the {@link $is_admin} magic property.
406
	 *
407
	 * @return boolean `true` if the user is the admin user, `false` otherwise.
408
	 */
409
	protected function get_is_admin()
410
	{
411
		return $this->uid == 1;
412
	}
413
414
	/**
415
	 * Checks if the user is a guest user.
416
	 *
417
	 * This is the getter for the {@link $is_guest} magic property.
418
	 *
419
	 * @return boolean `true` if the user is a guest user, `false` otherwise.
420
	 */
421
	protected function get_is_guest()
422
	{
423
		return !$this->uid;
424
	}
425
426
	/**
427
	 * Returns the ids of the sites the user is restricted to.
428
	 *
429
	 * This is the getter for the {@link $restricted_sites_ids} magic property.
430
	 *
431
	 * @return array The array is empty if the user has no site restriction.
432
	 */
433
	protected function lazy_get_restricted_sites_ids()
434
	{
435
		if ($this->is_admin)
436
		{
437
			return [];
438
		}
439
440
		return $this->model->models['users/has_many_sites']
441
		->select('site_id')
442
		->filter_by_uid($this->uid)
443
		->all(\PDO::FETCH_COLUMN);
444
	}
445
446
	/**
447
	 * Checks if the user has a given permission.
448
	 *
449
	 * @param string|int $permission
450
	 * @param mixed $target
451
	 *
452
	 * @return mixed
453
	 */
454
	public function has_permission($permission, $target = null)
455
	{
456
		if ($this->is_admin)
457
		{
458
			return Module::PERMISSION_ADMINISTER;
459
		}
460
461
		return $this->app->check_user_permission($this, $permission, $target);
462
	}
463
464
	/**
465
	 * Checks if the user has the ownership of an entry.
466
	 *
467
	 * If the ownership information is missing from the entry (the 'uid' property is null), the user
468
	 * must have the ADMINISTER level to be considered the owner.
469
	 *
470
	 * @param ActiveRecord $record
471
	 *
472
	 * @return boolean
473
	 */
474
	public function has_ownership($record)
475
	{
476
		return $this->app->check_user_ownership($this, $record);
477
	}
478
479
	/**
480
	 * Logs the user in.
481
	 *
482
	 * A user is logged in by setting its id in the `user_id` session key.
483
	 *
484
	 * Note: The method does *not* check user authentication!
485
	 *
486
	 * The following things happen when the user is logged in:
487
	 *
488
	 * - The `$app->user` property is set to the user.
489
	 * - The `$app->user_id` property is set to the user id.
490
	 * - The session id is regenerated and the user id, ip and user agent are stored in the session.
491
	 *
492
	 * @throws \Exception in attempt to log in a guest user.
493
	 *
494
	 * @see \Icybee\Modules\Users\Hooks\get_user_id
495
	 */
496
	public function login()
497
	{
498
		if (!$this->uid)
499
		{
500
			throw new \Exception('Guest users cannot login.');
501
		}
502
503
		$app = $this->app;
504
		$app->user = $this;
505
		$app->user_id = $this->uid;
506
		$app->session->regenerate();
507
		$app->session['user_id'] = $this->uid;
508
	}
509
510
	/**
511
	 * Log the user out.
512
	 *
513
	 * The following things happen when the user is logged out:
514
	 *
515
	 * - The `$app->user` property is unset.
516
	 * - The `$app->user_id` property is unset.
517
	 * - The `user_id` session property is removed.
518
	 */
519
	public function logout()
520
	{
521
		$app = $this->app;
522
		$app->session->regenerate();
523
524
		unset($app->user);
525
		unset($app->user_id);
526
		unset($app->session['user_id']);
527
	}
528
529
	/**
530
	 * @inheritdoc
531
	 */
532
	protected function get_css_class_names()
533
	{
534
		return [
535
536
			'type' => 'user',
537
			'id' => ($this->uid && !$this->is_guest) ? 'user-id-' . $this->uid : null,
538
			'username' => ($this->username && !$this->is_guest) ? 'user-' . $this->username : null,
539
			'constructor' => 'constructor-' . \ICanBoogie\normalize($this->constructor),
540
			'is-admin' => $this->is_admin,
541
			'is-guest' => $this->is_guest,
542
			'is-logged' => !$this->is_guest
543
544
		];
545
	}
546
}
547