loadExistingMember()   A
last analyzed

Complexity

Conditions 5
Paths 6

Size

Total Lines 65
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 5.5069

Importance

Changes 0
Metric Value
cc 5
eloc 18
nc 6
nop 2
dl 0
loc 65
ccs 8
cts 11
cp 0.7272
crap 5.5069
rs 9.3554
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * This file has functions in it to do with authentication, user handling, and the like.
5
 *
6
 * @package   ElkArte Forum
7
 * @copyright ElkArte Forum contributors
8
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause (see accompanying LICENSE.txt file)
9
 *
10
 * This file contains code covered by:
11
 * copyright: 2011 Simple Machines (http://www.simplemachines.org)
12
 *
13
 * @version 2.0 dev
14
 *
15
 */
16
17
use ElkArte\Errors\ErrorContext;
18
use ElkArte\Helper\TokenHash;
19
use ElkArte\Helper\Util;
20
use ElkArte\Languages\Txt;
21
use ElkArte\Request;
22
use ElkArte\User;
23
24
/**
25
 * Sets the login cookie and session based on the id_member and password passed.
26
 *
27
 * What it does:
28
 *
29
 * - password should be already encrypted with the cookie salt.
30
 * - logs the user out if id_member is zero.
31
 * - sets the cookie and session to last the number of seconds specified by cookie_length.
32
 * - when logging out, if the globalCookies setting is enabled, attempts to clear the subdomain's cookie too.
33
 *
34
 * @param int $cookie_length
35
 * @param int $id The id of the member
36
 * @param string $password = ''
37
 * @package Authorization
38
 */
39
function setLoginCookie($cookie_length, $id, $password = '')
40
{
41
	global $cookiename, $boardurl, $modSettings;
42
43
	// If changing state force them to re-address some permission caching.
44
	$_SESSION['mc']['time'] = 0;
45
46
	// Let's be sure it is an int to simplify the regexp used to validate the cookie
47
	$id = (int) $id;
48
49
	// The cookie may already exist, and have been set with different options.
50
	$cookie_state = (empty($modSettings['localCookies']) ? 0 : 1) | (empty($modSettings['globalCookies']) ? 0 : 2);
51
52
	if (isset($_COOKIE[$cookiename]))
53
	{
54
		$array = serializeToJson($_COOKIE[$cookiename], static function ($array_from) use ($cookiename) {
55
			global $modSettings;
56
57
			require_once(SUBSDIR . '/Auth.subs.php');
58
			$_COOKIE[$cookiename] = json_encode($array_from);
59
			setLoginCookie(60 * $modSettings['cookieTime'], $array_from[0], $array_from[1]);
60
		});
61
62
		// Out with the old, in with the new!
63
		if (isset($array[3]) && $array[3] != $cookie_state)
64
		{
65
			$cookie_url = url_parts($array[3] & 1 > 0, $array[3] & 2 > 0);
0 ignored issues
show
Bug introduced by
$array[3] & 2 > 0 of type integer is incompatible with the type boolean expected by parameter $global of url_parts(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

65
			$cookie_url = url_parts($array[3] & 1 > 0, /** @scrutinizer ignore-type */ $array[3] & 2 > 0);
Loading history...
Bug introduced by
$array[3] & 1 > 0 of type integer is incompatible with the type boolean expected by parameter $local of url_parts(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

65
			$cookie_url = url_parts(/** @scrutinizer ignore-type */ $array[3] & 1 > 0, $array[3] & 2 > 0);
Loading history...
66
			elk_setcookie($cookiename, json_encode([0, '', 0]), time() - 3600, $cookie_url[1], $cookie_url[0]);
67
		}
68
	}
69
70
	// Get the data and path to set it on.
71
	$data = json_encode(empty($id) ? [0, '', 0] : [$id, $password, time() + $cookie_length, $cookie_state]);
72
	$cookie_url = url_parts(!empty($modSettings['localCookies']), !empty($modSettings['globalCookies']));
73
74
	// Set the cookie, $_COOKIE, and session variable.
75
	elk_setcookie($cookiename, $data, time() + $cookie_length, $cookie_url[1], $cookie_url[0]);
76
77
	// If subdomain-independent cookies are on, unset the subdomain-dependent cookie too.
78
	if (empty($id) && !empty($modSettings['globalCookies']))
79
	{
80
		elk_setcookie($cookiename, $data, time() + $cookie_length, $cookie_url[1]);
81
	}
82
83
	// Any alias URLs?  This is mainly for use with frames, etc.
84
	if (!empty($modSettings['forum_alias_urls']))
85
	{
86
		$aliases = explode(',', $modSettings['forum_alias_urls']);
87
88
		$temp = $boardurl;
89
		foreach ($aliases as $alias)
90
		{
91
			// Fake the $boardurl so we can set a different cookie.
92
			$alias = strtr(trim($alias), ['http://' => '', 'https://' => '']);
93
			$boardurl = 'http://' . $alias;
94
95
			$cookie_url = url_parts(!empty($modSettings['localCookies']), !empty($modSettings['globalCookies']));
96
97
			if ($cookie_url[0] == '')
98
			{
99
				$cookie_url[0] = strtok($alias, '/');
100
			}
101
102
			elk_setcookie($cookiename, $data, time() + $cookie_length, $cookie_url[1], $cookie_url[0]);
103
		}
104
105
		$boardurl = $temp;
106
	}
107
108
	$_COOKIE[$cookiename] = $data;
109
110
	// Make sure the user logs in with a new session ID.
111
	if (!isset($_SESSION['login_' . $cookiename]) || $_SESSION['login_' . $cookiename] !== $data)
112
	{
113
		// We need to meddle with the session.
114
		require_once(SOURCEDIR . '/Session.php');
115
116
		// Backup the old session.
117
		$oldSessionData = $_SESSION;
118
119
		// Remove the old session data and file / db entry
120
		$_SESSION = [];
121
		session_destroy();
122
123
		// Recreate and restore the new session.
124
		loadSession();
125
126
		// Get a new session id, and load it with the data
127
		session_regenerate_id();
128
129
		// If we generated new session values, be sure to use them as well
130
		$oldSessionData['session_value'] = $_SESSION['session_value'] ?? $oldSessionData['session_value'];
131
		$oldSessionData['session_var'] = $_SESSION['session_var'] ?? $oldSessionData['session_var'];
132
		$_SESSION = $oldSessionData;
133
134
		$_SESSION['login_' . $cookiename] = $data;
135
	}
136
}
137
138
/**
139
 * Get the domain and path for the cookie
140
 *
141
 * What it does:
142
 *
143
 * - normally, local and global should be the localCookies and globalCookies settings, respectively.
144
 * - uses boardurl to determine these two things.
145
 *
146
 * @param bool $local
147
 * @param bool $global
148
 *
149
 * @return array
150
 * @package Authorization
151
 *
152
 */
153
function url_parts($local, $global)
154
{
155
	global $boardurl, $modSettings;
156
157
	// Parse the URL with PHP to make life easier.
158
	$parsed_url = parse_url($boardurl);
159
160
	// Is local cookies off?
161
	if (empty($parsed_url['path']) || !$local)
162
	{
163
		$parsed_url['path'] = '';
164
	}
165
166
	if (!empty($modSettings['globalCookiesDomain']) && str_contains($boardurl, $modSettings['globalCookiesDomain']))
167
	{
168
		$parsed_url['host'] = $modSettings['globalCookiesDomain'];
169
	}
170
171
	// Globalize cookies across domains (filter out IP-addresses)?
172
	elseif ($global && preg_match('~^\d{1,3}(\.\d{1,3}){3}$~', $parsed_url['host']) == 0 && preg_match('~(?:[^\.]+\.)?([^\.]{2,}\..+)\z~i', $parsed_url['host'], $parts) == 1)
173
	{
174
		$parsed_url['host'] = '.' . $parts[1];
175
	}
176
177
	// We shouldn't use a host at all if both options are off.
178
	elseif (!$local && !$global)
179
	{
180
		$parsed_url['host'] = '';
181
	}
182
183
	// The host also shouldn't be set if there aren't any dots in it.
184
	elseif (!isset($parsed_url['host']) || !str_contains($parsed_url['host'], '.'))
185
	{
186
		$parsed_url['host'] = '';
187
	}
188
189
	return [$parsed_url['host'], $parsed_url['path'] . '/'];
190
}
191
192
/**
193
 * Question the verity of the admin by asking for his or her password.
194
 *
195
 * What it does:
196
 *
197
 * - loads Login.template.php and uses the admin_login sub template.
198
 * - sends data to template so the admin is sent on to the page they
199
 *   wanted if their password is correct, otherwise they can try again.
200
 *
201
 * @param string $type = 'admin'
202
 * @package Authorization
203
 */
204
function adminLogin($type = 'admin'): never
205
{
206
	global $context, $txt;
207
208
	Txt::load('Admin');
209
	theme()->getTemplates()->load('Login');
210
211
	// Validate what type of session check this is.
212
	$types = [];
213
	call_integration_hook('integrate_validateSession', [&$types]);
214
	$type = in_array($type, $types) || $type === 'moderate' ? $type : 'admin';
215
216
	// They used a wrong password, log it and unset that.
217
	if (isset($_POST[$type . '_hash_pass']) || isset($_POST[$type . '_pass']))
218
	{
219
		// log some info along with it! referer, user agent
220
		$req = Request::instance();
221
		$txt['security_wrong'] = sprintf($txt['security_wrong'], $_SERVER['HTTP_REFERER'] ?? $txt['unknown'], $req->user_agent(), User::$info->ip);
0 ignored issues
show
Bug Best Practice introduced by
The property ip does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
222
		\ElkArte\Errors\Errors::instance()->log_error($txt['security_wrong'], 'critical');
0 ignored issues
show
Bug introduced by
The type ElkArte\Errors\Errors was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
223
224
		if (isset($_POST[$type . '_hash_pass']))
225
		{
226
			unset($_POST[$type . '_hash_pass']);
227
		}
228
229
		if (isset($_POST[$type . '_pass']))
230
		{
231
			unset($_POST[$type . '_pass']);
232
		}
233
234
		$context['incorrect_password'] = true;
235
	}
236
237
	createToken('admin-login');
238
239
	// Figure out the get data and post data.
240
	$context['get_data'] = '?' . construct_query_string($_GET);
241
	$context['post_data'] = '';
242
243
	// Now go through $_POST.  Make sure the session hash is sent.
244
	$_POST[$context['session_var']] = $context['session_id'];
245
	foreach ($_POST as $k => $v)
246
	{
247
		$context['post_data'] .= adminLogin_outputPostVars($k, $v);
248
	}
249
250
	// Now we'll use the admin_login sub template of the Login template.
251
	$context['sub_template'] = 'admin_login';
252
253
	// And title the page something like "Login".
254
	if (!isset($context['page_title']))
255
	{
256
		$context['page_title'] = $txt['admin_login'];
257
	}
258
259
	// The type of action.
260
	$context['sessionCheckType'] = $type;
261
262
	obExit();
263
264
	// We MUST exit at this point, because otherwise we CANNOT KNOW that the user is privileged.
265
	trigger_error('Hacking attempt...', E_USER_ERROR);
266
}
267
268
/**
269
 * Used by the adminLogin() function.
270
 *
271
 * What it does:
272
 *  - if 'value' is an array, the function is called recursively.
273
 *
274
 * @param string $k key
275
 * @param string|bool $v value
276
 * @return string 'hidden' HTML form fields, containing key-value-pairs
277
 * @package Authorization
278
 */
279
function adminLogin_outputPostVars($k, $v)
280
{
281
	if (!is_array($v))
0 ignored issues
show
introduced by
The condition is_array($v) is always false.
Loading history...
282
	{
283
		return '
284
<input type="hidden" name="' . htmlspecialchars($k, ENT_COMPAT, 'UTF-8') . '" value="' . strtr($v, ['"' => '&quot;', '<' => '&lt;', '>' => '&gt;']) . '" />';
0 ignored issues
show
Bug introduced by
It seems like $v can also be of type boolean; however, parameter $str of strtr() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

284
<input type="hidden" name="' . htmlspecialchars($k, ENT_COMPAT, 'UTF-8') . '" value="' . strtr(/** @scrutinizer ignore-type */ $v, ['"' => '&quot;', '<' => '&lt;', '>' => '&gt;']) . '" />';
Loading history...
285
	}
286
287
	$ret = '';
288
	foreach ($v as $k2 => $v2)
289
	{
290
		$ret .= adminLogin_outputPostVars($k . '[' . $k2 . ']', $v2);
291
	}
292
293
	return $ret;
294
}
295
296
/**
297
 * Properly urlencodes a string to be used in a query
298
 *
299
 * @param array $get associative array from $_GET
300
 * @return string query string
301
 * @package Authorization
302
 */
303
function construct_query_string($get)
304
{
305
	global $scripturl;
306
307
	$query_string = '';
308
309
	// Awww, darn.  The $scripturl contains GET stuff!
310
	$q = strpos($scripturl, '?');
311
	if ($q !== false)
312
	{
313
		parse_str(preg_replace('/&(\w+)(?=&|$)/', '&$1=', str_replace(';', '&', substr($scripturl, $q + 1))), $temp);
314
315
		foreach ($get as $k => $v)
316
		{
317
			// Only if it's not already in the $scripturl!
318
			if (!isset($temp[$k]))
319
			{
320
				$query_string .= urlencode($k) . '=' . urlencode($v) . ';';
321
			}
322
			// If it changed, put it out there, but with an ampersand.
323
			elseif ($temp[$k] != $v)
324
			{
325
				$query_string .= urlencode($k) . '=' . urlencode($v) . '&amp;';
326
			}
327
		}
328
	}
329
	else
330
	{
331
		// Add up all the data from $_GET into get_data.
332
		foreach ($get as $k => $v)
333
		{
334
			$query_string .= urlencode($k) . '=' . urlencode($v) . ';';
335
		}
336
	}
337
338
	return substr($query_string, 0, -1);
339
}
340
341
/**
342
 * Finds members by email address, username, or real name.
343
 *
344
 * What it does:
345
 *
346
 * - searches for members whose username, display name, or e-mail address match the given pattern of array names.
347
 * - searches only buddies if buddies_only is set.
348
 *
349
 * @param string[]|string $names
350
 * @param bool $use_wildcards = false, accepts wildcards ? and * in the pattern if true
351
 * @param bool $buddies_only = false,
352
 * @param int $max = 500 retrieves a maximum of max members, if passed
353
 * @return array containing information about the matching members
354
 * @package Authorization
355
 */
356
function findMembers($names, $use_wildcards = false, $buddies_only = false, $max = 500)
357
{
358
	global $scripturl;
359 2
360
	$db = database();
361 2
362
	// If it's not already an array, make it one.
363
	if (!is_array($names))
364 2
	{
365
		$names = explode(',', $names);
366
	}
367
368
	$maybe_email = false;
369 2
	foreach ($names as $i => $name)
370 2
	{
371
		// Trim, and fix wildcards for each name.
372
		$names[$i] = trim(Util::strtolower($name));
373 2
374
		$maybe_email |= str_contains($name, '@');
375 2
376
		// Make it so standard wildcards will work. (* and ?)
377
		if ($use_wildcards)
378 2
		{
379
			$names[$i] = strtr($names[$i], ['%' => '\%', '_' => '\_', '*' => '%', '?' => '_', '\'' => '&#039;']);
380
		}
381
		else
382
		{
383
			$names[$i] = strtr($names[$i], ['\'' => '&#039;']);
384 2
		}
385
386
		$names[$i] = $db->quote('{string:name}', ['name' => $names[$i]]);
387 2
	}
388
389
	// What are we using to compare?
390
	$comparison = $use_wildcards ? 'LIKE' : '=';
391 2
392
	// Nothing found yet.
393
	$results = [];
394 2
395
	// This ensures you can't search someones email address if you can't see it.
396
	$email_condition = allowedTo('moderate_forum') ? '' : '1=0 AND ';
397 2
398
	if ($use_wildcards || $maybe_email)
0 ignored issues
show
Bug Best Practice introduced by
The expression $maybe_email of type false|integer is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== false 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...
399 2
	{
400
		$email_condition = '
401
			OR (' . $email_condition . 'email_address ' . $comparison . ' ' . implode(') OR (' . $email_condition . ' email_address ' . $comparison . ' ', $names) . ')';
402
	}
403
404
	// Get the case of the columns right - but only if we need to as things like MySQL will go slow needlessly otherwise.
405
	$member_name = '{column_case_insensitive:member_name}';
406 2
	$real_name = '{column_case_insensitive:real_name}';
407
408
	// Search by username, display name, and email address.
409
	$db->fetchQuery('
410 2
		SELECT 
411 2
			id_member, member_name, real_name, email_address
412
		FROM {db_prefix}members
413
		WHERE ({raw:member_name_search}
414 2
			OR {raw:real_name_search} {raw:email_condition})
415
			' . ($buddies_only ? 'AND id_member IN ({array_int:buddy_list})' : '') . '
416
			AND is_activated IN (1, 11)
417
		LIMIT {int:limit}',
418
		[
419
			'buddy_list' => User::$info->buddies,
0 ignored issues
show
Bug Best Practice introduced by
The property buddies does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
420 2
			'member_name_search' => $member_name . ' ' . $comparison . ' ' . implode(' OR ' . $member_name . ' ' . $comparison . ' ', $names),
421
			'real_name_search' => $real_name . ' ' . $comparison . ' ' . implode(' OR ' . $real_name . ' ' . $comparison . ' ', $names),
422
			'email_condition' => $email_condition,
423
			'limit' => $max,
424 2
			'recursive' => true,
425 2
		]
426 2
	)->fetch_callback(
427 2
		function ($row) use (&$results, $scripturl) {
428 2
			$results[$row['id_member']] = [
429
				'id' => (int) $row['id_member'],
430
				'name' => $row['real_name'],
431 2
				'username' => $row['member_name'],
432
				'email' => showEmailAddress($row['id_member']) ? $row['email_address'] : '',
433 2
				'href' => $scripturl . '?action=profile;u=' . $row['id_member'],
434 2
				'link' => '<a href="' . $scripturl . '?action=profile;u=' . $row['id_member'] . '">' . $row['real_name'] . '</a>'
435 2
			];
436 2
		}
437 2
	);
438 2
439 2
	// Return all the results.
440
	return $results;
441 2
}
442
443
/**
444
 * Generates a random password for a user and emails it to them.
445 2
 *
446
 * What it does:
447
 *
448
 * - called by ProfileOptions controller when changing someone's username.
449
 * - checks the validity of the new username.
450
 * - generates and sets a new password for the given user.
451
 * - mails the new password to the email address of the user.
452
 * - if username is not set, only a new password is generated and sent.
453
 *
454
 * @param int $memID
455
 * @param string|null $username = null
456
 *
457
 * @throws \ElkArte\Exceptions\Exception
458
 * @package Authorization
459
 *
460
 */
461
function resetPassword($memID, $username = null)
462
{
463
	global $modSettings, $language;
464
465
	// Language... and a required file.
466
	Txt::load('Login');
467
	require_once(SUBSDIR . '/Mail.subs.php');
468
469
	// Get some important details.
470
	require_once(SUBSDIR . '/Members.subs.php');
471
	$result = getBasicMemberData($memID, ['preferences' => true]);
472
	$user = $result['member_name'];
473
	$email = $result['email_address'];
474
	$lngfile = $result['lngfile'];
475
	$old_user = '';
476
477
	if ($username !== null)
478
	{
479
		$old_user = $user;
480
		$user = trim($username);
481
	}
482
483
	// Generate a random password.
484
	$tokenizer = new TokenHash();
485
	$newPassword = $tokenizer->generate_hash(14);
486
487
	// Create a db hash for the generated password
488
	$newPassword_sha256 = hash('sha256', strtolower($user) . $newPassword);
489
	$db_hash = password_hash($newPassword_sha256, PASSWORD_BCRYPT, ['cost' => 8]);
490
491
	// Do some checks on the username if needed.
492
	require_once(SUBSDIR . '/Members.subs.php');
493
	if ($username !== null)
494
	{
495
		$errors = ErrorContext::context('reset_pwd', 0);
496
		validateUsername($memID, $user, 'reset_pwd');
497
498
		// If there are "important" errors and you are not an admin: log the first error
499
		// Otherwise grab all of them and don't log anything
500
		$error_severity = $errors->hasErrors(1) && User::$info->is_admin === false ? 1 : null;
0 ignored issues
show
Bug Best Practice introduced by
The property is_admin does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
501
		foreach ($errors->prepareErrors($error_severity) as $error)
502
		{
503
			throw new \ElkArte\Exceptions\Exception($error, $error_severity === null ? false : 'general');
504
		}
505
506
		// Update the database...
507
		updateMemberData($memID, ['member_name' => $user, 'passwd' => $db_hash]);
508
	}
509
	else
510
	{
511
		updateMemberData($memID, ['passwd' => $db_hash]);
512
	}
513
514
	call_integration_hook('integrate_reset_pass', [$old_user, $user, $newPassword]);
515
516
	$replacements = [
517
		'USERNAME' => $user,
518
		'PASSWORD' => $newPassword,
519
	];
520
521
	$emaildata = loadEmailTemplate('change_password', $replacements, empty($lngfile) || empty($modSettings['userLanguage']) ? $language : $lngfile);
522
523
	// Send them the email informing them of the change - then we're done!
524
	sendmail($email, $emaildata['subject'], $emaildata['body'], null, null, false, 0);
525
}
526
527
/**
528
 * Checks a username obeys a load of rules
529
 *
530
 * - Returns null if fine
531
 *
532
 * @param int $memID
533
 * @param string $username
534
 * @param string $ErrorContext
535
 * @param bool $check_reserved_name
536
 * @param bool $fatal pass through to isReservedName
537
 * @return string
538
 * @package Authorization
539
 */
540
function validateUsername($memID, $username, $ErrorContext = 'register', $check_reserved_name = true, $fatal = true)
541
{
542
	global $txt;
543
544
	$errors = ErrorContext::context($ErrorContext, 0);
545
546
	// Don't use too long a name.
547
	if (Util::strlen($username) > 25)
548 6
	{
549
		$errors->addError('error_long_name');
550 6
	}
551
552
	// No name?!  How can you register with no name?
553 6
	if ($username === '')
554
	{
555
		$errors->addError('need_username');
556
	}
557
558
	// Only these characters are permitted.
559 6
	if (in_array($username, ['_', '|']) || preg_match('~[<>&"\'=\\\\]~', preg_replace('~&#(?:\\d{1,7}|x[0-9a-fA-F]{1,6});~', '', $username)) != 0 || str_contains($username, '[code') || str_contains($username, '[/code'))
560
	{
561
		$errors->addError('error_invalid_characters_username');
562
	}
563
564
	if (stripos($username, $txt['guest_title']) !== false)
565 6
	{
566
		$errors->addError(['username_reserved', [$txt['guest_title']]], 1);
567
	}
568
569
	if ($check_reserved_name)
570 6
	{
571
		require_once(SUBSDIR . '/Members.subs.php');
572
		if (isReservedName($username, $memID, false, $fatal))
573
		{
574
			$errors->addError(['name_in_use', [htmlspecialchars($username, ENT_COMPAT, 'UTF-8')]]);
575 6
		}
576
	}
577 2
}
578 2
579
/**
580
 * Checks whether a password meets the current forum rules
581
 *
582
 * What it does:
583 6
 *
584
 * - called when registering/choosing a password.
585
 * - checks the password obeys the current forum settings for password strength.
586
 * - if password checking is enabled, will check that none of the words in restrict_in appear in the password.
587
 * - returns an error identifier if the password is invalid, or null.
588
 *
589
 * @param string $password
590
 * @param string $username
591
 * @param string[] $restrict_in = array()
592
 * @return string an error identifier if the password is invalid
593
 * @package Authorization
594
 */
595
function validatePassword($password, $username, $restrict_in = [])
596
{
597
	global $modSettings, $txt;
598
599
	// Perform basic requirements first.
600
	if (Util::strlen($password) < (empty($modSettings['password_strength']) ? 4 : 8))
601
	{
602
		Txt::load('Errors');
603 2
		$txt['profile_error_password_short'] = sprintf($txt['profile_error_password_short'], empty($modSettings['password_strength']) ? 4 : 8);
604
605
		return 'short';
606 2
	}
607
608
	if (Util::strlen($password) > 64)
609
	{
610
		Txt::load('Errors');
611
		$txt['profile_error_password_long'] = sprintf($txt['profile_error_password_long'], 64);
612
		return 'short';
613
	}
614
615 2
	// Is this enough?
616
	if (empty($modSettings['password_strength']))
617 2
	{
618
		return null;
619
	}
620
621
	// Otherwise, perform the medium strength test - checking if password appears in the restricted string.
622
	if (preg_match('~\b' . preg_quote($password, '~') . '\b~', implode(' ', $restrict_in)) != 0)
623
	{
624
		return 'restricted_words';
625
	}
626
627
	if (Util::strpos($password, $username) !== false)
628
	{
629
		return 'restricted_words';
630
	}
631
632
	// If just medium, we're done.
633
	if ($modSettings['password_strength'] == 1)
634
	{
635
		return null;
636
	}
637
638
	// Otherwise, hard test next, check for numbers and letters, uppercase too.
639
	$good = preg_match('~(\D\d|\d\D)~', $password) === 1;
640
	$good = $good && Util::strtolower($password) !== $password;
641
642
	return $good ? null : 'chars';
643
}
644
645
/**
646
 * Checks whether an entered password is correct for the user
647
 *
648
 * What it does:
649
 *
650
 * - called when logging in or whenever a password needs to be validated for a user
651
 * - used to generate a new hash for the db, used during registration or any password changes
652
 * - if a non SHA256 password is sent, will generate one with SHA256(user + password) and return it in password
653
 *
654
 * @param string $password user password if not already 64 characters long will be SHA256 with the user name
655
 * @param string $hash hash as generated from a SHA256 password
656
 * @param string $user user name only required if creating a SHA-256 password
657
 * @param bool $returnhash flag to determine if we are returning a hash suitable for the database
658
 *
659
 * @return bool|string
660
 * @package Authorization
661
 *
662
 */
663
function validateLoginPassword(&$password, $hash, $user = '', $returnhash = false)
664 10
{
665
	// If the password is not 64 characters, lets make it a (SHA-256)
666 10
	if (strlen($password) !== 64)
667
	{
668
		$password = hash('sha256', Util::strtolower($user) . un_htmlspecialchars($password));
669
	}
670 10
671
	// They need a password hash, something to save in the db?
672 6
	if ($returnhash)
673
	{
674
		return password_hash($password, PASSWORD_BCRYPT, ['cost' => 10]);
675
	}
676 4
677
	// Doing a password check?
678
	return password_verify($password, $hash);
679
}
680
681
/**
682
 * Quickly find out what moderation authority this user has
683
 *
684
 * What it does:
685
 *
686
 * - builds the moderator, group and board level querys for the user
687
 * - stores the information on the current users moderation powers in User::$info->mod_cache and $_SESSION['mc']
688
 *
689
 * @package Authorization
690
 */
691 1
function rebuildModCache()
692
{
693
	$db = database();
694 1
695
	// What groups can they moderate?
696 1
	$group_query = allowedTo('manage_membergroups') ? '1=1' : '0=1';
697
698 1
	if ($group_query === '0=1')
699
	{
700
		$groups = $db->fetchQuery('
701
			SELECT 
702
				id_group
703
			FROM {db_prefix}group_moderators
704 1
			WHERE id_member = {int:current_member}',
705
			[
706 1
				'current_member' => User::$info->id,
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
707
			]
708
		)->fetch_callback(
709 1
			function ($row) {
710
				return $row['id_group'];
711
			}
712 1
		);
713
714
		$group_query = empty($groups) ? '0=1' : 'id_group IN (' . implode(',', $groups) . ')';
715
	}
716 1
717
	// Then, same again, just the boards this time!
718 1
	$board_query = allowedTo('moderate_forum') ? '1=1' : '0=1';
719
720 1
	if ($board_query === '0=1')
721
	{
722 1
		$boards = boardsAllowedTo('moderate_board');
723
724
		$board_query = empty($boards) ? '0=1' : 'id_board IN (' . implode(',', $boards) . ')';
725
	}
726 1
727 1
	// What boards are they the moderator of?
728
	$boards_mod = [];
729
	if (User::$info->is_guest === false)
0 ignored issues
show
Bug Best Practice introduced by
The property is_guest does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
730
	{
731
		require_once(SUBSDIR . '/Boards.subs.php');
732
		$boards_mod = boardsModerated(User::$info->id);
733 1
	}
734
735 1
	$mod_query = empty($boards_mod) ? '0=1' : 'b.id_board IN (' . implode(',', $boards_mod) . ')';
736 1
737
	$_SESSION['mc'] = [
738 1
		'time' => time(),
739
		// This looks a bit funny but protects against the login redirect.
740 1
		'id' => User::$info->id && User::$info->name ? User::$info->id : 0,
0 ignored issues
show
Bug Best Practice introduced by
The property name does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
741 1
		// If you change the format of 'gq' and/or 'bq' make sure to adjust 'can_mod' in Load.php.
742 1
		'gq' => $group_query,
743 1
		'bq' => $board_query,
744 1
		'ap' => boardsAllowedTo('approve_posts'),
745
		'mb' => $boards_mod,
746 1
		'mq' => $mod_query,
747
	];
748 1
	call_integration_hook('integrate_mod_cache');
749
750
	User::$info->mod_cache = $_SESSION['mc'];
0 ignored issues
show
Bug Best Practice introduced by
The property mod_cache does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __set, consider adding a @property annotation.
Loading history...
751 1
752 1
	// Might as well clean up some tokens while we are at it.
753
	cleanTokens();
754
}
755
756
/**
757
 * The same thing as setcookie but allows for integration hook
758
 *
759
 * @param string $name
760
 * @param string $value = ''
761
 * @param int $expire = 0
762
 * @param string $path = ''
763
 * @param string $domain = ''
764
 * @param bool|null $secure = false
765
 * @param bool|null $httponly = null
766
 * @param string|null $samesite = null
767
 *
768
 * @return bool
769
 * @package Authorization
770
 *
771
 */
772
function elk_setcookie($name, $value = '', $expire = 0, $path = '', $domain = '', $secure = null, $httponly = null, $samesite = null)
773
{
774
	global $modSettings;
775
776
	// In case a customization wants to override the default settings
777
	if ($httponly === null)
778
	{
779
		$httponly = !empty($modSettings['httponlyCookies']);
780
	}
781
782
	if ($secure === null)
783
	{
784
		$secure = !empty($modSettings['secureCookies']);
785
	}
786
787
	// Default value in modern browsers is Lax
788
	// @todo admin panel setting?
789
	$samesite = empty($samesite) ? 'Lax' : $samesite;
790
791
	// Using SameSite=None requires Secure attribute in latest browser versions.
792
	$samesite = (!$secure && $samesite === 'None') ? 'Lax' : $samesite;
793
794
	// Intercept cookie?
795
	call_integration_hook('integrate_cookie', [$name, $value, $expire, $path, $domain, $secure, $httponly, $samesite]);
796
797
	if (PHP_VERSION_ID < 70300)
798
	{
799
		return setcookie($name, $value, $expire, $path, $domain, $secure, $httponly);
800
	}
801
802
	return setcookie($name, $value, [
803
		'expires' => $expire,
804
		'path' => $path,
805
		'domain' => $domain,
806
		'secure' => $secure,
807
		'httponly' => $httponly,
808
		'samesite' => $samesite]);
809
}
810
811
/**
812
 * This functions determines whether this is the first login of the given user.
813
 *
814
 * @param int $id_member the id of the member to check for
815
 * @return bool
816
 * @deprecated replaced by \ElkArte\User::$info->isFirstLogin()
817
 *
818
 * @package Authorization
819
 *
820
 */
821
function isFirstLogin($id_member)
822
{
823
	// First login?
824
	require_once(SUBSDIR . '/Members.subs.php');
825
	$member = getBasicMemberData($id_member, ['moderation' => true]);
826
827
	return !empty($member) && $member['last_login'] == 0;
828
}
829
830
/**
831
 * Search for a member by given criteria
832
 *
833
 * @param string $where
834
 * @param array $where_params array of values to used in the where statement
835
 * @param bool $fatal
836
 *
837
 * @return array|bool array of members data or false on failure
838
 * @throws \ElkArte\Exceptions\Exception no_user_with_email
839
 * @package Authorization
840
 *
841
 */
842
function findUser($where, $where_params, $fatal = true)
843
{
844
	$db = database();
845
846
	// Find the user!
847
	$request = $db->fetchQuery('
848
		SELECT 
849
			id_member, real_name, member_name, email_address, is_activated, validation_code, 
850
			lngfile, secret_question, passwd
851
		FROM {db_prefix}members
852
		WHERE ' . $where . '
853
		LIMIT 1',
854
		array_merge($where_params, [])
855
	);
856
857
	// Maybe email?
858
	if ($request->num_rows() === 0 && empty($_REQUEST['uid']) && isset($where_params['email_address']))
859
	{
860
		$request->free_result();
861
862
		$request = $db->fetchQuery('
863
			SELECT 
864
				id_member, real_name, member_name, email_address, is_activated, validation_code, 
865
				lngfile, secret_question
866
			FROM {db_prefix}members
867
			WHERE email_address = {string:email_address}
868
			LIMIT 1',
869
			array_merge($where_params, [])
870
		);
871
		if ($request->num_rows() === 0)
872
		{
873
			if ($fatal)
874
			{
875
				throw new \ElkArte\Exceptions\Exception('no_user_with_email', false);
876 10
			}
877
878 10
			return false;
879
		}
880
	}
881
882 10
	$member = $request->fetch_assoc();
883 10
	$member['id_member'] = (int) $member['id_member'];
884
	$member['is_activated'] = (int) $member['is_activated'];
885
886 10
	$request->free_result();
887 10
888
	return $member;
889
}
890
891 10
/**
892 10
 * Find users by their email address.
893
 *
894 10
 * @param string $email
895
 * @param string|null $username
896
 * @return false|int on failure, int of member on success
897
 * @package Authorization
898
 */
899
function userByEmail($email, $username = null)
900
{
901
	$db = database();
902
903
	$return = false;
904
	$db->fetchQuery('
905
		SELECT 
906
			id_member
907
		FROM {db_prefix}members
908 1
		WHERE email_address = {string:email_address}' . ($username === null ? '' : '
909
			OR email_address = {string:username}') . '
910 1
		LIMIT 1',
911
		[
912
			'email_address' => $email,
913
			'username' => $username,
914
		]
915
	)->fetch_callback(
916
		function ($row) use (&$return) {
917
			$return = (int) $row['id_member'];
918
		}
919
	);
920
921
	return $return;
922
}
923
924 6
/**
925
 * Generate a random validation code.
926 6
 *
927
 * @param int $length the number of characters to return
928 2
 *
929
 * @return string
930
 * @package Authorization
931
 *
932
 */
933
function generateValidationCode($length = 10)
934
{
935
	$tokenizer = new TokenHash();
936 2
937
	return $tokenizer->generate_hash((int) $length);
938
}
939
940
/**
941
 * This function loads many settings of a user given by name or email.
942
 *
943 4
 * @param string $name
944
 * @param bool $is_id if true it treats $name as a member ID and try to load the data for that ID
945
 * @return array|false false if nothing is found
946
 * @package Authorization
947
 */
948
function loadExistingMember($name, $is_id = false)
949
{
950
	$db = database();
951 4
952
	if ($is_id)
953
	{
954
		$request = $db->fetchQuery('
955 4
			SELECT 
956
				passwd, id_member, id_group, lngfile, is_activated, email_address, additional_groups, member_name, password_salt,
957
				passwd_flood, otp_secret, enable_otp, otp_used
958
			FROM {db_prefix}members
959
			WHERE id_member = {int:id_member}
960
			LIMIT 1',
961
			[
962
				'id_member' => (int) $name,
963
			]
964
		);
965
	}
966
	else
967
	{
968
		// Try to find the user, assuming a member_name was passed...
969
		$request = $db->fetchQuery('
970
			SELECT 
971
				passwd, id_member, id_group, lngfile, is_activated, email_address, additional_groups, member_name, password_salt,
972
				passwd_flood, otp_secret, enable_otp, otp_used
973
			FROM {db_prefix}members
974 6
			WHERE {column_case_insensitive:member_name} = {string_case_insensitive:user_name}
975
			LIMIT 1',
976 2
			[
977
				'user_name' => $name,
978
			]
979
		);
980 4
		// Didn't work. Try it as an email address.
981 4
		if ($request->num_rows() === 0 && str_contains($name, '@'))
982
		{
983
			$request->free_result();
984 6
985
			$request = $db->fetchQuery('
986 6
				SELECT 
987
					passwd, id_member, id_group, lngfile, is_activated, email_address, additional_groups, member_name, password_salt,
988
					passwd_flood, otp_secret, enable_otp, otp_used
989
				FROM {db_prefix}members
990
				WHERE email_address = {string:user_name}
991
				LIMIT 1',
992
				[
993
					'user_name' => $name,
994
				]
995
			);
996
		}
997
	}
998
999
	// Nothing? Ah the horror...
1000
	if ($request->num_rows() === 0)
1001
	{
1002
		$user_auth_data = false;
1003
	}
1004
	else
1005
	{
1006
		$user_auth_data = $request->fetch_assoc();
1007
		$user_auth_data['id_member'] = (int) $user_auth_data['id_member'];
1008
	}
1009
1010
	$request->free_result();
1011
1012
	return $user_auth_data;
1013
}
1014