Completed
Pull Request — development (#2960)
by Elk
09:10
created

Auth.subs.php ➔ generateValidationCode()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 1
dl 0
loc 6
ccs 2
cts 2
cp 1
crap 1
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * This file has functions in it to do with authentication, user handling, and the like.
5
 *
6
 * @name      ElkArte Forum
7
 * @copyright ElkArte Forum contributors
8
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause
9
 *
10
 * This file contains code covered by:
11
 * copyright:	2011 Simple Machines (http://www.simplemachines.org)
12
 * license:  	BSD, See included LICENSE.TXT for terms and conditions.
13
 *
14
 * @version 1.1 Release Candidate 1
15
 *
16
 */
17
18
/**
19
 * Sets the login cookie and session based on the id_member and password passed.
20
 *
21
 * What it does:
22
 *
23
 * - password should be already encrypted with the cookie salt.
24
 * - logs the user out if id_member is zero.
25
 * - sets the cookie and session to last the number of seconds specified by cookie_length.
26
 * - when logging out, if the globalCookies setting is enabled, attempts to clear the subdomain's cookie too.
27
 *
28
 * @package Authorization
29
 * @param int $cookie_length
30
 * @param int $id The id of the member
31
 * @param string $password = ''
32
 */
33
function setLoginCookie($cookie_length, $id, $password = '')
34
{
35 1
	global $cookiename, $boardurl, $modSettings;
36
37
	// If changing state force them to re-address some permission caching.
38 1
	$_SESSION['mc']['time'] = 0;
39
40
	// Let's be sure it is an int to simplify the regexp used to validate the cookie
41 1
	$id = (int) $id;
42
43
	// The cookie may already exist, and have been set with different options.
44 1
	$cookie_state = (empty($modSettings['localCookies']) ? 0 : 1) | (empty($modSettings['globalCookies']) ? 0 : 2);
45
46 1
	if (isset($_COOKIE[$cookiename]))
47 1
	{
48 View Code Duplication
		$array = serializeToJson($_COOKIE[$cookiename], function ($array_from) use ($cookiename) {
49
			global $modSettings;
50
51
			require_once(SUBSDIR . '/Auth.subs.php');
52
			$_COOKIE[$cookiename] = json_encode($array_from);
53
			setLoginCookie(60 * $modSettings['cookieTime'], $array_from[0], $array_from[1]);
54
		});
55
56
		// Out with the old, in with the new!
57
		if (isset($array[3]) && $array[3] != $cookie_state)
58
		{
59
			$cookie_url = url_parts($array[3] & 1 > 0, $array[3] & 2 > 0);
60
			elk_setcookie($cookiename, json_encode(array(0, '', 0)), time() - 3600, $cookie_url[1], $cookie_url[0]);
61
		}
62
	}
63
64
	// Get the data and path to set it on.
65 1
	$data = json_encode(empty($id) ? array(0, '', 0) : array($id, $password, time() + $cookie_length, $cookie_state));
66 1
	$cookie_url = url_parts(!empty($modSettings['localCookies']), !empty($modSettings['globalCookies']));
67
68
	// Set the cookie, $_COOKIE, and session variable.
69 1
	elk_setcookie($cookiename, $data, time() + $cookie_length, $cookie_url[1], $cookie_url[0]);
70
71
	// If subdomain-independent cookies are on, unset the subdomain-dependent cookie too.
72 1
	if (empty($id) && !empty($modSettings['globalCookies']))
73 1
		elk_setcookie($cookiename, $data, time() + $cookie_length, $cookie_url[1], '');
74
75
	// Any alias URLs?  This is mainly for use with frames, etc.
76 1
	if (!empty($modSettings['forum_alias_urls']))
77 1
	{
78
		$aliases = explode(',', $modSettings['forum_alias_urls']);
79
80
		$temp = $boardurl;
81
		foreach ($aliases as $alias)
82
		{
83
			// Fake the $boardurl so we can set a different cookie.
84
			$alias = strtr(trim($alias), array('http://' => '', 'https://' => ''));
85
			$boardurl = 'http://' . $alias;
86
87
			$cookie_url = url_parts(!empty($modSettings['localCookies']), !empty($modSettings['globalCookies']));
88
89
			if ($cookie_url[0] == '')
90
				$cookie_url[0] = strtok($alias, '/');
91
92
			elk_setcookie($cookiename, $data, time() + $cookie_length, $cookie_url[1], $cookie_url[0]);
93
		}
94
95
		$boardurl = $temp;
96
	}
97
98 1
	$_COOKIE[$cookiename] = $data;
99
100
	// Make sure the user logs in with a new session ID.
101 1
	if (!isset($_SESSION['login_' . $cookiename]) || $_SESSION['login_' . $cookiename] !== $data)
102 1
	{
103
		// We need to meddle with the session.
104 1
		require_once(SOURCEDIR . '/Session.php');
105
106
		// Backup the old session.
107 1
		$oldSessionData = $_SESSION;
108
109
		// Remove the old session data and file / db entry
110 1
		$_SESSION = array();
111 1
		session_destroy();
112
113
		// Recreate and restore the new session.
114 1
		loadSession();
115
116
		// Get a new session id, and load it with the data
117 1
		session_regenerate_id();
118 1
		$_SESSION = $oldSessionData;
119
120 1
		$_SESSION['login_' . $cookiename] = $data;
121 1
	}
122 1
}
123
124
/**
125
 * Get the domain and path for the cookie
126
 *
127
 * What it does:
128
 *
129
 * - normally, local and global should be the localCookies and globalCookies settings, respectively.
130
 * - uses boardurl to determine these two things.
131
 *
132
 * @package Authorization
133
 * @param bool $local
134
 * @param bool $global
135
 */
136
function url_parts($local, $global)
137
{
138 1
	global $boardurl, $modSettings;
139
140
	// Parse the URL with PHP to make life easier.
141 1
	$parsed_url = parse_url($boardurl);
142
143
	// Is local cookies off?
144 1
	if (empty($parsed_url['path']) || !$local)
145 1
		$parsed_url['path'] = '';
146
147 1
	if (!empty($modSettings['globalCookiesDomain']) && strpos($boardurl, $modSettings['globalCookiesDomain']) !== false)
148 1
		$parsed_url['host'] = $modSettings['globalCookiesDomain'];
149
150
	// Globalize cookies across domains (filter out IP-addresses)?
151 1
	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)
152
			$parsed_url['host'] = '.' . $parts[1];
153
154
	// We shouldn't use a host at all if both options are off.
155 1
	elseif (!$local && !$global)
156 1
		$parsed_url['host'] = '';
157
158
	// The host also shouldn't be set if there aren't any dots in it.
159
	elseif (!isset($parsed_url['host']) || strpos($parsed_url['host'], '.') === false)
160
		$parsed_url['host'] = '';
161
162 1
	return array($parsed_url['host'], $parsed_url['path'] . '/');
163
}
164
165
/**
166
 * Question the verity of the admin by asking for his or her password.
167
 *
168
 * What it does:
169
 *
170
 * - loads Login.template.php and uses the admin_login sub template.
171
 * - sends data to template so the admin is sent on to the page they
172
 *   wanted if their password is correct, otherwise they can try again.
173
 *
174
 * @package Authorization
175
 * @param string $type = 'admin'
176
 * @throws Elk_Exception
177
 */
178
function adminLogin($type = 'admin')
179
{
180
	global $context, $txt, $user_info;
181
182
	loadLanguage('Admin');
183
	loadTemplate('Login');
184
	loadJavascriptFile('sha256.js', array('defer' => true));
185
186
	// Validate what type of session check this is.
187
	$types = array();
188
	call_integration_hook('integrate_validateSession', array(&$types));
189
	$type = in_array($type, $types) || $type == 'moderate' ? $type : 'admin';
190
191
	// They used a wrong password, log it and unset that.
192
	if (isset($_POST[$type . '_hash_pass']) || isset($_POST[$type . '_pass']))
193
	{
194
		// log some info along with it! referer, user agent
195
		$req = request();
196
		$txt['security_wrong'] = sprintf($txt['security_wrong'], isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : $txt['unknown'], $req->user_agent(), $user_info['ip']);
197
		Errors::instance()->log_error($txt['security_wrong'], 'critical');
198
199
		if (isset($_POST[$type . '_hash_pass']))
200
			unset($_POST[$type . '_hash_pass']);
201
		if (isset($_POST[$type . '_pass']))
202
			unset($_POST[$type . '_pass']);
203
204
		$context['incorrect_password'] = true;
205
	}
206
207
	createToken('admin-login');
208
209
	// Figure out the get data and post data.
210
	$context['get_data'] = '?' . construct_query_string($_GET);
211
	$context['post_data'] = '';
212
213
	// Now go through $_POST.  Make sure the session hash is sent.
214
	$_POST[$context['session_var']] = $context['session_id'];
215
	foreach ($_POST as $k => $v)
216
		$context['post_data'] .= adminLogin_outputPostVars($k, $v);
217
218
	// Now we'll use the admin_login sub template of the Login template.
219
	$context['sub_template'] = 'admin_login';
220
221
	// And title the page something like "Login".
222
	if (!isset($context['page_title']))
223
		$context['page_title'] = $txt['admin_login'];
224
225
	// The type of action.
226
	$context['sessionCheckType'] = $type;
227
228
	obExit();
229
230
	// We MUST exit at this point, because otherwise we CANNOT KNOW that the user is privileged.
231
	trigger_error('Hacking attempt...', E_USER_ERROR);
232
}
233
234
/**
235
 * Used by the adminLogin() function.
236
 *
237
 * What it does:
238
 *  - if 'value' is an array, the function is called recursively.
239
 *
240
 * @package Authorization
241
 * @param string $k key
242
 * @param string|boolean $v value
243
 * @return string 'hidden' HTML form fields, containing key-value-pairs
244
 */
245
function adminLogin_outputPostVars($k, $v)
246
{
247
	if (!is_array($v))
248
		return '
249
<input type="hidden" name="' . htmlspecialchars($k, ENT_COMPAT, 'UTF-8') . '" value="' . strtr($v, array('"' => '&quot;', '<' => '&lt;', '>' => '&gt;')) . '" />';
0 ignored issues
show
Bug introduced by
It seems like $v defined by parameter $v on line 245 can also be of type boolean; however, strtr() does only seem to accept string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
250
	else
251
	{
252
		$ret = '';
253
		foreach ($v as $k2 => $v2)
254
			$ret .= adminLogin_outputPostVars($k . '[' . $k2 . ']', $v2);
255
256
		return $ret;
257
	}
258
}
259
260
/**
261
 * Properly urlencodes a string to be used in a query
262
 *
263
 * @package Authorization
264
 * @param mixed[] $get associative array from $_GET
265
 * @return string query string
266
 */
267
function construct_query_string($get)
268
{
269
	global $scripturl;
270
271
	$query_string = '';
272
273
	// Awww, darn.  The $scripturl contains GET stuff!
274
	$q = strpos($scripturl, '?');
275
	if ($q !== false)
276
	{
277
		parse_str(preg_replace('/&(\w+)(?=&|$)/', '&$1=', strtr(substr($scripturl, $q + 1), ';', '&')), $temp);
278
279
		foreach ($get as $k => $v)
280
		{
281
			// Only if it's not already in the $scripturl!
282
			if (!isset($temp[$k]))
283
				$query_string .= urlencode($k) . '=' . urlencode($v) . ';';
284
			// If it changed, put it out there, but with an ampersand.
285 View Code Duplication
			elseif ($temp[$k] != $get[$k])
286
				$query_string .= urlencode($k) . '=' . urlencode($v) . '&amp;';
287
		}
288
	}
289 View Code Duplication
	else
290
	{
291
		// Add up all the data from $_GET into get_data.
292
		foreach ($get as $k => $v)
293
			$query_string .= urlencode($k) . '=' . urlencode($v) . ';';
294
	}
295
296
	$query_string = substr($query_string, 0, -1);
297
	return $query_string;
298
}
299
300
/**
301
 * Finds members by email address, username, or real name.
302
 *
303
 * What it does:
304
 *
305
 * - searches for members whose username, display name, or e-mail address match the given pattern of array names.
306
 * - searches only buddies if buddies_only is set.
307
 *
308
 * @package Authorization
309
 * @param string[]|string $names
310
 * @param bool $use_wildcards = false, accepts wildcards ? and * in the pattern if true
311
 * @param bool $buddies_only = false,
312
 * @param int $max = 500 retrieves a maximum of max members, if passed
313
 * @return array containing information about the matching members
314
 */
315
function findMembers($names, $use_wildcards = false, $buddies_only = false, $max = 500)
316
{
317
	global $scripturl, $user_info;
318
319
	$db = database();
320
321
	// If it's not already an array, make it one.
322
	if (!is_array($names))
323
		$names = explode(',', $names);
324
325
	$maybe_email = false;
326
	foreach ($names as $i => $name)
327
	{
328
		// Trim, and fix wildcards for each name.
329
		$names[$i] = trim(Util::strtolower($name));
330
331
		$maybe_email |= strpos($name, '@') !== false;
332
333
		// Make it so standard wildcards will work. (* and ?)
334
		if ($use_wildcards)
335
			$names[$i] = strtr($names[$i], array('%' => '\%', '_' => '\_', '*' => '%', '?' => '_', '\'' => '&#039;'));
336
		else
337
			$names[$i] = strtr($names[$i], array('\'' => '&#039;'));
338
	}
339
340
	// What are we using to compare?
341
	$comparison = $use_wildcards ? 'LIKE' : '=';
342
343
	// Nothing found yet.
344
	$results = array();
345
346
	// This ensures you can't search someones email address if you can't see it.
347
	$email_condition = allowedTo('moderate_forum') ? '' : 'hide_email = 0 AND ';
348
349
	if ($use_wildcards || $maybe_email)
350
		$email_condition = '
351
			OR (' . $email_condition . 'email_address ' . $comparison . ' \'' . implode('\') OR (' . $email_condition . ' email_address ' . $comparison . ' \'', $names) . '\')';
352
	else
353
		$email_condition = '';
354
355
	// Get the case of the columns right - but only if we need to as things like MySQL will go slow needlessly otherwise.
356
	$member_name = defined('DB_CASE_SENSITIVE') ? 'LOWER(member_name)' : 'member_name';
357
	$real_name = defined('DB_CASE_SENSITIVE') ? 'LOWER(real_name)' : 'real_name';
358
359
	// Search by username, display name, and email address.
360
	$request = $db->query('', '
361
		SELECT id_member, member_name, real_name, email_address, hide_email
362
		FROM {db_prefix}members
363
		WHERE ({raw:member_name_search}
364
			OR {raw:real_name_search} {raw:email_condition})
365
			' . ($buddies_only ? 'AND id_member IN ({array_int:buddy_list})' : '') . '
366
			AND is_activated IN (1, 11)
367
		LIMIT {int:limit}',
368
		array(
369
			'buddy_list' => $user_info['buddies'],
370
			'member_name_search' => $member_name . ' ' . $comparison . ' \'' . implode('\' OR ' . $member_name . ' ' . $comparison . ' \'', $names) . '\'',
371
			'real_name_search' => $real_name . ' ' . $comparison . ' \'' . implode('\' OR ' . $real_name . ' ' . $comparison . ' \'', $names) . '\'',
372
			'email_condition' => $email_condition,
373
			'limit' => $max,
374
		)
375
	);
376
	while ($row = $db->fetch_assoc($request))
377
	{
378
		$results[$row['id_member']] = array(
379
			'id' => $row['id_member'],
380
			'name' => $row['real_name'],
381
			'username' => $row['member_name'],
382
			'email' => in_array(showEmailAddress(!empty($row['hide_email']), $row['id_member']), array('yes', 'yes_permission_override')) ? $row['email_address'] : '',
383
			'href' => $scripturl . '?action=profile;u=' . $row['id_member'],
384
			'link' => '<a href="' . $scripturl . '?action=profile;u=' . $row['id_member'] . '">' . $row['real_name'] . '</a>'
385
		);
386
	}
387
	$db->free_result($request);
388
389
	// Return all the results.
390
	return $results;
391
}
392
393
/**
394
 * Generates a random password for a user and emails it to them.
395
 *
396
 * What it does:
397
 *
398
 * - called by ProfileOptions controller when changing someone's username.
399
 * - checks the validity of the new username.
400
 * - generates and sets a new password for the given user.
401
 * - mails the new password to the email address of the user.
402
 * - if username is not set, only a new password is generated and sent.
403
 *
404
 * @package Authorization
405
 *
406
 * @param int         $memID
407
 * @param string|null $username = null
408
 *
409
 * @throws Elk_Exception
410
 */
411
function resetPassword($memID, $username = null)
412
{
413
	global $modSettings, $language, $user_info;
414
415
	// Language... and a required file.
416
	loadLanguage('Login');
417
	require_once(SUBSDIR . '/Mail.subs.php');
418
419
	// Get some important details.
420
	require_once(SUBSDIR . '/Members.subs.php');
421
	$result = getBasicMemberData($memID, array('preferences' => true));
422
	$user = $result['member_name'];
423
	$email = $result['email_address'];
424
	$lngfile = $result['lngfile'];
425
426
	if ($username !== null)
427
	{
428
		$old_user = $user;
429
		$user = trim($username);
430
	}
431
432
	// Generate a random password.
433
	$tokenizer = new Token_Hash();
434
	$newPassword = $tokenizer->generate_hash(14);
435
436
	// Create a db hash for the generated password
437
	require_once(EXTDIR . '/PasswordHash.php');
438
	$t_hasher = new PasswordHash(8, false);
439
	$newPassword_sha256 = hash('sha256', strtolower($user) . $newPassword);
440
	$db_hash = $t_hasher->HashPassword($newPassword_sha256);
441
442
	// Do some checks on the username if needed.
443
	require_once(SUBSDIR . '/Members.subs.php');
444
	if ($username !== null)
445
	{
446
		$errors = ElkArte\Errors\ErrorContext::context('reset_pwd', 0);
447
		validateUsername($memID, $user, 'reset_pwd');
448
449
		// If there are "important" errors and you are not an admin: log the first error
450
		// Otherwise grab all of them and don't log anything
451
		$error_severity = $errors->hasErrors(1) && !$user_info['is_admin'] ? 1 : null;
452 View Code Duplication
		foreach ($errors->prepareErrors($error_severity) as $error)
453
			throw new Elk_Exception($error, $error_severity === null ? false : 'general');
454
455
		// Update the database...
456
		updateMemberData($memID, array('member_name' => $user, 'passwd' => $db_hash));
457
	}
458
	else
459
		updateMemberData($memID, array('passwd' => $db_hash));
460
461
	call_integration_hook('integrate_reset_pass', array($old_user, $user, $newPassword));
0 ignored issues
show
Bug introduced by
The variable $old_user does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
462
463
	$replacements = array(
464
		'USERNAME' => $user,
465
		'PASSWORD' => $newPassword,
466
	);
467
468
	$emaildata = loadEmailTemplate('change_password', $replacements, empty($lngfile) || empty($modSettings['userLanguage']) ? $language : $lngfile);
469
470
	// Send them the email informing them of the change - then we're done!
471
	sendmail($email, $emaildata['subject'], $emaildata['body'], null, null, false, 0);
472
}
473
474
/**
475
 * Checks a username obeys a load of rules
476
 *
477
 * - Returns null if fine
478
 *
479
 * @package Authorization
480
 * @param int $memID
481
 * @param string $username
482
 * @param string $ErrorContext
483
 * @param boolean $check_reserved_name
484
 * @param boolean $fatal pass through to isReservedName
485
 * @return string
486
 * @throws Elk_Exception
487
 */
488
function validateUsername($memID, $username, $ErrorContext = 'register', $check_reserved_name = true, $fatal = true)
489
{
490 3
	global $txt;
491
492 3
	$errors = ElkArte\Errors\ErrorContext::context($ErrorContext, 0);
493
494
	// Don't use too long a name.
495 3
	if (Util::strlen($username) > 25)
496 3
		$errors->addError('error_long_name');
497
498
	// No name?!  How can you register with no name?
499 3
	if ($username == '')
500 3
		$errors->addError('need_username');
501
502
	// Only these characters are permitted.
503 3
	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)
504 3
		$errors->addError('error_invalid_characters_username');
505
506 3
	if (stristr($username, $txt['guest_title']) !== false)
507 3
		$errors->addError(array('username_reserved', array($txt['guest_title'])), 1);
508
509
	if ($check_reserved_name)
510 3
	{
511 1
		require_once(SUBSDIR . '/Members.subs.php');
512 1
		if (isReservedName($username, $memID, false, $fatal))
513 1
			$errors->addError(array('name_in_use', array(htmlspecialchars($username, ENT_COMPAT, 'UTF-8'))));
514 1
	}
515 3
}
516
517
/**
518
 * Checks whether a password meets the current forum rules
519
 *
520
 * What it does:
521
 *
522
 * - called when registering/choosing a password.
523
 * - checks the password obeys the current forum settings for password strength.
524
 * - if password checking is enabled, will check that none of the words in restrict_in appear in the password.
525
 * - returns an error identifier if the password is invalid, or null.
526
 *
527
 * @package Authorization
528
 * @param string $password
529
 * @param string $username
530
 * @param string[] $restrict_in = array()
531
 * @return string an error identifier if the password is invalid
532
 */
533
function validatePassword($password, $username, $restrict_in = array())
534
{
535 1
	global $modSettings, $txt;
536
537
	// Perform basic requirements first.
538 1
	if (Util::strlen($password) < (empty($modSettings['password_strength']) ? 4 : 8))
539 1
	{
540
		loadLanguage('Errors');
541
		$txt['profile_error_password_short'] = sprintf($txt['profile_error_password_short'], empty($modSettings['password_strength']) ? 4 : 8);
542
		return 'short';
543
	}
544
545
	// Is this enough?
546 1
	if (empty($modSettings['password_strength']))
547 1
		return null;
548
549
	// Otherwise, perform the medium strength test - checking if password appears in the restricted string.
550
	if (preg_match('~\b' . preg_quote($password, '~') . '\b~', implode(' ', $restrict_in)) != 0)
551
		return 'restricted_words';
552
	elseif (Util::strpos($password, $username) !== false)
553
		return 'restricted_words';
554
555
	// If just medium, we're done.
556
	if ($modSettings['password_strength'] == 1)
557
		return null;
558
559
	// Otherwise, hard test next, check for numbers and letters, uppercase too.
560
	$good = preg_match('~(\D\d|\d\D)~', $password) != 0;
561
	$good &= Util::strtolower($password) != $password;
562
563
	return $good ? null : 'chars';
564
}
565
566
/**
567
 * Checks whether an entered password is correct for the user
568
 *
569
 * What it does:
570
 *
571
 * - called when logging in or whenever a password needs to be validated for a user
572
 * - used to generate a new hash for the db, used during registration or any password changes
573
 * - if a non SHA256 password is sent, will generate one with SHA256(user + password) and return it in password
574
 *
575
 * @package Authorization
576
 * @param string $password user password if not already 64 characters long will be SHA256 with the user name
577
 * @param string $hash hash as generated from a SHA256 password
578
 * @param string $user user name only required if creating a SHA-256 password
579
 * @param boolean $returnhash flag to determine if we are returning a hash suitable for the database
580
 */
581
function validateLoginPassword(&$password, $hash, $user = '', $returnhash = false)
582
{
583
	// Our hashing controller
584 5
	require_once(EXTDIR . '/PasswordHash.php');
585
586
	// Base-2 logarithm of the iteration count used for password stretching, the
587
	// higher the number the more secure and CPU time consuming
588 5
	$hash_cost_log2 = 10;
589
590
	// Do we require the hashes to be portable to older systems (less secure)?
591 5
	$hash_portable = false;
592
593
	// Get an instance of the hasher
594 5
	$hasher = new PasswordHash($hash_cost_log2, $hash_portable);
595
596
	// If the password is not 64 characters, lets make it a (SHA-256)
597 5
	if (strlen($password) !== 64)
598 5
		$password = hash('sha256', Util::strtolower($user) . un_htmlspecialchars($password));
599
600
	// They need a password hash, something to save in the db?
601
	if ($returnhash)
602 5
	{
603 3
		$passhash = $hasher->HashPassword($password);
604
605
		// Something is not right, we can not generate a valid hash that's <20 characters
606 3
		if (strlen($passhash) < 20)
607 3
			$passhash = false;
608 3
	}
609
	// Or doing a password check?
610
	else
611 2
	 	$passhash = (bool) $hasher->CheckPassword($password, $hash);
612
613 5
	unset($hasher);
614
615 5
	return $passhash;
616
}
617
618
/**
619
 * Quickly find out what moderation authority this user has
620
 *
621
 * What it does:
622
 *
623
 * - builds the moderator, group and board level querys for the user
624
 * - stores the information on the current users moderation powers in $user_info['mod_cache'] and $_SESSION['mc']
625
 *
626
 * @package Authorization
627
 */
628
function rebuildModCache()
629
{
630
	global $user_info;
631
632
	$db = database();
633
634
	// What groups can they moderate?
635
	$group_query = allowedTo('manage_membergroups') ? '1=1' : '0=1';
636
637
	if ($group_query == '0=1')
638
	{
639
		$groups = $db->fetchQueryCallback('
640
			SELECT id_group
641
			FROM {db_prefix}group_moderators
642
			WHERE id_member = {int:current_member}',
643
			array(
644
				'current_member' => $user_info['id'],
645
			),
646
			function ($row)
647
			{
648
				return $row['id_group'];
649
			}
650
		);
651
652
		if (empty($groups))
653
			$group_query = '0=1';
654
		else
655
			$group_query = 'id_group IN (' . implode(',', $groups) . ')';
656
	}
657
658
	// Then, same again, just the boards this time!
659
	$board_query = allowedTo('moderate_forum') ? '1=1' : '0=1';
660
661
	if ($board_query == '0=1')
662
	{
663
		$boards = boardsAllowedTo('moderate_board', true);
664
665
		if (empty($boards))
666
			$board_query = '0=1';
667
		else
668
			$board_query = 'id_board IN (' . implode(',', $boards) . ')';
669
	}
670
671
	// What boards are they the moderator of?
672
	$boards_mod = array();
673
	if (!$user_info['is_guest'])
674
	{
675
		require_once(SUBSDIR . '/Boards.subs.php');
676
		$boards_mod = boardsModerated($user_info['id']);
677
	}
678
679
	$mod_query = empty($boards_mod) ? '0=1' : 'b.id_board IN (' . implode(',', $boards_mod) . ')';
680
681
	$_SESSION['mc'] = array(
682
		'time' => time(),
683
		// This looks a bit funny but protects against the login redirect.
684
		'id' => $user_info['id'] && $user_info['name'] ? $user_info['id'] : 0,
685
		// If you change the format of 'gq' and/or 'bq' make sure to adjust 'can_mod' in Load.php.
686
		'gq' => $group_query,
687
		'bq' => $board_query,
688
		'ap' => boardsAllowedTo('approve_posts'),
689
		'mb' => $boards_mod,
690
		'mq' => $mod_query,
691
	);
692
	call_integration_hook('integrate_mod_cache');
693
694
	$user_info['mod_cache'] = $_SESSION['mc'];
695
696
	// Might as well clean up some tokens while we are at it.
697
	cleanTokens();
698
}
699
700
/**
701
 * The same thing as setcookie but allows for integration hook
702
 *
703
 * @package Authorization
704
 * @param string $name
705
 * @param string $value = ''
706
 * @param int $expire = 0
707
 * @param string $path = ''
708
 * @param string $domain = ''
709
 * @param boolean|null $secure = false
710
 * @param boolean|null $httponly = null
711
 */
712
function elk_setcookie($name, $value = '', $expire = 0, $path = '', $domain = '', $secure = null, $httponly = null)
713
{
714 1
	global $modSettings;
715
716
	// In case a customization wants to override the default settings
717 1
	if ($httponly === null)
718 1
		$httponly = !empty($modSettings['httponlyCookies']);
719 1
	if ($secure === null)
720 1
		$secure = !empty($modSettings['secureCookies']);
721
722
	// Intercept cookie?
723 1
	call_integration_hook('integrate_cookie', array($name, $value, $expire, $path, $domain, $secure, $httponly));
724
725 1
	return setcookie($name, $value, $expire, $path, $domain, $secure, $httponly);
726
}
727
728
/**
729
 * This functions determines whether this is the first login of the given user.
730
 *
731
 * @package Authorization
732
 * @param int $id_member the id of the member to check for
733
 */
734
function isFirstLogin($id_member)
735
{
736
	// First login?
737
	require_once(SUBSDIR . '/Members.subs.php');
738
	$member = getBasicMemberData($id_member, array('moderation' => true));
739
740
	return !empty($member) && $member['last_login'] == 0;
741
}
742
743
/**
744
 * Search for a member by given criteria
745
 *
746
 * @package Authorization
747
 *
748
 * @param string  $where
749
 * @param mixed[] $where_params array of values to used in the where statement
750
 * @param bool    $fatal
751
 *
752
 * @return array of members data or false on failure
753
 * @throws Elk_Exception no_user_with_email
754
 */
755
function findUser($where, $where_params, $fatal = true)
756
{
757
	$db = database();
758
759
	// Find the user!
760
	$request = $db->query('', '
761
		SELECT id_member, real_name, member_name, email_address, is_activated, validation_code, lngfile, openid_uri, secret_question, passwd
762
		FROM {db_prefix}members
763
		WHERE ' . $where . '
764
		LIMIT 1',
765
		array_merge($where_params, array(
766
		))
767
	);
768
769
	// Maybe email?
770
	if ($db->num_rows($request) == 0 && empty($_REQUEST['uid']) && isset($where_params['email_address']))
771
	{
772
		$db->free_result($request);
773
774
		$request = $db->query('', '
775
			SELECT id_member, real_name, member_name, email_address, is_activated, validation_code, lngfile, openid_uri, secret_question
776
			FROM {db_prefix}members
777
			WHERE email_address = {string:email_address}
778
			LIMIT 1',
779
			array_merge($where_params, array(
780
			))
781
		);
782
		if ($db->num_rows($request) == 0)
783
		{
784
			if ($fatal)
785
				throw new Elk_Exception('no_user_with_email', false);
786
			else
787
				return false;
788
		}
789
	}
790
791
	$member = $db->fetch_assoc($request);
792
	$db->free_result($request);
793
794
	return $member;
795
}
796
797
/**
798
 * Find users by their email address.
799
 *
800
 * @package Authorization
801
 * @param string $email
802
 * @param string|null $username
803
 * @return boolean
804
 */
805
function userByEmail($email, $username = null)
806
{
807 4
	$db = database();
808
809 4
	$request = $db->query('', '
810
		SELECT id_member
811
		FROM {db_prefix}members
812 4
		WHERE email_address = {string:email_address}' . ($username === null ? '' : '
813 4
			OR email_address = {string:username}') . '
814 4
		LIMIT 1',
815
		array(
816 4
			'email_address' => $email,
817 4
			'username' => $username,
818
		)
819 4
	);
820
821 4
	$return = $db->num_rows($request) != 0;
822 4
	$db->free_result($request);
823
824 4
	return $return;
825
}
826
827
/**
828
 * Generate a random validation code.
829
 *
830
 * @package Authorization
831
 * @param int $length the number of characters to return
832
 */
833
function generateValidationCode($length = 10)
834
{
835 1
	$tokenizer = new Token_Hash();
836
837 1
	return $tokenizer->generate_hash((int) $length);
838
}
839
840
/**
841
 * This function loads many settings of a user given by name or email.
842
 *
843
 * @package Authorization
844
 * @param string $name
845
 * @param bool $is_id if true it treats $name as a member ID and try to load the data for that ID
846
 * @return mixed[]|false false if nothing is found
847
 */
848
function loadExistingMember($name, $is_id = false)
849
{
850 2
	$db = database();
851
852
	if ($is_id)
853 2
	{
854 1
		$request = $db->query('', '
855
			SELECT passwd, id_member, id_group, lngfile, is_activated, email_address, additional_groups, member_name, password_salt,
856
				openid_uri, passwd_flood, otp_secret, enable_otp, otp_used
857
			FROM {db_prefix}members
858
			WHERE id_member = {int:id_member}
859 1
			LIMIT 1',
860
			array(
861 1
				'id_member' => (int) $name,
862
			)
863 1
		);
864 1
	}
865
	else
866
	{
867
		// Try to find the user, assuming a member_name was passed...
868 1
		$request = $db->query('', '
869
			SELECT passwd, id_member, id_group, lngfile, is_activated, email_address, additional_groups, member_name, password_salt,
870
				openid_uri, passwd_flood, otp_secret, enable_otp, otp_used
871
			FROM {db_prefix}members
872 1
			WHERE ' . (defined('DB_CASE_SENSITIVE') ? 'LOWER(member_name) = LOWER({string:user_name})' : 'member_name = {string:user_name}') . '
873 1
			LIMIT 1',
874
			array(
875 1
				'user_name' => defined('DB_CASE_SENSITIVE') ? strtolower($name) : $name,
876
			)
877 1
		);
878
		// Didn't work. Try it as an email address.
879 1
		if ($db->num_rows($request) == 0 && strpos($name, '@') !== false)
880 1
		{
881
			$db->free_result($request);
882
883
			$request = $db->query('', '
884
				SELECT passwd, id_member, id_group, lngfile, is_activated, email_address, additional_groups, member_name, password_salt, openid_uri,
885
				passwd_flood, otp_secret, enable_otp, otp_used
886
				FROM {db_prefix}members
887
				WHERE email_address = {string:user_name}
888
				LIMIT 1',
889
				array(
890
					'user_name' => $name,
891
				)
892
			);
893
		}
894
	}
895
896
	// Nothing? Ah the horror...
897 2
	if ($db->num_rows($request) == 0)
898 2
		$user_settings = false;
899
	else
900
	{
901 2
		$user_settings = $db->fetch_assoc($request);
902 2
		$user_settings['id_member'] = (int) $user_settings['id_member'];
903
	}
904
905 2
	$db->free_result($request);
906
907 2
	return $user_settings;
908
}
909