Passed
Push — master ( 5063d9...a1994b )
by Jeroen
22:08
created

UsersTable   B

Complexity

Total Complexity 42

Size/Duplication

Total Lines 371
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 11

Test Coverage

Coverage 49.26%

Importance

Changes 0
Metric Value
dl 0
loc 371
rs 8.295
c 0
b 0
f 0
ccs 67
cts 136
cp 0.4926
wmc 42
lcom 2
cbo 11

11 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 9 1
B getByUsername() 0 29 4
A getByEmail() 0 18 3
B findActive() 0 38 3
C register() 0 68 16
A generateInviteCode() 0 4 1
A validateInviteCode() 0 10 2
B setValidationStatus() 0 19 5
A getValidationStatus() 0 7 3
A setLastAction() 0 18 2
A setLastLogin() 0 13 2

How to fix   Complexity   

Complex Class

Complex classes like UsersTable 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 UsersTable, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Elgg\Database;
4
5
use Elgg\Cache\EntityCache;
6
use Elgg\Config as Conf;
7
use Elgg\Database;
8
use Elgg\Database\MetadataTable;
9
use Elgg\EventsService;
10
use ElggUser;
11
use RegistrationException;
12
13
/**
14
 * WARNING: API IN FLUX. DO NOT USE DIRECTLY.
15
 *
16
 * @access private
17
 *
18
 * @package    Elgg.Core
19
 * @subpackage Database
20
 * @since      1.10.0
21
 */
22
class UsersTable {
23
24
	use \Elgg\TimeUsing;
25
26
	/**
27
	 * @var Conf
28
	 */
29
	protected $config;
30
31
	/**
32
	 * @var Database
33
	 */
34
	protected $db;
35
36
	/**
37
	 * @var MetadataTable
38
	 */
39
	protected $metadata;
40
41
	/**
42
	 * @var EntityCache
43
	 */
44
	protected $entity_cache;
45
46
	/**
47
	 * @var EventsService
48
	 */
49
	protected $events;
50
51
	/**
52
	 * @var string
53
	 */
54
	protected $table;
55
56
	/**
57
	 * Constructor
58
	 *
59
	 * @param Conf          $config   Config
60
	 * @param Database      $db       Database
61
	 * @param MetadataTable $metadata Metadata table
62
	 * @param EntityCache   $cache    Entity cache
63
	 * @param EventsService $events   Event service
64
	 */
65 3711
	public function __construct(
66
	Conf $config, Database $db, MetadataTable $metadata, EntityCache $cache, EventsService $events
67
	) {
68 3711
		$this->config = $config;
69 3711
		$this->db = $db;
70 3711
		$this->metadata = $metadata;
71 3711
		$this->entity_cache = $cache;
72 3711
		$this->events = $events;
73 3711
	}
74
75
	/**
76
	 * Get user by username
77
	 *
78
	 * @param string $username The user's username
79
	 *
80
	 * @return ElggUser|false Depending on success
81
	 */
82 286
	public function getByUsername($username) {
83
84
		// Fixes #6052. Username is frequently sniffed from the path info, which,
85
		// unlike $_GET, is not URL decoded. If the username was not URL encoded,
86
		// this is harmless.
87 286
		$username = rawurldecode($username);
88
89 286
		if (!$username) {
90
			return false;
91
		}
92
93 286
		$entity = $this->entity_cache->getByUsername($username);
94 286
		if ($entity) {
95 2
			return $entity;
96
		}
97
98 286
		$users = $this->metadata->getEntities([
99 286
			'types' => 'user',
100
			'metadata_name_value_pairs' => [
101
				[
102 286
					'name' => 'username',
103 286
					'value' => $username,
104
				],
105
			],
106 286
			'limit' => 1,
107
		]);
108
		
109 286
		return $users ? $users[0] : false;
110
	}
111
112
	/**
113
	 * Get an array of users from an email address
114
	 *
115
	 * @param string $email Email address
116
	 * @return array
117
	 */
118 63
	public function getByEmail($email) {
119 63
		if (!$email) {
120
			return [];
121
		}
122
		
123 63
		$users = $this->metadata->getEntities([
124 63
			'types' => 'user',
125
			'metadata_name_value_pairs' => [
126
				[
127 63
					'name' => 'email',
128 63
					'value' => $email,
129
				],
130
			],
131 63
			'limit' => 1,
132
		]);
133
134 63
		return $users ? : [];
135
	}
136
137
	/**
138
	 * Return users (or the number of them) who have been active within a recent period.
139
	 *
140
	 * @param array $options Array of options with keys:
141
	 *
142
	 *   seconds (int)  => Length of period (default 600 = 10min)
143
	 *   limit   (int)  => Limit (default 10)
144
	 *   offset  (int)  => Offset (default 0)
145
	 *   count   (bool) => Return a count instead of users? (default false)
146
	 *
147
	 * @return \ElggUser[]|int
148
	 */
149
	public function findActive(array $options = []) {
150
	
151
		$options = array_merge([
152
			'seconds' => 600,
153
			'limit' => $this->config->default_limit,
154
		], $options);
155
156
		// cast options we're sending to hook
157
		foreach (['seconds', 'limit', 'offset'] as $key) {
158
			$options[$key] = (int) $options[$key];
159
		}
160
		$options['count'] = (bool) $options['count'];
161
162
		// allow plugins to override
163
		$params = [
164
			'seconds' => $options['seconds'],
165
			'limit' => $options['limit'],
166
			'offset' => $options['offset'],
167
			'count' => $options['count'],
168
			'options' => $options,
169
		];
170
		$data = _elgg_services()->hooks->trigger('find_active_users', 'system', $params, null);
171
		// check null because the handler could legitimately return falsey values.
172
		if ($data !== null) {
173
			return $data;
174
		}
175
176
		$dbprefix = $this->config->dbprefix;
0 ignored issues
show
Unused Code introduced by
$dbprefix is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
177
		$time = $this->getCurrentTime()->getTimestamp() - $options['seconds'];
178
		return elgg_get_entities([
1 ignored issue
show
Bug Best Practice introduced by
The return type of return elgg_get_entities...'e.last_action desc')); (ElggBatch|false|integer|array) is incompatible with the return type documented by Elgg\Database\UsersTable::findActive of type ElggUser[]|integer.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
179
			'type' => 'user',
180
			'limit' => $options['limit'],
181
			'offset' => $options['offset'],
182
			'count' => $options['count'],
183
			'wheres' => ["e.last_action >= {$time}"],
184
			'order_by' => "e.last_action desc",
185
		]);
186
	}
187
188
	/**
189
	 * Registers a user, returning false if the username already exists
190
	 *
191
	 * @param string $username              The username of the new user
192
	 * @param string $password              The password
193
	 * @param string $name                  The user's display name
194
	 * @param string $email                 The user's email address
195
	 * @param bool   $allow_multiple_emails Allow the same email address to be
196
	 *                                      registered multiple times?
197
	 * @param string $subtype               Subtype of the user entity
198
	 *
199
	 * @return int|false The new user's GUID; false on failure
200
	 * @throws RegistrationException
201
	 */
202 63
	public function register($username, $password, $name, $email, $allow_multiple_emails = false, $subtype = null) {
203
204
		// no need to trim password
205 63
		$username = trim($username);
206 63
		$name = trim(strip_tags($name));
207 63
		$email = trim($email);
208
209
		// A little sanity checking
210 63
		if (empty($username) || empty($password) || empty($name) || empty($email)) {
211
			return false;
212
		}
213
214
		// Make sure a user with conflicting details hasn't registered and been disabled
215 63
		$access_status = access_get_show_hidden_status();
216 63
		access_show_hidden_entities(true);
217
218 63
		if (!validate_email_address($email)) {
219
			throw new RegistrationException(_elgg_services()->translator->translate('registration:emailnotvalid'));
220
		}
221
222 63
		if (!validate_password($password)) {
223
			throw new RegistrationException(_elgg_services()->translator->translate('registration:passwordnotvalid'));
224
		}
225
226 63
		if (!validate_username($username)) {
227
			throw new RegistrationException(_elgg_services()->translator->translate('registration:usernamenotvalid'));
228
		}
229
230 63
		if ($user = get_user_by_username($username)) {
0 ignored issues
show
Unused Code introduced by
$user is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
231
			throw new RegistrationException(_elgg_services()->translator->translate('registration:userexists'));
232
		}
233
234 63
		if ((!$allow_multiple_emails) && (get_user_by_email($email))) {
235
			throw new RegistrationException(_elgg_services()->translator->translate('registration:dupeemail'));
236
		}
237
238 63
		access_show_hidden_entities($access_status);
239
240
		// Create user
241 63
		$constructor = ElggUser::class;
242 63
		if ($subtype) {
1 ignored issue
show
Bug Best Practice introduced by
The expression $subtype of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
243 59
			$class = get_subtype_class('user', $subtype);
244 59
			if ($class && class_exists($class) && is_subclass_of($class, ElggUser::class)) {
1 ignored issue
show
Bug Best Practice introduced by
The expression $class of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
Bug introduced by
Due to PHP Bug #53727, is_subclass_of might return inconsistent results on some PHP versions if \ElggUser::class can be an interface. If so, you could instead use ReflectionClass::implementsInterface.
Loading history...
245
				$constructor = $class;
246
			}
247
		}
248
249 63
		$user = new $constructor();
250 63
		$user->subtype = $subtype;
251 63
		$user->username = $username;
252 63
		$user->email = $email;
253 63
		$user->name = $name;
254 63
		$user->access_id = ACCESS_PUBLIC;
255 63
		$user->owner_guid = 0; // Users aren't owned by anyone, even if they are admin created.
256 63
		$user->container_guid = 0; // Users aren't contained by anyone, even if they are admin created.
257 63
		$user->language = _elgg_services()->translator->getCurrentLanguage();
258 63
		if ($user->save() === false) {
259
			return false;
260
		}
261
		
262
		// doing this after save to prevent metadata save notices on unwritable metadata password_hash
263 63
		$user->setPassword($password);
264
265
		// Turn on email notifications by default
266 63
		$user->setNotificationSetting('email', true);
267
	
268 63
		return $user->getGUID();
269
	}
270
271
	/**
272
	 * Generates a unique invite code for a user
273
	 *
274
	 * @param string $username The username of the user sending the invitation
275
	 *
276
	 * @return string Invite code
277
	 * @see validateInviteCode
278
	 */
279
	public function generateInviteCode($username) {
280
		$time = $this->getCurrentTime()->getTimestamp();
281
		return "$time." . _elgg_services()->hmac->getHmac([(int) $time, $username])->getToken();
282
	}
283
284
	/**
285
	 * Validate a user's invite code
286
	 *
287
	 * @param string $username The username
288
	 * @param string $code     The invite code
289
	 *
290
	 * @return bool
291
	 * @see generateInviteCode
292
	 */
293
	public function validateInviteCode($username, $code) {
294
		// validate the format of the token created by ->generateInviteCode()
295
		if (!preg_match('~^(\d+)\.([a-zA-Z0-9\-_]+)$~', $code, $m)) {
296
			return false;
297
		}
298
		$time = $m[1];
299
		$mac = $m[2];
300
301
		return _elgg_services()->hmac->getHmac([(int) $time, $username])->matchesToken($mac);
302
	}
303
304
	/**
305
	 * Set the validation status for a user.
306
	 *
307
	 * @param int    $user_guid The user's GUID
308
	 * @param bool   $status    Validated (true) or unvalidated (false)
309
	 * @param string $method    Optional method to say how a user was validated
310
	 * @return bool
311
	 */
312 63
	public function setValidationStatus($user_guid, $status, $method = '') {
313 63
		$user = get_user($user_guid);
314 63
		if (!$user) {
315
			return false;
316
		}
317
318 63
		$result1 = create_metadata($user->guid, 'validated', (int) $status);
319 63
		$result2 = create_metadata($user->guid, 'validated_method', $method);
320 63
		if ($result1 && $result2) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $result1 of type false|integer is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
Bug Best Practice introduced by
The expression $result2 of type false|integer is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
321 63
			if ((bool) $status) {
322 37
				elgg_trigger_after_event('validate', 'user', $user);
0 ignored issues
show
Documentation introduced by
$user is of type object<ElggEntity>|object<stdClass>, but the function expects a string|null.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
323
			} else {
324 32
				elgg_trigger_after_event('invalidate', 'user', $user);
0 ignored issues
show
Documentation introduced by
$user is of type object<ElggEntity>|object<stdClass>, but the function expects a string|null.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
325
			}
326 63
			return true;
327
		} else {
328
			return false;
329
		}
330
	}
331
332
	/**
333
	 * Gets the validation status of a user.
334
	 *
335
	 * @param int $user_guid The user's GUID
336
	 * @return bool|null Null means status was not set for this user.
337
	 */
338
	public function getValidationStatus($user_guid) {
339
		$user = get_entity($user_guid);
340
		if (!$user || !isset($user->validated)) {
341
			return null;
342
		}
343
		return (bool) $user->validated;
344
	}
345
346
	/**
347
	 * Sets the last action time of the given user to right now.
348
	 *
349
	 * @see _elgg_session_boot The session boot calls this at the beginning of every request
350
	 *
351
	 * @param ElggUser $user User entity
352
	 * @return void
353
	 */
354
	public function setLastAction(ElggUser $user) {
355
356
		$time = $this->getCurrentTime()->getTimestamp();
357
358
		if ($user->last_action == $time) {
359
			// no change required
360
			return;
361
		}
362
363
		// these writes actually work, we just type hint read-only.
364
		$user->prev_last_action = $user->last_action;
365
		$user->last_action = $time;
366
		
367
		register_shutdown_function(function () use ($user, $time) {
368
			$user->updateLastAction($user, $time); // keep entity table in sync
0 ignored issues
show
Documentation introduced by
$user is of type object<ElggUser>, but the function expects a integer|null.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
Unused Code introduced by
The call to ElggUser::updateLastAction() has too many arguments starting with $time.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
369
			$user->storeInPersistedCache(_elgg_get_memcache('new_entity_cache'), $time);
370
		});
371
	}
372
373
	/**
374
	 * Sets the last logon time of the given user to right now.
375
	 *
376
	 * @param ElggUser $user User entity
377
	 * @return void
378
	 */
379
	public function setLastLogin(ElggUser $user) {
380
381
		$time = $this->getCurrentTime()->getTimestamp();
382
383
		if ($user->last_login == $time) {
384
			// no change required
385
			return;
386
		}
387
388
		// these writes actually work, we just type hint read-only.
389
		$user->prev_last_login = $user->last_login;
390
		$user->last_login = $time;
391
	}
392
}
393