Issues (1696)

sources/ElkArte/Controller/Auth.php (16 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(): void
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(): bool
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'] = [$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'] = [$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'] = [$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'] = [$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'] = [$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', [$_POST['user'], null, $modSettings['cookieTime']]), true))
228
		{
229
			$context['login_errors'] = [$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'] = [$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'] = [$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'] = [$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'] = [$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'], ['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'] = [$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'], ['passwd_flood' => '']);
337
		}
338
339
		if ($user_setting->fixSalt() === true)
340
		{
341
			updateMemberData($user_setting['id_member'], ['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'], ['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): array
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
			// BurningBoard3 style of hashing.
444
			$other_passwords[] = sha1($password_salt . sha1($password_salt . sha1($posted_password)));
445
446
			// PunBB 1.4 and later
447
			$other_passwords[] = sha1($password_salt . sha1($posted_password));
448
449
			// Perhaps we converted from a non UTF-8 db and have a valid password being hashed differently.
450
			if (!empty($modSettings['previousCharacterSet']) && $modSettings['previousCharacterSet'] !== 'utf8')
451
			{
452
				// Try iconv first, for no particular reason.
453
				if (function_exists('iconv'))
454
				{
455
					$other_passwords['iconv'] = sha1(strtolower(iconv('UTF-8', $modSettings['previousCharacterSet'], $member_name)) . un_htmlspecialchars(iconv('UTF-8', $modSettings['previousCharacterSet'], $posted_password)));
456
				}
457
458
				// Say it aint so, iconv failed!
459
				if (empty($other_passwords['iconv']) && function_exists('mb_convert_encoding'))
460
				{
461
					$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($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

461
					$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...
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

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

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

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

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