Completed
Push — master ( e72c23...de047a )
by Jeroen
25:41
created

UsersTable   A

Complexity

Total Complexity 30

Size/Duplication

Total Lines 273
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 10

Test Coverage

Coverage 56%

Importance

Changes 0
Metric Value
dl 0
loc 273
ccs 56
cts 100
cp 0.56
rs 10
c 0
b 0
f 0
wmc 30
lcom 2
cbo 10

7 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 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
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 ElggUser;
9
use RegistrationException;
10
11
/**
12
 * WARNING: API IN FLUX. DO NOT USE DIRECTLY.
13
 *
14
 * @access private
15
 *
16
 * @package    Elgg.Core
17
 * @subpackage Database
18
 * @since      1.10.0
19
 */
20
class UsersTable {
21
22
	use \Elgg\TimeUsing;
23
24
	/**
25
	 * @var Conf
26
	 */
27
	protected $config;
28
29
	/**
30
	 * @var Database
31
	 */
32
	protected $db;
33
34
	/**
35
	 * @var MetadataTable
36
	 */
37
	protected $metadata;
38
39
	/**
40
	 * @var EntityCache
41
	 */
42
	protected $entity_cache;
43
44
	/**
45
	 * @var string
46
	 */
47
	protected $table;
48
49
	/**
50
	 * Constructor
51
	 *
52
	 * @param Conf          $config   Config
53
	 * @param Database      $db       Database
54
	 * @param MetadataTable $metadata Metadata table
55
	 * @param EntityCache   $cache    Entity cache
56
	 */
57 3711
	public function __construct(Conf $config, Database $db, MetadataTable $metadata, EntityCache $cache) {
58 3711
		$this->config = $config;
59 3711
		$this->db = $db;
60 3711
		$this->metadata = $metadata;
61 3711
		$this->entity_cache = $cache;
62 3711
	}
63
64
	/**
65
	 * Get user by username
66
	 *
67
	 * @param string $username The user's username
68
	 *
69
	 * @return ElggUser|false Depending on success
70
	 */
71 286
	public function getByUsername($username) {
72
73
		// Fixes #6052. Username is frequently sniffed from the path info, which,
74
		// unlike $_GET, is not URL decoded. If the username was not URL encoded,
75
		// this is harmless.
76 286
		$username = rawurldecode($username);
77
78 286
		if (!$username) {
79
			return false;
80
		}
81
82 286
		$entity = $this->entity_cache->getByUsername($username);
83 286
		if ($entity) {
84 2
			return $entity;
85
		}
86
87 286
		$users = $this->metadata->getEntities([
88 286
			'types' => 'user',
89
			'metadata_name_value_pairs' => [
90
				[
91 286
					'name' => 'username',
92 286
					'value' => $username,
93
				],
94
			],
95 286
			'limit' => 1,
96
		]);
97
		
98 286
		return $users ? $users[0] : false;
99
	}
100
101
	/**
102
	 * Get an array of users from an email address
103
	 *
104
	 * @param string $email Email address
105
	 * @return array
106
	 */
107 63
	public function getByEmail($email) {
108 63
		if (!$email) {
109
			return [];
110
		}
111
		
112 63
		$users = $this->metadata->getEntities([
113 63
			'types' => 'user',
114
			'metadata_name_value_pairs' => [
115
				[
116 63
					'name' => 'email',
117 63
					'value' => $email,
118
				],
119
			],
120 63
			'limit' => 1,
121
		]);
122
123 63
		return $users ? : [];
124
	}
125
126
	/**
127
	 * Return users (or the number of them) who have been active within a recent period.
128
	 *
129
	 * @param array $options Array of options with keys:
130
	 *
131
	 *   seconds (int)  => Length of period (default 600 = 10min)
132
	 *   limit   (int)  => Limit (default 10)
133
	 *   offset  (int)  => Offset (default 0)
134
	 *   count   (bool) => Return a count instead of users? (default false)
135
	 *
136
	 * @return \ElggUser[]|int
137
	 */
138
	public function findActive(array $options = []) {
139
	
140
		$options = array_merge([
141
			'seconds' => 600,
142
			'limit' => $this->config->default_limit,
143
		], $options);
144
145
		// cast options we're sending to hook
146
		foreach (['seconds', 'limit', 'offset'] as $key) {
147
			$options[$key] = (int) $options[$key];
148
		}
149
		$options['count'] = (bool) $options['count'];
150
151
		// allow plugins to override
152
		$params = [
153
			'seconds' => $options['seconds'],
154
			'limit' => $options['limit'],
155
			'offset' => $options['offset'],
156
			'count' => $options['count'],
157
			'options' => $options,
158
		];
159
		$data = _elgg_services()->hooks->trigger('find_active_users', 'system', $params, null);
160
		// check null because the handler could legitimately return falsey values.
161
		if ($data !== null) {
162
			return $data;
163
		}
164
165
		$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...
166
		$time = $this->getCurrentTime()->getTimestamp() - $options['seconds'];
167
		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...
168
			'type' => 'user',
169
			'limit' => $options['limit'],
170
			'offset' => $options['offset'],
171
			'count' => $options['count'],
172
			'wheres' => ["e.last_action >= {$time}"],
173
			'order_by' => "e.last_action desc",
174
		]);
175
	}
176
177
	/**
178
	 * Registers a user, returning false if the username already exists
179
	 *
180
	 * @param string $username              The username of the new user
181
	 * @param string $password              The password
182
	 * @param string $name                  The user's display name
183
	 * @param string $email                 The user's email address
184
	 * @param bool   $allow_multiple_emails Allow the same email address to be
185
	 *                                      registered multiple times?
186
	 * @param string $subtype               Subtype of the user entity
187
	 *
188
	 * @return int|false The new user's GUID; false on failure
189
	 * @throws RegistrationException
190
	 */
191 63
	public function register($username, $password, $name, $email, $allow_multiple_emails = false, $subtype = null) {
192
193
		// no need to trim password
194 63
		$username = trim($username);
195 63
		$name = trim(strip_tags($name));
196 63
		$email = trim($email);
197
198
		// A little sanity checking
199 63
		if (empty($username) || empty($password) || empty($name) || empty($email)) {
200
			return false;
201
		}
202
203
		// Make sure a user with conflicting details hasn't registered and been disabled
204 63
		$access_status = access_get_show_hidden_status();
205 63
		access_show_hidden_entities(true);
206
207 63
		if (!validate_email_address($email)) {
208
			throw new RegistrationException(_elgg_services()->translator->translate('registration:emailnotvalid'));
209
		}
210
211 63
		if (!validate_password($password)) {
212
			throw new RegistrationException(_elgg_services()->translator->translate('registration:passwordnotvalid'));
213
		}
214
215 63
		if (!validate_username($username)) {
216
			throw new RegistrationException(_elgg_services()->translator->translate('registration:usernamenotvalid'));
217
		}
218
219 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...
220
			throw new RegistrationException(_elgg_services()->translator->translate('registration:userexists'));
221
		}
222
223 63
		if ((!$allow_multiple_emails) && (get_user_by_email($email))) {
224
			throw new RegistrationException(_elgg_services()->translator->translate('registration:dupeemail'));
225
		}
226
227 63
		access_show_hidden_entities($access_status);
228
229
		// Create user
230 63
		$constructor = ElggUser::class;
231 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...
232 59
			$class = get_subtype_class('user', $subtype);
233 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...
234
				$constructor = $class;
235
			}
236
		}
237
238 63
		$user = new $constructor();
239 63
		$user->subtype = $subtype;
240 63
		$user->username = $username;
241 63
		$user->email = $email;
242 63
		$user->name = $name;
243 63
		$user->access_id = ACCESS_PUBLIC;
244 63
		$user->owner_guid = 0; // Users aren't owned by anyone, even if they are admin created.
245 63
		$user->container_guid = 0; // Users aren't contained by anyone, even if they are admin created.
246 63
		$user->language = _elgg_services()->translator->getCurrentLanguage();
247 63
		if ($user->save() === false) {
248
			return false;
249
		}
250
		
251
		// doing this after save to prevent metadata save notices on unwritable metadata password_hash
252 63
		$user->setPassword($password);
253
254
		// Turn on email notifications by default
255 63
		$user->setNotificationSetting('email', true);
256
	
257 63
		return $user->getGUID();
258
	}
259
260
	/**
261
	 * Generates a unique invite code for a user
262
	 *
263
	 * @param string $username The username of the user sending the invitation
264
	 *
265
	 * @return string Invite code
266
	 * @see validateInviteCode
267
	 */
268
	public function generateInviteCode($username) {
269
		$time = $this->getCurrentTime()->getTimestamp();
270
		return "$time." . _elgg_services()->hmac->getHmac([(int) $time, $username])->getToken();
271
	}
272
273
	/**
274
	 * Validate a user's invite code
275
	 *
276
	 * @param string $username The username
277
	 * @param string $code     The invite code
278
	 *
279
	 * @return bool
280
	 * @see generateInviteCode
281
	 */
282
	public function validateInviteCode($username, $code) {
283
		// validate the format of the token created by ->generateInviteCode()
284
		if (!preg_match('~^(\d+)\.([a-zA-Z0-9\-_]+)$~', $code, $m)) {
285
			return false;
286
		}
287
		$time = $m[1];
288
		$mac = $m[2];
289
290
		return _elgg_services()->hmac->getHmac([(int) $time, $username])->matchesToken($mac);
291
	}
292
}
293