Passed
Branch development (176841)
by Elk
15:26
created

allowedTo()   B

Complexity

Conditions 11
Paths 23

Size

Total Lines 82
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 38.8581

Importance

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

517
		User::$info->permissions = array_diff(/** @scrutinizer ignore-type */ User::$info->permissions, $denied_permissions);
Loading history...
518
	}
519
	// Are they absolutely under moderation?
520
	elseif (!empty($modSettings['warning_moderate']) && $modSettings['warning_moderate'] <= User::$info->warning)
521
	{
522
		// Work out what permissions should change...
523
		$permission_change = array(
524
			'post_new' => 'post_unapproved_topics',
525
			'post_reply_own' => 'post_unapproved_replies_own',
526
			'post_reply_any' => 'post_unapproved_replies_any',
527
			'post_attachment' => 'post_unapproved_attachments',
528
		);
529
		call_integration_hook('integrate_warn_permissions', array(&$permission_change));
530
		foreach ($permission_change as $old => $new)
531
		{
532
			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

532
			if (!in_array($old, /** @scrutinizer ignore-type */ User::$info->permissions))
Loading history...
533
			{
534
				unset($permission_change[$old]);
535
			}
536
			else
537
			{
538
				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|null expected by parameter $array2 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

538
				User::$info->permissions = array_merge((array) User::$info->permissions, /** @scrutinizer ignore-type */ $new);
Loading history...
539
			}
540
		}
541
		User::$info->permissions = array_diff(User::$info->permissions, array_keys($permission_change));
542 1
	}
543
544
	// @todo Find a better place to call this? Needs to be after permissions loaded!
545
	// Finally, some bits we cache in the session because it saves queries.
546
	if (isset($_SESSION['mc']) && $_SESSION['mc']['time'] > $modSettings['settings_updated'] && $_SESSION['mc']['id'] == User::$info->id)
547
	{
548
		User::$info->mod_cache = $_SESSION['mc'];
0 ignored issues
show
Bug Best Practice introduced by
The property mod_cache does not exist on ElkArte\ValuesContainer. Since you implemented __set, consider adding a @property annotation.
Loading history...
549
	}
550
	else
551
	{
552
		require_once(SUBSDIR . '/Auth.subs.php');
553
		rebuildModCache();
554
	}
555
556
	// Now that we have the mod cache taken care of lets setup a cache for the number of mod reports still open
557
	if (isset($_SESSION['rc']) && $_SESSION['rc']['time'] > $modSettings['last_mod_report_action'] && $_SESSION['rc']['id'] == User::$info->id)
558
	{
559
		$context['open_mod_reports'] = $_SESSION['rc']['reports'];
560
		if (allowedTo('admin_forum'))
561
		{
562
			$context['open_pm_reports'] = $_SESSION['rc']['pm_reports'];
563
		}
564 1
	}
565
	elseif ($_SESSION['mc']['bq'] != '0=1')
566
	{
567
		require_once(SUBSDIR . '/Moderation.subs.php');
568 1
		recountOpenReports(true, allowedTo('admin_forum'));
569 1
	}
570
	else
571
		$context['open_mod_reports'] = 0;
572
}
573 1
574
/**
575
 * Log a ban in the database.
576
 *
577
 * What it does:
578
 *
579
 * - Log the current user in the ban logs.
580
 * - Increment the hit counters for the specified ban ID's (if any.)
581 1
 *
582
 * @package Bans
583
 * @param int[] $ban_ids = array()
584
 * @param string|null $email = null
585
 */
586
function log_ban($ban_ids = array(), $email = null)
587 1
{
588 1
	$db = database();
589
590
	// Don't log web accelerators, it's very confusing...
591
	if (isset($_SERVER['HTTP_X_MOZ']) && $_SERVER['HTTP_X_MOZ'] == 'prefetch')
592
		return;
593
594
	$db->insert('',
595
		'{db_prefix}log_banned',
596
		array(
597
			'id_member' => 'int',
598
			'ip' => 'string-16',
599
			'email' => 'string',
600
			'log_time' => 'int'
601
		),
602
		array(
603
			User::$info->id,
604
			User::$info->ip,
605
			$email === null ? (string) User::$info->email : $email,
606
			time()
607
		),
608
		array('id_ban_log')
609
	);
610
611
	// One extra point for these bans.
612
	if (!empty($ban_ids))
613
	{
614
		$db->query('', '
615
			UPDATE {db_prefix}ban_items
616
			SET hits = hits + 1
617
			WHERE id_ban IN ({array_int:ban_ids})',
618
			array(
619
				'ban_ids' => $ban_ids,
620
			)
621
		);
622
	}
623
}
624
625
/**
626
 * Checks if a given email address might be banned.
627
 *
628
 * What it does:
629
 *
630
 * - Check if a given email is banned.
631
 * - Performs an immediate ban if the turns turns out positive.
632
 *
633
 * @package Bans
634
 * @param string $email
635
 * @param string $restriction
636
 * @param string $error
637
 *
638
 * @throws \ElkArte\Exceptions\Exception
639
 */
640
function isBannedEmail($email, $restriction, $error)
641
{
642
	global $txt;
643
644
	$db = database();
645
646
	// Can't ban an empty email
647
	if (empty($email) || trim($email) == '')
648 2
	{
649
		return;
650 2
	}
651
652
	// Let's start with the bans based on your IP/hostname/memberID...
653 2
	$ban_ids = isset($_SESSION['ban'][$restriction]) ? $_SESSION['ban'][$restriction]['ids'] : array();
654
	$ban_reason = isset($_SESSION['ban'][$restriction]) ? $_SESSION['ban'][$restriction]['reason'] : '';
655
656
	// ...and add to that the email address you're trying to register.
657 2
	$request = $db->query('', '
658 2
		SELECT bi.id_ban, bg.' . $restriction . ', bg.cannot_access, bg.reason
659
		FROM {db_prefix}ban_items AS bi
660
			INNER JOIN {db_prefix}ban_groups AS bg ON (bg.id_ban_group = bi.id_ban_group)
661 2
		WHERE {string:email} LIKE bi.email_address
662 2
			AND (bg.' . $restriction . ' = {int:cannot_access} OR bg.cannot_access = {int:cannot_access})
663
			AND (bg.expire_time IS NULL OR bg.expire_time >= {int:now})',
664
		array(
665
			'email' => $email,
666 2
			'cannot_access' => 1,
667
			'now' => time(),
668
		)
669 2
	);
670 2
	while ($row = $db->fetch_assoc($request))
0 ignored issues
show
Bug introduced by
The method fetch_assoc() does not exist on ElkArte\Database\QueryInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to ElkArte\Database\QueryInterface. ( Ignorable by Annotation )

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

670
	while ($row = $db->/** @scrutinizer ignore-call */ fetch_assoc($request))
Loading history...
671 2
	{
672
		if (!empty($row['cannot_access']))
673
		{
674 2
			$_SESSION['ban']['cannot_access']['ids'][] = $row['id_ban'];
675
			$_SESSION['ban']['cannot_access']['reason'] = $row['reason'];
676
		}
677
		if (!empty($row[$restriction]))
678
		{
679
			$ban_ids[] = $row['id_ban'];
680
			$ban_reason = $row['reason'];
681
		}
682
	}
683
	$db->free_result($request);
0 ignored issues
show
Bug introduced by
The method free_result() does not exist on ElkArte\Database\QueryInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to ElkArte\Database\QueryInterface. ( Ignorable by Annotation )

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

683
	$db->/** @scrutinizer ignore-call */ 
684
      free_result($request);
Loading history...
684
685
	// You're in biiig trouble.  Banned for the rest of this session!
686
	if (isset($_SESSION['ban']['cannot_access']))
687 2
	{
688
		log_ban($_SESSION['ban']['cannot_access']['ids']);
689
		$_SESSION['ban']['last_checked'] = time();
690 2
691
		throw new \ElkArte\Exceptions\Exception(sprintf($txt['your_ban'], $txt['guest_title']) . $_SESSION['ban']['cannot_access']['reason'], false);
692
	}
693
694
	if (!empty($ban_ids))
695
	{
696
		// Log this ban for future reference.
697
		log_ban($ban_ids, $email);
698 2
		throw new \ElkArte\Exceptions\Exception($error . $ban_reason, false);
699
	}
700
}
701
702
/**
703
 * Make sure the user's correct session was passed, and they came from here.
704 2
 *
705
 * What it does:
706
 *
707
 * - Checks the current session, verifying that the person is who he or she should be.
708
 * - Also checks the referrer to make sure they didn't get sent here.
709
 * - Depends on the disableCheckUA setting, which is usually missing.
710
 * - Will check GET, POST, or REQUEST depending on the passed type.
711
 * - Also optionally checks the referring action if passed. (note that the referring action must be by GET.)
712
 *
713
 * @param string $type = 'post' (post, get, request)
714
 * @param string $from_action = ''
715
 * @param bool   $is_fatal = true
716
 *
717
 * @return string the error message if is_fatal is false.
718
 * @throws \ElkArte\Exceptions\Exception
719
 */
720
function checkSession($type = 'post', $from_action = '', $is_fatal = true)
721
{
722
	global $modSettings, $boardurl;
723
724
	// We'll work out user agent checks
725
	$req = request();
726
727
	// Is it in as $_POST['sc']?
728
	if ($type == 'post')
729
	{
730
		$check = isset($_POST[$_SESSION['session_var']]) ? $_POST[$_SESSION['session_var']] : (empty($modSettings['strictSessionCheck']) && isset($_POST['sc']) ? $_POST['sc'] : null);
731
		if ($check !== $_SESSION['session_value'])
732
			$error = 'session_timeout';
733
	}
734
	// How about $_GET['sesc']?
735
	elseif ($type === 'get')
736
	{
737
		$check = isset($_GET[$_SESSION['session_var']]) ? $_GET[$_SESSION['session_var']] : (empty($modSettings['strictSessionCheck']) && isset($_GET['sesc']) ? $_GET['sesc'] : null);
738
		if ($check !== $_SESSION['session_value'])
739
			$error = 'session_verify_fail';
740
	}
741
	// Or can it be in either?
742
	elseif ($type == 'request')
743
	{
744
		$check = isset($_GET[$_SESSION['session_var']]) ? $_GET[$_SESSION['session_var']] : (empty($modSettings['strictSessionCheck']) && isset($_GET['sesc']) ? $_GET['sesc'] : (isset($_POST[$_SESSION['session_var']]) ? $_POST[$_SESSION['session_var']] : (empty($modSettings['strictSessionCheck']) && isset($_POST['sc']) ? $_POST['sc'] : null)));
745
746
		if ($check !== $_SESSION['session_value'])
747
			$error = 'session_verify_fail';
748
	}
749
750
	// Verify that they aren't changing user agents on us - that could be bad.
751
	if ((!isset($_SESSION['USER_AGENT']) || $_SESSION['USER_AGENT'] != $req->user_agent()) && empty($modSettings['disableCheckUA']))
752
		$error = 'session_verify_fail';
753
754
	// Make sure a page with session check requirement is not being prefetched.
755
	stop_prefetching();
756
757
	// Check the referring site - it should be the same server at least!
758
	if (isset($_SESSION['request_referer']))
759
		$referrer_url = $_SESSION['request_referer'];
760
	else
761
		$referrer_url = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '';
762
763
	$referrer = @parse_url($referrer_url);
764
765
	if (!empty($referrer['host']))
766
	{
767
		if (strpos($_SERVER['HTTP_HOST'], ':') !== false)
768
			$real_host = substr($_SERVER['HTTP_HOST'], 0, strpos($_SERVER['HTTP_HOST'], ':'));
769
		else
770
			$real_host = $_SERVER['HTTP_HOST'];
771
772
		$parsed_url = parse_url($boardurl);
773
774
		// Are global cookies on? If so, let's check them ;).
775
		if (!empty($modSettings['globalCookies']))
776
		{
777
			if (preg_match('~(?:[^\.]+\.)?([^\.]{3,}\..+)\z~i', $parsed_url['host'], $parts) == 1)
778
				$parsed_url['host'] = $parts[1];
779
780
			if (preg_match('~(?:[^\.]+\.)?([^\.]{3,}\..+)\z~i', $referrer['host'], $parts) == 1)
781
				$referrer['host'] = $parts[1];
782
783
			if (preg_match('~(?:[^\.]+\.)?([^\.]{3,}\..+)\z~i', $real_host, $parts) == 1)
784
				$real_host = $parts[1];
785
		}
786
787
		// Okay: referrer must either match parsed_url or real_host.
788
		if (isset($parsed_url['host']) && strtolower($referrer['host']) != strtolower($parsed_url['host']) && strtolower($referrer['host']) != strtolower($real_host))
789
		{
790
			$error = 'verify_url_fail';
791
			$log_error = true;
792
			$sprintf = array(\ElkArte\Util::htmlspecialchars($referrer_url));
793
		}
794
	}
795
796
	// Well, first of all, if a from_action is specified you'd better have an old_url.
797
	if (!empty($from_action) && (!isset($_SESSION['old_url']) || preg_match('~[?;&]action=' . $from_action . '([;&]|$)~', $_SESSION['old_url']) == 0))
798
	{
799
		$error = 'verify_url_fail';
800
		$log_error = true;
801
		$sprintf = array(\ElkArte\Util::htmlspecialchars($referrer_url));
802
	}
803
804
	// Everything is ok, return an empty string.
805
	if (!isset($error))
806
		return '';
807
	// A session error occurred, show the error.
808
	elseif ($is_fatal)
809
	{
810
		if (isset($_GET['xml']) || isset($_REQUEST['api']))
811
		{
812
			@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

812
			/** @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...
813
			header('HTTP/1.1 403 Forbidden - Session timeout');
814
			die;
815
		}
816
		else
817
			throw new \ElkArte\Exceptions\Exception($error, isset($log_error) ? 'user' : false, isset($sprintf) ? $sprintf : array());
818
	}
819
	// A session error occurred, return the error to the calling function.
820
	else
821
		return $error;
822
823
	// We really should never fall through here, for very important reasons.  Let's make sure.
824
	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...
825
}
826
827
/**
828
 * Lets give you a token of our appreciation.
829
 *
830
 * What it does:
831
 *
832
 * - Creates a one time use form token
833
 *
834
 * @param string $action The specific site action that a token will be generated for
835
 * @param string $type = 'post' If the token will be returned via post or get
836
 *
837
 * @return string[] array of token var, time, csrf, token
838
 */
839
function createToken($action, $type = 'post')
840
{
841
	global $context;
842
843
	// Generate a new token token_var pair
844
	$tokenizer = new \ElkArte\TokenHash();
845 11
	$token_var = $tokenizer->generate_hash(rand(7, 12));
846
	$token = $tokenizer->generate_hash(32);
847
848 11
	// We need user agent and the client IP
849 11
	$req = request();
850 11
	$csrf_hash = hash('sha1', $token . $req->client_ip() . $req->user_agent());
851
852
	// Save the session token and make it available to the forms
853 11
	$_SESSION['token'][$type . '-' . $action] = array($token_var, $csrf_hash, time(), $token);
854 11
	$context[$action . '_token'] = $token;
855
	$context[$action . '_token_var'] = $token_var;
856
857 11
	return array($action . '_token_var' => $token_var, $action . '_token' => $token);
858 11
}
859 11
860
/**
861 11
 * Only patrons with valid tokens can ride this ride.
862
 *
863
 * What it does:
864
 *
865
 * - Validates that the received token is correct
866
 * - 1. The token exists in session.
867
 * - 2. The {$type} variable should exist.
868
 * - 3. We concatenate the variable we received with the user agent
869
 * - 4. Match that result against what is in the session.
870
 * - 5. If it matches, success, otherwise we fallout.
871
 *
872
 * @param string $action
873
 * @param string $type = 'post' (get, request, or post)
874
 * @param bool   $reset = true Reset the token on failure
875
 * @param bool   $fatal if true a fatal_lang_error is issued for invalid tokens, otherwise false is returned
876
 *
877
 * @return bool except for $action == 'login' where the token is returned
878
 * @throws \ElkArte\Exceptions\Exception token_verify_fail
879
 */
880
function validateToken($action, $type = 'post', $reset = true, $fatal = true)
881
{
882
	$type = ($type === 'get' || $type === 'request') ? $type : 'post';
883
	$token_index = $type . '-' . $action;
884
885
	// Logins are special: the token is used to have the password with javascript before POST it
886 6
	if ($action == 'login')
887 6
	{
888
		if (isset($_SESSION['token'][$token_index]))
889
		{
890 6
			$return = $_SESSION['token'][$token_index][3];
891
			unset($_SESSION['token'][$token_index]);
892
893
			return $return;
894
		}
895
		else
896
			return '';
0 ignored issues
show
Bug Best Practice introduced by
The expression return '' returns the type string which is incompatible with the documented return type boolean.
Loading history...
897
	}
898
899
	if (!isset($_SESSION['token'][$token_index]))
900
		return false;
901
902
	// We need the user agent and client IP
903 6
	$req = request();
904 6
905
	// Shortcut
906
	$passed_token_var = isset($GLOBALS['_' . strtoupper($type)][$_SESSION['token'][$token_index][0]]) ? $GLOBALS['_' . strtoupper($type)][$_SESSION['token'][$token_index][0]] : null;
907
	$csrf_hash = hash('sha1', $passed_token_var . $req->client_ip() . $req->user_agent());
908
909
	// Checked what was passed in combination with the user agent
910
	if (isset($_SESSION['token'][$token_index], $passed_token_var)
911
		&& $csrf_hash === $_SESSION['token'][$token_index][1])
912
	{
913
		// Consume the token, let them pass
914
		unset($_SESSION['token'][$token_index]);
915
916
		return true;
917
	}
918
919
	// Patrons with invalid tokens get the boot.
920
	if ($reset)
921
	{
922
		// Might as well do some cleanup on this.
923
		cleanTokens();
924
925
		// I'm back baby.
926
		createToken($action, $type);
927
928
		if ($fatal)
929
			throw new \ElkArte\Exceptions\Exception('token_verify_fail', false);
930
	}
931
	// You don't get a new token
932
	else
933
	{
934
		// Explicitly remove this token
935
		unset($_SESSION['token'][$token_index]);
936
937
		// Remove older tokens.
938
		cleanTokens();
939
	}
940
941
	return false;
942
}
943
944
/**
945
 * Removes old unused tokens from session
946
 *
947
 * What it does:
948
 *
949
 * - Defaults to 3 hours before a token is considered expired
950
 * - if $complete = true will remove all tokens
951
 *
952
 * @param bool $complete = false
953
 * @param string $suffix = false
954
 */
955
function cleanTokens($complete = false, $suffix = '')
956
{
957
	// We appreciate cleaning up after yourselves.
958
	if (!isset($_SESSION['token']))
959
		return;
960
961
	// Clean up tokens, trying to give enough time still.
962 1
	foreach ($_SESSION['token'] as $key => $data)
963
	{
964
		if (!empty($suffix))
965
			$force = $complete || strpos($key, $suffix);
966 1
		else
967
			$force = $complete;
968 1
969
		if ($data[2] + 10800 < time() || $force)
970
			unset($_SESSION['token'][$key]);
971 1
	}
972
}
973 1
974
/**
975
 * Check whether a form has been submitted twice.
976 1
 *
977
 * What it does:
978
 *
979
 * - Registers a sequence number for a form.
980
 * - Checks whether a submitted sequence number is registered in the current session.
981
 * - Depending on the value of is_fatal shows an error or returns true or false.
982
 * - Frees a sequence number from the stack after it's been checked.
983
 * - Frees a sequence number without checking if action == 'free'.
984
 *
985
 * @param string $action
986
 * @param bool   $is_fatal = true
987
 *
988
 * @return bool
989
 * @throws \ElkArte\Exceptions\Exception error_form_already_submitted
990
 */
991
function checkSubmitOnce($action, $is_fatal = false)
992
{
993
	global $context;
994
995
	if (!isset($_SESSION['forms']))
996
		$_SESSION['forms'] = array();
997
998
	// Register a form number and store it in the session stack. (use this on the page that has the form.)
999
	if ($action == 'register')
1000
	{
1001
		$tokenizer = new \ElkArte\TokenHash();
1002
		$context['form_sequence_number'] = '';
1003
		while (empty($context['form_sequence_number']) || in_array($context['form_sequence_number'], $_SESSION['forms']))
1004
			$context['form_sequence_number'] = $tokenizer->generate_hash();
1005
	}
1006
	// Check whether the submitted number can be found in the session.
1007
	elseif ($action == 'check')
1008
	{
1009
		if (!isset($_REQUEST['seqnum']))
1010
			return true;
1011
		elseif (!in_array($_REQUEST['seqnum'], $_SESSION['forms']))
1012
		{
1013
			// Mark this one as used
1014
			$_SESSION['forms'][] = (string) $_REQUEST['seqnum'];
1015
			return true;
1016
		}
1017
		elseif ($is_fatal)
1018
			throw new \ElkArte\Exceptions\Exception('error_form_already_submitted', false);
1019
		else
1020
			return false;
1021
	}
1022
	// Don't check, just free the stack number.
1023
	elseif ($action == 'free' && isset($_REQUEST['seqnum']) && in_array($_REQUEST['seqnum'], $_SESSION['forms']))
1024
		$_SESSION['forms'] = array_diff($_SESSION['forms'], array($_REQUEST['seqnum']));
1025
	elseif ($action != 'free')
1026
		trigger_error('checkSubmitOnce(): Invalid action \'' . $action . '\'', E_USER_WARNING);
1027
}
1028
1029
/**
1030
 * This function checks whether the user is allowed to do permission. (ie. post_new.)
1031
 *
1032
 * What it does:
1033
 *
1034
 * - If boards parameter is specified, checks those boards instead of the current one (if applicable).
1035
 * - Always returns true if the user is an administrator.
1036
 *
1037
 * @param string[]|string $permission permission
1038
 * @param int[]|int|null $boards array of board IDs, a single id or null
1039
 *
1040
 * @return boolean if the user can do the permission
1041
 */
1042
function allowedTo($permission, $boards = null)
1043
{
1044
	$db = database();
1045
1046
	// You're always allowed to do nothing. (unless you're a working man, MR. LAZY :P!)
1047
	if (empty($permission))
1048 76
	{
1049
		return true;
1050 76
	}
1051
1052
	// You're never allowed to do something if your data hasn't been loaded yet!
1053 76
	if (empty(User::$info))
1054
	{
1055
		return false;
1056
	}
1057 76
1058
	// Administrators are supermen :P.
1059
	if (User::$info->is_admin)
1060
	{
1061 76
		return true;
1062 58
	}
1063
1064
	// Make sure permission is a valid array
1065 18
	if (!is_array($permission))
1066 18
	{
1067
		$permission = array($permission);
1068
	}
1069 18
1070
	// Are we checking the _current_ board, or some other boards?
1071 18
	if ($boards === null)
1072 6
	{
1073
		if (empty(User::$info->permissions))
1074
		{
1075 12
			return false;
1076
		}
1077
1078
		// Check if they can do it, you aren't allowed, by default.
1079
		return count(array_intersect($permission, User::$info->permissions)) !== 0;
1080
	}
1081
1082
	if (!is_array($boards))
1083
		$boards = array($boards);
1084
1085
	if (empty(User::$info->groups))
1086
	{
1087
		return false;
1088
	}
1089
1090
	$request = $db->query('', '
1091
		SELECT MIN(bp.add_deny) AS add_deny
1092
		FROM {db_prefix}boards AS b
1093
			INNER JOIN {db_prefix}board_permissions AS bp ON (bp.id_profile = b.id_profile)
1094
			LEFT JOIN {db_prefix}moderators AS mods ON (mods.id_board = b.id_board AND mods.id_member = {int:current_member})
1095
		WHERE b.id_board IN ({array_int:board_list})
1096
			AND bp.id_group IN ({array_int:group_list}, {int:moderator_group})
1097
			AND bp.permission IN ({array_string:permission_list})
1098
			AND (mods.id_member IS NOT NULL OR bp.id_group != {int:moderator_group})
1099
		GROUP BY b.id_board',
1100
		array(
1101
			'current_member' => User::$info->id,
1102
			'board_list' => $boards,
1103
			'group_list' => User::$info->groups,
1104
			'moderator_group' => 3,
1105
			'permission_list' => $permission,
1106
		)
1107
	);
1108
1109
	// Make sure they can do it on all of the boards.
1110
	if ($db->num_rows($request) != count($boards))
0 ignored issues
show
Bug introduced by
The method num_rows() does not exist on ElkArte\Database\QueryInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to ElkArte\Database\QueryInterface. ( Ignorable by Annotation )

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

1110
	if ($db->/** @scrutinizer ignore-call */ num_rows($request) != count($boards))
Loading history...
1111
	{
1112
		return false;
1113
	}
1114
1115
	$result = true;
1116
	while ($row = $db->fetch_assoc($request))
1117
	{
1118
		$result &= !empty($row['add_deny']);
1119
	}
1120
	$db->free_result($request);
1121
1122
	// If the query returned 1, they can do it... otherwise, they can't.
1123
	return $result;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $result also could return the type integer which is incompatible with the documented return type boolean.
Loading history...
1124
}
1125
1126
/**
1127
 * This function returns fatal error if the user doesn't have the respective permission.
1128
 *
1129
 * What it does:
1130
 *
1131
 * - Uses allowedTo() to check if the user is allowed to do permission.
1132
 * - Checks the passed boards or current board for the permission.
1133 32
 * - If they are not, it loads the Errors language file and shows an error using $txt['cannot_' . $permission].
1134
 * - If they are a guest and cannot do it, this calls is_not_guest().
1135 32
 *
1136
 * @param string[]|string $permission array of or single string, of permissions to check
1137
 * @param int[]|null      $boards = null
1138
 *
1139
 * @throws \ElkArte\Exceptions\Exception
1140
 */
1141
function isAllowedTo($permission, $boards = null)
1142
{
1143
	global $txt;
1144
1145
	static $heavy_permissions = array(
1146
		'admin_forum',
1147
		'manage_attachments',
1148 32
		'manage_smileys',
1149
		'manage_boards',
1150
		'edit_news',
1151 32
		'moderate_forum',
1152
		'manage_bans',
1153
		'manage_membergroups',
1154
		'manage_permissions',
1155
	);
1156
1157
	// Make it an array, even if a string was passed.
1158
	$permission = is_array($permission) ? $permission : array($permission);
1159
1160
	// Check the permission and return an error...
1161
	if (!allowedTo($permission, $boards))
1162
	{
1163
		// Pick the last array entry as the permission shown as the error.
1164
		$error_permission = array_shift($permission);
1165
1166
		// If they are a guest, show a login. (because the error might be gone if they do!)
1167
		if (User::$info->is_guest)
1168
		{
1169
			theme()->getTemplates()->loadLanguageFile('Errors');
1170
			is_not_guest($txt['cannot_' . $error_permission]);
1171
		}
1172
1173
		// Clear the action because they aren't really doing that!
1174 32
		$_GET['action'] = '';
1175
		$_GET['board'] = '';
1176 32
		$_GET['topic'] = '';
1177
		writeLog(true);
1178
1179
		throw new \ElkArte\Exceptions\Exception('cannot_' . $error_permission, false);
1180
	}
1181
1182
	// If you're doing something on behalf of some "heavy" permissions, validate your session.
1183
	// (take out the heavy permissions, and if you can't do anything but those, you need a validated session.)
1184
	if (!allowedTo(array_diff($permission, $heavy_permissions), $boards))
1185
		validateSession();
1186
}
1187
1188
/**
1189
 * Return the boards a user has a certain (board) permission on. (array(0) if all.)
1190
 *
1191
 * What it does:
1192
 *
1193
 * - Returns a list of boards on which the user is allowed to do the specified permission.
1194
 * - Returns an array with only a 0 in it if the user has permission to do this on every board.
1195
 * - Returns an empty array if he or she cannot do this on any board.
1196
 * - If check_access is true will also make sure the group has proper access to that board.
1197
 *
1198
 * @param string[]|string $permissions array of permission names to check access against
1199 3
 * @param bool $check_access = true
1200
 * @param bool $simple = true Set $simple to true to use this function in compatibility mode
1201 3
 *             otherwise, the resultant array becomes split into the multiple
1202
 *             permissions that were passed. Other than that, it's just the normal
1203
 *             state of play that you're used to.
1204 3
 *
1205 3
 * @return array
1206
 */
1207
function boardsAllowedTo($permissions, $check_access = true, $simple = true)
1208 3
{
1209
	$db = database();
1210 2
1211 2
	// Arrays are nice, most of the time.
1212
	if (!is_array($permissions))
1213
	{
1214
		$permissions = array($permissions);
1215
	}
1216
1217
	// I am the master, the master of the universe!
1218
	if (User::$info->is_admin)
1219
	{
1220
		if ($simple)
1221
		{
1222
			return array(0);
1223 1
		}
1224
		else
1225 1
		{
1226 1
			$boards = array();
1227
			foreach ($permissions as $permission)
1228
			{
1229
				$boards[$permission] = array(0);
1230
			}
1231
1232
			return $boards;
1233 1
		}
1234
	}
1235 1
1236 1
	// All groups the user is in except 'moderator'.
1237 1
	$groups = array_diff(User::$info->groups, array(3));
0 ignored issues
show
Bug introduced by
It seems like ElkArte\User::info->groups can also be of type null; however, parameter $array1 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

1237
	$groups = array_diff(/** @scrutinizer ignore-type */ User::$info->groups, array(3));
Loading history...
1238 1
1239
	$request = $db->query('', '
1240
		SELECT b.id_board, bp.add_deny' . ($simple ? '' : ', bp.permission') . '
1241 1
		FROM {db_prefix}board_permissions AS bp
1242 1
			INNER JOIN {db_prefix}boards AS b ON (b.id_profile = bp.id_profile)
1243 1
			LEFT JOIN {db_prefix}moderators AS mods ON (mods.id_board = b.id_board AND mods.id_member = {int:current_member})
1244
		WHERE bp.id_group IN ({array_int:group_list}, {int:moderator_group})
1245
			AND bp.permission IN ({array_string:permissions})
1246
			AND (mods.id_member IS NOT NULL OR bp.id_group != {int:moderator_group})' .
1247
			($check_access ? ' AND {query_see_board}' : ''),
1248
		array(
1249
			'current_member' => User::$info->id,
1250
			'group_list' => $groups,
1251
			'moderator_group' => 3,
1252
			'permissions' => $permissions,
1253
		)
1254
	);
1255
	$boards = array();
1256
	$deny_boards = array();
1257
	while ($row = $db->fetch_assoc($request))
1258
	{
1259
		if ($simple)
1260 1
		{
1261
			if (empty($row['add_deny']))
1262 1
				$deny_boards[] = $row['id_board'];
1263 1
			else
1264
				$boards[] = $row['id_board'];
1265
		}
1266
		else
1267
		{
1268
			if (empty($row['add_deny']))
1269
				$deny_boards[$row['permission']][] = $row['id_board'];
1270
			else
1271
				$boards[$row['permission']][] = $row['id_board'];
1272
		}
1273
	}
1274
	$db->free_result($request);
1275
1276
	if ($simple)
1277
		$boards = array_unique(array_values(array_diff($boards, $deny_boards)));
1278
	else
1279
	{
1280 1
		foreach ($permissions as $permission)
1281
		{
1282
			// Never had it to start with
1283
			if (empty($boards[$permission]))
1284
				$boards[$permission] = array();
1285
			else
1286
			{
1287
				// Or it may have been removed
1288
				$deny_boards[$permission] = isset($deny_boards[$permission]) ? $deny_boards[$permission] : array();
1289
				$boards[$permission] = array_unique(array_values(array_diff($boards[$permission], $deny_boards[$permission])));
1290
			}
1291
		}
1292
	}
1293
1294
	return $boards;
1295
}
1296
1297
/**
1298
 * Returns whether an email address should be shown and how.
1299
 *
1300
 * What it does:
1301
 *
1302
 * Possible outcomes are:
1303 2
 * - 'yes': show the full email address
1304
 * - 'yes_permission_override': show the full email address, either you
1305
 * are a moderator or it's your own email address.
1306
 * - 'no_through_forum': don't show the email address, but do allow
1307
 * things to be mailed using the built-in forum mailer.
1308
 * - 'no': keep the email address hidden.
1309
 *
1310
 * @param bool $userProfile_hideEmail
1311
 * @param int $userProfile_id
1312
 *
1313 2
 * @return string (yes, yes_permission_override, no_through_forum, no)
1314
 */
1315 2
function showEmailAddress($userProfile_hideEmail, $userProfile_id)
1316 2
{
1317
	// Should this user's email address be shown?
1318
	// If you're guest: no.
1319
	// If the user is post-banned: no.
1320
	// If it's your own profile and you've not set your address hidden: yes_permission_override.
1321
	// If you're a moderator with sufficient permissions: yes_permission_override.
1322
	// If the user has set their profile to do not email me: no.
1323
	// Otherwise: no_through_forum. (don't show it but allow emailing the member)
1324
1325
	if (User::$info->is_guest || isset($_SESSION['ban']['cannot_post']))
1326
	{
1327
		return 'no';
1328
	}
1329
	elseif ((User::$info->is_guest === false && User::$info->id == $userProfile_id && !$userProfile_hideEmail))
1330
	{
1331
		return 'yes_permission_override';
1332
	}
1333
	elseif (allowedTo('moderate_forum'))
1334
	{
1335
		return 'yes_permission_override';
1336
	}
1337
	elseif ($userProfile_hideEmail)
1338
	{
1339
		return 'no';
1340
	}
1341
	else
1342
	{
1343
		return 'no_through_forum';
1344
	}
1345
}
1346
1347
/**
1348
 * This function attempts to protect from carrying out specific actions repeatedly.
1349
 *
1350
 * What it does:
1351
 *
1352
 * - Checks if a user is trying specific actions faster than a given minimum wait threshold.
1353
 * - The time taken depends on error_type - generally uses the modSetting.
1354
 * - Generates a fatal message when triggered, suspending execution.
1355
 *
1356
 * @event integrate_spam_protection Allows to update action wait timeOverrides
1357
 * @param string  $error_type used also as a $txt index. (not an actual string.)
1358
 * @param boolean $fatal is the spam check a fatal error on failure
1359
 *
1360
 * @return bool|int|mixed
1361
 * @throws \ElkArte\Exceptions\Exception
1362
 */
1363
function spamProtection($error_type, $fatal = true)
1364
{
1365
	global $modSettings;
1366
1367
	$db = database();
1368
1369
	// Certain types take less/more time.
1370
	$timeOverrides = array(
1371
		'login' => 2,
1372
		'register' => 2,
1373
		'remind' => 30,
1374
		'contact' => 30,
1375
		'sendtopic' => $modSettings['spamWaitTime'] * 4,
1376
		'sendmail' => $modSettings['spamWaitTime'] * 5,
1377
		'reporttm' => $modSettings['spamWaitTime'] * 4,
1378
		'search' => !empty($modSettings['search_floodcontrol_time']) ? $modSettings['search_floodcontrol_time'] : 1,
1379
	);
1380
	call_integration_hook('integrate_spam_protection', array(&$timeOverrides));
1381
1382
	// Moderators are free...
1383
	if (!allowedTo('moderate_board'))
1384
	{
1385
		$timeLimit = isset($timeOverrides[$error_type]) ? $timeOverrides[$error_type] : $modSettings['spamWaitTime'];
1386
	}
1387
	else
1388
	{
1389
		$timeLimit = 2;
1390
	}
1391
1392
	// Delete old entries...
1393
	$db->query('', '
1394
		DELETE FROM {db_prefix}log_floodcontrol
1395
		WHERE log_time < {int:log_time}
1396
			AND log_type = {string:log_type}',
1397
		array(
1398
			'log_time' => time() - $timeLimit,
1399
			'log_type' => $error_type,
1400
		)
1401
	);
1402
1403
	// Add a new entry, deleting the old if necessary.
1404
	$db->insert('replace',
1405
		'{db_prefix}log_floodcontrol',
1406
		array('ip' => 'string-16', 'log_time' => 'int', 'log_type' => 'string'),
1407
		array(User::$info->ip, time(), $error_type),
1408
		array('ip', 'log_type')
1409
	);
1410
1411
	// If affected is 0 or 2, it was there already.
1412
	if ($db->affected_rows() != 1)
0 ignored issues
show
Bug introduced by
The method affected_rows() does not exist on ElkArte\Database\QueryInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to ElkArte\Database\QueryInterface. ( Ignorable by Annotation )

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

1412
	if ($db->/** @scrutinizer ignore-call */ affected_rows() != 1)
Loading history...
1413
	{
1414
		// Spammer!  You only have to wait a *few* seconds!
1415
		if ($fatal)
1416
		{
1417
			throw new \ElkArte\Exceptions\Exception($error_type . '_WaitTime_broken', false, array($timeLimit));
1418
		}
1419
		else
1420
			return $timeLimit;
1421
	}
1422
1423
	// They haven't posted within the limit.
1424
	return false;
1425
}
1426
1427
/**
1428
 * A generic function to create a pair of index.php and .htaccess files in a directory
1429
 *
1430
 * @param string $path the (absolute) directory path
1431
 * @param boolean $allow_localhost if access should be allowed to localhost
1432
 * @param string $files (optional, default '*') parameter for the Files tag
1433
 *
1434
 * @return string[]|bool on success error string if anything fails
1435
 */
1436
function secureDirectory($path, $allow_localhost = false, $files = '*')
1437
{
1438
	if (empty($path))
1439
		return 'empty_path';
0 ignored issues
show
Bug Best Practice introduced by
The expression return 'empty_path' returns the type string which is incompatible with the documented return type boolean|string[].
Loading history...
1440
1441
	if (!is_writable($path))
1442
		return 'path_not_writable';
0 ignored issues
show
Bug Best Practice introduced by
The expression return 'path_not_writable' returns the type string which is incompatible with the documented return type boolean|string[].
Loading history...
1443
1444
	$directoryname = basename($path);
1445
1446
	$errors = array();
1447
1448
	if (file_exists($path . '/.htaccess'))
1449
		$errors[] = 'htaccess_exists';
1450
	else
1451
	{
1452
		$fh = @fopen($path . '/.htaccess', 'w');
1453
		if ($fh)
0 ignored issues
show
introduced by
$fh is of type false|resource, thus it always evaluated to false.
Loading history...
1454
		{
1455
			fwrite($fh, '# Apache 2.4
1456
<IfModule mod_authz_core.c>
1457
	Require all denied
1458
	<Files ~ ' . $files . '>
1459
		<RequireAll>
1460
			Require all granted
1461
			Require not env blockAccess' . (empty($allow_localhost) ? '
1462
		</RequireAll>
1463
	</Files>' : '
1464
		Require host localhost
1465
		</RequireAll>
1466
	</Files>
1467
1468
	RemoveHandler .php .php3 .phtml .cgi .fcgi .pl .fpl .shtml') . '
1469
</IfModule>
1470
1471
# Apache 2.2
1472
<IfModule !mod_authz_core.c>
1473
	Order Deny,Allow
1474
	Deny from all
1475
1476
	<Files ' . $files . '>
1477
		Allow from all' . (empty($allow_localhost) ? '
1478
	</Files>' : '
1479
		Allow from localhost
1480
	</Files>
1481
1482
	RemoveHandler .php .php3 .phtml .cgi .fcgi .pl .fpl .shtml') . '
1483
</IfModule>');
1484
			fclose($fh);
1485
		}
1486
		$errors[] = 'htaccess_cannot_create_file';
1487
	}
1488
1489
	if (file_exists($path . '/index.php'))
1490
		$errors[] = 'index-php_exists';
1491
	else
1492
	{
1493
		$fh = @fopen($path . '/index.php', 'w');
1494
		if ($fh)
0 ignored issues
show
introduced by
$fh is of type false|resource, thus it always evaluated to false.
Loading history...
1495
		{
1496
			fwrite($fh, '<?php
1497
1498
/**
1499
 * This file is here solely to protect your ' . $directoryname . ' directory.
1500
 */
1501
1502
// Look for Settings.php....
1503
if (file_exists(dirname(dirname(__FILE__)) . \'/Settings.php\'))
1504
{
1505
	// Found it!
1506
	require(dirname(dirname(__FILE__)) . \'/Settings.php\');
1507
	header(\'Location: \' . $boardurl);
1508
}
1509
// Can\'t find it... just forget it.
1510
else
1511 2
	exit;');
1512
			fclose($fh);
1513
		}
1514
		$errors[] = 'index-php_cannot_create_file';
1515
	}
1516
1517
	if (!empty($errors))
1518
		return $errors;
1519
	else
1520
		return true;
1521
}
1522
1523
/**
1524
 * Helper function that puts together a ban query for a given ip
1525 2
 *
1526 2
 * What it does:
1527 2
 *
1528 2
 * - Builds the query for ipv6, ipv4 or 255.255.255.255 depending on whats supplied
1529 2
 *
1530
 * @param string $fullip An IP address either IPv6 or not
1531
 *
1532
 * @return string A SQL condition
1533
 */
1534
function constructBanQueryIP($fullip)
1535
{
1536
	// First attempt a IPv6 address.
1537 2
	if (isValidIPv6($fullip))
1538
	{
1539
		$ip_parts = convertIPv6toInts($fullip);
1540
1541
		$ban_query = '((' . $ip_parts[0] . ' BETWEEN bi.ip_low1 AND bi.ip_high1)
1542
			AND (' . $ip_parts[1] . ' BETWEEN bi.ip_low2 AND bi.ip_high2)
1543
			AND (' . $ip_parts[2] . ' BETWEEN bi.ip_low3 AND bi.ip_high3)
1544
			AND (' . $ip_parts[3] . ' BETWEEN bi.ip_low4 AND bi.ip_high4)
1545
			AND (' . $ip_parts[4] . ' BETWEEN bi.ip_low5 AND bi.ip_high5)
1546
			AND (' . $ip_parts[5] . ' BETWEEN bi.ip_low6 AND bi.ip_high6)
1547
			AND (' . $ip_parts[6] . ' BETWEEN bi.ip_low7 AND bi.ip_high7)
1548
			AND (' . $ip_parts[7] . ' BETWEEN bi.ip_low8 AND bi.ip_high8))';
1549
	}
1550
	// Check if we have a valid IPv4 address.
1551
	elseif (preg_match('/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $fullip, $ip_parts) == 1)
1552
	{
1553
		$ban_query = '((' . $ip_parts[1] . ' BETWEEN bi.ip_low1 AND bi.ip_high1)
1554
			AND (' . $ip_parts[2] . ' BETWEEN bi.ip_low2 AND bi.ip_high2)
1555
			AND (' . $ip_parts[3] . ' BETWEEN bi.ip_low3 AND bi.ip_high3)
1556
			AND (' . $ip_parts[4] . ' BETWEEN bi.ip_low4 AND bi.ip_high4))';
1557
	}
1558
	// We use '255.255.255.255' for 'unknown' since it's not valid anyway.
1559
	else
1560
	{
1561
		$ban_query = '(bi.ip_low1 = 255 AND bi.ip_high1 = 255
1562
			AND bi.ip_low2 = 255 AND bi.ip_high2 = 255
1563
			AND bi.ip_low3 = 255 AND bi.ip_high3 = 255
1564
			AND bi.ip_low4 = 255 AND bi.ip_high4 = 255)';
1565
	}
1566
1567
	return $ban_query;
1568
}
1569
1570
/**
1571
 * Decide if we are going to enable bad behavior scanning for this user
1572
 *
1573
 * What it does:
1574
 *
1575
 * - Admins and Moderators get a free pass
1576
 * - Optionally existing users with post counts over a limit are bypassed
1577
 * - Others get a humane frisking
1578
 * @return bool|int
1579
 */
1580
function loadBadBehavior()
1581
{
1582
	global $modSettings;
1583
1584
	$bb2_results = false;
1585
	// Bad Behavior Enabled?
1586
	if (!empty($modSettings['badbehavior_enabled']))
1587
	{
1588
		require_once(EXTDIR . '/bad-behavior/badbehavior-plugin.php');
1589
		$bb_run = true;
1590
1591
		// We may want to give some folks a hallway pass
1592
		if (User::$info->is_guest === false)
1593
		{
1594
			if (!empty(User::$info->is_moderator) || !empty(User::$info->is_admin))
1595
			{
1596
				$bb_run = false;
1597
			}
1598
			elseif (!empty($modSettings['badbehavior_postcount_wl']) && $modSettings['badbehavior_postcount_wl'] < 0)
1599
			{
1600
				$bb_run = false;
1601
			}
1602
			elseif (!empty($modSettings['badbehavior_postcount_wl']) && $modSettings['badbehavior_postcount_wl'] > 0 && (User::$info->posts > $modSettings['badbehavior_postcount_wl']))
1603
			{
1604
				$bb_run = false;
1605
			}
1606
		}
1607
1608
		// Put on the sanitary gloves, its time for a patdown !
1609
		if ($bb_run === true)
1610
		{
1611
			$bb2_results = bb2_start(bb2_read_settings());
0 ignored issues
show
Bug introduced by
The function bb2_read_settings was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

1611
			$bb2_results = bb2_start(/** @scrutinizer ignore-call */ bb2_read_settings());
Loading history...
Bug introduced by
The function bb2_start was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

1611
			$bb2_results = /** @scrutinizer ignore-call */ bb2_start(bb2_read_settings());
Loading history...
1612
			theme()->addInlineJavascript(bb2_insert_head());
0 ignored issues
show
Bug introduced by
The function bb2_insert_head was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

1612
			theme()->addInlineJavascript(/** @scrutinizer ignore-call */ bb2_insert_head());
Loading history...
1613
		}
1614
	}
1615
1616
	return $bb2_results;
1617
}
1618
1619
/**
1620
 * This protects against brute force attacks on a member's password.
1621
 *
1622
 * What it does:
1623
 *
1624
 * - Importantly, even if the password was right we DON'T TELL THEM!
1625
 * - Allows 5 attempts every 10 seconds
1626
 *
1627
 * @param int         $id_member
1628
 * @param string|bool $password_flood_value = false or string joined on |'s
1629
 * @param boolean     $was_correct = false
1630
 *
1631
 * @throws \ElkArte\Exceptions\Exception no_access
1632
 */
1633
function validatePasswordFlood($id_member, $password_flood_value = false, $was_correct = false)
1634
{
1635
	global $cookiename;
1636
1637
	// As this is only brute protection, we allow 5 attempts every 10 seconds.
1638
1639
	// Destroy any session or cookie data about this member, as they validated wrong.
1640
	require_once(SUBSDIR . '/Auth.subs.php');
1641
	setLoginCookie(-3600, 0);
1642
1643
	if (isset($_SESSION['login_' . $cookiename]))
1644
		unset($_SESSION['login_' . $cookiename]);
1645
1646
	// We need a member!
1647
	if (!$id_member)
1648
	{
1649
		// Redirect back!
1650
		redirectexit();
1651
1652
		// Probably not needed, but still make sure...
1653
		throw new \ElkArte\Exceptions\Exception('no_access', false);
1654
	}
1655
1656
	// Let's just initialize to something (and 0 is better than nothing)
1657
	$time_stamp = 0;
1658
	$number_tries = 0;
1659
1660
	// Right, have we got a flood value?
1661
	if ($password_flood_value !== false)
1662
		@list ($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

1662
		@list ($time_stamp, $number_tries) = explode('|', /** @scrutinizer ignore-type */ $password_flood_value);
Loading history...
1663
1664
	// Timestamp invalid or non-existent?
1665
	if (empty($number_tries) || $time_stamp < (time() - 10))
1666
	{
1667
		// If it wasn't *that* long ago, don't give them another five goes.
1668
		$number_tries = !empty($number_tries) && $time_stamp < (time() - 20) ? 2 : $number_tries;
1669
		$time_stamp = time();
1670
	}
1671
1672
	$number_tries++;
1673
1674
	// Broken the law?
1675
	if ($number_tries > 5)
1676
		throw new \ElkArte\Exceptions\Exception('login_threshold_brute_fail', 'critical');
1677
1678
	// Otherwise set the members data. If they correct on their first attempt then we actually clear it, otherwise we set it!
1679
	require_once(SUBSDIR . '/Members.subs.php');
1680
	updateMemberData($id_member, array('passwd_flood' => $was_correct && $number_tries == 1 ? '' : $time_stamp . '|' . $number_tries));
1681
}
1682
1683
/**
1684
 * This sets the X-Frame-Options header.
1685
 *
1686
 * @param string|null $override the frame option, defaults to deny.
1687
 */
1688
function frameOptionsHeader($override = null)
1689
{
1690
	global $modSettings;
1691
1692
	$option = 'SAMEORIGIN';
1693
1694
	if (is_null($override) && !empty($modSettings['frame_security']))
1695
		$option = $modSettings['frame_security'];
1696
	elseif (in_array($override, array('SAMEORIGIN', 'DENY')))
1697
		$option = $override;
1698
1699 2
	// Don't bother setting the header if we have disabled it.
1700 2
	if ($option == 'DISABLE')
1701
		return;
1702
1703
	// Finally set it.
1704
	header('X-Frame-Options: ' . $option);
1705
}
1706 2
1707
/**
1708
 * This adds additional security headers that may prevent browsers from doing something they should not
1709
 *
1710
 * What it does:
1711
 *
1712
 * - X-XSS-Protection header - This header enables the Cross-site scripting (XSS) filter
1713
 * built into most recent web browsers. It's usually enabled by default, so the role of this
1714
 * header is to re-enable the filter for this particular website if it was disabled by the user.
1715
 * - X-Content-Type-Options header - It prevents the browser from doing MIME-type sniffing,
1716
 * only IE and Chrome are honouring this header. This reduces exposure to drive-by download attacks
1717
 * and sites serving user uploaded content that could be treated as executable or dynamic HTML files.
1718
 *
1719
 * @param boolean|null $override
1720
 */
1721
function securityOptionsHeader($override = null)
1722
{
1723
	if ($override !== true)
1724
	{
1725
		header('X-XSS-Protection: 1');
1726
		header('X-Content-Type-Options: nosniff');
1727
	}
1728
}
1729
1730
/**
1731
 * Stop some browsers pre fetching activity to reduce server load
1732
 */
1733
function stop_prefetching()
1734
{
1735
	if (isset($_SERVER['HTTP_X_PURPOSE']) && in_array($_SERVER['HTTP_X_PURPOSE'], array('preview', 'instant'))
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: (IssetNode && in_array($..._X_MOZ'] === 'prefetch', Probably Intended Meaning: IssetNode && (in_array($...X_MOZ'] === 'prefetch')
Loading history...
1736
		|| (isset($_SERVER['HTTP_X_MOZ']) && $_SERVER['HTTP_X_MOZ'] === 'prefetch'))
1737
	{
1738
		@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

1738
		/** @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...
1739
		header('HTTP/1.1 403 Forbidden');
1740
		die;
1741
	}
1742
}
1743
1744
/**
1745
 * Check if the admin's session is active
1746
 *
1747
 * @return bool
1748
 */
1749
function isAdminSessionActive()
1750
{
1751
	global $modSettings;
1752
1753
	return empty($modSettings['securityDisable']) && (isset($_SESSION['admin_time']) && $_SESSION['admin_time'] + ($modSettings['admin_session_lifetime'] * 60) > time());
1754
}
1755
1756
/**
1757
 * Check if security files exist
1758
 *
1759
 * If files are found, populate $context['security_controls_files']:
1760
 * * 'title'	- $txt['security_risk']
1761
 * * 'errors'	- An array of strings with the key being the filename and the value an error with the filename in it
1762
 *
1763
 * @event integrate_security_files Allows to add / modify to security files array
1764
 *
1765
 * @return bool
1766
 */
1767
function checkSecurityFiles()
1768
{
1769
	global $txt, $context;
1770
1771
	$has_files = false;
1772
1773
	$securityFiles = array('install.php', 'upgrade.php', 'convert.php', 'repair_paths.php', 'repair_settings.php', 'Settings.php~', 'Settings_bak.php~');
1774
	call_integration_hook('integrate_security_files', array(&$securityFiles));
1775
1776
	foreach ($securityFiles as $securityFile)
1777
	{
1778
		if (file_exists(BOARDDIR . '/' . $securityFile))
1779
		{
1780
			$has_files = true;
1781
1782
			$context['security_controls_files']['title'] = $txt['security_risk'];
1783
			$context['security_controls_files']['errors'][$securityFile] = sprintf($txt['not_removed'], $securityFile);
1784
1785
			if ($securityFile == 'Settings.php~' || $securityFile == 'Settings_bak.php~')
1786
			{
1787
				$context['security_controls_files']['errors'][$securityFile] .= '<span class="smalltext">' . sprintf($txt['not_removed_extra'], $securityFile, substr($securityFile, 0, -1)) . '</span>';
1788
			}
1789
		}
1790
	}
1791
1792
	return $has_files;
1793
}
1794
1795
/**
1796
 * The login URL should not redirect to certain areas (attachments, js actions, etc)
1797
 * this function does these checks and return if the URL is valid or not.
1798
 *
1799
 * @param string $url - The URL to validate
1800
 * @param bool $match_board - If true tries to match board|topic in the URL as well
1801
 * @return bool
1802
 */
1803
function validLoginUrl($url, $match_board = false)
1804
{
1805
	if (empty($url))
1806
	{
1807
		return false;
1808
	}
1809
1810
	if (substr($url, 0, 7) !== 'http://' && substr($url, 0, 8) !== 'https://')
1811
	{
1812
		return false;
1813
	}
1814
1815
	$invalid_strings = array('dlattach' => '~(board|topic)[=,]~', 'jslocale' => '', 'login' => '');
1816
	call_integration_hook('integrate_validLoginUrl', array(&$invalid_strings));
1817
1818
	foreach ($invalid_strings as $invalid_string => $valid_match)
1819
	{
1820
		if (strpos($url, $invalid_string) !== false || ($match_board === true && !empty($valid_match) && preg_match($valid_match, $url) == 0))
1821
		{
1822
			return false;
1823
		}
1824
	}
1825
1826
	return true;
1827
}
1828