Issues (1686)

sources/ElkArte/Controller/Auth.php (15 issues)

1
<?php
2
3
/**
4
 * This file is concerned pretty entirely, as you see from its name, with
5
 * logging in and out members, and the validation of that.
6
 *
7
 * @package   ElkArte Forum
8
 * @copyright ElkArte Forum contributors
9
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause (see accompanying LICENSE.txt file)
10
 *
11
 * This file contains code covered by:
12
 * copyright: 2011 Simple Machines (http://www.simplemachines.org)
13
 *
14
 * @version 2.0 dev
15
 *
16
 */
17
18
namespace ElkArte\Controller;
19
20
use ElkArte\AbstractController;
21
use ElkArte\Cache\Cache;
22
use ElkArte\Errors\Errors;
0 ignored issues
show
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...
23
use ElkArte\Exceptions\Exception;
24
use ElkArte\Helper\Util;
25
use ElkArte\Http\Headers;
26
use ElkArte\Languages\Txt;
27
use ElkArte\Request;
28
use ElkArte\User;
29
use ElkArte\UserSettingsLoader;
30
31
/**
32
 * Deals with logging in and out members, and the validation of them
33
 *
34
 * @package Authorization
35
 */
36
class Auth extends AbstractController
37
{
38
	/**
39
	 * {@inheritDoc}
40
	 */
41
	public function needSecurity($action = '')
42
	{
43
		return $action !== 'action_keepalive';
44
	}
45
46
	/**
47
	 * Entry point in Auth controller
48
	 *
49
	 * - (well no, not really. We route directly to the rest.)
50 2
	 *
51
	 * @see AbstractController::action_index
52
	 */
53 2
	public function action_index()
54 2
	{
55
		// What can we do? login page!
56
		$this->action_login();
57
	}
58
59
	/**
60
	 * Ask them for their login information.
61
	 *
62
	 * What it does:
63
	 *  - Shows a page for the user to type in their username and password.
64
	 *  - It caches the referring URL in $_SESSION['login_url'].
65
	 *  - It is accessed from ?action=login.
66 2
	 *
67
	 * @uses Login template and language file with the login sub-template.
68 2
	 */
69
	public function action_login()
70
	{
71 2
		global $txt, $context;
72
73
		// You are already logged in, go take a tour of the boards
74
		if (!empty($this->user->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...
75
		{
76
			redirectexit();
77 2
		}
78 2
79 2
		// Load the Login template/language file.
80 2
		Txt::load('Login');
81
82
		// If API, clear out the header/footer and only return the form
83 2
		if ($this->getApi())
84 2
		{
85 2
			$template_layers = theme()->getLayers();
86 2
			$template_layers->removeAll();
87 2
		}
88 2
89
		theme()->getTemplates()->load('Login');
90
		$context['sub_template'] = 'login';
91 2
92 2
		// Get the template ready.... not really much else to do.
93 2
		$context['page_title'] = $txt['login'];
94
		$_REQUEST['u'] = isset($_REQUEST['u']) ? Util::htmlspecialchars($_REQUEST['u']) : '';
95
		$context['default_username'] = &$_REQUEST['u'];
96
		$context['default_password'] = '';
97 2
		$context['never_expire'] = false;
98
99
		// Add the login chain to the link tree.
100
		$context['breadcrumbs'][] = [
101
			'url' => getUrl('action', ['action' => 'login']),
102
			'name' => $txt['login'],
103 2
		];
104
105
		// Set the login URL - will be used when the login process is done (but careful not to send us to an attachment).
106
		if (isset($_SESSION['old_url']) && validLoginUrl($_SESSION['old_url'], true))
107 2
		{
108 2
			$_SESSION['login_url'] = $_SESSION['old_url'];
109
		}
110
		else
111
		{
112
			unset($_SESSION['login_url']);
113
		}
114
115
		// Create a one time token.
116
		createToken('login');
117
	}
118
119
	/**
120
	 * Actually logs you in.
121
	 *
122
	 * What it does:
123
	 *
124 2
	 * - Checks credentials and checks that login was successful.
125
	 * - It employs protection against a specific IP or user trying to brute force
126 2
	 *   a login to an account.
127
	 * - Upgrades password encryption on login, if necessary.
128
	 * - After successful login, redirects you to $_SESSION['login_url'].
129 2
	 * - Accessed from ?action=login2, by forms.
130
	 *
131
	 * @uses the same templates action_login()
132 2
	 */
133
	public function action_login2()
134
	{
135
		global $txt, $modSettings, $context;
136
137
		// Load cookie authentication and all stuff.
138 2
		require_once(SUBSDIR . '/Auth.subs.php');
139 2
140 2
		// Beyond this point you are assumed to be a guest trying to login.
141
		if (empty(User::$info->is_guest))
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...
142
		{
143 2
			redirectexit();
144
		}
145
146
		// Are you guessing with a script?
147
		checkSession();
148
		validateToken('login');
149 2
		spamProtection('login');
150
151
		// Set the login_url if it's not already set (but careful not to send us to an attachment).
152
		if (empty($_SESSION['login_url']) && isset($_SESSION['old_url']) && validLoginUrl($_SESSION['old_url'], true))
153
		{
154
			$_SESSION['login_url'] = $_SESSION['old_url'];
155 2
		}
156
157
		// Been guessing a lot, haven't we?
158
		if (isset($_SESSION['failed_login']) && $_SESSION['failed_login'] >= $modSettings['failed_login_threshold'] * 3)
159 2
		{
160
			throw new Exception('login_threshold_fail', 'critical');
161
		}
162
163
		// Set up the cookie length.  (if it's invalid, just fall through and use the default.)
164 2
		if (isset($_POST['cookieneverexp']))
165
		{
166
			$modSettings['cookieTime'] = 3153600;
167 2
		}
168 2
169 2
		Txt::load('Login');
170
171
		// Load the template stuff
172 2
		theme()->getTemplates()->load('Login');
173 2
		$context['sub_template'] = 'login';
174 2
175 2
		// Set up the default/fallback stuff.
176 2
		$context['default_username'] = isset($_POST['user']) ? preg_replace('~&amp;#(\\d{1,7}|x[0-9a-fA-F]{1,6});~', '&#\\1;', htmlspecialchars($_POST['user'], ENT_COMPAT, 'UTF-8')) : '';
177
		$context['default_password'] = '';
178
		$context['never_expire'] = $modSettings['cookieTime'] === 525600 || $modSettings['cookieTime'] === 3153600;
179 2
		$context['login_errors'] = array($txt['error_occurred']);
180 2
		$context['page_title'] = $txt['login'];
181 2
182
		// Add the login chain to the link tree.
183
		$context['breadcrumbs'][] = [
184
			'url' => getUrl('action', ['action' => 'login']),
185 2
			'name' => $txt['login'],
186
		];
187
188
		// You forgot to type your username, dummy!
189
		if (!isset($_POST['user']) || $_POST['user'] === '')
190
		{
191
			$context['login_errors'] = array($txt['need_username']);
192
193
			return false;
194
		}
195
196
		// No one needs a username that long, plus we only support 80 chars in the db
197
		if (Util::strlen($_POST['user']) > 80)
198
		{
199
			$_POST['user'] = Util::substr($_POST['user'], 0, 80);
200
		}
201
202 2
		// Can't use a password > 64 characters sorry, to long and only good for a DoS attack
203
		if (isset($_POST['passwrd']) && strlen($_POST['passwrd']) > 64)
204
		{
205
			$context['login_errors'] = array($txt['improper_password']);
206
207
			return false;
208
		}
209
210 2
		// Hmm... maybe 'admin' will login with no password. Uhh... NO!
211
		if (!isset($_POST['passwrd']) || $_POST['passwrd'] === '')
212
		{
213
			$context['login_errors'] = array($txt['no_password']);
214
215
			return false;
216
		}
217 2
218
		// No funky symbols either.
219
		if (preg_match('~[<>&"\'=\\\]~', preg_replace('~(&#(\\d{1,7}|x[0-9a-fA-F]{1,6});)~', '', $_POST['user'])) != 0)
220
		{
221
			$context['login_errors'] = array($txt['error_invalid_characters_username']);
222
223
			return false;
224
		}
225 2
226
		// Are we using any sort of integration to validate the login?
227
		if (in_array('retry', call_integration_hook('integrate_validate_login', array($_POST['user'], null, $modSettings['cookieTime'])), true))
228
		{
229
			$context['login_errors'] = array($txt['login_hash_error']);
230
231
			return false;
232
		}
233 2
234
		// Find them... if we can
235
		$member_found = loadExistingMember($_POST['user']);
236
		$db = database();
237
		$cache = Cache::instance();
238
		$req = Request::instance();
239
240
		$user = new UserSettingsLoader($db, $cache, $req);
241 2
		$user->loadUserById($member_found === false ? 0 : $member_found['id_member'], true, '');
242
243
		$user_setting = $user->getSettings();
244
245
		// User using 2FA for login? Let's validate the token...
246
		if (!empty($modSettings['enableOTP']) && !empty($user_setting['enable_otp']) && empty($_POST['otp_token']))
247
		{
248
			$context['login_errors'] = array($txt['otp_required']);
249
250 2
			return false;
251 2
		}
252 2
253 2
		if (!empty($_POST['otp_token']))
254
		{
255 2
			require_once(EXTDIR . '/GoogleAuthenticator.php');
256 2
			$ga = new \GoogleAuthenticator();
0 ignored issues
show
The type GoogleAuthenticator 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...
257 2
258
			$ga->getCode($user_setting['otp_secret']);
259
			$checkResult = $ga->verifyCode($user_setting['otp_secret'], $_POST['otp_token'], 2);
260 2
			if (!$checkResult)
261
			{
262
				$context['login_errors'] = array($txt['invalid_otptoken']);
263
264
				return false;
265
			}
266
267 2
			// OTP already used? Sorry, but this is a ONE TIME password..
268
			if ($user_setting['otp_used'] === $_POST['otp_token'])
269
			{
270
				$context['login_errors'] = array($txt['otp_used']);
271
272
				return false;
273
			}
274
		}
275
276
		// Let them try again, it didn't match anything...
277
		if (empty($member_found))
278
		{
279
			$context['login_errors'] = array($txt['username_no_exist']);
280
281
			return false;
282
		}
283
284
		// validateLoginPassword will hash this and check its valid
285
		$sha_passwd = $_POST['passwrd'];
286
		$valid_password = $user->validatePassword($sha_passwd);
287
288
		require_once(SUBSDIR . '/Members.subs.php');
289
290 2
		// Bad password!  Thought you could fool the database?!
291
		if (!$valid_password)
292 2
		{
293
			// Let's be cautious, no hacking please. thanx.
294 2
			validatePasswordFlood($user_setting['id_member'], $user_setting['passwd_flood']);
295
296
			// Maybe we were too hasty... let's try some other authentication methods.
297
			$other_passwords = $this->_other_passwords($_POST['passwrd'], $user_setting['password_salt'], $user_setting['passwd'], $user_setting['member_name']);
298
299
			// Whichever encryption it was using, let's make it use ElkArte's now ;).
300
			if (in_array($user_setting['passwd'], $other_passwords, true))
301
			{
302
				$user->rehashPassword($sha_passwd);
303
304
				// Update the password hash and set up the salt.
305
				updateMemberData($user_setting['id_member'], array('passwd' => $user_setting['passwd'], 'password_salt' => $user_setting['password_salt'], 'passwd_flood' => ''));
306
			}
307
			// Okay, they for sure didn't enter the password!
308
			else
309
			{
310
				// They've messed up again - keep a count to see if they need a hand.
311
				$_SESSION['failed_login'] = isset($_SESSION['failed_login']) ? ($_SESSION['failed_login'] + 1) : 1;
312
313
				// Hmm... don't remember it, do you?  Here, try the password reminder ;).
314
				if ($_SESSION['failed_login'] >= $modSettings['failed_login_threshold'])
315
				{
316
					redirectexit('action=reminder');
317
				}
318
				// We'll give you another chance...
319
				else
320
				{
321
					// Log an error so we know that it didn't go well in the error log.
322
					Errors::instance()->log_error($txt['incorrect_password'] . ' - <span class="remove">' . $user_setting['member_name'] . '</span>', 'user');
323
324
					$context['login_errors'] = array($txt['incorrect_password']);
325
326
					return false;
327
				}
328
			}
329
		}
330
		elseif (!empty($user_setting['passwd_flood']))
331
		{
332
			// Let's be sure they weren't a little hacker.
333
			validatePasswordFlood($user_setting['id_member'], $user_setting['passwd_flood'], true);
334
335
			// If we got here then we can reset the flood counter.
336
			updateMemberData($user_setting['id_member'], array('passwd_flood' => ''));
337
		}
338
339
		if ($user_setting->fixSalt() === true)
340
		{
341
			updateMemberData($user_setting['id_member'], array('password_salt' => $user_setting['password_salt']));
342
		}
343
344
		// Let's track the last used one-time password.
345
		if (!empty($_POST['otp_token']))
346
		{
347
			updateMemberData($user_setting['id_member'], array('otp_used' => (int) $_POST['otp_token']));
348
		}
349
350
		// Check their activation status.
351
		if ($user->checkActivation(isset($_REQUEST['undelete'])))
352
		{
353
			doLogin($user);
354
		}
355
356
		return false;
357
	}
358
359
	/**
360
	 * Loads other possible password hash / crypts using the post data
361
	 *
362
	 * What it does:
363
	 *
364
	 * - Used when a board is converted to see if the user credentials and a 3rd
365
	 * party hash satisfy whats in the db passwd field
366
	 *
367
	 * @param string $member_name
368
	 * @param string $passwrd
369
	 * @param string $password_salt
370
	 *
371
	 * @return array
372
	 */
373
	private function _other_passwords($posted_password, $member_name, $passwrd, $password_salt)
374
	{
375
		global $modSettings;
376
377
		// What kind of data are we dealing with
378
		$pw_strlen = strlen($passwrd);
379
380
		// Start off with none, that's safe
381
		$other_passwords = [];
382
383
		if (empty($modSettings['enable_password_conversion']))
384
		{
385
			return $other_passwords;
386
		}
387
388
		// None of the below cases will be used most of the time (because the salt is normally set.)
389
		if ($password_salt === '')
390
		{
391
			// YaBB SE, Discus, MD5 (used a lot), SHA-1 (used some), SMF 1.0.x, IkonBoard, and none at all.
392
			$other_passwords[] = crypt($posted_password, substr($posted_password, 0, 2));
393
			$other_passwords[] = crypt($posted_password, substr($passwrd, 0, 2));
394
			$other_passwords[] = md5($posted_password);
395
			$other_passwords[] = sha1($posted_password);
396
			$other_passwords[] = md5_hmac($posted_password, strtolower($member_name));
397
			$other_passwords[] = md5($posted_password . strtolower($member_name));
398
			$other_passwords[] = md5(md5($posted_password));
399
			$other_passwords[] = $posted_password;
400
401
			// This one is a strange one... MyPHP, crypt() on the MD5 hash.
402
			$other_passwords[] = crypt(md5($posted_password), md5($posted_password));
403
404
			// SHA-256
405
			if ($pw_strlen === 64)
406
			{
407
				// Snitz style
408
				$other_passwords[] = bin2hex(hash('sha256', $posted_password, true));
409
410
				// Normal SHA-256
411
				$other_passwords[] = hash('sha256', $posted_password);
412
			}
413
414
			// phpBB3 users new hashing.  We now support it as well ;).
415
			$other_passwords[] = phpBB3_password_check($posted_password, $passwrd);
416
417
			// APBoard 2 Login Method.
418
			$other_passwords[] = md5(crypt($posted_password, 'CRYPT_MD5'));
419
420
			// Xenforo 1.2+
421
			$other_passwords[] = crypt($posted_password, $passwrd);
422
		}
423
		// The hash should be 40 if it's SHA-1, so we're safe with more here too.
424
		elseif ($pw_strlen === 32)
425
		{
426
			// vBulletin 3 style hashing?  Let's welcome them with open arms \o/.
427
			$other_passwords[] = md5(md5($posted_password) . stripslashes($password_salt));
428
429
			// Hmm.. p'raps it's Invision 2 style?
430
			$other_passwords[] = md5(md5($password_salt) . md5($posted_password));
431
432
			// Some common md5 ones.
433
			$other_passwords[] = md5($password_salt . $posted_password);
434
			$other_passwords[] = md5($posted_password . $password_salt);
435
		}
436
		// The hash is 40 characters, lets try some SHA-1 style auth
437
		elseif ($pw_strlen === 40)
438
		{
439
			// Maybe they are using a hash from before our password upgrade
440
			$other_passwords[] = sha1(strtolower($member_name) . un_htmlspecialchars($posted_password));
441
			$other_passwords[] = sha1($passwrd . $_SESSION['session_value']);
442
443
			if (!empty($modSettings['enable_password_conversion']))
444
			{
445
				// BurningBoard3 style of hashing.
446
				$other_passwords[] = sha1($password_salt . sha1($password_salt . sha1($posted_password)));
447
448
				// PunBB 1.4 and later
449
				$other_passwords[] = sha1($password_salt . sha1($posted_password));
450
			}
451
452
			// Perhaps we converted from a non UTF-8 db and have a valid password being hashed differently.
453
			if (!empty($modSettings['previousCharacterSet']) && $modSettings['previousCharacterSet'] !== 'utf8')
454
			{
455
				// Try iconv first, for no particular reason.
456
				if (function_exists('iconv'))
457
				{
458
					$other_passwords['iconv'] = sha1(strtolower(iconv('UTF-8', $modSettings['previousCharacterSet'], $member_name)) . un_htmlspecialchars(iconv('UTF-8', $modSettings['previousCharacterSet'], $posted_password)));
459
				}
460
461
				// Say it aint so, iconv failed!
462
				if (empty($other_passwords['iconv']) && function_exists('mb_convert_encoding'))
463
				{
464
					$other_passwords[] = sha1(strtolower(mb_convert_encoding($member_name, 'UTF-8', $modSettings['previousCharacterSet'])) . un_htmlspecialchars(mb_convert_encoding($posted_password, 'UTF-8', $modSettings['previousCharacterSet'])));
0 ignored issues
show
It seems like mb_convert_encoding($mem...previousCharacterSet']) can also be of type array; however, parameter $string of strtolower() 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

464
					$other_passwords[] = sha1(strtolower(/** @scrutinizer ignore-type */ mb_convert_encoding($member_name, 'UTF-8', $modSettings['previousCharacterSet'])) . un_htmlspecialchars(mb_convert_encoding($posted_password, 'UTF-8', $modSettings['previousCharacterSet'])));
Loading history...
It seems like mb_convert_encoding($pos...previousCharacterSet']) can also be of type array; however, parameter $string of un_htmlspecialchars() 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

464
					$other_passwords[] = sha1(strtolower(mb_convert_encoding($member_name, 'UTF-8', $modSettings['previousCharacterSet'])) . un_htmlspecialchars(/** @scrutinizer ignore-type */ mb_convert_encoding($posted_password, 'UTF-8', $modSettings['previousCharacterSet'])));
Loading history...
465
				}
466
			}
467
		}
468
		// SHA-256 will be 64 characters long, lets check some of these possibilities
469
		elseif ($pw_strlen === 64)
470
		{
471
			// PHP-Fusion7
472
			$other_passwords[] = hash_hmac('sha256', $posted_password, $password_salt);
473
474
			// Plain SHA-256?
475
			$other_passwords[] = hash('sha256', $posted_password . $password_salt);
476
477
			// Xenforo?
478
			$other_passwords[] = sha1(sha1($posted_password) . $password_salt);
479
			$other_passwords[] = hash('sha256', (hash('sha256', ($posted_password) . $password_salt)));
480
		}
481
482
		// ElkArte's sha1 function can give a funny result on Linux (Not our fault!). If we've now got the real one let the old one be valid!
483
		if (strpos(PHP_OS_FAMILY, 'Win') !== 0)
484
		{
485
			$other_passwords[] = bin2hex(hash('sha1', strtolower($member_name) . un_htmlspecialchars($posted_password), true));
486
		}
487
488
		// Allows mods to easily extend the $other_passwords array
489
		call_integration_hook('integrate_other_passwords', array(&$other_passwords));
490
491
		return $other_passwords;
492
	}
493
494
	/**
495
	 * Logs the current user out of their account.
496
	 *
497
	 * What it does:
498
	 *
499
	 * - It requires that the session hash is sent as well, to prevent automatic logouts by images or javascript.
500
	 * - It redirects back to $_SESSION['logout_url'], if it exists.
501
	 * - It is accessed via ?action=logout;session_var=...
502
	 *
503
	 * @param bool $internal if true, it doesn't check the session
504
	 * @param bool $redirect if true, redirect to the board index
505
	 * @throws \ElkArte\Exceptions\Exception
506
	 */
507
	public function action_logout($internal = false, $redirect = true)
508
	{
509
		// Make sure they aren't being auto-logged out.
510
		if (!$internal)
511
		{
512
			checkSession('get');
513
		}
514
515
		require_once(SUBSDIR . '/Auth.subs.php');
516
517
		if (isset($_SESSION['ftp_connection']))
518
		{
519
			$_SESSION['ftp_connection'] = null;
520
		}
521
522
		// It won't be first login anymore.
523
		unset($_SESSION['first_login']);
524
525
		// Just ensure they aren't a guest!
526
		if (empty(User::$info['is_guest']))
527
		{
528
			// Pass the logout information to integrations.
529
			call_integration_hook('integrate_logout', array(User::$settings['member_name']));
530
531
			// If you log out, you aren't online anymore :P.
532
			require_once(SUBSDIR . '/Logging.subs.php');
533
			logOnline(User::$info['id'], false);
534
		}
535
536
		// Logout? Let's kill the admin/moderate/other sessions, too.
537
		$types = array('admin', 'moderate');
538
		call_integration_hook('integrate_validateSession', array(&$types));
539
		foreach ($types as $type)
540
		{
541
			unset($_SESSION[$type . '_time']);
542
		}
543
544
		$_SESSION['log_time'] = 0;
545
546
		// Empty the cookie! (set it in the past, and for id_member = 0)
547
		setLoginCookie(-3600, 0);
548
549
		// And some other housekeeping while we're at it.
550
		session_destroy();
551
		if (!empty(User::$info['id']))
552
		{
553
			User::$settings->fixSalt(true);
0 ignored issues
show
The method fixSalt() does not exist on ElkArte\Helper\ValuesContainerReadOnly. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

553
			User::$settings->/** @scrutinizer ignore-call */ 
554
                    fixSalt(true);
Loading history...
554
			require_once(SUBSDIR . '/Members.subs.php');
555
			updateMemberData(User::$info['id'], array('password_salt' => User::$settings['password_salt']));
556
		}
557
558
		// Off to the merry board index we go!
559
		if ($redirect)
560
		{
561
			if (empty($_SESSION['logout_url']))
562
			{
563
				redirectexit();
564
			}
565
			elseif ((strpos($_SESSION['logout_url'], 'http://') !== 0 && strpos($_SESSION['logout_url'], 'https://') !== 0))
566
			{
567
				unset($_SESSION['logout_url']);
568
				redirectexit();
569
			}
570
			else
571
			{
572
				$temp = $_SESSION['logout_url'];
573
				unset($_SESSION['logout_url']);
574
575
				redirectexit($temp);
576
			}
577
		}
578
	}
579
580
	/**
581
	 * Throws guests out to the login screen when guest access is off.
582
	 *
583
	 * What it does:
584
	 *
585
	 * - It sets $_SESSION['login_url'] to $_SERVER['REQUEST_URL'].
586
	 *
587
	 * @uses 'kick_guest' sub template found in Login.template.php.
588
	 */
589
	public function action_kickguest()
590
	{
591
		global $txt, $context;
592
593
		Txt::load('Login');
594
		theme()->getTemplates()->load('Login');
595
		createToken('login');
596
597
		// Never redirect to an attachment
598
		if (validLoginUrl($_SERVER['REQUEST_URL']))
599
		{
600
			$_SESSION['login_url'] = $_SERVER['REQUEST_URL'];
601
		}
602
603
		$context['sub_template'] = 'kick_guest';
604
		$context['page_title'] = $txt['login'];
605
		$context['default_password'] = '';
606
	}
607
608
	/**
609
	 * Display a message about the forum being in maintenance mode.
610
	 *
611
	 * What it does:
612
	 *
613
	 * - Displays a login screen with sub template 'maintenance'.
614
	 * - It sends a 503 header, so search engines don't index while we're in maintenance mode.
615
	 */
616
	public function action_maintenance_mode()
617
	{
618
		global $txt, $mtitle, $mmessage, $context;
619
620
		Txt::load('Login');
621
		theme()->getTemplates()->load('Login');
622
		createToken('login');
623
624
		// Send a 503 header, so search engines don't bother indexing while we're in maintenance mode.
625
		Headers::instance()
626
			->headerSpecial('HTTP/1.1 503 Service Temporarily Unavailable')
627
			->header('Status', '503 Service Temporarily Unavailable')
628
			->header('Retry-After', '3600');
629
630
		// Basic template stuff..
631
		$context['sub_template'] = 'maintenance';
632
		$context['title'] = &$mtitle;
633
		$context['description'] = un_htmlspecialchars($mmessage);
634
		$context['page_title'] = $txt['maintain_mode'];
635
	}
636
637
	/**
638
	 * Double check the cookie.
639
	 */
640
	public function action_check()
641
	{
642
		// Only our members, please.
643
		if ($this->user->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...
644
		{
645
			// Strike!  You're outta there!
646
			if ($_GET['member'] != $this->user->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...
647
			{
648
				throw new Exception('login_cookie_error', false);
649
			}
650
651
			// Some pass listing for login_url...
652
			$temp = empty($_SESSION['login_url']) || validLoginUrl($_SESSION['login_url']) === false ? '' : $_SESSION['login_url'];
653
			unset($_SESSION['login_url']);
654
			redirectexit($temp);
655
		}
656
657 2
		// It'll never get here... until it does :P
658
		redirectexit();
659 2
	}
660
661 2
	/**
662 2
	 * Ping the server to keep the session alive and not let it disappear.
663 2
	 */
664 2
	public function action_keepalive()
665
	{
666
		dieGif();
667 2
	}
668
}
669 2
670
/**
671
 * Check activation status of the current user.
672 2
 *
673 2
 * What it does:
674 2
 *
675 2
 * is_activated value key is as follows:
676
 *  - > 10 Banned with activation status as value - 10
677
 *  - 5 = Awaiting COPPA consent
678
 *  - 4 = Awaiting Deletion approval
679
 *  - 3 = Awaiting Admin approval
680
 *  - 2 = Awaiting reactivation from email change
681
 *  - 1 = Approved and active
682
 *  - 0 = Not active
683
 *
684
 * @package Authorization
685
 */
686
function checkActivation()
687
{
688
	global $context, $txt, $modSettings;
689
690
	if (!isset($context['login_errors']))
691
	{
692
		$context['login_errors'] = array();
693
	}
694
695
	// What is the true activation status of this account?
696
	$activation_status = User::$settings->getActivationStatus();
0 ignored issues
show
The method getActivationStatus() does not exist on ElkArte\Helper\ValuesContainerReadOnly. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

696
	/** @scrutinizer ignore-call */ 
697
 $activation_status = User::$settings->getActivationStatus();
Loading history...
697
698
	// Check if the account is activated - COPPA first...
699
	if ($activation_status === 5)
700
	{
701
		$context['login_errors'][] = $txt['coppa_no_concent'] . ' <a href="' . getUrl('action', ['action' => 'register', 'sa' => 'coppa', 'member' => User::$settings['id_member']]) . '">' . $txt['coppa_need_more_details'] . '</a>';
702
703
		return false;
704
	}
705
706
	// Awaiting approval still?
707
	if ($activation_status === 3)
708
	{
709
		throw new Exception('still_awaiting_approval', 'user');
710
	}
711
712
	if ($activation_status === 4)
713
	{
714
		if (isset($_REQUEST['undelete']))
715
		{
716
			require_once(SUBSDIR . '/Members.subs.php');
717
			updateMemberData(User::$settings['id_member'], array('is_activated' => 1));
718
			updateSettings(array('unapprovedMembers' => ($modSettings['unapprovedMembers'] > 0 ? $modSettings['unapprovedMembers'] - 1 : 0)));
719
		}
720
		else
721
		{
722
			$context['login_errors'][] = $txt['awaiting_delete_account'];
723
			$context['login_show_undelete'] = true;
724
725
			return false;
726
		}
727
	}
728
	// Awaiting deletion, changed their mind?
729
	// Standard activation?
730
	elseif ($activation_status !== 1)
731
	{
732
		Errors::instance()->log_error($txt['activate_not_completed1'] . ' - <span class="remove">' . User::$settings['member_name'] . '</span>', false);
733
734
		$context['login_errors'][] = $txt['activate_not_completed1'] . ' <a class="linkbutton" href="' . getUrl('action', ['action' => 'register', 'sa' => 'activate', 'resend', 'u' => User::$settings['id_member']]) . '">' . $txt['activate_not_completed2'] . '</a>';
735
736
		return false;
737
	}
738
739
	return true;
740
}
741
742
/**
743
 * This function performs the logging in.
744
 *
745
 * What it does:
746
 *  - It sets the cookie, it call hooks, updates runtime settings for the user.
747
 *
748
 * @param UserSettingsLoader $user
749
 *
750
 * @throws Exception
751
 * @package Authorization
752
 */
753
function doLogin(UserSettingsLoader $user)
754
{
755
	global $maintenance, $modSettings, $context;
756
757
	// Load authentication stuffs.
758
	require_once(SUBSDIR . '/Auth.subs.php');
759
760
	User::reloadByUser($user, true);
761
762
	// Call login integration functions.
763
	call_integration_hook('integrate_login', array(User::$settings['member_name'], $modSettings['cookieTime']));
764
765
	// Bam!  Cookie set.  A session too, just in case.
766
	setLoginCookie(60 * $modSettings['cookieTime'], User::$settings['id_member'], hash('sha256', (User::$settings['passwd'] . User::$settings['password_salt'])));
767
768
	// Reset the login threshold.
769
	if (isset($_SESSION['failed_login']))
770
	{
771
		unset($_SESSION['failed_login']);
772
	}
773
774
	// Are you banned?
775
	is_not_banned(true);
776
777
	// Don't stick the language or theme after this point.
778
	unset($_SESSION['language'], $_SESSION['theme']);
779
780
	// We want to know if this is first login
781
	if (User::$info->isFirstLogin())
0 ignored issues
show
The method isFirstLogin() does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

781
	if (User::$info->/** @scrutinizer ignore-call */ isFirstLogin())
Loading history...
782
	{
783
		$_SESSION['first_login'] = true;
784
	}
785
	else
786
	{
787
		unset($_SESSION['first_login']);
788
	}
789
790
	// You're one of us: need to know all about you now, IP, stuff.
791
	$req = Request::instance();
792
793
	// You've logged in, haven't you?
794
	require_once(SUBSDIR . '/Members.subs.php');
795
	updateMemberData(User::$info->id, array('last_login' => time(), 'member_ip' => User::$info->ip, 'member_ip2' => $req->ban_ip()));
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...
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...
796
797
	// Get rid of the online entry for that old guest....
798
	require_once(SUBSDIR . '/Logging.subs.php');
799
	deleteOnline('ip' . User::$info->ip);
800
	$_SESSION['log_time'] = 0;
801
802
	// Log this entry, only if we have it enabled.
803
	if (!empty($modSettings['loginHistoryDays']))
804
	{
805
		logLoginHistory(User::$info->id, User::$info->ip, User::$info->ip2);
0 ignored issues
show
Bug Best Practice introduced by
The property ip2 does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
806
	}
807
808
	// Just log you back out if it's in maintenance mode and you AREN'T an admin.
809
	if (empty($maintenance) || allowedTo('admin_forum'))
810
	{
811
		redirectexit('action=auth;sa=check;member=' . User::$info->id);
812
	}
813
	else
814
	{
815
		redirectexit('action=logout;' . $context['session_var'] . '=' . $context['session_id']);
816
	}
817
}
818
819
/**
820
 * MD5 Encryption used for older passwords. (SMF 1.0.x/YaBB SE 1.5.x hashing)
821
 *
822
 * @param string $data
823
 * @param string $key
824
 * @return string the HMAC MD5 of data with key
825
 * @package Authorization
826
 */
827
function md5_hmac($data, $key)
828
{
829
	$key = str_pad(strlen($key) <= 64 ? $key : pack('H*', md5($key)), 64, chr(0x00));
830
831
	return md5(($key ^ str_repeat(chr(0x5c), 64)) . pack('H*', md5(($key ^ str_repeat(chr(0x36), 64)) . $data)));
832
}
833
834
/**
835
 * Custom encryption for phpBB3 based passwords.
836
 *
837
 * @param string $passwd
838
 * @param string $passwd_hash
839
 * @return string
840
 * @package Authorization
841
 */
842
function phpBB3_password_check($passwd, $passwd_hash)
843
{
844
	// Too long or too short?
845
	if (strlen($passwd_hash) !== 34)
846
	{
847
		return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type string.
Loading history...
848
	}
849
850
	// Range of characters allowed.
851
	$range = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
852
853
	// Tests
854
	$strpos = strpos($range, $passwd_hash[3]);
855
	$count = 1 << $strpos;
856
	$salt = substr($passwd_hash, 4, 8);
857
858
	$hash = md5($salt . $passwd, true);
859
	for (; $count !== 0; --$count)
860
	{
861
		$hash = md5($hash . $passwd, true);
862
	}
863
864
	$output = substr($passwd_hash, 0, 12);
865
	$i = 0;
866
	while ($i < 16)
867
	{
868
		$value = ord($hash[$i++]);
869
		$output .= $range[$value & 0x3f];
870
871
		if ($i < 16)
872
		{
873
			$value |= ord($hash[$i]) << 8;
874
		}
875
876
		$output .= $range[($value >> 6) & 0x3f];
877
878
		if ($i++ >= 16)
879
		{
880
			break;
881
		}
882
883
		if ($i < 16)
884
		{
885
			$value |= ord($hash[$i]) << 16;
886
		}
887
888
		$output .= $range[($value >> 12) & 0x3f];
889
890
		if ($i++ >= 16)
891
		{
892
			break;
893
		}
894
895
		$output .= $range[($value >> 18) & 0x3f];
896
	}
897
898
	// Return now.
899
	return $output;
900
}
901