smf_setcookie()   A
last analyzed

Complexity

Conditions 6
Paths 24

Size

Total Lines 25
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 18
nc 24
nop 8
dl 0
loc 25
rs 9.0444
c 0
b 0
f 0

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
 * Simple Machines Forum (SMF)
7
 *
8
 * @package SMF
9
 * @author Simple Machines https://www.simplemachines.org
10
 * @copyright 2022 Simple Machines and individual contributors
11
 * @license https://www.simplemachines.org/about/smf/license.php BSD
12
 *
13
 * @version 2.1.2
14
 */
15
16
if (!defined('SMF'))
17
	die('No direct access...');
18
19
/**
20
 * Sets the SMF-style login cookie and session based on the id_member and password passed.
21
 * - password should be already encrypted with the cookie salt.
22
 * - logs the user out if id_member is zero.
23
 * - sets the cookie and session to last the number of seconds specified by cookie_length, or
24
 *   ends them if cookie_length is less than 0.
25
 * - when logging out, if the globalCookies setting is enabled, attempts to clear the subdomain's
26
 *   cookie too.
27
 *
28
 * @param int $cookie_length How many seconds the cookie should last. If negative, forces logout.
29
 * @param int $id The ID of the member to set the cookie for
30
 * @param string $password The hashed password
31
 */
32
function setLoginCookie($cookie_length, $id, $password = '')
33
{
34
	global $smcFunc, $cookiename, $boardurl, $modSettings, $sourcedir;
35
36
	$id = (int) $id;
37
38
	$expiry_time = ($cookie_length >= 0 ? time() + $cookie_length : 1);
39
40
	// If changing state force them to re-address some permission caching.
41
	$_SESSION['mc']['time'] = 0;
42
43
	// Extract our cookie domain and path from $boardurl
44
	$cookie_url = url_parts(!empty($modSettings['localCookies']), !empty($modSettings['globalCookies']));
45
46
	// The cookie may already exist, and have been set with different options.
47
	if (isset($_COOKIE[$cookiename]))
48
	{
49
		// First check for 2.1 json-format cookie
50
		if (preg_match('~^{"0":\d+,"1":"[0-9a-f]*","2":\d+,"3":"[^"]+","4":"[^"]+"~', $_COOKIE[$cookiename]) === 1)
51
			list(,,, $old_domain, $old_path) = $smcFunc['json_decode']($_COOKIE[$cookiename], true);
52
53
		// Legacy format (for recent 2.0 --> 2.1 upgrades)
54
		elseif (preg_match('~^a:[34]:\{i:0;i:\d+;i:1;s:(0|40):"([a-fA-F0-9]{40})?";i:2;[id]:\d+;(i:3;i:\d;)?~', $_COOKIE[$cookiename]) === 1)
55
		{
56
			list(,,, $old_state) = safe_unserialize($_COOKIE[$cookiename]);
57
58
			$cookie_state = (empty($modSettings['localCookies']) ? 0 : 1) | (empty($modSettings['globalCookies']) ? 0 : 2);
59
60
			// Maybe we need to temporarily pretend to be using local cookies
61
			if ($cookie_state == 0 && $old_state == 1)
62
				list($old_domain, $old_path) = url_parts(true, false);
63
			else
64
				list($old_domain, $old_path) = url_parts($old_state & 1 > 0, $old_state & 2 > 0);
0 ignored issues
show
Bug introduced by
$old_state & 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

64
				list($old_domain, $old_path) = url_parts(/** @scrutinizer ignore-type */ $old_state & 1 > 0, $old_state & 2 > 0);
Loading history...
Bug introduced by
$old_state & 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

64
				list($old_domain, $old_path) = url_parts($old_state & 1 > 0, /** @scrutinizer ignore-type */ $old_state & 2 > 0);
Loading history...
65
		}
66
67
		// Out with the old, in with the new!
68
		if (isset($old_domain) && $old_domain != $cookie_url[0] || isset($old_path) && $old_path != $cookie_url[1])
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: (IssetNode && $old_domai..._path != $cookie_url[1], Probably Intended Meaning: IssetNode && ($old_domai...path != $cookie_url[1])
Loading history...
69
			smf_setcookie($cookiename, $smcFunc['json_encode'](array(0, '', 0, $old_domain, $old_path), JSON_FORCE_OBJECT), 1, $old_path, $old_domain);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $old_path does not seem to be defined for all execution paths leading up to this point.
Loading history...
Comprehensibility Best Practice introduced by
The variable $old_domain does not seem to be defined for all execution paths leading up to this point.
Loading history...
70
	}
71
72
	// Get the data and path to set it on.
73
	$data = empty($id) ? array(0, '', 0, $cookie_url[0], $cookie_url[1]) : array($id, $password, $expiry_time, $cookie_url[0], $cookie_url[1]);
74
75
	// Allow mods to add custom info to the cookie
76
	$custom_data = array();
77
	call_integration_hook('integrate_cookie_data', array($data, &$custom_data));
78
79
	$data = $smcFunc['json_encode'](array_merge($data, $custom_data), JSON_FORCE_OBJECT);
80
81
	// Set the cookie, $_COOKIE, and session variable.
82
	smf_setcookie($cookiename, $data, $expiry_time, $cookie_url[1], $cookie_url[0]);
83
84
	// If subdomain-independent cookies are on, unset the subdomain-dependent cookie too.
85
	if (empty($id) && !empty($modSettings['globalCookies']))
86
		smf_setcookie($cookiename, $data, $expiry_time, $cookie_url[1], '');
87
88
	// Any alias URLs?  This is mainly for use with frames, etc.
89
	if (!empty($modSettings['forum_alias_urls']))
90
	{
91
		$aliases = explode(',', $modSettings['forum_alias_urls']);
92
93
		$temp = $boardurl;
94
		foreach ($aliases as $alias)
95
		{
96
			// Fake the $boardurl so we can set a different cookie.
97
			$alias = strtr(trim($alias), array('http://' => '', 'https://' => ''));
98
			$boardurl = 'http://' . $alias;
99
100
			$cookie_url = url_parts(!empty($modSettings['localCookies']), !empty($modSettings['globalCookies']));
101
102
			if ($cookie_url[0] == '')
103
				$cookie_url[0] = strtok($alias, '/');
104
105
			$alias_data = $smcFunc['json_decode']($data, true);
106
			$alias_data[3] = $cookie_url[0];
107
			$alias_data[4] = $cookie_url[1];
108
			$alias_data = $smcFunc['json_encode']($alias_data, JSON_FORCE_OBJECT);
109
110
			smf_setcookie($cookiename, $alias_data, $expiry_time, $cookie_url[1], $cookie_url[0]);
111
		}
112
113
		$boardurl = $temp;
114
	}
115
116
	$_COOKIE[$cookiename] = $data;
117
118
	// Make sure the user logs in with a new session ID.
119
	if (!isset($_SESSION['login_' . $cookiename]) || $_SESSION['login_' . $cookiename] !== $data)
120
	{
121
		// We need to meddle with the session.
122
		require_once($sourcedir . '/Session.php');
123
124
		// Backup and remove the old session.
125
		$oldSessionData = $_SESSION;
126
		$_SESSION = array();
127
		session_destroy();
128
129
		// Recreate and restore the new session.
130
		loadSession();
131
		// @todo should we use session_regenerate_id(true); now that we are 5.1+
132
		session_regenerate_id();
133
		$_SESSION = $oldSessionData;
134
135
		$_SESSION['login_' . $cookiename] = $data;
136
	}
137
}
138
139
/**
140
 * Sets Two Factor Auth cookie
141
 *
142
 * @param int $cookie_length How long the cookie should last, in seconds
143
 * @param int $id The ID of the member
144
 * @param string $secret Should be a salted secret using hash_salt
145
 */
146
function setTFACookie($cookie_length, $id, $secret)
147
{
148
	global $smcFunc, $modSettings, $cookiename;
149
150
	$expiry_time = ($cookie_length >= 0 ? time() + $cookie_length : 1);
151
152
	$identifier = $cookiename . '_tfa';
153
	$cookie_url = url_parts(!empty($modSettings['localCookies']), !empty($modSettings['globalCookies']));
154
155
	// Get the data and path to set it on.
156
	$data = $smcFunc['json_encode'](empty($id) ? array(0, '', 0, $cookie_url[0], $cookie_url[1], false) : array($id, $secret, $expiry_time, $cookie_url[0], $cookie_url[1]), JSON_FORCE_OBJECT);
157
158
	// Set the cookie, $_COOKIE, and session variable.
159
	smf_setcookie($identifier, $data, $expiry_time, $cookie_url[1], $cookie_url[0]);
160
161
	// If subdomain-independent cookies are on, unset the subdomain-dependent cookie too.
162
	if (empty($id) && !empty($modSettings['globalCookies']))
163
		smf_setcookie($identifier, $data, $expiry_time, $cookie_url[1], '');
164
165
	$_COOKIE[$identifier] = $data;
166
}
167
168
/**
169
 * Get the domain and path for the cookie
170
 * - normally, local and global should be the localCookies and globalCookies settings, respectively.
171
 * - uses boardurl to determine these two things.
172
 *
173
 * @param bool $local Whether we want local cookies
174
 * @param bool $global Whether we want global cookies
175
 * @return array An array to set the cookie on with domain and path in it, in that order
176
 */
177
function url_parts($local, $global)
178
{
179
	global $boardurl, $modSettings;
180
181
	// Parse the URL with PHP to make life easier.
182
	$parsed_url = parse_iri($boardurl);
183
184
	// Is local cookies off?
185
	if (empty($parsed_url['path']) || !$local)
186
		$parsed_url['path'] = '';
187
188
	if (!empty($modSettings['globalCookiesDomain']) && strpos($boardurl, $modSettings['globalCookiesDomain']) !== false)
189
		$parsed_url['host'] = $modSettings['globalCookiesDomain'];
190
191
	// Globalize cookies across domains (filter out IP-addresses)?
192
	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)
193
		$parsed_url['host'] = '.' . $parts[1];
194
195
	// We shouldn't use a host at all if both options are off.
196
	elseif (!$local && !$global)
197
		$parsed_url['host'] = '';
198
199
	// The host also shouldn't be set if there aren't any dots in it.
200
	elseif (!isset($parsed_url['host']) || strpos($parsed_url['host'], '.') === false)
201
		$parsed_url['host'] = '';
202
203
	return array($parsed_url['host'], $parsed_url['path'] . '/');
204
}
205
206
/**
207
 * Throws guests out to the login screen when guest access is off.
208
 * - sets $_SESSION['login_url'] to $_SERVER['REQUEST_URL'].
209
 * - uses the 'kick_guest' sub template found in Login.template.php.
210
 */
211
function KickGuest()
212
{
213
	global $txt, $context;
214
215
	loadLanguage('Login');
216
	loadTemplate('Login');
217
	createToken('login');
218
219
	// Never redirect to an attachment
220
	if (strpos($_SERVER['REQUEST_URL'], 'dlattach') === false)
221
		$_SESSION['login_url'] = $_SERVER['REQUEST_URL'];
222
223
	$context['sub_template'] = 'kick_guest';
224
	$context['page_title'] = $txt['login'];
225
}
226
227
/**
228
 * Display a message about the forum being in maintenance mode.
229
 * - display a login screen with sub template 'maintenance'.
230
 * - sends a 503 header, so search engines don't bother indexing while we're in maintenance mode.
231
 */
232
function InMaintenance()
233
{
234
	global $txt, $mtitle, $mmessage, $context, $smcFunc;
235
236
	loadLanguage('Login');
237
	loadTemplate('Login');
238
	createToken('login');
239
240
	// Send a 503 header, so search engines don't bother indexing while we're in maintenance mode.
241
	send_http_status(503, 'Service Temporarily Unavailable');
242
243
	// Basic template stuff..
244
	$context['sub_template'] = 'maintenance';
245
	$context['title'] = $smcFunc['htmlspecialchars']($mtitle);
246
	$context['description'] = &$mmessage;
247
	$context['page_title'] = $txt['maintain_mode'];
248
}
249
250
/**
251
 * Question the verity of the admin by asking for his or her password.
252
 * - loads Login.template.php and uses the admin_login sub template.
253
 * - sends data to template so the admin is sent on to the page they
254
 *   wanted if their password is correct, otherwise they can try again.
255
 *
256
 * @param string $type What login type is this - can be 'admin' or 'moderate'
257
 */
258
function adminLogin($type = 'admin')
259
{
260
	global $context, $txt, $user_info;
261
262
	loadLanguage('Admin');
263
	loadTemplate('Login');
264
265
	// Validate what type of session check this is.
266
	$types = array();
267
	call_integration_hook('integrate_validateSession', array(&$types));
268
	$type = in_array($type, $types) || $type == 'moderate' ? $type : 'admin';
269
270
	// They used a wrong password, log it and unset that.
271
	if (isset($_POST[$type . '_hash_pass']) || isset($_POST[$type . '_pass']))
272
	{
273
		$txt['security_wrong'] = sprintf($txt['security_wrong'], isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : $txt['unknown'], $_SERVER['HTTP_USER_AGENT'], $user_info['ip']);
274
		log_error($txt['security_wrong'], 'critical');
275
276
		if (isset($_POST[$type . '_hash_pass']))
277
			unset($_POST[$type . '_hash_pass']);
278
		if (isset($_POST[$type . '_pass']))
279
			unset($_POST[$type . '_pass']);
280
281
		$context['incorrect_password'] = true;
282
	}
283
284
	createToken('admin-login');
285
286
	// Figure out the get data and post data.
287
	$context['get_data'] = '?' . construct_query_string($_GET);
288
	$context['post_data'] = '';
289
290
	// Now go through $_POST.  Make sure the session hash is sent.
291
	$_POST[$context['session_var']] = $context['session_id'];
292
	foreach ($_POST as $k => $v)
293
		$context['post_data'] .= adminLogin_outputPostVars($k, $v);
294
295
	// Now we'll use the admin_login sub template of the Login template.
296
	$context['sub_template'] = 'admin_login';
297
298
	// And title the page something like "Login".
299
	if (!isset($context['page_title']))
300
		$context['page_title'] = $txt['login'];
301
302
	// The type of action.
303
	$context['sessionCheckType'] = $type;
304
305
	obExit();
306
307
	// We MUST exit at this point, because otherwise we CANNOT KNOW that the user is privileged.
308
	trigger_error('No direct access...', E_USER_ERROR);
309
}
310
311
/**
312
 * Used by the adminLogin() function.
313
 * if 'value' is an array, the function is called recursively.
314
 *
315
 * @param string $k The keys
316
 * @param string $v The values
317
 * @return string 'hidden' HTML form fields, containing key-value-pairs
318
 */
319
function adminLogin_outputPostVars($k, $v)
320
{
321
	global $smcFunc;
322
323
	if (!is_array($v))
0 ignored issues
show
introduced by
The condition is_array($v) is always false.
Loading history...
324
		return '
325
<input type="hidden" name="' . $smcFunc['htmlspecialchars']($k) . '" value="' . strtr($v, array('"' => '&quot;', '<' => '&lt;', '>' => '&gt;')) . '">';
326
	else
327
	{
328
		$ret = '';
329
		foreach ($v as $k2 => $v2)
330
			$ret .= adminLogin_outputPostVars($k . '[' . $k2 . ']', $v2);
331
332
		return $ret;
333
	}
334
}
335
336
/**
337
 * Properly urlencodes a string to be used in a query
338
 *
339
 * @param string $get
340
 * @return string Our query string
341
 */
342
function construct_query_string($get)
343
{
344
	global $scripturl;
345
346
	$query_string = '';
347
348
	// Awww, darn.  The $scripturl contains GET stuff!
349
	$q = strpos($scripturl, '?');
350
	if ($q !== false)
351
	{
352
		parse_str(preg_replace('/&(\w+)(?=&|$)/', '&$1=', strtr(substr($scripturl, $q + 1), ';', '&')), $temp);
353
354
		foreach ($get as $k => $v)
0 ignored issues
show
Bug introduced by
The expression $get of type string is not traversable.
Loading history...
355
		{
356
			// Only if it's not already in the $scripturl!
357
			if (!isset($temp[$k]))
358
				$query_string .= urlencode($k) . '=' . urlencode($v) . ';';
359
			// If it changed, put it out there, but with an ampersand.
360
			elseif ($temp[$k] != $get[$k])
361
				$query_string .= urlencode($k) . '=' . urlencode($v) . '&amp;';
362
		}
363
	}
364
	else
365
	{
366
		// Add up all the data from $_GET into get_data.
367
		foreach ($get as $k => $v)
0 ignored issues
show
Bug introduced by
The expression $get of type string is not traversable.
Loading history...
368
			$query_string .= urlencode($k) . '=' . urlencode($v) . ';';
369
	}
370
371
	$query_string = substr($query_string, 0, -1);
372
	return $query_string;
373
}
374
375
/**
376
 * Finds members by email address, username, or real name.
377
 * - searches for members whose username, display name, or e-mail address match the given pattern of array names.
378
 * - searches only buddies if buddies_only is set.
379
 *
380
 * @param array $names The names of members to search for
381
 * @param bool $use_wildcards Whether to use wildcards. Accepts wildcards ? and * in the pattern if true
382
 * @param bool $buddies_only Whether to only search for the user's buddies
383
 * @param int $max The maximum number of results
384
 * @return array An array containing information about the matching members
385
 */
386
function findMembers($names, $use_wildcards = false, $buddies_only = false, $max = 500)
387
{
388
	global $scripturl, $user_info, $smcFunc;
389
390
	// If it's not already an array, make it one.
391
	if (!is_array($names))
0 ignored issues
show
introduced by
The condition is_array($names) is always true.
Loading history...
392
		$names = explode(',', $names);
393
394
	$maybe_email = false;
395
	$names_list = array();
396
	foreach ($names as $i => $name)
397
	{
398
		// Trim, and fix wildcards for each name.
399
		$names[$i] = trim($smcFunc['strtolower']($name));
400
401
		$maybe_email |= strpos($name, '@') !== false;
402
403
		// Make it so standard wildcards will work. (* and ?)
404
		if ($use_wildcards)
405
			$names[$i] = strtr($names[$i], array('%' => '\%', '_' => '\_', '*' => '%', '?' => '_', '\'' => '&#039;'));
406
		else
407
			$names[$i] = strtr($names[$i], array('\'' => '&#039;'));
408
409
		$names_list[] = '{string:lookup_name_' . $i . '}';
410
		$where_params['lookup_name_' . $i] = $names[$i];
411
	}
412
413
	// What are we using to compare?
414
	$comparison = $use_wildcards ? 'LIKE' : '=';
415
416
	// Nothing found yet.
417
	$results = array();
418
419
	// This ensures you can't search someones email address if you can't see it.
420
	if (($use_wildcards || $maybe_email) && allowedTo('moderate_forum'))
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...
421
		$email_condition = '
422
			OR (email_address ' . $comparison . ' \'' . implode('\') OR (email_address ' . $comparison . ' \'', $names) . '\')';
423
	else
424
		$email_condition = '';
425
426
	// Get the case of the columns right - but only if we need to as things like MySQL will go slow needlessly otherwise.
427
	$member_name = $smcFunc['db_case_sensitive'] ? 'LOWER(member_name)' : 'member_name';
428
	$real_name = $smcFunc['db_case_sensitive'] ? 'LOWER(real_name)' : 'real_name';
429
430
	// Searches.
431
	$member_name_search = $member_name . ' ' . $comparison . ' ' . implode(' OR ' . $member_name . ' ' . $comparison . ' ', $names_list);
432
	$real_name_search = $real_name . ' ' . $comparison . ' ' . implode(' OR ' . $real_name . ' ' . $comparison . ' ', $names_list);
433
434
	// Search by username, display name, and email address.
435
	$request = $smcFunc['db_query']('', '
436
		SELECT id_member, member_name, real_name, email_address
437
		FROM {db_prefix}members
438
		WHERE (' . $member_name_search . '
439
			OR ' . $real_name_search . ' ' . $email_condition . ')
440
			' . ($buddies_only ? 'AND id_member IN ({array_int:buddy_list})' : '') . '
441
			AND is_activated IN (1, 11)
442
		LIMIT {int:limit}',
443
		array_merge($where_params, array(
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $where_params seems to be defined by a foreach iteration on line 396. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
444
			'buddy_list' => $user_info['buddies'],
445
			'limit' => $max,
446
		))
447
	);
448
	while ($row = $smcFunc['db_fetch_assoc']($request))
449
	{
450
		$results[$row['id_member']] = array(
451
			'id' => $row['id_member'],
452
			'name' => $row['real_name'],
453
			'username' => $row['member_name'],
454
			'email' => allowedTo('moderate_forum') ? $row['email_address'] : '',
455
			'href' => $scripturl . '?action=profile;u=' . $row['id_member'],
456
			'link' => '<a href="' . $scripturl . '?action=profile;u=' . $row['id_member'] . '">' . $row['real_name'] . '</a>'
457
		);
458
	}
459
	$smcFunc['db_free_result']($request);
460
461
	// Return all the results.
462
	return $results;
463
}
464
465
/**
466
 * Called by index.php?action=findmember.
467
 * - is used as a popup for searching members.
468
 * - uses sub template find_members of the Help template.
469
 * - also used to add members for PM's sent using wap2/imode protocol.
470
 */
471
function JSMembers()
472
{
473
	global $context, $scripturl, $user_info, $smcFunc;
474
475
	checkSession('get');
476
477
	// Why is this in the Help template, you ask?  Well, erm... it helps you.  Does that work?
478
	loadTemplate('Help');
479
480
	$context['template_layers'] = array();
481
	$context['sub_template'] = 'find_members';
482
483
	if (isset($_REQUEST['search']))
484
		$context['last_search'] = $smcFunc['htmlspecialchars']($_REQUEST['search'], ENT_QUOTES);
485
	else
486
		$_REQUEST['start'] = 0;
487
488
	// Allow the user to pass the input to be added to to the box.
489
	$context['input_box_name'] = isset($_REQUEST['input']) && preg_match('~^[\w-]+$~', $_REQUEST['input']) === 1 ? $_REQUEST['input'] : 'to';
490
491
	// Take the delimiter over GET in case it's \n or something.
492
	$context['delimiter'] = isset($_REQUEST['delim']) ? ($_REQUEST['delim'] == 'LB' ? "\n" : $_REQUEST['delim']) : ', ';
493
	$context['quote_results'] = !empty($_REQUEST['quote']);
494
495
	// List all the results.
496
	$context['results'] = array();
497
498
	// Some buddy related settings ;)
499
	$context['show_buddies'] = !empty($user_info['buddies']);
500
	$context['buddy_search'] = isset($_REQUEST['buddies']);
501
502
	// If the user has done a search, well - search.
503
	if (isset($_REQUEST['search']))
504
	{
505
		$_REQUEST['search'] = $smcFunc['htmlspecialchars']($_REQUEST['search'], ENT_QUOTES);
506
507
		$context['results'] = findMembers(array($_REQUEST['search']), true, $context['buddy_search']);
508
		$total_results = count($context['results']);
509
510
		$context['page_index'] = constructPageIndex($scripturl . '?action=findmember;search=' . $context['last_search'] . ';' . $context['session_var'] . '=' . $context['session_id'] . ';input=' . $context['input_box_name'] . ($context['quote_results'] ? ';quote=1' : '') . ($context['buddy_search'] ? ';buddies' : ''), $_REQUEST['start'], $total_results, 7);
511
512
		// Determine the navigation context.
513
		$base_url = $scripturl . '?action=findmember;search=' . urlencode($context['last_search']) . (empty($_REQUEST['u']) ? '' : ';u=' . $_REQUEST['u']) . ';' . $context['session_var'] . '=' . $context['session_id'];
514
		$context['links'] = array(
515
			'first' => $_REQUEST['start'] >= 7 ? $base_url . ';start=0' : '',
516
			'prev' => $_REQUEST['start'] >= 7 ? $base_url . ';start=' . ($_REQUEST['start'] - 7) : '',
517
			'next' => $_REQUEST['start'] + 7 < $total_results ? $base_url . ';start=' . ($_REQUEST['start'] + 7) : '',
518
			'last' => $_REQUEST['start'] + 7 < $total_results ? $base_url . ';start=' . (floor(($total_results - 1) / 7) * 7) : '',
519
			'up' => $scripturl . '?action=pm;sa=send' . (empty($_REQUEST['u']) ? '' : ';u=' . $_REQUEST['u']),
520
		);
521
		$context['page_info'] = array(
522
			'current_page' => $_REQUEST['start'] / 7 + 1,
523
			'num_pages' => floor(($total_results - 1) / 7) + 1
524
		);
525
526
		$context['results'] = array_slice($context['results'], $_REQUEST['start'], 7);
527
	}
528
	else
529
		$context['links']['up'] = $scripturl . '?action=pm;sa=send' . (empty($_REQUEST['u']) ? '' : ';u=' . $_REQUEST['u']);
530
}
531
532
/**
533
 * Outputs each member name on its own line.
534
 * - used by javascript to find members matching the request.
535
 */
536
function RequestMembers()
537
{
538
	global $user_info, $txt, $smcFunc;
539
540
	checkSession('get');
541
542
	$_REQUEST['search'] = $smcFunc['htmlspecialchars']($_REQUEST['search']) . '*';
543
	$_REQUEST['search'] = trim($smcFunc['strtolower']($_REQUEST['search']));
544
	$_REQUEST['search'] = strtr($_REQUEST['search'], array('%' => '\%', '_' => '\_', '*' => '%', '?' => '_', '&#038;' => '&amp;'));
545
546
	if (function_exists('iconv'))
547
		header('content-type: text/plain; charset=UTF-8');
548
549
	$request = $smcFunc['db_query']('', '
550
		SELECT real_name
551
		FROM {db_prefix}members
552
		WHERE {raw:real_name} LIKE {string:search}' . (isset($_REQUEST['buddies']) ? '
553
			AND id_member IN ({array_int:buddy_list})' : '') . '
554
			AND is_activated IN (1, 11)
555
		LIMIT ' . ($smcFunc['strlen']($_REQUEST['search']) <= 2 ? '100' : '800'),
556
		array(
557
			'real_name' => $smcFunc['db_case_sensitive'] ? 'LOWER(real_name)' : 'real_name',
558
			'buddy_list' => $user_info['buddies'],
559
			'search' => $_REQUEST['search'],
560
		)
561
	);
562
	while ($row = $smcFunc['db_fetch_assoc']($request))
563
	{
564
		if (function_exists('iconv'))
565
		{
566
			$utf8 = iconv($txt['lang_character_set'], 'UTF-8', $row['real_name']);
567
			if ($utf8)
568
				$row['real_name'] = $utf8;
569
		}
570
571
		$row['real_name'] = strtr($row['real_name'], array('&amp;' => '&#038;', '&lt;' => '&#060;', '&gt;' => '&#062;', '&quot;' => '&#034;'));
572
573
		if (preg_match('~&#\d+;~', $row['real_name']) != 0)
574
			$row['real_name'] = preg_replace_callback('~&#(\d+);~', 'fixchar__callback', $row['real_name']);
575
576
		echo $row['real_name'], "\n";
577
	}
578
	$smcFunc['db_free_result']($request);
579
580
	obExit(false);
581
}
582
583
/**
584
 * Generates a random password for a user and emails it to them.
585
 * - called by Profile.php when changing someone's username.
586
 * - checks the validity of the new username.
587
 * - generates and sets a new password for the given user.
588
 * - mails the new password to the email address of the user.
589
 * - if username is not set, only a new password is generated and sent.
590
 *
591
 * @param int $memID The ID of the member
592
 * @param string $username The new username. If set, also checks the validity of the username
593
 */
594
function resetPassword($memID, $username = null)
595
{
596
	global $sourcedir, $modSettings, $smcFunc, $language;
597
598
	// Language... and a required file.
599
	loadLanguage('Login');
600
	require_once($sourcedir . '/Subs-Post.php');
601
602
	// Get some important details.
603
	$request = $smcFunc['db_query']('', '
604
		SELECT member_name, email_address, lngfile
605
		FROM {db_prefix}members
606
		WHERE id_member = {int:id_member}',
607
		array(
608
			'id_member' => $memID,
609
		)
610
	);
611
	list ($user, $email, $lngfile) = $smcFunc['db_fetch_row']($request);
612
	$smcFunc['db_free_result']($request);
613
614
	if ($username !== null)
615
	{
616
		$old_user = $user;
617
		$user = trim($username);
618
	}
619
620
	// Generate a random password.
621
	$newPassword = substr(preg_replace('/\W/', '', md5($smcFunc['random_int']())), 0, 10);
622
	$newPassword_sha1 = hash_password($user, $newPassword);
623
624
	// Do some checks on the username if needed.
625
	if ($username !== null)
626
	{
627
		validateUsername($memID, $user);
628
629
		// Update the database...
630
		updateMemberData($memID, array('member_name' => $user, 'passwd' => $newPassword_sha1));
631
	}
632
	else
633
		updateMemberData($memID, array('passwd' => $newPassword_sha1));
634
635
	call_integration_hook('integrate_reset_pass', array($old_user, $user, $newPassword));
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $old_user does not seem to be defined for all execution paths leading up to this point.
Loading history...
636
637
	$replacements = array(
638
		'USERNAME' => $user,
639
		'PASSWORD' => $newPassword,
640
	);
641
642
	$emaildata = loadEmailTemplate('change_password', $replacements, empty($lngfile) || empty($modSettings['userLanguage']) ? $language : $lngfile);
643
644
	// Send them the email informing them of the change - then we're done!
645
	sendmail($email, $emaildata['subject'], $emaildata['body'], null, 'chgpass' . $memID, $emaildata['is_html'], 0);
646
}
647
648
/**
649
 * Checks a username obeys a load of rules
650
 *
651
 * @param int $memID The ID of the member
652
 * @param string $username The username to validate
653
 * @param boolean $return_error Whether to return errors
654
 * @param boolean $check_reserved_name Whether to check this against the list of reserved names
655
 * @return array|null Null if there are no errors, otherwise an array of errors if return_error is true
656
 */
657
function validateUsername($memID, $username, $return_error = false, $check_reserved_name = true)
658
{
659
	global $sourcedir, $txt, $smcFunc, $user_info;
660
661
	$errors = array();
662
663
	// Don't use too long a name.
664
	if ($smcFunc['strlen']($username) > 25)
665
		$errors[] = array('lang', 'error_long_name');
666
667
	// No name?!  How can you register with no name?
668
	if ($username == '')
669
		$errors[] = array('lang', 'need_username');
670
671
	// Only these characters are permitted.
672
	if (in_array($username, array('_', '|')) || preg_match('~[<>&"\'=\\\\]~', preg_replace('~&#(?:\\d{1,7}|x[0-9a-fA-F]{1,6});~', '', $username)) != 0 || strpos($username, '[code') !== false || strpos($username, '[/code') !== false)
673
		$errors[] = array('lang', 'error_invalid_characters_username');
674
675
	if (stristr($username, $txt['guest_title']) !== false)
676
		$errors[] = array('lang', 'username_reserved', 'general', array($txt['guest_title']));
677
678
	if ($check_reserved_name)
679
	{
680
		require_once($sourcedir . '/Subs-Members.php');
681
		if (isReservedName($username, $memID, false))
682
			$errors[] = array('done', '(' . $smcFunc['htmlspecialchars']($username) . ') ' . $txt['name_in_use']);
683
	}
684
685
	// Maybe a mod wants to perform more checks?
686
	call_integration_hook('integrate_validate_username', array($username, &$errors));
687
688
	if ($return_error)
689
		return $errors;
690
	elseif (empty($errors))
691
		return null;
692
693
	loadLanguage('Errors');
694
	$error = $errors[0];
695
696
	$message = $error[0] == 'lang' ? (empty($error[3]) ? $txt[$error[1]] : vsprintf($txt[$error[1]], (array) $error[3])) : $error[1];
697
	fatal_error($message, empty($error[2]) || $user_info['is_admin'] ? false : $error[2]);
698
}
699
700
/**
701
 * Checks whether a password meets the current forum rules
702
 * - called when registering/choosing a password.
703
 * - checks the password obeys the current forum settings for password strength.
704
 * - if password checking is enabled, will check that none of the words in restrict_in appear in the password.
705
 * - returns an error identifier if the password is invalid, or null.
706
 *
707
 * @param string $password The desired password
708
 * @param string $username The username
709
 * @param array $restrict_in An array of restricted strings that cannot be part of the password (email address, username, etc.)
710
 * @return null|string Null if valid or a string indicating what the problem was
711
 */
712
function validatePassword($password, $username, $restrict_in = array())
713
{
714
	global $modSettings, $smcFunc;
715
716
	// Perform basic requirements first.
717
	if ($smcFunc['strlen']($password) < (empty($modSettings['password_strength']) ? 4 : 8))
718
		return 'short';
719
720
	// Maybe we need some more fancy password checks.
721
	$pass_error = '';
722
	call_integration_hook('integrate_validatePassword', array($password, $username, $restrict_in, &$pass_error));
723
	if (!empty($pass_error))
0 ignored issues
show
introduced by
The condition empty($pass_error) is always true.
Loading history...
724
		return $pass_error;
725
726
	// Is this enough?
727
	if (empty($modSettings['password_strength']))
728
		return null;
729
730
	// Otherwise, perform the medium strength test - checking if password appears in the restricted string.
731
	if (preg_match('~\b' . preg_quote($password, '~') . '\b~', implode(' ', $restrict_in)) != 0)
732
		return 'restricted_words';
733
	elseif ($smcFunc['strpos']($password, $username) !== false)
734
		return 'restricted_words';
735
736
	// If just medium, we're done.
737
	if ($modSettings['password_strength'] == 1)
738
		return null;
739
740
	// Otherwise, hard test next, check for numbers and letters, uppercase too.
741
	$good = preg_match('~(\D\d|\d\D)~', $password) != 0;
742
	$good &= $smcFunc['strtolower']($password) != $password;
743
744
	return $good ? null : 'chars';
745
}
746
747
/**
748
 * Quickly find out what moderation authority this user has
749
 * - builds the moderator, group and board level querys for the user
750
 * - stores the information on the current users moderation powers in $user_info['mod_cache'] and $_SESSION['mc']
751
 */
752
function rebuildModCache()
753
{
754
	global $user_info, $smcFunc;
755
756
	// What groups can they moderate?
757
	$group_query = allowedTo('manage_membergroups') ? '1=1' : '0=1';
758
759
	if ($group_query == '0=1' && !$user_info['is_guest'])
760
	{
761
		$request = $smcFunc['db_query']('', '
762
			SELECT id_group
763
			FROM {db_prefix}group_moderators
764
			WHERE id_member = {int:current_member}',
765
			array(
766
				'current_member' => $user_info['id'],
767
			)
768
		);
769
		$groups = array();
770
		while ($row = $smcFunc['db_fetch_assoc']($request))
771
			$groups[] = $row['id_group'];
772
		$smcFunc['db_free_result']($request);
773
774
		if (empty($groups))
775
			$group_query = '0=1';
776
		else
777
			$group_query = 'id_group IN (' . implode(',', $groups) . ')';
778
	}
779
780
	// Then, same again, just the boards this time!
781
	$board_query = allowedTo('moderate_forum') ? '1=1' : '0=1';
782
783
	if ($board_query == '0=1' && !$user_info['is_guest'])
784
	{
785
		$boards = boardsAllowedTo('moderate_board', true);
786
787
		if (empty($boards))
788
			$board_query = '0=1';
789
		else
790
			$board_query = 'id_board IN (' . implode(',', $boards) . ')';
791
	}
792
793
	// What boards are they the moderator of?
794
	$boards_mod = array();
795
	if (!$user_info['is_guest'])
796
	{
797
		$request = $smcFunc['db_query']('', '
798
			SELECT id_board
799
			FROM {db_prefix}moderators
800
			WHERE id_member = {int:current_member}',
801
			array(
802
				'current_member' => $user_info['id'],
803
			)
804
		);
805
		while ($row = $smcFunc['db_fetch_assoc']($request))
806
			$boards_mod[] = $row['id_board'];
807
		$smcFunc['db_free_result']($request);
808
809
		// Can any of the groups they're in moderate any of the boards?
810
		$request = $smcFunc['db_query']('', '
811
			SELECT id_board
812
			FROM {db_prefix}moderator_groups
813
			WHERE id_group IN({array_int:groups})',
814
			array(
815
				'groups' => $user_info['groups'],
816
			)
817
		);
818
		while ($row = $smcFunc['db_fetch_assoc']($request))
819
			$boards_mod[] = $row['id_board'];
820
		$smcFunc['db_free_result']($request);
821
822
		// Just in case we've got duplicates here...
823
		$boards_mod = array_unique($boards_mod);
824
	}
825
826
	$mod_query = empty($boards_mod) ? '0=1' : 'b.id_board IN (' . implode(',', $boards_mod) . ')';
827
828
	$_SESSION['mc'] = array(
829
		'time' => time(),
830
		// This looks a bit funny but protects against the login redirect.
831
		'id' => $user_info['id'] && $user_info['name'] ? $user_info['id'] : 0,
832
		// If you change the format of 'gq' and/or 'bq' make sure to adjust 'can_mod' in Load.php.
833
		'gq' => $group_query,
834
		'bq' => $board_query,
835
		'ap' => boardsAllowedTo('approve_posts'),
836
		'mb' => $boards_mod,
837
		'mq' => $mod_query,
838
	);
839
	call_integration_hook('integrate_mod_cache');
840
841
	$user_info['mod_cache'] = $_SESSION['mc'];
842
843
	// Might as well clean up some tokens while we are at it.
844
	cleanTokens();
845
}
846
847
/**
848
 * A wrapper for setcookie that gives integration hook access to it
849
 *
850
 * @param string $name
851
 * @param string $value = ''
852
 * @param int $expire = 0
853
 * @param string $path = ''
854
 * @param string $domain = ''
855
 * @param bool $secure = false
856
 * @param bool $httponly = true
857
 * @param string $samesite = lax
858
 */
859
function smf_setcookie($name, $value = '', $expire = 0, $path = '', $domain = '', $secure = null, $httponly = true, $samesite = null)
860
{
861
	global $modSettings;
862
863
	// In case a customization wants to override the default settings
864
	if ($httponly === null)
0 ignored issues
show
introduced by
The condition $httponly === null is always false.
Loading history...
865
		$httponly = !empty($modSettings['httponlyCookies']);
866
	if ($secure === null)
867
		$secure = !empty($modSettings['secureCookies']);
868
	if ($samesite === null)
869
		$samesite = !empty($modSettings['samesiteCookies']) ? $modSettings['samesiteCookies'] : 'lax';
870
871
	// Intercept cookie?
872
	call_integration_hook('integrate_cookie', array($name, $value, $expire, $path, $domain, $secure, $httponly, $samesite));
873
874
	if(PHP_VERSION_ID < 70300)
875
		return setcookie($name, $value, $expire, $path . ';samesite=' . $samesite, $domain, $secure, $httponly);
876
	else
877
		return setcookie($name, $value, array(
878
			'expires' 	=> $expire,
879
			'path'		=> $path,
880
			'domain' 	=> $domain,
881
			'secure'	=> $secure,
882
			'httponly'	=> $httponly,    
883
			'samesite'	=> $samesite
884
		));
885
}
886
887
/**
888
 * Hashes username with password
889
 *
890
 * @param string $username The username
891
 * @param string $password The unhashed password
892
 * @param int $cost The cost
893
 * @return string The hashed password
894
 */
895
function hash_password($username, $password, $cost = null)
896
{
897
	global $smcFunc, $modSettings;
898
899
	$cost = empty($cost) ? (empty($modSettings['bcrypt_hash_cost']) ? 10 : $modSettings['bcrypt_hash_cost']) : $cost;
900
901
	return password_hash($smcFunc['strtolower']($username) . $password, PASSWORD_BCRYPT, array(
902
		'cost' => $cost,
903
	));
904
}
905
906
/**
907
 * Hashes password with salt and authentication secret. This is solely used for cookies.
908
 *
909
 * @param string $password The password
910
 * @param string $salt The salt
911
 * @return string The hashed password
912
 */
913
function hash_salt($password, $salt)
914
{
915
	// Append the salt to get a user-specific authentication secret.
916
	$secret_key = get_auth_secret() . $salt;
917
918
	// Now use that to generate an HMAC of the password.
919
	return hash_hmac('sha512', $password, $secret_key);
920
}
921
922
/**
923
 * Verifies a raw SMF password against the bcrypt'd string
924
 *
925
 * @param string $username The username
926
 * @param string $password The password
927
 * @param string $hash The hashed string
928
 * @return bool Whether the hashed password matches the string
929
 */
930
function hash_verify_password($username, $password, $hash)
931
{
932
	global $smcFunc;
933
934
	return password_verify($smcFunc['strtolower']($username) . $password, $hash);
935
}
936
937
/**
938
 * Returns the length for current hash
939
 *
940
 * @return int The length for the current hash
941
 */
942
function hash_length()
943
{
944
	return 60;
945
}
946
947
/**
948
 * Benchmarks the server to figure out an appropriate cost factor (minimum 9)
949
 *
950
 * @param float $hashTime Time to target, in seconds
951
 * @return int The cost
952
 */
953
function hash_benchmark($hashTime = 0.2)
954
{
955
	$cost = 9;
956
	do
957
	{
958
		$timeStart = microtime(true);
959
		hash_password('test', 'thisisatestpassword', $cost);
960
		$timeTaken = microtime(true) - $timeStart;
961
		$cost++;
962
	}
963
	while ($timeTaken < $hashTime);
964
965
	return $cost;
966
}
967
968
// Based on code by "examplehash at user dot com".
969
// https://www.php.net/manual/en/function.hash-equals.php#125034
970
if (!function_exists('hash_equals'))
971
{
972
	/**
973
	 * A compatibility function for when PHP's "hash_equals" function isn't available
974
	 * @param string $known_string A known hash
975
	 * @param string $user_string The hash of the user string
976
	 * @return bool Whether or not the two are equal
977
	 */
978
	function hash_equals($known_string, $user_string)
979
	{
980
		$known_string = (string) $known_string;
981
		$user_string = (string) $user_string;
982
983
		$sx = 0;
984
		$sy = strlen($known_string);
985
		$uy = strlen($user_string);
986
		$result = $sy - $uy;
987
		for ($ux = 0; $ux < $uy; $ux++)
988
		{
989
			$result |= ord($user_string[$ux]) ^ ord($known_string[$sx]);
990
			$sx = ($sx + 1) % $sy;
991
		}
992
993
		return !$result;
994
	}
995
}
996
997
?>