Passed
Push — development ( 080d06...0379dc )
by Spuds
01:04 queued 23s
created

showEmailAddress()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 14
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
cc 4
eloc 5
c 0
b 0
f 0
nc 3
nop 1
dl 0
loc 14
rs 10
ccs 0
cts 0
cp 0
crap 20
1
<?php
2
3
/**
4
 * This file has the very important job of ensuring forum security.
5
 * This task includes banning and permissions, namely.
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
use ElkArte\Cache\Cache;
19
use ElkArte\Controller\Auth;
20
use ElkArte\EventManager;
21
use ElkArte\Helper\FileFunctions;
22
use ElkArte\Helper\TokenHash;
23
use ElkArte\Helper\Util;
24
use ElkArte\Http\Headers;
25
use ElkArte\Languages\Txt;
26
use ElkArte\User;
27
28
/**
29
 * Check if the user is who they say they are.
30
 *
31
 * What it does:
32
 *
33
 * - This function makes sure the user is who they claim to be by requiring a
34
 * password to be typed in every hour.
35
 * - This check can be turned on and off by the securityDisable setting.
36
 * - Uses the adminLogin() function of subs/Auth.subs.php if they need to login,
37
 * which saves all request (POST and GET) data.
38
 *
39
 * @event integrate_validateSession Called at start of validateSession
40
 *
41
 * @param string $type = admin
42
 *
43
 * @return bool|string
44
 */
45 8
function validateSession($type = 'admin')
46
{
47
	global $modSettings;
48 8
49
	// Guests are not welcome here.
50
	is_not_guest();
51 8
52 8
	// Validate what type of session check this is.
53 8
	$types = [];
54
	call_integration_hook('integrate_validateSession', [&$types]);
55
	$type = in_array($type, $types, true) || $type === 'moderate' ? $type : 'admin';
56 8
57
	// Set the lifetime for our admin session. Default is ten minutes.
58 8
	$refreshTime = 10;
59
60
	if (isset($modSettings['admin_session_lifetime']))
61 8
	{
62
		// Maybe someone is paranoid or mistakenly misconfigured the param? Give them at least 5 minutes.
63
		if ($modSettings['admin_session_lifetime'] < 5)
64
		{
65
			$refreshTime = 5;
66
		}
67 8
68
		// A whole day should be more than enough..
69
		elseif ($modSettings['admin_session_lifetime'] > 14400)
70
		{
71
			$refreshTime = 14400;
72
		}
73
74
		// We are between our internal min and max. Let's keep the board owner's value.
75 8
		else
76
		{
77
			$refreshTime = $modSettings['admin_session_lifetime'];
78
		}
79
	}
80 8
81
	// If we're using XML give an additional ten minutes grace as an admin can't log on in XML mode.
82
	if (isset($_GET['api']) && $_GET['api'] === 'xml')
83
	{
84
		$refreshTime += 10;
85 8
	}
86
87
	$refreshTime *= 60;
88
89 8
	// Is the security option off?
90
	// @todo remove the exception (means update the db as well)
91 6
	if (!empty($modSettings['securityDisable' . ($type !== 'admin' ? '_' . $type : '')]))
92
	{
93
		return true;
94
	}
95 2
96
	// If their admin or moderator session hasn't expired yet, let it pass, let the admin session trump a moderation one as well
97 2
	if ((!empty($_SESSION[$type . '_time']) && $_SESSION[$type . '_time'] + $refreshTime >= time()) || (!empty($_SESSION['admin_time']) && $_SESSION['admin_time'] + $refreshTime >= time()))
98
	{
99
		return true;
100
	}
101
102
	require_once(SUBSDIR . '/Auth.subs.php');
103
104
	// Coming from the login screen
105
	if (isset($_POST[$type . '_pass']) || isset($_POST[$type . '_hash_pass']))
106
	{
107
		checkSession();
108
		validateToken('admin-login');
109
110
		// Hashed password, ahoy!
111
		if (isset($_POST[$type . '_hash_pass']) && strlen($_POST[$type . '_hash_pass']) === 64
112
			&& checkPassword($type, true))
113
		{
114
			return true;
115
		}
116
117
		// Posting the password... check it.
118
		if (isset($_POST[$type . '_pass']) && str_replace('*', '', $_POST[$type . '_pass']) !== '' && checkPassword($type))
119
		{
120
			return true;
121
		}
122
	}
123
124
	// Better be sure to remember the real referer
125
	if (empty($_SESSION['request_referer']))
126
	{
127
		$_SESSION['request_referer'] = $_SERVER['HTTP_REFERER'] ?? '';
128
	}
129
	elseif (empty($_POST))
130
	{
131
		unset($_SESSION['request_referer']);
132
	}
133
134
	// Need to type in a password for that, man.
135
	if (!isset($_GET['api']))
136
	{
137
		adminLogin($type);
138
	}
139
140
	return 'session_verify_fail';
141
}
142
143
/**
144
 * Validates a supplied password is correct
145
 *
146
 * What it does:
147
 *
148
 * - Uses integration function to verify password is enabled
149
 * - Uses validateLoginPassword to check using standard ElkArte methods
150
 *
151
 * @event integrate_verify_password allows integration to verify the password
152
 * @param string $type
153
 * @param bool $hash if the supplied password is in _hash_pass
154
 *
155
 * @return bool
156
 */
157
function checkPassword($type, $hash = false)
158
{
159
	$password = $_POST[$type . ($hash ? '_hash_pass' : '_pass')];
160
161
	// Allow integration to verify the password
162
	$good_password = in_array(true, call_integration_hook('integrate_verify_password', [User::$info->username, $password, $hash]), true);
0 ignored issues
show
Bug Best Practice introduced by
The property username does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
163
164
	// Password correct?
165
	if ($good_password || validateLoginPassword($password, User::$info->passwd, $hash ? '' : User::$info->username))
0 ignored issues
show
Bug Best Practice introduced by
The property passwd does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
166
	{
167
		$_SESSION[$type . '_time'] = time();
168
		unset($_SESSION['request_referer']);
169
170
		return true;
171
	}
172
173
	return false;
174
}
175
176
/**
177
 * Require a user who is logged in. (not a guest.)
178
 *
179
 * What it does:
180
 *
181
 * - Checks if the user is currently a guest, and if so asks them to login with a message telling them why.
182
 * - Message is what to tell them when asking them to login.
183
 *
184
 * @param string $message = ''
185
 * @param bool $is_fatal = true
186
 *
187
 * @return bool
188
 */
189
function is_not_guest($message = '', $is_fatal = true)
190
{
191
	global $txt, $context, $scripturl;
192
193
	// Luckily, this person isn't a guest.
194
	if (isset(User::$info->is_guest) && User::$info->is_guest === false)
0 ignored issues
show
Bug Best Practice introduced by
The property is_guest does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
195
	{
196
		return true;
197
	}
198
199
	// People always worry when they see people doing things they aren't actually doing...
200
	$_GET['action'] = '';
201
	$_GET['board'] = '';
202
	$_GET['topic'] = '';
203
	writeLog(true);
204
205
	// Just die.
206
	if ((isset($_REQUEST['api']) && $_REQUEST['api'] === 'xml') || !$is_fatal)
207 14
	{
208
		obExit(false);
209
	}
210 14
211
	// Attempt to detect if they came from dlattach.
212 14
	if (ELK !== 'SSI' && empty($context['theme_loaded']))
0 ignored issues
show
introduced by
The condition ELK !== 'SSI' is always false.
Loading history...
213
	{
214
		new ElkArte\Themes\ThemeLoader();
215
	}
216
217
	// Never redirect to an attachment
218
	if (validLoginUrl($_SERVER['REQUEST_URL']))
219
	{
220
		$_SESSION['login_url'] = $_SERVER['REQUEST_URL'];
221
	}
222
223
	// Load the Login template and language file.
224
	Txt::load('Login');
225
226
	// Apparently we're not in a position to handle this now. Let's go to a safer location for now.
227
	if (!theme()->getLayers()->hasLayers())
228
	{
229
		$_SESSION['login_url'] = $scripturl . '?' . $_SERVER['QUERY_STRING'];
230
		redirectexit('action=login');
231
	}
232
	elseif (isset($_GET['api']))
233
	{
234
		return false;
235
	}
236
	else
237
	{
238
		theme()->getTemplates()->load('Login');
239
		createToken('login');
240
		$context['sub_template'] = 'kick_guest';
241
		$context['robot_no_index'] = true;
242
	}
243
244
	// Use the kick_guest sub template...
245
	$context['kick_message'] = $message;
246
	$context['page_title'] = $txt['login'];
247
	$context['default_password'] = '';
248
249
	obExit();
250
251
	// We should never get to this point, but if we did we wouldn't know the user isn't a guest.
252
	trigger_error('Hacking attempt...', E_USER_ERROR);
253
}
254
255
/**
256
 * Apply restrictions for banned users. For example, disallow access.
257
 *
258
 * What it does:
259
 *
260
 * - If the user is banned, it dies with an error.
261
 * - Caches this information for optimization purposes.
262
 * - Forces a recheck if force_check is true.
263
 *
264
 * @param bool $forceCheck = false
265
 *
266
 * @throws \ElkArte\Exceptions\Exception
267
 */
268
function is_not_banned($forceCheck = false)
269
{
270
	global $txt, $modSettings, $cookiename;
271
272
	$db = database();
273
274
	// You cannot be banned if you are an admin - doesn't help if you log out.
275
	if (User::$info->is_admin)
0 ignored issues
show
Bug Best Practice introduced by
The property is_admin does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
276
	{
277
		return;
278
	}
279
280
	// Only check the ban every so often. (to reduce load.)
281
	if ($forceCheck
282
		|| !isset($_SESSION['ban'])
283
		|| empty($modSettings['banLastUpdated'])
284
		|| ($_SESSION['ban']['last_checked'] < $modSettings['banLastUpdated'])
285
		|| $_SESSION['ban']['id_member'] !== User::$info->id
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
286
		|| $_SESSION['ban']['ip'] !== User::$info->ip
0 ignored issues
show
Bug Best Practice introduced by
The property ip does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
287
		|| $_SESSION['ban']['ip2'] !== 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...
288
		|| (isset(User::$info->email) && $_SESSION['ban']['email'] !== User::$info->email))
0 ignored issues
show
Bug Best Practice introduced by
The property email does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
289
	{
290
		// Innocent until proven guilty.  (but we know you are! :P)
291
		$_SESSION['ban'] = [
292
			'last_checked' => time(),
293
			'id_member' => User::$info->id,
294
			'ip' => User::$info->ip,
295
			'ip2' => User::$info->ip2,
296
			'email' => User::$info->email,
297
		];
298
299
		$ban_query = [];
300
		$ban_query_vars = ['current_time' => time()];
301
		$flag_is_activated = false;
302
303
		// Check both IP addresses.
304
		foreach (['ip', 'ip2'] as $ip_number)
305
		{
306
			if ($ip_number === 'ip2' && User::$info->ip2 === User::$info->ip)
307
			{
308
				continue;
309
			}
310
311
			$ban_query[] = constructBanQueryIP(User::$info->{$ip_number});
312
313
			// IP was valid, maybe there's also a hostname...
314
			if (empty($modSettings['disableHostnameLookup']) && User::$info->{$ip_number} !== 'unknown')
315
			{
316
				$hostname = host_from_ip(User::$info->{$ip_number});
317
				if ($hostname !== '')
318
				{
319
					$ban_query[] = '({string:hostname} LIKE bi.hostname)';
320
					$ban_query_vars['hostname'] = $hostname;
321
				}
322
			}
323
		}
324
325
		// Is their email address banned?
326
		if (User::$info->email !== '')
327
		{
328
			$ban_query[] = '({string:email} LIKE bi.email_address)';
329
			$ban_query_vars['email'] = User::$info->email;
330
		}
331
332
		// How about this user?
333
		if (User::$info->is_guest === false && !empty(User::$info->id))
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...
334
		{
335
			$ban_query[] = 'bi.id_member = {int:id_member}';
336
			$ban_query_vars['id_member'] = User::$info->id;
337
		}
338
339
		// Check the ban, if there's information.
340
		if (!empty($ban_query))
341
		{
342
			$restrictions = [
343
				'cannot_access',
344
				'cannot_login',
345
				'cannot_post',
346
				'cannot_register',
347
			];
348
			$db->fetchQuery('
349
				SELECT 
350
					bi.id_ban, bi.email_address, bi.id_member, bg.cannot_access, bg.cannot_register,
351
					bg.cannot_post, bg.cannot_login, bg.reason, COALESCE(bg.expire_time, 0) AS expire_time
352
				FROM {db_prefix}ban_items AS bi
353
					INNER JOIN {db_prefix}ban_groups AS bg ON (bg.id_ban_group = bi.id_ban_group AND (bg.expire_time IS NULL OR bg.expire_time > {int:current_time}))
354
				WHERE
355
					(' . implode(' OR ', $ban_query) . ')',
356
				$ban_query_vars
357
			)->fetch_callback(
358
				static function ($row) use ($restrictions, &$flag_is_activated) {
359
					// Store every type of ban that applies to you in your session.
360
					foreach ($restrictions as $restriction)
361
					{
362
						if (!empty($row[$restriction]))
363
						{
364
							$_SESSION['ban'][$restriction]['reason'] = $row['reason'];
365
							$_SESSION['ban'][$restriction]['ids'][] = $row['id_ban'];
366
							if (!isset($_SESSION['ban']['expire_time']) || ($_SESSION['ban']['expire_time'] != 0 && ($row['expire_time'] == 0 || $row['expire_time'] > $_SESSION['ban']['expire_time'])))
367
							{
368
								$_SESSION['ban']['expire_time'] = $row['expire_time'];
369
							}
370
371
							if (User::$info->is_guest === false && $restriction === 'cannot_access' && ($row['id_member'] == User::$info->id || $row['email_address'] === User::$info->email))
0 ignored issues
show
Bug Best Practice introduced by
The property email 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 is_guest 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 id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
372
							{
373
								$flag_is_activated = true;
374
							}
375
						}
376
					}
377
				}
378
			);
379
		}
380
381
		// Mark the cannot_access and cannot_post bans as being 'hit'.
382
		if (isset($_SESSION['ban']['cannot_access']) || isset($_SESSION['ban']['cannot_post']) || isset($_SESSION['ban']['cannot_login']))
383
		{
384
			log_ban(array_merge(isset($_SESSION['ban']['cannot_access']) ? $_SESSION['ban']['cannot_access']['ids'] : [], isset($_SESSION['ban']['cannot_post']) ? $_SESSION['ban']['cannot_post']['ids'] : [], isset($_SESSION['ban']['cannot_login']) ? $_SESSION['ban']['cannot_login']['ids'] : []));
385
		}
386
387
		// If for whatever reason the is_activated flag seems wrong, do a little work to clear it up.
388
		if (User::$info->id && ((User::$settings['is_activated'] >= 10 && !$flag_is_activated)
389
				|| (User::$settings['is_activated'] < 10 && $flag_is_activated)))
390
		{
391
			require_once(SUBSDIR . '/Bans.subs.php');
392
			updateBanMembers();
393
		}
394
	}
395
396
	// Hey, I know you! You're ehm...
397
	if (!isset($_SESSION['ban']['cannot_access']) && !empty($_COOKIE[$cookiename . '_']))
398
	{
399
		$bans = explode(',', $_COOKIE[$cookiename . '_']);
400
		foreach ($bans as $key => $value)
401
		{
402
			$bans[$key] = (int) $value;
403
		}
404
405
		$db->fetchQuery('
406
			SELECT 
407
				bi.id_ban, bg.reason
408
			FROM {db_prefix}ban_items AS bi
409
				INNER JOIN {db_prefix}ban_groups AS bg ON (bg.id_ban_group = bi.id_ban_group)
410
			WHERE bi.id_ban IN ({array_int:ban_list})
411
				AND (bg.expire_time IS NULL OR bg.expire_time > {int:current_time})
412
				AND bg.cannot_access = {int:cannot_access}
413
			LIMIT ' . count($bans),
414
			[
415
				'cannot_access' => 1,
416
				'ban_list' => $bans,
417
				'current_time' => time(),
418
			]
419
		)->fetch_callback(
420
			static function ($row) {
421
				$_SESSION['ban']['cannot_access']['ids'][] = $row['id_ban'];
422
				$_SESSION['ban']['cannot_access']['reason'] = $row['reason'];
423
			}
424
		);
425
426
		// My mistake. Next time better.
427
		if (!isset($_SESSION['ban']['cannot_access']))
428
		{
429
			require_once(SUBSDIR . '/Auth.subs.php');
430
			$cookie_url = url_parts(!empty($modSettings['localCookies']), !empty($modSettings['globalCookies']));
431
			elk_setcookie($cookiename . '_', '', time() - 3600, $cookie_url[1], $cookie_url[0], false, false);
432
		}
433
	}
434
435
	// If you're fully banned, it's end of the story for you.
436
	if (isset($_SESSION['ban']['cannot_access']))
437
	{
438
		require_once(SUBSDIR . '/Auth.subs.php');
439
440
		// We don't wanna see you!
441
		if (User::$info->is_guest === false)
442
		{
443
			$controller = new Auth(new EventManager());
444
			$controller->setUser(User::$info);
445
			$controller->action_logout(true, false);
446
		}
447
448
		// 'Log' the user out.  Can't have any funny business... (save the name!)
449
		$old_name = (string) User::$info->name !== '' ? User::$info->name : $txt['guest_title'];
0 ignored issues
show
Bug Best Practice introduced by
The property name does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
450
		User::logOutUser(true);
451
		loadUserContext();
452
453
		// A goodbye present.
454
		$cookie_url = url_parts(!empty($modSettings['localCookies']), !empty($modSettings['globalCookies']));
455
		elk_setcookie($cookiename . '_', implode(',', $_SESSION['ban']['cannot_access']['ids']), time() + 3153600, $cookie_url[1], $cookie_url[0], false, false);
456
457
		// Don't scare anyone, now.
458
		$_GET['action'] = '';
459
		$_GET['board'] = '';
460
		$_GET['topic'] = '';
461
		writeLog(true);
462
463
		// You banned, sucka!
464
		throw new \ElkArte\Exceptions\Exception(sprintf($txt['your_ban'], $old_name) . (empty($_SESSION['ban']['cannot_access']['reason']) ? '' : '<br />' . $_SESSION['ban']['cannot_access']['reason']) . '<br />' . (empty($_SESSION['ban']['expire_time']) ? $txt['your_ban_expires_never'] : sprintf($txt['your_ban_expires'], standardTime($_SESSION['ban']['expire_time'], false))), 'user');
465
	}
466
467
	// You're not allowed to log in but yet you are. Let's fix that.
468
	if (isset($_SESSION['ban']['cannot_login']) && User::$info->is_guest === false)
469
	{
470
		// We don't wanna see you!
471
		require_once(SUBSDIR . '/Logging.subs.php');
472
		deleteMemberLogOnline();
473
474
		// 'Log' the user out.  Can't have any funny business... (save the name!)
475
		$old_name = (string) User::$info->name !== '' ? User::$info->name : $txt['guest_title'];
476
		User::logOutUser(true);
477
		loadUserContext();
478
479
		// Wipe 'n Clean(r) erases all traces.
480
		$_GET['action'] = '';
481
		$_GET['board'] = '';
482
		$_GET['topic'] = '';
483
		writeLog(true);
484
485
		// Log them out
486
		$controller = new Auth(new EventManager());
487
		$controller->setUser(User::$info);
488
		$controller->action_logout(true, false);
489
490
		// Tell them thanks
491
		throw new \ElkArte\Exceptions\Exception(sprintf($txt['your_ban'], $old_name) . (empty($_SESSION['ban']['cannot_login']['reason']) ? '' : '<br />' . $_SESSION['ban']['cannot_login']['reason']) . '<br />' . (empty($_SESSION['ban']['expire_time']) ? $txt['your_ban_expires_never'] : sprintf($txt['your_ban_expires'], standardTime($_SESSION['ban']['expire_time'], false))) . '<br />' . $txt['ban_continue_browse'], 'user');
492
	}
493
494
	// Fix up the banning permissions.
495
	if (!property_exists(User::$info, 'permissions'))
496
	{
497
		return;
498
	}
499
500
	if (User::$info->permissions === null)
0 ignored issues
show
Bug Best Practice introduced by
The property permissions does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
501
	{
502
		return;
503
	}
504
505
	banPermissions();
506
}
507
508
/**
509
 * Fix permissions according to ban status.
510
 *
511
 * What it does:
512
 *
513
 * - Applies any states of banning by removing permissions the user cannot have.
514
 *
515
 * @event integrate_post_ban_permissions Allows to update denied permissions
516
 * @event integrate_warn_permissions Allows changing of permissions for users on warning moderate
517
 * @package Bans
518
 */
519
function banPermissions()
520
{
521 1
	global $modSettings, $context;
522
523
	// Somehow they got here, at least take away all permissions...
524 1
	if (isset($_SESSION['ban']['cannot_access']))
525
	{
526
		User::$info->permissions = [];
0 ignored issues
show
Bug Best Practice introduced by
The property permissions does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __set, consider adding a @property annotation.
Loading history...
527
	}
528
	// Okay, well, you can watch, but don't touch a thing.
529 1
	elseif (isset($_SESSION['ban']['cannot_post']) || (!empty($modSettings['warning_mute']) && $modSettings['warning_mute'] <= User::$info->warning))
0 ignored issues
show
Bug Best Practice introduced by
The property warning does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
530
	{
531
		$denied_permissions = [
532
			'pm_send',
533
			'calendar_post', 'calendar_edit_own', 'calendar_edit_any',
534
			'poll_post',
535
			'poll_add_own', 'poll_add_any',
536
			'poll_edit_own', 'poll_edit_any',
537
			'poll_lock_own', 'poll_lock_any',
538
			'poll_remove_own', 'poll_remove_any',
539
			'manage_attachments', 'manage_smileys', 'manage_boards', 'admin_forum', 'manage_permissions',
540
			'moderate_forum', 'manage_membergroups', 'manage_bans', 'send_mail', 'edit_news',
541
			'profile_identity_any', 'profile_extra_any', 'profile_title_any',
542
			'post_new', 'post_reply_own', 'post_reply_any',
543
			'delete_own', 'delete_any', 'delete_replies',
544
			'make_sticky',
545
			'merge_any', 'split_any',
546
			'modify_own', 'modify_any', 'modify_replies',
547
			'move_any',
548
			'lock_own', 'lock_any',
549
			'remove_own', 'remove_any',
550
			'post_unapproved_topics', 'post_unapproved_replies_own', 'post_unapproved_replies_any',
551
		];
552
		theme()->getLayers()->addAfter('admin_warning', 'body');
553
554
		call_integration_hook('integrate_post_ban_permissions', [&$denied_permissions]);
555
		User::$info->permissions = array_diff(User::$info->permissions, $denied_permissions);
0 ignored issues
show
Bug Best Practice introduced by
The property permissions does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
Bug introduced by
It seems like ElkArte\User::info->permissions can also be of type null; however, parameter $array of array_diff() does only seem to accept array, 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

555
		User::$info->permissions = array_diff(/** @scrutinizer ignore-type */ User::$info->permissions, $denied_permissions);
Loading history...
556
	}
557
	// Are they absolutely under moderation?
558
	elseif (!empty($modSettings['warning_moderate']) && $modSettings['warning_moderate'] <= User::$info->warning)
559 1
	{
560
		// Work out what permissions should change...
561
		$permission_change = [
562
			'post_new' => 'post_unapproved_topics',
563
			'post_reply_own' => 'post_unapproved_replies_own',
564
			'post_reply_any' => 'post_unapproved_replies_any',
565
			'post_attachment' => 'post_unapproved_attachments',
566
		];
567
		call_integration_hook('integrate_warn_permissions', [&$permission_change]);
568
		foreach ($permission_change as $old => $new)
569
		{
570
			if (!in_array($old, User::$info->permissions))
0 ignored issues
show
Bug introduced by
It seems like ElkArte\User::info->permissions can also be of type null; however, parameter $haystack of in_array() does only seem to accept array, 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

570
			if (!in_array($old, /** @scrutinizer ignore-type */ User::$info->permissions))
Loading history...
571
			{
572
				unset($permission_change[$old]);
573
			}
574
			else
575
			{
576
				User::$info->permissions = array_merge((array) User::$info->permissions, $new);
0 ignored issues
show
Bug introduced by
$new of type string is incompatible with the type array expected by parameter $arrays of array_merge(). ( Ignorable by Annotation )

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

576
				User::$info->permissions = array_merge((array) User::$info->permissions, /** @scrutinizer ignore-type */ $new);
Loading history...
577
			}
578
		}
579
580
		User::$info->permissions = array_diff(User::$info->permissions, array_keys($permission_change));
581
	}
582
583
	// @todo Find a better place to call this? Needs to be after permissions loaded!
584
	// Finally, some bits we cache in the session because it saves queries.
585 1
	if (isset($_SESSION['mc']) && $_SESSION['mc']['time'] > $modSettings['settings_updated'] && $_SESSION['mc']['id'] == User::$info->id)
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
586
	{
587
		User::$info->mod_cache = $_SESSION['mc'];
0 ignored issues
show
Bug Best Practice introduced by
The property mod_cache does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __set, consider adding a @property annotation.
Loading history...
588
	}
589
	else
590
	{
591 1
		require_once(SUBSDIR . '/Auth.subs.php');
592 1
		rebuildModCache();
593
	}
594
595
	// Now that we have the mod cache taken care of lets setup a cache for the number of mod reports still open
596 1
	if (isset($_SESSION['rc']) && $_SESSION['rc']['time'] > $modSettings['last_mod_report_action'] && $_SESSION['rc']['id'] == User::$info->id)
597
	{
598
		$context['open_mod_reports'] = $_SESSION['rc']['reports'];
599
		if (allowedTo('admin_forum'))
600
		{
601
			$context['open_pm_reports'] = $_SESSION['rc']['pm_reports'];
602
		}
603
	}
604 1
	elseif ($_SESSION['mc']['bq'] != '0=1')
605
	{
606
		require_once(SUBSDIR . '/Moderation.subs.php');
607
		recountOpenReports(true, allowedTo('admin_forum'));
608
	}
609
	else
610
	{
611 1
		$context['open_mod_reports'] = 0;
612
	}
613 1
}
614
615
/**
616
 * Log a ban in the database.
617
 *
618
 * What it does:
619
 *
620
 * - Log the current user in the ban logs.
621
 * - Increment the hit counters for the specified ban ID's (if any.)
622
 *
623
 * @param int[] $ban_ids = array()
624
 * @param string|null $email = null
625
 * @package Bans
626
 */
627
function log_ban($ban_ids = [], $email = null)
628
{
629
	$db = database();
630
631
	// Don't log web accelerators, it's very confusing...
632
	if (isset($_SERVER['HTTP_X_MOZ']) && $_SERVER['HTTP_X_MOZ'] === 'prefetch')
633
	{
634
		return;
635
	}
636
637
	$db->insert('',
638
		'{db_prefix}log_banned',
639
		[
640
			'id_member' => 'int',
641
			'ip' => 'string-16',
642
			'email' => 'string',
643
			'log_time' => 'int'
644
		],
645
		[
646
			User::$info->id,
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
647
			User::$info->ip,
0 ignored issues
show
Bug Best Practice introduced by
The property ip does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
648
			$email ?? (string) User::$info->email,
0 ignored issues
show
Bug Best Practice introduced by
The property email does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
649
			time()
650
		],
651
		['id_ban_log']
652
	);
653
654
	// One extra point for these bans.
655
	if (!empty($ban_ids))
656
	{
657
		$db->query('', '
658
			UPDATE {db_prefix}ban_items
659
			SET hits = hits + 1
660
			WHERE id_ban IN ({array_int:ban_ids})',
661
			[
662
				'ban_ids' => $ban_ids,
663
			]
664
		);
665
	}
666
}
667
668
/**
669
 * Checks if a given email address might be banned.
670
 *
671
 * What it does:
672
 *
673
 * - Check if a given email is banned.
674
 * - Performs an immediate ban if the turns turns out positive.
675
 *
676
 * @param string $email
677
 * @param string $restriction
678
 * @param string $error
679
 *
680
 * @throws \ElkArte\Exceptions\Exception
681
 * @package Bans
682
 */
683
function isBannedEmail($email, $restriction, $error)
684
{
685
	global $txt;
686 2
687
	$db = database();
688 2
689
	// Can't ban an empty email
690
	if (empty($email) || trim($email) === '')
691 2
	{
692
		return;
693
	}
694
695
	// Let's start with the bans based on your IP/hostname/memberID...
696
	$ban_ids = isset($_SESSION['ban'][$restriction]) ? $_SESSION['ban'][$restriction]['ids'] : [];
697 2
	$ban_reason = isset($_SESSION['ban'][$restriction]) ? $_SESSION['ban'][$restriction]['reason'] : '';
698 2
699
	// ...and add to that the email address you're trying to register.
700
	$db->fetchQuery('
701 2
		SELECT 
702
			bi.id_ban, bg.' . $restriction . ', bg.cannot_access, bg.reason
703 2
		FROM {db_prefix}ban_items AS bi
704
			INNER JOIN {db_prefix}ban_groups AS bg ON (bg.id_ban_group = bi.id_ban_group)
705
		WHERE {string:email} LIKE bi.email_address
706
			AND (bg.' . $restriction . ' = {int:cannot_access} OR bg.cannot_access = {int:cannot_access})
707 2
			AND (bg.expire_time IS NULL OR bg.expire_time >= {int:now})',
708
		[
709
			'email' => $email,
710 2
			'cannot_access' => 1,
711 2
			'now' => time(),
712 2
		]
713
	)->fetch_callback(
714 2
		static function ($row) use (&$ban_ids, &$ban_reason, $restriction) {
715
			if (!empty($row['cannot_access']))
716
			{
717
				$_SESSION['ban']['cannot_access']['ids'][] = $row['id_ban'];
718
				$_SESSION['ban']['cannot_access']['reason'] = $row['reason'];
719
			}
720
721
			if (!empty($row[$restriction]))
722
			{
723
				$ban_ids[] = $row['id_ban'];
724
				$ban_reason = $row['reason'];
725
			}
726
		}
727 2
	);
728
729
	// You're in biiig trouble.  Banned for the rest of this session!
730
	if (isset($_SESSION['ban']['cannot_access']))
731 2
	{
732
		log_ban($_SESSION['ban']['cannot_access']['ids']);
733
		$_SESSION['ban']['last_checked'] = time();
734
735
		throw new \ElkArte\Exceptions\Exception(sprintf($txt['your_ban'], $txt['guest_title']) . $_SESSION['ban']['cannot_access']['reason'], false);
736
	}
737
738
	if (!empty($ban_ids))
739 2
	{
740
		// Log this ban for future reference.
741
		log_ban($ban_ids, $email);
742
		throw new \ElkArte\Exceptions\Exception($error . $ban_reason, false);
743
	}
744
}
745 2
746
/**
747
 * Make sure the user's correct session was passed, and they came from here.
748
 *
749
 * What it does:
750
 *
751
 * - Checks the current session, verifying that the person is who he or she should be.
752
 * - Also checks the referrer to make sure they didn't get sent here.
753
 * - Depends on the disableCheckUA setting, which is usually missing.
754
 * - Will check GET, POST, or REQUEST depending on the passed type.
755
 * - Also optionally checks the referring action if passed. (note that the referring action must be by GET.)
756
 *
757
 * @param string $type = 'post' (post, get, request)
758
 * @param string $from_action = ''
759
 * @param bool $is_fatal = true
760
 *
761
 * @return string the error message if is_fatal is false.
762
 */
763
function checkSession($type = 'post', $from_action = '', $is_fatal = true)
764
{
765
	global $modSettings, $boardurl;
766
767 18
	// We'll work out user agent checks
768
	$req = request();
769
770 18
	// Is it in as $_POST['sc']?
771
	if ($type === 'post')
772
	{
773 18
		$check = $_POST[$_SESSION['session_var']] ?? (empty($modSettings['strictSessionCheck']) && isset($_POST['sc']) ? $_POST['sc'] : null);
774
		if ($check !== $_SESSION['session_value'])
775 14
		{
776 14
			$error = 'session_timeout';
777
		}
778 14
	}
779
	// How about $_GET['sesc']?
780
	elseif ($type === 'get')
781
	{
782 4
		$check = $_GET[$_SESSION['session_var']] ?? (empty($modSettings['strictSessionCheck']) && isset($_GET['sesc']) ? $_GET['sesc'] : null);
783
		if ($check !== $_SESSION['session_value'])
784 2
		{
785 2
			$error = 'session_verify_fail';
786
		}
787 2
	}
788
	// Or can it be in either?
789
	elseif ($type === 'request')
790
	{
791 2
		$check = null;
792
		if (isset($_GET[$_SESSION['session_var']]))
793 2
		{
794
			$check = $_GET[$_SESSION['session_var']];
795 2
		}
796
		elseif (empty($modSettings['strictSessionCheck']) && isset($_GET['sesc']))
797
		{
798
			$check = $_GET['sesc'];
799
		}
800
		elseif (isset($_POST[$_SESSION['session_var']]))
801
		{
802 18
			$check = $_POST[$_SESSION['session_var']];
803
		}
804
		elseif (empty($modSettings['strictSessionCheck']) && isset($_POST['sc']))
805
		{
806
			$check = $_POST['sc'];
807
		}
808 18
809
		if ($check !== $_SESSION['session_value'])
810
		{
811 18
			$error = 'session_verify_fail';
812
		}
813
	}
814
815
	// Verify that they aren't changing user agents on us - that could be bad.
816
	if ((!isset($_SESSION['USER_AGENT']) || $_SESSION['USER_AGENT'] !== $req->user_agent()) && empty($modSettings['disableCheckUA']))
817 18
	{
818
		$error = 'session_verify_fail';
819
	}
820 18
821
	// Make sure a page with session check requirement is not being prefetched.
822 18
	stop_prefetching();
823
824
	// Check the referring site - it should be the same server at least!
825
826
	$referrer_url = $_SESSION['request_referer'] ?? ($_SERVER['HTTP_REFERER'] ?? '');
827
828
	$referrer = @parse_url($referrer_url);
829
	if (!empty($referrer['host']))
830
	{
831
		if (strpos($_SERVER['HTTP_HOST'], ':') !== false)
832
		{
833
			$real_host = substr($_SERVER['HTTP_HOST'], 0, strpos($_SERVER['HTTP_HOST'], ':'));
834
		}
835
		else
836
		{
837
			$real_host = $_SERVER['HTTP_HOST'];
838
		}
839
840
		$parsed_url = parse_url($boardurl);
841
842
		// Are global cookies on? If so, let's check them ;).
843
		if (!empty($modSettings['globalCookies']))
844
		{
845
			if (preg_match('~(?:[^\.]+\.)?([^\.]{3,}\..+)\z~i', $parsed_url['host'], $parts) == 1)
846
			{
847
				$parsed_url['host'] = $parts[1];
848
			}
849
850
			if (preg_match('~(?:[^\.]+\.)?([^\.]{3,}\..+)\z~i', $referrer['host'], $parts) == 1)
851
			{
852
				$referrer['host'] = $parts[1];
853
			}
854
855
			if (preg_match('~(?:[^\.]+\.)?([^\.]{3,}\..+)\z~i', $real_host, $parts) == 1)
856
			{
857
				$real_host = $parts[1];
858
			}
859
		}
860
861
		// Okay: referrer must either match parsed_url or real_host.
862
		if (isset($parsed_url['host']) && strtolower($referrer['host']) !== strtolower($parsed_url['host']) && strtolower($referrer['host']) !== strtolower($real_host))
863
		{
864 18
			$error = 'verify_url_fail';
865
			$log_error = true;
866
			$sprintf = [Util::htmlspecialchars($referrer_url)];
867
		}
868
	}
869
870
	// Well, first of all, if a from_action is specified you'd better have an old_url.
871
	if (!empty($from_action) && (!isset($_SESSION['old_url']) || preg_match('~[?;&]action=' . $from_action . '([;&]|$)~', $_SESSION['old_url']) !== 1))
872 18
	{
873
		$error = 'verify_url_fail';
874 18
		$log_error = true;
875
		$sprintf = [Util::htmlspecialchars($referrer_url)];
876
	}
877
878
	// Everything is ok, return an empty string.
879
	if (!isset($error))
880
	{
881
		return '';
882
	}
883
884
	// A session error occurred, show the error.
885
	if ($is_fatal)
886
	{
887
		if (isset($_REQUEST['api']))
888
		{
889
			@ob_end_clean();
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for ob_end_clean(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

889
			/** @scrutinizer ignore-unhandled */ @ob_end_clean();

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
890
			Headers::instance()
891
				->removeHeader('all')
892
				->headerSpecial('HTTP/1.1 403 Forbidden - Session timeout')
893
				->sendHeaders();
894
			die;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
895
		}
896
		throw new \ElkArte\Exceptions\Exception($error, isset($log_error) ? 'user' : false, $sprintf ?? []);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $error does not seem to be defined for all execution paths leading up to this point.
Loading history...
897
	}
898
	// A session error occurred, return the error to the calling function.
899
	else
900
	{
901
		return $error;
902
	}
903
904
	// We really should never fall through here, for very important reasons.  Let's make sure.
905
	trigger_error('Hacking attempt...', E_USER_ERROR);
0 ignored issues
show
Unused Code introduced by
trigger_error('Hacking attempt...', E_USER_ERROR) is not reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
906
}
907
908
/**
909
 * Let's give you a token of our appreciation.
910
 *
911
 * What it does:
912
 *
913
 * - Creates a one time use form token
914 13
 *
915
 * @param string $action The specific site action that a token will be generated for
916
 * @param string $type = 'post' If the token will be returned via post or get
917 13
 *
918 13
 * @return string[] array of token var, time, csrf, token
919 13
 */
920
function createToken($action, $type = 'post')
921
{
922 13
	global $context;
923 13
924
	// Generate a new token token_var pair
925
	$tokenizer = new TokenHash();
926 13
	$token_var = $tokenizer->generate_hash(rand(7, 12));
927 13
	$token = $tokenizer->generate_hash(32);
928 13
929
	// We need user agent and the client IP
930 13
	$req = request();
931
	$csrf_hash = hash('sha1', $token . $req->client_ip() . $req->user_agent());
932
933
	// Save the session token and make it available to the forms
934
	$_SESSION['token'][$type . '-' . $action] = [$token_var, $csrf_hash, time(), $token];
935
	$context[$action . '_token'] = $token;
936
	$context[$action . '_token_var'] = $token_var;
937
938
	return [$action . '_token_var' => $token_var, $action . '_token' => $token];
939
}
940
941
/**
942
 * Only patrons with valid tokens can ride this ride.
943
 *
944
 * What it does:
945
 *
946
 * Validates that the received token is correct
947
 *  1. The token exists in session.
948
 *  2. The {$type} variable should exist.
949
 *  3. We concatenate the variable we received with the user agent
950
 *  4. Match that result against what is in the session.
951
 *  5. If it matches, success, otherwise we fallout.
952
 *
953
 * @param string $action
954
 * @param string $type = 'post' (get, request, or post)
955 8
 * @param bool $reset = true Reset the token on failure
956 8
 * @param bool $fatal if true a fatal_lang_error is issued for invalid tokens, otherwise false is returned
957
 *
958
 * @return bool|string except for $action == 'login' where the token is returned
959 8
 * @throws \ElkArte\Exceptions\Exception token_verify_fail
960
 */
961 2
function validateToken($action, $type = 'post', $reset = true, $fatal = true)
962
{
963 2
	$type = ($type === 'get' || $type === 'request') ? $type : 'post';
964 2
	$token_index = $type . '-' . $action;
965
966 2
	// Logins are special: the token is used to have the password with javascript before POST it
967
	if ($action === 'login')
968
	{
969
		if (isset($_SESSION['token'][$token_index]))
970
		{
971
			$return = $_SESSION['token'][$token_index][3];
972 6
			unset($_SESSION['token'][$token_index]);
973
974 6
			return $return;
975
		}
976
977
		return '';
978
	}
979
980
	if (!isset($_SESSION['token'][$token_index]))
981
	{
982
		return false;
983
	}
984
985
	// We need the user agent and client IP
986
	$req = request();
987
988
	// Shortcut
989
	$passed_token_var = $GLOBALS['_' . strtoupper($type)][$_SESSION['token'][$token_index][0]] ?? null;
990
	$csrf_hash = hash('sha1', $passed_token_var . $req->client_ip() . $req->user_agent());
991
992
	// Checked what was passed in combination with the user agent
993
	if (isset($passed_token_var)
994
		&& $csrf_hash === $_SESSION['token'][$token_index][1])
995
	{
996
		// Consume the token, let them pass
997
		unset($_SESSION['token'][$token_index]);
998
999
		return true;
1000
	}
1001
1002
	// Patrons with invalid tokens get the boot.
1003
	if ($reset)
1004
	{
1005
		// Might as well do some cleanup on this.
1006
		cleanTokens();
1007
1008
		// I'm back baby.
1009
		createToken($action, $type);
1010
1011
		if ($fatal)
1012
		{
1013
			throw new \ElkArte\Exceptions\Exception('token_verify_fail', false);
1014
		}
1015
	}
1016
	// You don't get a new token
1017
	else
1018
	{
1019
		// Explicitly remove this token
1020
		unset($_SESSION['token'][$token_index]);
1021
1022
		// Remove older tokens.
1023
		cleanTokens();
1024
	}
1025
1026
	return false;
1027
}
1028
1029
/**
1030
 * Removes old unused tokens from session
1031
 *
1032
 * What it does:
1033
 *
1034
 * - Defaults to 3 hours before a token is considered expired
1035 1
 * - if $complete = true will remove all tokens
1036
 *
1037
 * @param bool $complete = false
1038
 * @param string $suffix = false
1039
 */
1040
function cleanTokens($complete = false, $suffix = '')
1041 1
{
1042
	// We appreciate cleaning up after yourselves.
1043 1
	if (!isset($_SESSION['token']))
1044
	{
1045
		return;
1046
	}
1047
1048
	// Clean up tokens, trying to give enough time still.
1049 1
	foreach ($_SESSION['token'] as $key => $data)
1050
	{
1051
		$force = empty($suffix) ? $complete : $complete || strpos($key, $suffix);
1052 1
1053
		if ($data[2] + 10800 < time() || $force)
1054
		{
1055
			unset($_SESSION['token'][$key]);
1056
		}
1057 1
	}
1058
}
1059
1060
/**
1061
 * Check whether a form has been submitted twice.
1062
 *
1063
 * What it does:
1064
 *
1065
 * - Registers a sequence number for a form.
1066
 * - Checks whether a submitted sequence number is registered in the current session.
1067
 * - Depending on the value of is_fatal shows an error or returns true or false.
1068
 * - Frees a sequence number from the stack after it's been checked.
1069
 * - Frees a sequence number without checking if action == 'free'.
1070
 *
1071
 * @param string $action
1072
 * @param bool $is_fatal = true
1073
 *
1074
 * @return bool|void
1075
 * @throws \ElkArte\Exceptions\Exception error_form_already_submitted
1076
 */
1077
function checkSubmitOnce($action, $is_fatal = false)
1078 12
{
1079
	global $context;
1080 12
1081
	if (!isset($_SESSION['forms']))
1082 12
	{
1083
		$_SESSION['forms'] = [];
1084
	}
1085
1086 12
	// Register a form number and store it in the session stack. (use this on the page that has the form.)
1087
	if ($action === 'register')
1088 6
	{
1089 6
		$tokenizer = new TokenHash();
1090 6
		$context['form_sequence_number'] = '';
1091
		while (empty($context['form_sequence_number']) || in_array($context['form_sequence_number'], $_SESSION['forms'], true))
1092 6
		{
1093
			$context['form_sequence_number'] = $tokenizer->generate_hash();
1094
		}
1095
	}
1096 8
	// Check whether the submitted number can be found in the session.
1097
	elseif ($action === 'check')
1098 8
	{
1099
		if (!isset($_REQUEST['seqnum']))
1100 8
		{
1101
			return true;
1102
		}
1103
1104
		if (!in_array($_REQUEST['seqnum'], $_SESSION['forms'], true))
1105
		{
1106
			// Mark this one as used
1107
			$_SESSION['forms'][] = (string) $_REQUEST['seqnum'];
1108
			return true;
1109
		}
1110
1111
		if ($is_fatal)
1112
		{
1113
			throw new \ElkArte\Exceptions\Exception('error_form_already_submitted', false);
1114
		}
1115
		else
1116
		{
1117
			return false;
1118
		}
1119
	}
1120
	// Don't check, just free the stack number.
1121
	elseif ($action === 'free' && isset($_REQUEST['seqnum']) && in_array($_REQUEST['seqnum'], $_SESSION['forms'], true))
1122
	{
1123
		$_SESSION['forms'] = array_diff($_SESSION['forms'], [$_REQUEST['seqnum']]);
1124
	}
1125
	elseif ($action !== 'free')
1126
	{
1127 6
		trigger_error("checkSubmitOnce(): Invalid action '" . $action . "'", E_USER_WARNING);
1128
	}
1129
}
1130
1131
/**
1132
 * This function checks whether the user is allowed to do permission. (ie. post_new.)
1133
 *
1134
 * What it does:
1135
 *
1136
 * - If boards parameter is specified, checks those boards instead of the current one (if applicable).
1137
 * - Always returns true if the user is an administrator.
1138
 *
1139
 * @param string[]|string $permission permission
1140
 * @param int[]|int|null $boards array of board IDs, a single id or null
1141
 *
1142
 * @return bool if the user can do the permission
1143
 */
1144
function allowedTo($permission, $boards = null)
1145 330
{
1146
	$db = database();
1147
1148 330
	// You're always allowed to do nothing. (unless you're a working man, MR. LAZY :P!)
1149
	if (empty($permission))
1150 12
	{
1151
		return true;
1152
	}
1153
1154 330
	// You're never allowed to do something if your data hasn't been loaded yet!
1155
	if (empty(User::$info) || !isset(User::$info['permissions']))
1156
	{
1157
		return false;
1158
	}
1159
1160 330
	// Administrators are supermen :P.
1161
	if (User::$info->is_admin)
0 ignored issues
show
Bug Best Practice introduced by
The property is_admin does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
1162 306
	{
1163
		return true;
1164
	}
1165
1166 24
	// Make sure permission is a valid array
1167
	if (!is_array($permission))
1168 22
	{
1169
		$permission = [$permission];
1170
	}
1171
1172 24
	// Are we checking the _current_ board, or some other boards?
1173
	if ($boards === null)
1174 24
	{
1175
		if (empty(User::$info->permissions))
0 ignored issues
show
Bug Best Practice introduced by
The property permissions does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
1176 12
		{
1177
			return false;
1178
		}
1179
1180 12
		// Check if they can do it, you aren't allowed, by default.
1181
		return array_intersect($permission, User::$info->permissions) !== [];
1182
	}
1183
1184
	if (!is_array($boards))
1185
	{
1186
		$boards = [$boards];
1187
	}
1188
1189
	if (empty(User::$info->groups))
0 ignored issues
show
Bug Best Practice introduced by
The property groups does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
1190
	{
1191
		return false;
1192
	}
1193
1194
	$request = $db->query('', '
1195
		SELECT 
1196
			MIN(bp.add_deny) AS add_deny
1197
		FROM {db_prefix}boards AS b
1198
			INNER JOIN {db_prefix}board_permissions AS bp ON (bp.id_profile = b.id_profile)
1199
			LEFT JOIN {db_prefix}moderators AS mods ON (mods.id_board = b.id_board AND mods.id_member = {int:current_member})
1200
		WHERE b.id_board IN ({array_int:board_list})
1201
			AND bp.id_group IN ({array_int:group_list}, {int:moderator_group})
1202
			AND bp.permission IN ({array_string:permission_list})
1203
			AND (mods.id_member IS NOT NULL OR bp.id_group != {int:moderator_group})
1204
		GROUP BY b.id_board',
1205
		[
1206
			'current_member' => User::$info->id,
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
1207
			'board_list' => $boards,
1208
			'group_list' => User::$info->groups,
1209
			'moderator_group' => 3,
1210
			'permission_list' => $permission,
1211
		]
1212
	);
1213
1214
	// Make sure they can do it on all the boards.
1215
	if ($request->num_rows() !== count($boards))
1216
	{
1217
		return false;
1218
	}
1219
1220
	$result = true;
1221
	while (($row = $request->fetch_assoc()))
1222
	{
1223
		$result = $result && !empty($row['add_deny']);
1224
	}
1225
1226
	$request->free_result();
1227
1228
	// If the query returned 1, they can do it... otherwise, they can't.
1229
	return $result;
1230
}
1231
1232
/**
1233
 * This function returns fatal error if the user doesn't have the respective permission.
1234
 *
1235
 * What it does:
1236
 *
1237
 * - Uses allowedTo() to check if the user is allowed to do permission.
1238
 * - Checks the passed boards or current board for the permission.
1239
 * - If they are not, it loads the Errors language file and shows an error using $txt['cannot_' . $permission].
1240
 * - If they are a guest and cannot do it, this calls is_not_guest().
1241
 *
1242
 * @param string[]|string $permission array of or single string, of permissions to check
1243
 * @param int[]|null $boards = null
1244
 *
1245
 * @throws \ElkArte\Exceptions\Exception cannot_xyz where xyz is the permission
1246
 */
1247 68
function isAllowedTo($permission, $boards = null)
1248
{
1249 68
	global $txt;
1250
1251
	static $heavy_permissions = [
1252
		'admin_forum',
1253
		'manage_attachments',
1254
		'manage_smileys',
1255
		'manage_boards',
1256
		'edit_news',
1257
		'moderate_forum',
1258
		'manage_bans',
1259
		'manage_membergroups',
1260
		'manage_permissions',
1261
	];
1262 68
1263
	// Make it an array, even if a string was passed.
1264
	$permission = is_array($permission) ? $permission : [$permission];
1265 68
1266
	// Check the permission and return an error...
1267
	if (!allowedTo($permission, $boards))
1268
	{
1269
		// Pick the last array entry as the permission shown as the error.
1270
		$error_permission = array_shift($permission);
1271
1272
		// If they are a guest, show a login. (because the error might be gone if they do!)
1273
		if (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...
1274
		{
1275
			Txt::load('Errors');
1276
			is_not_guest($txt['cannot_' . $error_permission]);
1277
		}
1278
1279
		// Clear the action because they aren't really doing that!
1280
		$_GET['action'] = '';
1281
		$_GET['board'] = '';
1282
		$_GET['topic'] = '';
1283
		writeLog(true);
1284
1285
		throw new \ElkArte\Exceptions\Exception('cannot_' . $error_permission, false);
1286
	}
1287
1288 68
	// If you're doing something on behalf of some "heavy" permissions, validate your session.
1289
	// (take out the heavy permissions, and if you can't do anything but those, you need a validated session.)
1290
	if (!allowedTo(array_diff($permission, $heavy_permissions), $boards))
1291
	{
1292 68
		validateSession();
1293
	}
1294
}
1295
1296
/**
1297
 * Return the boards a user has a certain (board) permission on. (array(0) if all.)
1298
 *
1299
 * What it does:
1300
 *
1301
 * - Returns a list of boards on which the user is allowed to do the specified permission.
1302
 * - Returns an array with only a 0 in it if the user has permission to do this on every board.
1303
 * - Returns an empty array if he or she cannot do this on any board.
1304
 * - If check_access is true will also make sure the group has proper access to that board.
1305
 *
1306
 * @param string[]|string $permissions array of permission names to check access against
1307
 * @param bool $check_access = true
1308
 * @param bool $simple = true Set $simple to true to use this function in compatibility mode
1309
 *             otherwise, the resultant array becomes split into the multiple
1310
 *             permissions that were passed. Other than that, it's just the normal
1311
 *             state of play that you're used to.
1312
 *
1313
 * @return int[]
1314
 * @throws \ElkArte\Exceptions\Exception
1315
 */
1316 3
function boardsAllowedTo($permissions, $check_access = true, $simple = true)
1317
{
1318
	$db = database();
1319 3
1320
	// Arrays are nice, most of the time.
1321 3
	if (!is_array($permissions))
1322
	{
1323
		$permissions = [$permissions];
1324
	}
1325 3
1326
	// I am the master, the master of the universe!
1327 2
	if (User::$info->is_admin)
0 ignored issues
show
Bug Best Practice introduced by
The property is_admin does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
1328
	{
1329 2
		if ($simple)
1330
		{
1331
			return [0];
1332
		}
1333
1334
		$boards = [];
1335
		foreach ($permissions as $permission)
1336
		{
1337
			$boards[$permission] = [0];
1338
		}
1339
1340
		return $boards;
1341
	}
1342
1343
	// All groups the user is in except 'moderator'.
1344 1
	$groups = array_diff(User::$info->groups, [3]);
0 ignored issues
show
Bug introduced by
It seems like ElkArte\User::info->groups can also be of type null; however, parameter $array of array_diff() does only seem to accept array, 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

1344
	$groups = array_diff(/** @scrutinizer ignore-type */ User::$info->groups, [3]);
Loading history...
Bug Best Practice introduced by
The property groups does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
1345
1346 1
	$boards = [];
1347 1
	$deny_boards = [];
1348 1
	$db->fetchQuery('
1349
		SELECT 
1350 1
			b.id_board, bp.add_deny' . ($simple ? '' : ', bp.permission') . '
1351
		FROM {db_prefix}board_permissions AS bp
1352
			INNER JOIN {db_prefix}boards AS b ON (b.id_profile = bp.id_profile)
1353
			LEFT JOIN {db_prefix}moderators AS mods ON (mods.id_board = b.id_board AND mods.id_member = {int:current_member})
1354
		WHERE bp.id_group IN ({array_int:group_list}, {int:moderator_group})
1355
			AND bp.permission IN ({array_string:permissions})
1356
			AND (mods.id_member IS NOT NULL OR bp.id_group != {int:moderator_group})' .
1357 1
		($check_access ? ' AND {query_see_board}' : ''),
1358
		[
1359 1
			'current_member' => User::$info->id,
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
1360 1
			'group_list' => $groups,
1361 1
			'moderator_group' => 3,
1362 1
			'permissions' => $permissions,
1363
		]
1364 1
	)->fetch_callback(
1365
		static function ($row) use ($simple, &$deny_boards, &$boards) {
1366
			if ($simple)
1367
			{
1368
				if (empty($row['add_deny']))
1369
				{
1370
					$deny_boards[] = (int) $row['id_board'];
1371
				}
1372
				else
1373
				{
1374
					$boards[] = (int) $row['id_board'];
1375
				}
1376
			}
1377
			elseif (empty($row['add_deny']))
1378
			{
1379
				$deny_boards[$row['permission']][] = (int) $row['id_board'];
1380
			}
1381
			else
1382
			{
1383
				$boards[$row['permission']][] = (int) $row['id_board'];
1384
			}
1385 1
		}
1386
	);
1387
1388 1
	if ($simple)
1389
	{
1390 1
		$boards = array_unique(array_values(array_diff($boards, $deny_boards)));
1391
	}
1392
	else
1393
	{
1394
		foreach ($permissions as $permission)
1395
		{
1396
			// Never had it to start with
1397
			if (empty($boards[$permission]))
1398
			{
1399
				$boards[$permission] = [];
1400
			}
1401
			else
1402
			{
1403
				// Or it may have been removed
1404
				$deny_boards[$permission] = $deny_boards[$permission] ?? [];
1405
				$boards[$permission] = array_unique(array_values(array_diff($boards[$permission], $deny_boards[$permission])));
1406
			}
1407
		}
1408
	}
1409
1410 1
	return $boards;
1411
}
1412
1413
/**
1414
 * Returns whether an email address should be shown and how.
1415
 *
1416
 * What it does:
1417
 *
1418
 * Possible outcomes are:
1419
 *  If it's your own profile yes.
1420
 *  If you're a moderator with sufficient permissions: yes.
1421
 *  Otherwise: no
1422
 *
1423
 * @param int $userProfile_id
1424
 *
1425
 * @return bool
1426
 */
1427
function showEmailAddress($userProfile_id)
1428
{
1429
	// Should this user's email address be shown?
1430
	if ((User::$info->is_guest === false && User::$info->id === (int) $userProfile_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...
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...
1431
	{
1432
		return true;
1433
	}
1434
1435
	if (allowedTo('moderate_forum'))
1436
	{
1437
		return true;
1438
	}
1439
1440
	return false;
1441 8
}
1442
1443
/**
1444
 * This function attempts to protect from carrying out specific actions repeatedly.
1445
 *
1446 8
 * What it does:
1447
 *
1448 6
 * - Checks if a user is trying specific actions faster than a given minimum wait threshold.
1449
 * - The time taken depends on error_type - generally uses the modSetting.
1450
 * - Generates a fatal message when triggered, suspending execution.
1451 2
 *
1452
 * @event integrate_spam_protection Allows updating action wait timeOverrides
1453 2
 * @param string $error_type used also as a $txt index. (not an actual string.)
1454
 * @param bool $fatal is the spam check a fatal error on failure
1455
 *
1456
 * @return bool|int|mixed
1457
 * @throws \ElkArte\Exceptions\Exception
1458
 */
1459
function spamProtection($error_type, $fatal = true)
1460
{
1461
	global $modSettings;
1462
1463
	$db = database();
1464
1465
	// Certain types take less/more time.
1466
	$timeOverrides = [
1467
		'login' => 2,
1468
		'register' => 2,
1469
		'remind' => 30,
1470
		'contact' => 30,
1471
		'sendmail' => $modSettings['spamWaitTime'] * 5,
1472
		'reporttm' => $modSettings['spamWaitTime'] * 4,
1473
		'search' => empty($modSettings['search_floodcontrol_time']) ? 1 : $modSettings['search_floodcontrol_time'],
1474
	];
1475
	call_integration_hook('integrate_spam_protection', [&$timeOverrides]);
1476
1477
	// Moderators are free...
1478
	$timeLimit = allowedTo('moderate_board') ? 2 : $timeOverrides[$error_type] ?? $modSettings['spamWaitTime'];
1479
1480
	// Delete old entries...
1481
	$db->query('', '
1482 12
		DELETE FROM {db_prefix}log_floodcontrol
1483
		WHERE log_time < {int:log_time}
1484 12
			AND log_type = {string:log_type}',
1485
		[
1486
			'log_time' => time() - $timeLimit,
1487
			'log_type' => $error_type,
1488 12
		]
1489 12
	);
1490 12
1491 12
	// Add a new entry, deleting the old if necessary.
1492 12
	$request = $db->replace(
1493 12
		'{db_prefix}log_floodcontrol',
1494 12
		['ip' => 'string-16', 'log_time' => 'int', 'log_type' => 'string'],
1495 12
		[User::$info->ip, time(), $error_type],
0 ignored issues
show
Bug Best Practice introduced by
The property ip does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
1496
		['ip', 'log_type']
1497 12
	);
1498
1499
	// If affected is 0 or 2, it was there already.
1500 12
	if ($request->affected_rows() != 1)
1501
	{
1502
		// Spammer!  You only have to wait a *few* seconds!
1503
		if ($fatal)
1504
		{
1505
			throw new \ElkArte\Exceptions\Exception($error_type . '_WaitTime_broken', false, [$timeLimit]);
1506 12
		}
1507
1508
		return $timeLimit;
1509
	}
1510 12
1511
	// They haven't posted within the limit.
1512
	return false;
1513
}
1514
1515 12
/**
1516 12
 * A generic function to create a pair of index.php and .htaccess files in a directory
1517
 *
1518
 * @param string $path the (absolute) directory path
1519
 * @param bool $allow_localhost if access should be allowed to localhost
1520
 * @param string $files (optional, default '*') parameter for the Files tag
1521 12
 *
1522 12
 * @return string[]|string|bool on success error string if anything fails
1523 12
 */
1524 12
function secureDirectory($path, $allow_localhost = false, $files = '*')
1525 12
{
1526
	if (empty($path))
1527
	{
1528
		return 'empty_path';
1529 12
	}
1530
1531
	if (!FileFunctions::instance()->isWritable($path))
1532
	{
1533
		return 'path_not_writable';
1534
	}
1535
1536
	$directoryname = basename($path);
1537
1538
	// How deep is this from our boarddir
1539
	$tree = explode(DIRECTORY_SEPARATOR, $path);
1540
	$root = explode(DIRECTORY_SEPARATOR, BOARDDIR);
1541
	$count = max(count($tree) - count($root), 0);
1542
1543 12
	$errors = [];
1544
1545
	if (file_exists($path . '/.htaccess'))
1546
	{
1547
		$errors[] = 'htaccess_exists';
1548
	}
1549
	else
1550
	{
1551
		$fh = @fopen($path . '/.htaccess', 'wb');
1552
		if ($fh)
0 ignored issues
show
introduced by
$fh is of type false|resource, thus it always evaluated to false.
Loading history...
1553
		{
1554
			fwrite($fh, '# Apache 2.4
1555
<IfModule mod_authz_core.c>
1556
	Require all denied
1557
	<Files ' . ($files === '*' ? $files : '~ ' . $files) . '>
1558
		<RequireAll>
1559
			Require all granted
1560
			Require not env blockAccess' . (empty($allow_localhost) ? '
1561
		</RequireAll>
1562
	</Files>' : '
1563
		Require host localhost
1564
		</RequireAll>
1565
	</Files>
1566
1567
	RemoveHandler .php .php3 .phtml .cgi .fcgi .pl .fpl .shtml') . '
1568
</IfModule>
1569
1570
# Apache 2.2
1571
<IfModule !mod_authz_core.c>
1572
	Order Deny,Allow
1573
	Deny from all
1574
1575
	<Files ' . $files . '>
1576
		Allow from all' . (empty($allow_localhost) ? '
1577
	</Files>' : '
1578
		Allow from localhost
1579
	</Files>
1580
1581
	RemoveHandler .php .php3 .phtml .cgi .fcgi .pl .fpl .shtml') . '
1582
</IfModule>');
1583
			fclose($fh);
1584
		}
1585
1586
		$errors[] = 'htaccess_cannot_create_file';
1587
	}
1588
1589
	if (file_exists($path . '/index.php'))
1590
	{
1591
		$errors[] = 'index-php_exists';
1592
	}
1593
	else
1594
	{
1595
		$fh = @fopen($path . '/index.php', 'wb');
1596
		if ($fh)
0 ignored issues
show
introduced by
$fh is of type false|resource, thus it always evaluated to false.
Loading history...
1597
		{
1598
			fwrite($fh, '<?php
1599
1600
/**
1601
 * This file is here solely to protect your ' . $directoryname . ' directory.
1602
 */
1603
1604
// Look for Settings.php....
1605
if (file_exists(dirname(__FILE__, ' . ($count + 1) . ') . \'/Settings.php\'))
1606
{
1607
	// Found it!
1608
	require(dirname(__FILE__, ' . ($count + 1) . ') . \'/Settings.php\');
1609
	header(\'Location: \' . $boardurl);
1610
}
1611
// Can\'t find it... just forget it.
1612
else
1613
	exit;');
1614
			fclose($fh);
1615
		}
1616
1617
		$errors[] = 'index-php_cannot_create_file';
1618
	}
1619
1620
	if (!empty($errors))
1621
	{
1622
		return $errors;
1623
	}
1624
1625
	return true;
1626
}
1627
1628
/**
1629
 * Helper function that puts together a ban query for a given ip
1630
 *
1631
 * What it does:
1632
 *
1633
 * - Builds the query for ipv6, ipv4 or 255.255.255.255 depending on what's supplied
1634
 *
1635
 * @param string $fullip An IP address either IPv6 or not
1636
 *
1637
 * @return string A SQL condition
1638
 */
1639
function constructBanQueryIP($fullip)
1640
{
1641
	// First attempt a IPv6 address.
1642
	if (isValidIPv6($fullip))
1643
	{
1644
		$ip_parts = convertIPv6toInts($fullip);
1645
1646
		$ban_query = '((' . $ip_parts[0] . ' BETWEEN bi.ip_low1 AND bi.ip_high1)
1647
			AND (' . $ip_parts[1] . ' BETWEEN bi.ip_low2 AND bi.ip_high2)
1648
			AND (' . $ip_parts[2] . ' BETWEEN bi.ip_low3 AND bi.ip_high3)
1649
			AND (' . $ip_parts[3] . ' BETWEEN bi.ip_low4 AND bi.ip_high4)
1650
			AND (' . $ip_parts[4] . ' BETWEEN bi.ip_low5 AND bi.ip_high5)
1651
			AND (' . $ip_parts[5] . ' BETWEEN bi.ip_low6 AND bi.ip_high6)
1652
			AND (' . $ip_parts[6] . ' BETWEEN bi.ip_low7 AND bi.ip_high7)
1653
			AND (' . $ip_parts[7] . ' BETWEEN bi.ip_low8 AND bi.ip_high8))';
1654
	}
1655
	// Check if we have a valid IPv4 address.
1656
	elseif (preg_match('/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $fullip, $ip_parts) == 1)
1657
	{
1658
		$ban_query = '((' . $ip_parts[1] . ' BETWEEN bi.ip_low1 AND bi.ip_high1)
1659
			AND (' . $ip_parts[2] . ' BETWEEN bi.ip_low2 AND bi.ip_high2)
1660
			AND (' . $ip_parts[3] . ' BETWEEN bi.ip_low3 AND bi.ip_high3)
1661
			AND (' . $ip_parts[4] . ' BETWEEN bi.ip_low4 AND bi.ip_high4))';
1662
	}
1663
	// We use '255.255.255.255' for 'unknown' since it's not valid anyway.
1664
	else
1665
	{
1666
		$ban_query = '(bi.ip_low1 = 255 AND bi.ip_high1 = 255
1667
			AND bi.ip_low2 = 255 AND bi.ip_high2 = 255
1668 2
			AND bi.ip_low3 = 255 AND bi.ip_high3 = 255
1669
			AND bi.ip_low4 = 255 AND bi.ip_high4 = 255)';
1670
	}
1671
1672
	return $ban_query;
1673
}
1674
1675
/**
1676
 * Decide if we are going to do any "bad behavior" scanning for this user
1677
 *
1678
 * What it does:
1679
 *
1680
 * - Admins and Moderators get a free pass
1681
 * - Returns true if Accept header is missing
1682 2
 * - Check with project Honey Pot for known miscreants
1683
 *
1684 2
 * @return bool true if bad, false otherwise
1685 2
 */
1686 2
function runBadBehavior()
1687 2
{
1688
	global $modSettings;
1689
1690
	// Admins and Mods get a free pass
1691
	if (!empty(User::$info->is_moderator) || !empty(User::$info->is_admin))
0 ignored issues
show
Bug Best Practice introduced by
The property is_moderator 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 is_admin does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
1692
	{
1693
		return false;
1694
	}
1695
1696
	// Clients will have an "Accept" header, generally only bots or scrappers don't
1697
	if (!empty($modSettings['badbehavior_accept_header']) && !array_key_exists('HTTP_ACCEPT', $_SERVER))
1698 2
	{
1699
		return true;
1700
	}
1701
1702
	// Do not block private IP ranges 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 or 127.0.0.0/8
1703
	if (preg_match('~^((10|172\.(1[6-9]|2\d|3[01])|192\.168|127)\.)~', $_SERVER['REMOTE_ADDR']) === 1)
1704
	{
1705
		return false;
1706
	}
1707
1708
	// Project honey pot blacklist check [Your Access Key] [Octet-Reversed IP] [List-Specific Domain]
1709
	if (empty($modSettings['badbehavior_httpbl_key']) || empty($_SERVER['REMOTE_ADDR']))
1710
	{
1711
		return false;
1712
	}
1713
1714
	// Try to load it from the cache first
1715
	$cache = Cache::instance();
1716
	$dnsQuery = $modSettings['badbehavior_httpbl_key'] . '.' . implode('.', array_reverse(explode('.', $_SERVER['REMOTE_ADDR']))) . '.dnsbl.httpbl.org';
1717
	if (!$cache->getVar($dnsResult, 'dnsQuery-' . $_SERVER['REMOTE_ADDR'], 240))
1718
	{
1719
		$dnsResult = gethostbyname($dnsQuery);
1720
		$cache->put('dnsQuery-' . $_SERVER['REMOTE_ADDR'], $dnsResult, 240);
1721
	}
1722
1723
	if (!empty($dnsResult) && $dnsResult !== $dnsQuery)
1724
	{
1725
		$result = explode('.', $dnsResult);
1726
		$result = array_map('intval', $result);
1727
		if ($result[0] === 127 // Valid Response
1728
			&& ($result[3] & 3 || $result[3] & 5) // Listed as Suspicious + Harvester || Suspicious + Comment Spammer
1729
			&& $result[2] >= $modSettings['badbehavior_httpbl_threat'] // Level
1730
			&& $result[1] <= $modSettings['badbehavior_httpbl_maxage']) // Age
1731
		{
1732
			return true;
1733
		}
1734
	}
1735
1736
	return false;
1737
}
1738
1739
/**
1740
 * This protects against brute force attacks on a member's password.
1741
 *
1742
 * What it does:
1743
 *
1744
 * - Importantly, even if the password was right we DON'T TELL THEM!
1745
 * - Allows 5 attempts every 10 seconds
1746
 *
1747
 * @param int $id_member
1748
 * @param string|bool $password_flood_value = false or string joined on |'s
1749
 * @param bool $was_correct = false
1750
 *
1751
 * @throws \ElkArte\Exceptions\Exception no_access
1752
 */
1753
function validatePasswordFlood($id_member, $password_flood_value = false, $was_correct = false)
1754
{
1755
	global $cookiename;
1756
1757
	// As this is only brute protection, we allow 5 attempts every 10 seconds.
1758
1759
	// Destroy any session or cookie data about this member, as they validated wrong.
1760
	require_once(SUBSDIR . '/Auth.subs.php');
1761
	setLoginCookie(-3600, 0);
1762
1763
	if (isset($_SESSION['login_' . $cookiename]))
1764
	{
1765
		unset($_SESSION['login_' . $cookiename]);
1766
	}
1767
1768
	// We need a member!
1769
	if ($id_member === 0)
1770
	{
1771
		// Redirect back!
1772
		redirectexit();
1773
1774
		// Probably not needed, but still make sure...
1775
		throw new \ElkArte\Exceptions\Exception('no_access', false);
1776
	}
1777
1778
	// Let's just initialize to something (and 0 is better than nothing)
1779
	$time_stamp = 0;
1780
	$number_tries = 0;
1781
1782
	// Right, have we got a flood value?
1783
	if ($password_flood_value !== false)
1784
	{
1785
		@[$time_stamp, $number_tries] = explode('|', $password_flood_value);
0 ignored issues
show
Bug introduced by
It seems like $password_flood_value can also be of type true; however, parameter $string of explode() 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

1785
		@[$time_stamp, $number_tries] = explode('|', /** @scrutinizer ignore-type */ $password_flood_value);
Loading history...
1786
	}
1787
1788
	// Timestamp invalid or non-existent?
1789
	if (empty($number_tries) || $time_stamp < (time() - 10))
1790
	{
1791
		// If it wasn't *that* long ago, don't give them another five goes.
1792
		$number_tries = !empty($number_tries) && $time_stamp < (time() - 20) ? 2 : $number_tries;
1793
		$time_stamp = time();
1794
	}
1795
1796
	$number_tries++;
1797
1798
	// Broken the law?
1799
	if ($number_tries > 5)
1800
	{
1801
		throw new \ElkArte\Exceptions\Exception('login_threshold_brute_fail', 'critical');
1802
	}
1803
1804
	// Otherwise set the members data. If they correct on their first attempt then we actually clear it, otherwise we set it!
1805
	require_once(SUBSDIR . '/Members.subs.php');
1806
	updateMemberData($id_member, ['passwd_flood' => $was_correct && $number_tries == 1 ? '' : $time_stamp . '|' . $number_tries]);
1807
}
1808
1809
/**
1810
 * This sets the X-Frame-Options header.
1811
 *
1812
 * @param string|null $override the frame option, defaults to deny.
1813
 */
1814
function frameOptionsHeader($override = null)
1815
{
1816
	global $modSettings;
1817
1818
	$option = 'SAMEORIGIN';
1819
1820
	if (is_null($override) && !empty($modSettings['frame_security']))
1821
	{
1822
		$option = $modSettings['frame_security'];
1823
	}
1824
	elseif (in_array($override, ['SAMEORIGIN', 'DENY']))
1825
	{
1826
		$option = $override;
1827
	}
1828
1829
	// Don't bother setting the header if we have disabled it.
1830
	if ($option === 'DISABLE')
1831
	{
1832
		return;
1833
	}
1834
1835
	// Finally set it.
1836
	Headers::instance()->header('X-Frame-Options', $option);
1837
}
1838
1839
/**
1840
 * This adds additional security headers that may prevent browsers from doing something they should not
1841
 *
1842
 * What it does:
1843
 *
1844
 * - X-XSS-Protection header - This header enables the Cross-site scripting (XSS) filter
1845
 * built into most recent web browsers. It's usually enabled by default, so the role of this
1846
 * header is to re-enable the filter for this particular website if it was disabled by the user.
1847
 * - X-Content-Type-Options header - It prevents the browser from doing MIME-type sniffing,
1848
 * only IE and Chrome are honouring this header. This reduces exposure to drive-by download attacks
1849
 * and sites serving user uploaded content that could be treated as executable or dynamic HTML files.
1850
 *
1851
 * @param bool|null $override
1852
 */
1853
function securityOptionsHeader($override = null)
1854
{
1855
	if ($override !== true)
1856
	{
1857
		Headers::instance()
1858
			->header('X-XSS-Protection', '1')
1859
			->header('X-Content-Type-Options', 'nosniff');
1860
	}
1861
}
1862
1863
/**
1864
 * Stop some browsers pre fetching activity to reduce server load
1865
 */
1866
function stop_prefetching()
1867
{
1868
	if ((isset($_SERVER['HTTP_PURPOSE']) && $_SERVER['HTTP_PURPOSE'] === 'prefetch')
1869
		|| (isset($_SERVER['HTTP_X_MOZ']) && $_SERVER['HTTP_X_MOZ'] === 'prefetch'))
1870
	{
1871
		@ob_end_clean();
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for ob_end_clean(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

1871
		/** @scrutinizer ignore-unhandled */ @ob_end_clean();

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
1872
		Headers::instance()
1873
			->removeHeader('all')
1874
			->headerSpecial('HTTP/1.1 403 Prefetch Forbidden')
1875
			->sendHeaders();
1876
		die;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
1877
	}
1878
}
1879
1880 24
/**
1881 24
 * Check if the admin's session is active
1882
 *
1883
 * @return bool
1884
 */
1885
function isAdminSessionActive()
1886
{
1887 24
	global $modSettings;
1888
1889
	return empty($modSettings['securityDisable']) && (isset($_SESSION['admin_time']) && $_SESSION['admin_time'] + ($modSettings['admin_session_lifetime'] * 60) > time());
1890
}
1891
1892
/**
1893
 * Check if security files exist
1894
 *
1895
 * If files are found, populate $context['security_controls_files']:
1896
 * * 'title'    - $txt['security_risk']
1897
 * * 'errors'    - An array of strings with the key being the filename and the value an error with the filename in it
1898
 *
1899
 * @event integrate_security_files Allows adding / modifying security files array
1900
 *
1901
 * @return bool
1902
 */
1903
function checkSecurityFiles()
1904
{
1905
	global $txt, $context;
1906
1907
	$has_files = false;
1908
1909
	$securityFiles = ['install.php', 'upgrade.php', 'convert.php', 'repair_paths.php', 'repair_settings.php', 'Settings.php~', 'Settings_bak.php~'];
1910
	call_integration_hook('integrate_security_files', [&$securityFiles]);
1911
1912
	foreach ($securityFiles as $securityFile)
1913
	{
1914
		if (file_exists(BOARDDIR . '/' . $securityFile))
1915
		{
1916
			$has_files = true;
1917
1918
			$context['security_controls_files']['title'] = $txt['security_risk'];
1919
			$context['security_controls_files']['errors'][$securityFile] = sprintf($txt['not_removed'], $securityFile);
1920
1921
			if ($securityFile === 'Settings.php~' || $securityFile === 'Settings_bak.php~')
1922
			{
1923
				$context['security_controls_files']['errors'][$securityFile] .= '<span class="smalltext">' . sprintf($txt['not_removed_extra'], $securityFile, substr($securityFile, 0, -1)) . '</span>';
1924
			}
1925
		}
1926
	}
1927
1928
	return $has_files;
1929
}
1930
1931
/**
1932
 * The login URL should not redirect to certain areas (attachments, js actions, etc)
1933
 * this function does these checks and return if the URL is valid or not.
1934
 *
1935
 * @param string $url - The URL to validate
1936
 * @param bool $match_board - If true tries to match board|topic in the URL as well
1937
 * @return bool
1938
 */
1939
function validLoginUrl($url, $match_board = false)
1940
{
1941
	if (empty($url))
1942
	{
1943
		return false;
1944
	}
1945
1946
	if (substr($url, 0, 7) !== 'http://' && substr($url, 0, 8) !== 'https://')
1947
	{
1948
		return false;
1949
	}
1950 2
1951
	$invalid_strings = ['dlattach' => '~(board|topic)[=,]~', 'jslocale' => '', 'login' => ''];
1952
	call_integration_hook('integrate_validLoginUrl', [&$invalid_strings]);
1953
1954
	foreach ($invalid_strings as $invalid_string => $valid_match)
1955 2
	{
1956
		if (strpos($url, $invalid_string) !== false || ($match_board === true && !empty($valid_match) && preg_match($valid_match, $url) == 0))
1957
		{
1958
			return false;
1959
		}
1960 2
	}
1961 2
1962
	return true;
1963
}
1964