ElggUser   F
last analyzed

Complexity

Total Complexity 61

Size/Duplication

Total Lines 471
Duplicated Lines 0 %

Test Coverage

Coverage 83.63%

Importance

Changes 0
Metric Value
eloc 156
dl 0
loc 471
ccs 143
cts 171
cp 0.8363
rs 3.52
c 0
b 0
f 0
wmc 61

23 Methods

Rating   Name   Duplication   Size   Complexity  
A setValidationStatus() 0 24 4
A getObjects() 0 5 1
C __set() 0 35 12
A getLanguage() 0 10 3
A initializeAttributes() 0 17 1
A removeAdmin() 0 15 3
A isAdmin() 0 2 1
A isValidated() 0 6 2
A prepareObject() 0 7 1
A makeAdmin() 0 15 3
A unban() 0 16 3
A setLastLogin() 0 12 2
A setPassword() 0 6 2
A getOwnerGUID() 0 7 2
A getGroups() 0 6 1
A ban() 0 15 3
A isBanned() 0 2 1
A setNotificationSetting() 0 7 2
A setLastAction() 0 16 2
A getNotificationSettings() 0 18 6
A notify() 0 2 1
A getPluginSetting() 0 11 3
A persistentDelete() 0 8 2

How to fix   Complexity   

Complex Class

Complex classes like ElggUser often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ElggUser, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
use Elgg\Exceptions\InvalidArgumentException as ElggInvalidArgumentException;
4
use Elgg\Exceptions\Configuration\RegistrationException;
5
use Elgg\Traits\Entity\Friends;
6
use Elgg\Traits\Entity\PluginSettings;
7
use Elgg\Traits\Entity\ProfileData;
8
9
/**
10
 * A user entity
11
 *
12
 * @property      string $name             The display name that the user will be known by in the network
13
 * @property      string $username         The short, reference name for the user in the network
14
 * @property      string $email            The email address to which Elgg will send email notifications
15
 * @property      string $language         The language preference of the user (ISO 639-1 formatted)
16
 * @property-read string $banned           'yes' if the user is banned from the network, 'no' otherwise
17
 * @property      string $ban_reason       The reason why the user was banned
18
 * @property-read string $admin            'yes' if the user is an administrator of the network, 'no' otherwise
19
 * @property      bool   $validated        User validation status
20
 * @property      string $validated_method User validation method
21
 * @property      int    $validated_ts     A UNIX timestamp of the last moment a users validation status is set to true
22
 * @property-read string $password_hash    The hashed password of the user
23
 * @property-read int    $prev_last_action A UNIX timestamp of the previous last action
24
 * @property-read int    $first_login      A UNIX timestamp of the first login
25
 * @property-read int    $last_login       A UNIX timestamp of the last login
26
 * @property-read int    $prev_last_login  A UNIX timestamp of the previous login
27
 */
28
class ElggUser extends \ElggEntity {
29
30
	use Friends;
31
	use PluginSettings {
32
		getPluginSetting as protected psGetPluginSetting;
33
	}
34
	use ProfileData;
0 ignored issues
show
Bug introduced by
The trait Elgg\Traits\Entity\ProfileData requires the property $userCapabilities which is not provided by ElggUser.
Loading history...
35
	
36
	/**
37
	 * {@inheritdoc}
38
	 */
39 1848
	protected function initializeAttributes() {
40 1848
		parent::initializeAttributes();
41
		
42 1848
		$this->attributes['type'] = 'user';
43 1848
		$this->attributes['subtype'] = 'user';
44
		
45 1848
		$this->attributes['access_id'] = ACCESS_PUBLIC;
46 1848
		$this->attributes['owner_guid'] = 0; // Users aren't owned by anyone, even if they are admin created.
47 1848
		$this->attributes['container_guid'] = 0; // Users aren't contained by anyone, even if they are admin created.
48
		
49
		// Before Elgg 3.0 this was handled by database logic
50 1848
		$this->setMetadata('banned', 'no');
51 1848
		$this->setMetadata('admin', 'no');
52 1848
		$this->language = elgg_get_config('language');
53 1848
		$this->prev_last_action = 0;
54 1848
		$this->last_login = 0;
55 1848
		$this->prev_last_login = 0;
56
	}
57
	
58
	/**
59
	 * Get user language or default to site language
60
	 *
61
	 * @param string|null $fallback If this is provided, it will be returned if the user doesn't have a language set.
62
	 *                              If null, the site language will be returned.
63
	 *
64
	 * @return string
65
	 */
66 58
	public function getLanguage(?string $fallback = null): string {
67 58
		if (!empty($this->language)) {
68 53
			return $this->language;
69
		}
70
		
71 5
		if ($fallback !== null) {
72
			return $fallback;
73
		}
74
		
75 5
		return elgg_get_config('language');
76
	}
77
78
	/**
79
	 * {@inheritdoc}
80
	 *
81
	 * @throws \Elgg\Exceptions\InvalidArgumentException
82
	 */
83 1848
	public function __set($name, $value) {
84
		switch ($name) {
85 1848
			case 'salt':
86 1848
			case 'password':
87
				_elgg_services()->logger->error("User entities no longer contain {$name}");
88
				return;
89 1848
			case 'password_hash':
90
				_elgg_services()->logger->error('password_hash is a readonly attribute.');
91
				return;
92 1848
			case 'email':
93
				try {
94 1530
					_elgg_services()->accounts->assertValidEmail($value);
95
				} catch (RegistrationException $ex) {
96
					throw new ElggInvalidArgumentException($ex->getMessage(), $ex->getCode(), $ex);
97
				}
98 1530
				break;
99 1848
			case 'username':
100
				try {
101 1541
					_elgg_services()->accounts->assertValidUsername($value);
102
				} catch (RegistrationException $ex) {
103
					throw new ElggInvalidArgumentException($ex->getMessage(), $ex->getCode(), $ex);
104
				}
105
				
106 1541
				$existing_user = elgg_get_user_by_username($value);
107 1541
				if ($existing_user instanceof \ElggUser && ($existing_user->guid !== $this->guid)) {
108
					throw new ElggInvalidArgumentException("{$name} is supposed to be unique for ElggUser");
109
				}
110 1541
				break;
111 1848
			case 'admin':
112 1
				throw new ElggInvalidArgumentException(_elgg_services()->translator->translate('ElggUser:Error:SetAdmin', ['makeAdmin() / removeAdmin()']));
113 1848
			case 'banned':
114 1
				throw new ElggInvalidArgumentException(_elgg_services()->translator->translate('ElggUser:Error:SetBanned', ['ban() / unban()']));
115
		}
116
		
117 1848
		parent::__set($name, $value);
118
	}
119
	
120
	/**
121
	 * Ban this user.
122
	 *
123
	 * @param string $reason Optional reason
124
	 *
125
	 * @return bool
126
	 */
127 1361
	public function ban(string $reason = ''): bool {
128 1361
		if (!$this->canEdit()) {
129
			return false;
130
		}
131
		
132 1361
		if (!_elgg_services()->events->trigger('ban', 'user', $this)) {
133
			return false;
134
		}
135
136 1361
		$this->ban_reason = $reason;
137 1361
		$this->setMetadata('banned', 'yes');
138
139 1361
		$this->invalidateCache();
140
141 1361
		return true;
142
	}
143
144
	/**
145
	 * Unban this user.
146
	 *
147
	 * @return bool
148
	 */
149 1
	public function unban(): bool {
150
		
151 1
		if (!$this->canEdit()) {
152
			return false;
153
		}
154
155 1
		if (!_elgg_services()->events->trigger('unban', 'user', $this)) {
156
			return false;
157
		}
158
159 1
		unset($this->ban_reason);
160 1
		$this->setMetadata('banned', 'no');
161
162 1
		$this->invalidateCache();
163
164 1
		return true;
165
	}
166
167
	/**
168
	 * Is this user banned or not?
169
	 *
170
	 * @return bool
171
	 */
172 85
	public function isBanned(): bool {
173 85
		return $this->banned === 'yes';
174
	}
175
176
	/**
177
	 * Is this user admin?
178
	 *
179
	 * @return bool
180
	 */
181 758
	public function isAdmin(): bool {
182 758
		return $this->admin === 'yes';
183
	}
184
185
	/**
186
	 * Make the user an admin
187
	 *
188
	 * @return bool
189
	 */
190 33
	public function makeAdmin(): bool {
191
		
192 33
		if ($this->isAdmin()) {
193
			return true;
194
		}
195
196 33
		if (!_elgg_services()->events->trigger('make_admin', 'user', $this)) {
197
			return false;
198
		}
199
200 33
		$this->setMetadata('admin', 'yes');
201
202 33
		$this->invalidateCache();
203
		
204 33
		return true;
205
	}
206
207
	/**
208
	 * Remove the admin flag for user
209
	 *
210
	 * @return bool
211
	 */
212 3
	public function removeAdmin(): bool {
213
214 3
		if (!$this->isAdmin()) {
215 2
			return true;
216
		}
217
218 1
		if (!_elgg_services()->events->trigger('remove_admin', 'user', $this)) {
219
			return false;
220
		}
221
222 1
		$this->setMetadata('admin', 'no');
223
224 1
		$this->invalidateCache();
225
		
226 1
		return true;
227
	}
228
	
229
	/**
230
	 * Sets the last logon time of the user to right now.
231
	 *
232
	 * @return void
233
	 */
234 12
	public function setLastLogin(): void {
235 12
		$time = $this->getCurrentTime()->getTimestamp();
236
		
237 12
		if ($this->last_login == $time) {
238
			// no change required
239 1
			return;
240
		}
241
		
242 12
		elgg_call(ELGG_IGNORE_ACCESS | ELGG_DISABLE_SYSTEM_LOG, function() use ($time) {
243
			// these writes actually work, we just type hint read-only.
244 12
			$this->prev_last_login = $this->last_login;
245 12
			$this->last_login = $time;
246 12
		});
247
	}
248
	
249
	/**
250
	 * Sets the last action time of the given user to right now.
251
	 *
252
	 * @return void
253
	 */
254 1
	public function setLastAction(): void {
255
		
256 1
		$time = $this->getCurrentTime()->getTimestamp();
257
		
258 1
		if ($this->last_action == $time) {
259
			// no change required
260 1
			return;
261
		}
262
		
263
		$user = $this;
264
		
265
		elgg_register_event_handler('shutdown', 'system', function () use ($user, $time) {
266
			// these writes actually work, we just type hint read-only.
267
			$user->prev_last_action = $user->last_action;
268
		
269
			$user->updateLastAction($time);
270
		});
271
	}
272
	
273
	/**
274
	 * Gets the validation status of a user.
275
	 *
276
	 * @return bool|null Null means status was not set for this user.
277
	 */
278 1364
	public function isValidated(): ?bool {
279 1364
		if (!isset($this->validated)) {
280 1364
			return null;
281
		}
282
		
283 1357
		return (bool) $this->validated;
284
	}
285
	
286
	/**
287
	 * Set the validation status for a user.
288
	 *
289
	 * @param bool   $status Validated (true) or unvalidated (false)
290
	 * @param string $method Optional method to say how a user was validated
291
	 *
292
	 * @return void
293
	 */
294 1361
	public function setValidationStatus(bool $status, string $method = ''): void {
295 1361
		if ($status === $this->isValidated()) {
296
			// no change needed
297 1353
			return;
298
		}
299
		
300 1361
		$this->validated = $status;
301
		
302 1361
		if ($status) {
303 1358
			$this->validated_method = $method;
304 1358
			$this->validated_ts = time();
305
		
306
			// make sure the user is enabled
307 1358
			if (!$this->isEnabled()) {
308 1
				$this->enable();
309
			}
310
			
311
			// let the system know the user is validated
312 1358
			_elgg_services()->events->triggerAfter('validate', 'user', $this);
313
		} else {
314
			// invalidating
315 7
			unset($this->validated_ts);
316 7
			unset($this->validated_method);
317 7
			_elgg_services()->events->triggerAfter('invalidate', 'user', $this);
318
		}
319
	}
320
321
	/**
322
	 * Gets the user's groups
323
	 *
324
	 * @param array $options Options array.
325
	 *
326
	 * @return \ElggGroup[]|int|mixed
327
	 */
328 2
	public function getGroups(array $options = []) {
329 2
		$options['type'] = 'group';
330 2
		$options['relationship'] = 'member';
331 2
		$options['relationship_guid'] = $this->guid;
332
333 2
		return elgg_get_entities($options);
334
	}
335
336
	/**
337
	 * {@inheritdoc}
338
	 */
339
	public function getObjects(array $options = []) {
340
		$options['type'] = 'object';
341
		$options['owner_guid'] = $this->guid;
342
343
		return elgg_get_entities($options);
344
	}
345
346
	/**
347
	 * Get a user's owner GUID
348
	 *
349
	 * Returns its own GUID if the user is not owned.
350
	 *
351
	 * @return int
352
	 */
353 60
	public function getOwnerGUID(): int {
354 60
		$owner_guid = parent::getOwnerGUID();
355 60
		if ($owner_guid === 0) {
356 60
			$owner_guid = (int) $this->guid;
357
		}
358
359 60
		return $owner_guid;
360
	}
361
362
	/**
363
	 * {@inheritdoc}
364
	 */
365 60
	protected function prepareObject(\Elgg\Export\Entity $object) {
366 60
		$object = parent::prepareObject($object);
367 60
		$object->name = $this->getDisplayName();
368 60
		$object->username = $this->username;
369 60
		$object->language = $this->language;
370 60
		unset($object->read_access);
371 60
		return $object;
372
	}
373
374
	/**
375
	 * Set the necessary metadata to store a hash of the user's password.
376
	 *
377
	 * @param string $password The password to be hashed
378
	 *
379
	 * @return void
380
	 * @since 1.10.0
381
	 */
382 1361
	public function setPassword(string $password): void {
383 1361
		$this->setMetadata('password_hash', _elgg_services()->passwords->generateHash($password));
384 1361
		if ($this->guid === elgg_get_logged_in_user_guid()) {
385
			// update the session user token, so this session remains valid
386
			// other sessions for this user will be invalidated
387 2
			_elgg_services()->session_manager->setUserToken();
388
		}
389
	}
390
391
	/**
392
	 * Enable or disable a notification delivery method
393
	 *
394
	 * @param string $method  Method name
395
	 * @param bool   $enabled Enabled or disabled (default: true)
396
	 * @param string $purpose For what purpose is the notification setting used (default: 'default')
397
	 *
398
	 * @return bool
399
	 * @throws \Elgg\Exceptions\InvalidArgumentException
400
	 */
401 1374
	public function setNotificationSetting(string $method, bool $enabled = true, string $purpose = 'default'): bool {
402 1374
		if (empty($purpose)) {
403
			throw new ElggInvalidArgumentException(__METHOD__ . ' requires $purpose to be set to a non-empty string');
404
		}
405
		
406 1374
		$this->{"notification:{$purpose}:{$method}"} = (int) $enabled;
407 1374
		return $this->save();
408
	}
409
410
	/**
411
	 * Return the user notification settings
412
	 * <code>
413
	 *    [
414
	 *       'email' => true, // enabled
415
	 *       'ajax' => false, // disabled
416
	 *    ]
417
	 *
418
	 *    // or when $only_active_methods === true
419
	 *    ['email']
420
	 * </code>
421
	 *
422
	 * @param string $purpose             For what purpose to get the notification settings (default: 'default')
423
	 * @param bool   $only_active_methods Only return the active methods (useful for notifications)
424
	 *
425
	 * @return array
426
	 * @throws \Elgg\Exceptions\InvalidArgumentException
427
	 */
428 371
	public function getNotificationSettings(string $purpose = 'default', bool $only_active_methods = false): array {
429 371
		if (empty($purpose)) {
430
			throw new ElggInvalidArgumentException(__METHOD__ . ' requires $purpose to be set to a non-empty string');
431
		}
432
		
433 371
		$settings = [];
434
435 371
		$methods = _elgg_services()->notifications->getMethods();
436 371
		foreach ($methods as $method) {
437 371
			if ($purpose !== 'default' && !isset($this->{"notification:{$purpose}:{$method}"})) {
438
				// fallback to the default settings
439 340
				$settings[$method] = (bool) $this->{"notification:default:{$method}"};
440
			} else {
441 38
				$settings[$method] = (bool) $this->{"notification:{$purpose}:{$method}"};
442
			}
443
		}
444
445 371
		return $only_active_methods ? array_keys(array_filter($settings)) : $settings;
446
	}
447
	
448
	/**
449
	 * {@inheritdoc}
450
	 */
451 1355
	public function persistentDelete(bool $recursive = true): bool {
452 1355
		$result = parent::persistentDelete($recursive);
453 1355
		if ($result) {
454
			// cleanup remember me cookie records
455 1354
			_elgg_services()->users_remember_me_cookies_table->deleteAllHashes($this);
456
		}
457
		
458 1355
		return $result;
459
	}
460
	
461
	/**
462
	 * Get a plugin setting.
463
	 * Will return $default if the plugin isn't active
464
	 *
465
	 * @param string $plugin_id plugin ID
466
	 * @param string $name      setting name
467
	 * @param mixed  $default   default setting value
468
	 *
469
	 * @return mixed
470
	 * @see \Elgg\Traits\Entity\PluginSettings::getPluginSetting()
471
	 */
472 14
	public function getPluginSetting(string $plugin_id, string $name, $default = null) {
473 14
		$plugin = _elgg_services()->plugins->get($plugin_id);
474 14
		if (!$plugin instanceof \ElggPlugin || !$plugin->isActive()) {
475 9
			return $default;
476
		}
477
		
478 5
		$static_defaults = (array) $plugin->getStaticConfig('user_settings', []);
479
		
480 5
		$default = elgg_extract($name, $static_defaults, $default);
481
		
482 5
		return $this->psGetPluginSetting($plugin_id, $name, $default);
483
	}
484
	
485
	/**
486
	 * Notify the user about a given action on a subject
487
	 *
488
	 * @param string           $action  The action on $subject
489
	 * @param \ElggData        $subject The notification subject
490
	 * @param array            $params  Additional params
491
	 *                                  use $params['methods_override'] to override the recipient notification methods (eg 'email' or 'site')
492
	 * @param null|\ElggEntity $from    Sender of the message
493
	 *
494
	 * @return array Compound array of each delivery user/delivery method's success or failure.
495
	 * @since 6.3
496
	 */
497 15
	public function notify(string $action, \ElggData $subject, array $params = [], ?\ElggEntity $from = null): array {
498 15
		return _elgg_services()->notifications->sendInstantNotification($this, $action, $subject, $params, $from);
499
	}
500
}
501