elk_setcookie()   B
last analyzed

Complexity

Conditions 7
Paths 64

Size

Total Lines 37
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 56

Importance

Changes 0
Metric Value
cc 7
eloc 17
c 0
b 0
f 0
nc 64
nop 8
dl 0
loc 37
ccs 0
cts 9
cp 0
crap 56
rs 8.8333

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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