is_not_banned()   F
last analyzed

Complexity

Conditions 58
Paths 892

Size

Total Lines 238
Code Lines 113

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 3422

Importance

Changes 0
Metric Value
cc 58
eloc 113
nc 892
nop 1
dl 0
loc 238
ccs 0
cts 101
cp 0
crap 3422
rs 0.12
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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

552
		User::$info->permissions = array_diff(/** @scrutinizer ignore-type */ User::$info->permissions, $denied_permissions);
Loading history...
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...
553
	}
554
	// Are they absolutely under moderation?
555
	elseif (!empty($modSettings['warning_moderate']) && $modSettings['warning_moderate'] <= User::$info->warning)
556
	{
557
		// Work out what permissions should change...
558
		$permission_change = [
559 1
			'post_new' => 'post_unapproved_topics',
560
			'post_reply_own' => 'post_unapproved_replies_own',
561
			'post_reply_any' => 'post_unapproved_replies_any',
562
			'post_attachment' => 'post_unapproved_attachments',
563
		];
564
		call_integration_hook('integrate_warn_permissions', [&$permission_change]);
565
		foreach ($permission_change as $old => $new)
566
		{
567
			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

567
			if (!in_array($old, /** @scrutinizer ignore-type */ User::$info->permissions))
Loading history...
568
			{
569
				unset($permission_change[$old]);
570
			}
571
			else
572
			{
573
				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

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

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

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

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

1858
		/** @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...
1859
		Headers::instance()
1860
			->removeHeader('all')
1861
			->header('X-DNS-Prefetch-Control', 'off')
1862
			->header('Permissions-Policy', 'browsing-topics=(), prefetch-src=()')
1863
			->header('Cache-Control', 'no-store, no-cache, must-revalidate')
1864
			->header('X-Prefetch-Reason', 'Prefetch Forbidden')
1865
			->httpCode(403)
1866
			->sendHeaders();
1867
		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...
1868
	}
1869
}
1870
1871
/**
1872
 * Check if the admin's session is active
1873
 *
1874
 * @return bool
1875
 */
1876
function isAdminSessionActive()
1877
{
1878
	global $modSettings;
1879
1880 24
	return empty($modSettings['securityDisable']) && (isset($_SESSION['admin_time']) && $_SESSION['admin_time'] + ($modSettings['admin_session_lifetime'] * 60) > time());
1881 24
}
1882
1883
/**
1884
 * Check if security files exist
1885
 *
1886
 * If files are found, populate $context['security_controls_files']:
1887 24
 * * 'title' - $txt['security_risk']
1888
 * * 'errors' - An array of strings with the key being the filename and the value an error with the filename in it
1889
 *
1890
 * @event integrate_security_files Allows adding / modifying security files array
1891
 *
1892
 * @return bool
1893
 */
1894
function checkSecurityFiles()
1895
{
1896
	global $txt, $context;
1897
1898
	$has_files = false;
1899
1900
	$securityFiles = ['install.php', 'upgrade.php', 'convert.php', 'repair_paths.php', 'repair_settings.php', 'Settings.php~', 'Settings_bak.php~'];
1901
	call_integration_hook('integrate_security_files', [&$securityFiles]);
1902
1903
	foreach ($securityFiles as $securityFile)
1904
	{
1905
		if (file_exists(BOARDDIR . '/' . $securityFile))
1906
		{
1907
			$has_files = true;
1908
1909
			$context['security_controls_files']['title'] = $txt['security_risk'];
1910
			$context['security_controls_files']['errors'][$securityFile] = sprintf($txt['not_removed'], $securityFile);
1911
1912
			if ($securityFile === 'Settings.php~' || $securityFile === 'Settings_bak.php~')
1913
			{
1914
				$context['security_controls_files']['errors'][$securityFile] .= '<span class="smalltext">' . sprintf($txt['not_removed_extra'], $securityFile, substr($securityFile, 0, -1)) . '</span>';
1915
			}
1916
		}
1917
	}
1918
1919
	return $has_files;
1920
}
1921
1922
/**
1923
 * The login URL should not redirect to certain areas (attachments, js actions, etc.)
1924
 * this function does these checks and return if the URL is valid or not.
1925
 *
1926
 * @param string $url - The URL to validate
1927
 * @param bool $match_board - If true, tries to match board|topic in the URL as well
1928
 * @return bool
1929
 */
1930
function validLoginUrl($url, $match_board = false)
1931
{
1932
	if (empty($url))
1933
	{
1934
		return false;
1935
	}
1936
1937
	if (!str_starts_with($url, 'http://') && !str_starts_with($url, 'https://'))
1938
	{
1939
		return false;
1940
	}
1941
1942
	$invalid_strings = ['dlattach' => '~(board|topic)[=,]~', 'jslocale' => '', 'login' => ''];
1943
	call_integration_hook('integrate_validLoginUrl', [&$invalid_strings]);
1944
1945
	foreach ($invalid_strings as $invalid_string => $valid_match)
1946
	{
1947
		if (str_contains($url, $invalid_string)
1948
			|| ($match_board === true && !empty($valid_match) && preg_match($valid_match, $url) !== 1))
1949
		{
1950 2
			return false;
1951
		}
1952
	}
1953
1954
	return true;
1955
}
1956