showEmailAddress()   A
last analyzed

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 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, COALESCE(bg.expire_time, 0) AS expire_time
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 {int:limit}',
411
			[
412
				'cannot_access' => 1,
413
				'ban_list' => $bans,
414
				'current_time' => time(),
415
				'limit' => count($bans),
416
			]
417
		)->fetch_callback(
418
			static function ($row) {
419
				$_SESSION['ban']['cannot_access']['ids'][] = $row['id_ban'];
420
				$_SESSION['ban']['cannot_access']['reason'] = $row['reason'];
421
				$_SESSION['ban']['expire_time'] = $row['expire_time'];
422
			}
423
		);
424
425
		// My mistake. Next time better.
426
		if (!isset($_SESSION['ban']['cannot_access']))
427
		{
428
			require_once(SUBSDIR . '/Auth.subs.php');
429
			$cookie_url = url_parts(!empty($modSettings['localCookies']), !empty($modSettings['globalCookies']));
430
			elk_setcookie($cookiename . '_', '', time() - 3600, $cookie_url[1], $cookie_url[0], false, false);
431
		}
432
	}
433
434
	// If you're fully banned, it's end of the story for you.
435
	if (isset($_SESSION['ban']['cannot_access']))
436
	{
437
		require_once(SUBSDIR . '/Auth.subs.php');
438
439
		// We don't wanna see you!
440
		if (User::$info->is_guest === false)
441
		{
442
			$controller = new Auth(new EventManager());
443
			$controller->setUser(User::$info);
444
			$controller->action_logout(true, false);
445
		}
446
447
		// 'Log' the user out.  Can't have any funny business... (save the name!)
448
		$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...
449
		User::logOutUser(true);
450
		loadUserContext();
451
452
		// A goodbye present.
453
		$cookie_url = url_parts(!empty($modSettings['localCookies']), !empty($modSettings['globalCookies']));
454
		elk_setcookie($cookiename . '_', implode(',', $_SESSION['ban']['cannot_access']['ids']), time() + 3153600, $cookie_url[1], $cookie_url[0], false, false);
455
456
		// Don't scare anyone, now.
457
		$_GET['action'] = '';
458
		$_GET['board'] = '';
459
		$_GET['topic'] = '';
460
		writeLog(true);
461
462
		// You banned, sucka!
463
		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');
464
	}
465
466
	// You're not allowed to log in, but yet you are. Let's fix that.
467
	if (isset($_SESSION['ban']['cannot_login']) && User::$info->is_guest === false)
468
	{
469
		// We don't wanna see you!
470
		require_once(SUBSDIR . '/Logging.subs.php');
471
		deleteMemberLogOnline();
472
473
		// 'Log' the user out.  Can't have any funny business... (save the name!)
474
		$old_name = (string) User::$info->name !== '' ? User::$info->name : $txt['guest_title'];
475
		User::logOutUser(true);
476
		loadUserContext();
477
478
		// Wipe 'n Clean(r) erases all traces.
479
		$_GET['action'] = '';
480
		$_GET['board'] = '';
481
		$_GET['topic'] = '';
482
		writeLog(true);
483
484
		// Log them out
485
		$controller = new Auth(new EventManager());
486
		$controller->setUser(User::$info);
487
		$controller->action_logout(true, false);
488
489
		// Tell them thanks
490
		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');
491
	}
492
493
	// Fix up the banning permissions.
494
	if (!property_exists(User::$info, 'permissions'))
495
	{
496
		return;
497
	}
498
499
	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...
500
	{
501
		return;
502
	}
503
504
	banPermissions();
505
}
506
507
/**
508
 * Fix permissions according to ban status.
509
 *
510
 * What it does:
511
 *
512
 * - Applies any states of banning by removing permissions the user cannot have.
513
 *
514
 * @event integrate_post_ban_permissions Allows to update denied permissions
515
 * @event integrate_warn_permissions Allows changing of permissions for users on warning moderate
516
 * @package Bans
517
 */
518
function banPermissions()
519
{
520
	global $modSettings, $context;
521 1
522
	// Somehow they got here, at least take away all permissions...
523
	if (isset($_SESSION['ban']['cannot_access']))
524 1
	{
525
		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...
526
	}
527
	// Okay, well, you can watch, but don't touch a thing.
528
	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...
529 1
	{
530
		$denied_permissions = [
531
			'pm_send',
532
			'calendar_post', 'calendar_edit_own', 'calendar_edit_any',
533
			'poll_post',
534
			'poll_add_own', 'poll_add_any',
535
			'poll_edit_own', 'poll_edit_any',
536
			'poll_lock_own', 'poll_lock_any',
537
			'poll_remove_own', 'poll_remove_any',
538
			'manage_attachments', 'manage_smileys', 'manage_boards', 'admin_forum', 'manage_permissions',
539
			'moderate_forum', 'manage_membergroups', 'manage_bans', 'send_mail', 'edit_news',
540
			'profile_identity_any', 'profile_extra_any', 'profile_title_any',
541
			'post_new', 'post_reply_own', 'post_reply_any',
542
			'delete_own', 'delete_any', 'delete_replies',
543
			'make_sticky',
544
			'merge_any', 'split_any',
545
			'modify_own', 'modify_any', 'modify_replies',
546
			'move_any',
547
			'lock_own', 'lock_any',
548
			'remove_own', 'remove_any',
549
			'post_unapproved_topics', 'post_unapproved_replies_own', 'post_unapproved_replies_any',
550
		];
551
		theme()->getLayers()->addAfter('admin_warning', 'body');
552
553
		call_integration_hook('integrate_post_ban_permissions', [&$denied_permissions]);
554
		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

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

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

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

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

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

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