Completed
Pull Request — development (#2960)
by Stephen
10:02
created

Security.php ➔ banPermissions()   D

Complexity

Conditions 17
Paths 32

Size

Total Lines 85
Code Lines 56

Duplication

Lines 7
Ratio 8.24 %

Code Coverage

Tests 0
CRAP Score 306

Importance

Changes 0
Metric Value
cc 17
eloc 56
nc 32
nop 0
dl 7
loc 85
ccs 0
cts 61
cp 0
crap 306
rs 4.8361
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
 * @name      ElkArte Forum
8
 * @copyright ElkArte Forum contributors
9
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause
10
 *
11
 * This file contains code covered by:
12
 * copyright:	2011 Simple Machines (http://www.simplemachines.org)
13
 * license:		BSD, See included LICENSE.TXT for terms and conditions.
14
 *
15
 * @version 1.1 Release Candidate 1
16
 *
17
 */
18
19
/**
20
 * Check if the user is who he/she says he is.
21
 *
22
 * What it does:
23
 *
24
 * - This function makes sure the user is who they claim to be by requiring a
25
 * password to be typed in every hour.
26
 * - This check can be turned on and off by the securityDisable setting.
27
 * - Uses the adminLogin() function of subs/Auth.subs.php if they need to login,
28
 * which saves all request (POST and GET) data.
29
 *
30
 * @event integrate_validateSession Called at start of validateSession
31
 * @param string $type = admin
32
 * @throws Elk_Exception
33
 */
34
function validateSession($type = 'admin')
35
{
36
	global $modSettings, $user_settings;
37
38
	// Guests are not welcome here.
39
	is_not_guest();
40
41
	// Validate what type of session check this is.
42
	$types = array();
43
	call_integration_hook('integrate_validateSession', array(&$types));
44
	$type = in_array($type, $types) || $type == 'moderate' ? $type : 'admin';
45
46
	// Set the lifetime for our admin session. Default is ten minutes.
47
	$refreshTime = 10;
48
49
	if (isset($modSettings['admin_session_lifetime']))
50
	{
51
		// Maybe someone is paranoid or mistakenly misconfigured the param? Give them at least 5 minutes.
52
		if ($modSettings['admin_session_lifetime'] < 5)
53
			$refreshTime = 5;
54
55
		// A whole day should be more than enough..
56
		elseif ($modSettings['admin_session_lifetime'] > 14400)
57
			$refreshTime = 14400;
58
59
		// We are between our internal min and max. Let's keep the board owner's value.
60
		else
61
			$refreshTime = $modSettings['admin_session_lifetime'];
62
	}
63
64
	// If we're using XML give an additional ten minutes grace as an admin can't log on in XML mode.
65
	if (isset($_GET['xml']))
66
		$refreshTime += 10;
67
68
	$refreshTime = $refreshTime * 60;
69
70
	// Is the security option off?
71
	// @todo remove the exception (means update the db as well)
72
	if (!empty($modSettings['securityDisable' . ($type != 'admin' ? '_' . $type : '')]))
73
		return true;
74
75
	// If their admin or moderator session hasn't expired yet, let it pass, let the admin session trump a moderation one as well
76
	if ((!empty($_SESSION[$type . '_time']) && $_SESSION[$type . '_time'] + $refreshTime >= time()) || (!empty($_SESSION['admin_time']) && $_SESSION['admin_time'] + $refreshTime >= time()))
77
		return true;
78
79
	require_once(SUBSDIR . '/Auth.subs.php');
80
81
	// Coming from the login screen
82
	if (isset($_POST[$type . '_pass']) || isset($_POST[$type . '_hash_pass']))
83
	{
84
		checkSession();
85
		validateToken('admin-login');
86
87
		// Hashed password, ahoy!
88 View Code Duplication
		if (isset($_POST[$type . '_hash_pass']) && strlen($_POST[$type . '_hash_pass']) === 64)
89
		{
90
			if (checkPassword($type, true))
91
				return true;
92
		}
93
94
		// Posting the password... check it.
95 View Code Duplication
		if (isset($_POST[$type . '_pass']) && str_replace('*', '', $_POST[$type . '_pass']) !== '')
96
		{
97
			if (checkPassword($type))
98
				return true;
99
		}
100
	}
101
102
	// OpenID?
103
	if (!empty($user_settings['openid_uri']))
104
	{
105
		require_once(SUBSDIR . '/OpenID.subs.php');
106
		$openID = new OpenID();
107
		$openID->revalidate();
108
109
		$_SESSION[$type . '_time'] = time();
110
		unset($_SESSION['request_referer']);
111
112
		return true;
113
	}
114
115
	// Better be sure to remember the real referer
116
	if (empty($_SESSION['request_referer']))
117
		$_SESSION['request_referer'] = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '';
118
	elseif (empty($_POST))
119
		unset($_SESSION['request_referer']);
120
121
	// Need to type in a password for that, man.
122
	if (!isset($_GET['xml']))
123
		adminLogin($type);
124
125
	return 'session_verify_fail';
126
}
127
128
/**
129
 * Validates a supplied password is correct
130
 *
131
 * What it does:
132
 *
133
 * - Uses integration function to verify password is enabled
134
 * - Uses validateLoginPassword to check using standard ElkArte methods
135
 *
136
 * @event integrate_verify_password allows integration to verify the password
137
 * @param string $type
138
 * @param bool $hash if the supplied password is in _hash_pass
139
 *
140
 * @return bool
141
 */
142
function checkPassword($type, $hash = false)
143
{
144
	global $user_info;
145
146
	$password = $_POST[$type . ($hash ? '_hash_pass' : '_pass')];
147
148
	// Allow integration to verify the password
149
	$good_password = in_array(true, call_integration_hook('integrate_verify_password', array($user_info['username'], $password, $hash ? true : false)), true);
150
151
	// Password correct?
152
	if ($good_password || validateLoginPassword($password, $user_info['passwd'], $hash ? '' : $user_info['username']))
153
	{
154
		$_SESSION[$type . '_time'] = time();
155
		unset($_SESSION['request_referer']);
156
157
		return true;
158
	}
159
160
	return false;
161
}
162
163
/**
164
 * Require a user who is logged in. (not a guest.)
165
 *
166
 * What it does:
167
 *
168
 * - Checks if the user is currently a guest, and if so asks them to login with a message telling them why.
169
 * - Message is what to tell them when asking them to login.
170
 *
171
 * @param string $message = ''
172
 * @param boolean $is_fatal = true
173
 * @throws Elk_Exception
174
 */
175
function is_not_guest($message = '', $is_fatal = true)
176
{
177 1
	global $user_info, $txt, $context, $scripturl;
178
179
	// Luckily, this person isn't a guest.
180 1
	if (isset($user_info['is_guest']) && !$user_info['is_guest'])
181 1
		return true;
182
183
	// People always worry when they see people doing things they aren't actually doing...
184
	$_GET['action'] = '';
185
	$_GET['board'] = '';
186
	$_GET['topic'] = '';
187
	writeLog(true);
188
189
	// Just die.
190
	if (isset($_REQUEST['xml']) || !$is_fatal)
191
		obExit(false);
192
193
	// Attempt to detect if they came from dlattach.
194
	if (ELK != 'SSI' && empty($context['theme_loaded']))
195
		loadTheme();
196
197
	// Never redirect to an attachment
198
	if (validLoginUrl($_SERVER['REQUEST_URL']))
199
	{
200
		$_SESSION['login_url'] = $_SERVER['REQUEST_URL'];
201
	}
202
203
	// Load the Login template and language file.
204
	loadLanguage('Login');
205
206
	// Apparently we're not in a position to handle this now. Let's go to a safer location for now.
207
	if (!Template_Layers::getInstance()->hasLayers())
208
	{
209
		$_SESSION['login_url'] = $scripturl . '?' . $_SERVER['QUERY_STRING'];
210
		redirectexit('action=login');
211
	}
212
	elseif (isset($_GET['api']))
213
		return false;
214
	else
215
	{
216
		loadTemplate('Login');
217
		loadJavascriptFile('sha256.js', array('defer' => true));
218
		$context['sub_template'] = 'kick_guest';
219
		$context['robot_no_index'] = true;
220
	}
221
222
	// Use the kick_guest sub template...
223
	$context['kick_message'] = $message;
224
	$context['page_title'] = $txt['login'];
225
	$context['default_password'] = '';
226
227
	obExit();
228
229
	// We should never get to this point, but if we did we wouldn't know the user isn't a guest.
230
	trigger_error('Hacking attempt...', E_USER_ERROR);
231
}
232
233
/**
234
 * Apply restrictions for banned users. For example, disallow access.
235
 *
236
 * What it does:
237
 *
238
 * - If the user is banned, it dies with an error.
239
 * - Caches this information for optimization purposes.
240
 * - Forces a recheck if force_check is true.
241
 *
242
 * @param bool $forceCheck = false
243
 *
244
 * @throws Elk_Exception
245
 */
246
function is_not_banned($forceCheck = false)
247
{
248
	global $txt, $modSettings, $context, $user_info, $cookiename, $user_settings;
249
250
	$db = database();
251
252
	// You cannot be banned if you are an admin - doesn't help if you log out.
253
	if ($user_info['is_admin'])
254
		return;
255
256
	// Only check the ban every so often. (to reduce load.)
257
	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']))
258
	{
259
		// Innocent until proven guilty.  (but we know you are! :P)
260
		$_SESSION['ban'] = array(
261
			'last_checked' => time(),
262
			'id_member' => $user_info['id'],
263
			'ip' => $user_info['ip'],
264
			'ip2' => $user_info['ip2'],
265
			'email' => $user_info['email'],
266
		);
267
268
		$ban_query = array();
269
		$ban_query_vars = array('current_time' => time());
270
		$flag_is_activated = false;
271
272
		// Check both IP addresses.
273
		foreach (array('ip', 'ip2') as $ip_number)
274
		{
275
			if ($ip_number == 'ip2' && $user_info['ip2'] == $user_info['ip'])
276
				continue;
277
			$ban_query[] = constructBanQueryIP($user_info[$ip_number]);
278
279
			// IP was valid, maybe there's also a hostname...
280
			if (empty($modSettings['disableHostnameLookup']) && $user_info[$ip_number] != 'unknown')
281
			{
282
				$hostname = host_from_ip($user_info[$ip_number]);
283
				if (strlen($hostname) > 0)
284
				{
285
					$ban_query[] = '({string:hostname} LIKE bi.hostname)';
286
					$ban_query_vars['hostname'] = $hostname;
287
				}
288
			}
289
		}
290
291
		// Is their email address banned?
292
		if (strlen($user_info['email']) != 0)
293
		{
294
			$ban_query[] = '({string:email} LIKE bi.email_address)';
295
			$ban_query_vars['email'] = $user_info['email'];
296
		}
297
298
		// How about this user?
299
		if (!$user_info['is_guest'] && !empty($user_info['id']))
300
		{
301
			$ban_query[] = 'bi.id_member = {int:id_member}';
302
			$ban_query_vars['id_member'] = $user_info['id'];
303
		}
304
305
		// Check the ban, if there's information.
306
		if (!empty($ban_query))
307
		{
308
			$restrictions = array(
309
				'cannot_access',
310
				'cannot_login',
311
				'cannot_post',
312
				'cannot_register',
313
			);
314
			$db->fetchQueryCallback('
315
				SELECT bi.id_ban, bi.email_address, bi.id_member, bg.cannot_access, bg.cannot_register,
316
					bg.cannot_post, bg.cannot_login, bg.reason, COALESCE(bg.expire_time, 0) AS expire_time
317
				FROM {db_prefix}ban_items AS bi
318
					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}))
319
				WHERE
320
					(' . implode(' OR ', $ban_query) . ')',
321
				$ban_query_vars,
322
				function ($row) use($user_info, $restrictions, &$flag_is_activated)
323
				{
324
					// Store every type of ban that applies to you in your session.
325
					foreach ($restrictions as $restriction)
326
					{
327
						if (!empty($row[$restriction]))
328
						{
329
							$_SESSION['ban'][$restriction]['reason'] = $row['reason'];
330
							$_SESSION['ban'][$restriction]['ids'][] = $row['id_ban'];
331
							if (!isset($_SESSION['ban']['expire_time']) || ($_SESSION['ban']['expire_time'] != 0 && ($row['expire_time'] == 0 || $row['expire_time'] > $_SESSION['ban']['expire_time'])))
332
								$_SESSION['ban']['expire_time'] = $row['expire_time'];
333
334
							if (!$user_info['is_guest'] && $restriction == 'cannot_access' && ($row['id_member'] == $user_info['id'] || $row['email_address'] == $user_info['email']))
335
								$flag_is_activated = true;
336
						}
337
					}
338
				}
339
			);
340
		}
341
342
		// Mark the cannot_access and cannot_post bans as being 'hit'.
343
		if (isset($_SESSION['ban']['cannot_access']) || isset($_SESSION['ban']['cannot_post']) || isset($_SESSION['ban']['cannot_login']))
344
			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()));
345
346
		// If for whatever reason the is_activated flag seems wrong, do a little work to clear it up.
347
		if ($user_info['id'] && (($user_settings['is_activated'] >= 10 && !$flag_is_activated)
348
			|| ($user_settings['is_activated'] < 10 && $flag_is_activated)))
349
		{
350
			require_once(SUBSDIR . '/Bans.subs.php');
351
			updateBanMembers();
352
		}
353
	}
354
355
	// Hey, I know you! You're ehm...
356
	if (!isset($_SESSION['ban']['cannot_access']) && !empty($_COOKIE[$cookiename . '_']))
357
	{
358
		$bans = explode(',', $_COOKIE[$cookiename . '_']);
359
		foreach ($bans as $key => $value)
360
			$bans[$key] = (int) $value;
361
362
		$db->fetchQueryCallback('
363
			SELECT bi.id_ban, bg.reason
364
			FROM {db_prefix}ban_items AS bi
365
				INNER JOIN {db_prefix}ban_groups AS bg ON (bg.id_ban_group = bi.id_ban_group)
366
			WHERE bi.id_ban IN ({array_int:ban_list})
367
				AND (bg.expire_time IS NULL OR bg.expire_time > {int:current_time})
368
				AND bg.cannot_access = {int:cannot_access}
369
			LIMIT ' . count($bans),
370
			array(
371
				'cannot_access' => 1,
372
				'ban_list' => $bans,
373
				'current_time' => time(),
374
			),
375
			function ($row)
376
			{
377
				$_SESSION['ban']['cannot_access']['ids'][] = $row['id_ban'];
378
				$_SESSION['ban']['cannot_access']['reason'] = $row['reason'];
379
			}
380
		);
381
382
		// My mistake. Next time better.
383
		if (!isset($_SESSION['ban']['cannot_access']))
384
		{
385
			require_once(SUBSDIR . '/Auth.subs.php');
386
			$cookie_url = url_parts(!empty($modSettings['localCookies']), !empty($modSettings['globalCookies']));
387
			elk_setcookie($cookiename . '_', '', time() - 3600, $cookie_url[1], $cookie_url[0], false, false);
388
		}
389
	}
390
391
	// If you're fully banned, it's end of the story for you.
392
	if (isset($_SESSION['ban']['cannot_access']))
393
	{
394
		require_once(SUBSDIR . '/Auth.subs.php');
395
396
		// We don't wanna see you!
397
		if (!$user_info['is_guest'])
398
		{
399
			$controller = new Auth_Controller();
400
			$controller->action_logout(true, false);
401
		}
402
403
		// 'Log' the user out.  Can't have any funny business... (save the name!)
404
		$old_name = isset($user_info['name']) && $user_info['name'] != '' ? $user_info['name'] : $txt['guest_title'];
405
		$user_info['name'] = '';
406
		$user_info['username'] = '';
407
		$user_info['is_guest'] = true;
408
		$user_info['is_admin'] = false;
409
		$user_info['permissions'] = array();
410
		$user_info['id'] = 0;
411
		$context['user'] = array(
412
			'id' => 0,
413
			'username' => '',
414
			'name' => $txt['guest_title'],
415
			'is_guest' => true,
416
			'is_logged' => false,
417
			'is_admin' => false,
418
			'is_mod' => false,
419
			'is_moderator' => false,
420
			'can_mod' => false,
421
			'language' => $user_info['language'],
422
		);
423
424
		// A goodbye present.
425
		$cookie_url = url_parts(!empty($modSettings['localCookies']), !empty($modSettings['globalCookies']));
426
		elk_setcookie($cookiename . '_', implode(',', $_SESSION['ban']['cannot_access']['ids']), time() + 3153600, $cookie_url[1], $cookie_url[0], false, false);
427
428
		// Don't scare anyone, now.
429
		$_GET['action'] = '';
430
		$_GET['board'] = '';
431
		$_GET['topic'] = '';
432
		writeLog(true);
433
434
		// You banned, sucka!
435
		throw new Elk_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');
436
	}
437
	// You're not allowed to log in but yet you are. Let's fix that.
438
	elseif (isset($_SESSION['ban']['cannot_login']) && !$user_info['is_guest'])
439
	{
440
		// We don't wanna see you!
441
		require_once(SUBSDIR . '/Logging.subs.php');
442
		deleteMemberLogOnline();
443
444
		// 'Log' the user out.  Can't have any funny business... (save the name!)
445
		$old_name = isset($user_info['name']) && $user_info['name'] != '' ? $user_info['name'] : $txt['guest_title'];
446
		$user_info['name'] = '';
447
		$user_info['username'] = '';
448
		$user_info['is_guest'] = true;
449
		$user_info['is_admin'] = false;
450
		$user_info['permissions'] = array();
451
		$user_info['id'] = 0;
452
		$context['user'] = array(
453
			'id' => 0,
454
			'username' => '',
455
			'name' => $txt['guest_title'],
456
			'is_guest' => true,
457
			'is_logged' => false,
458
			'is_admin' => false,
459
			'is_mod' => false,
460
			'is_moderator' => false,
461
			'can_mod' => false,
462
			'language' => $user_info['language'],
463
		);
464
465
		// Wipe 'n Clean(r) erases all traces.
466
		$_GET['action'] = '';
467
		$_GET['board'] = '';
468
		$_GET['topic'] = '';
469
		writeLog(true);
470
471
		// Log them out
472
		$controller = new Auth_Controller();
473
		$controller->action_logout(true, false);
474
475
		// Tell them thanks
476
		throw new Elk_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');
477
	}
478
479
	// Fix up the banning permissions.
480
	if (isset($user_info['permissions']))
481
		banPermissions();
482
}
483
484
/**
485
 * Fix permissions according to ban status.
486
 *
487
 * What it does:
488
 *
489
 * - Applies any states of banning by removing permissions the user cannot have.
490
 *
491
 * @event integrate_post_ban_permissions Allows to update denied permissions
492
 * @event integrate_warn_permissions Allows changing of permissions for users on warning moderate
493
 * @package Bans
494
 */
495
function banPermissions()
496
{
497
	global $user_info, $modSettings, $context;
498
499
	// Somehow they got here, at least take away all permissions...
500
	if (isset($_SESSION['ban']['cannot_access']))
501
		$user_info['permissions'] = array();
502
	// Okay, well, you can watch, but don't touch a thing.
503
	elseif (isset($_SESSION['ban']['cannot_post']) || (!empty($modSettings['warning_mute']) && $modSettings['warning_mute'] <= $user_info['warning']))
504
	{
505
		$denied_permissions = array(
506
			'pm_send',
507
			'calendar_post', 'calendar_edit_own', 'calendar_edit_any',
508
			'poll_post',
509
			'poll_add_own', 'poll_add_any',
510
			'poll_edit_own', 'poll_edit_any',
511
			'poll_lock_own', 'poll_lock_any',
512
			'poll_remove_own', 'poll_remove_any',
513
			'manage_attachments', 'manage_smileys', 'manage_boards', 'admin_forum', 'manage_permissions',
514
			'moderate_forum', 'manage_membergroups', 'manage_bans', 'send_mail', 'edit_news',
515
			'profile_identity_any', 'profile_extra_any', 'profile_title_any',
516
			'post_new', 'post_reply_own', 'post_reply_any',
517
			'delete_own', 'delete_any', 'delete_replies',
518
			'make_sticky',
519
			'merge_any', 'split_any',
520
			'modify_own', 'modify_any', 'modify_replies',
521
			'move_any',
522
			'send_topic',
523
			'lock_own', 'lock_any',
524
			'remove_own', 'remove_any',
525
			'post_unapproved_topics', 'post_unapproved_replies_own', 'post_unapproved_replies_any',
526
		);
527
		Template_Layers::getInstance()->addAfter('admin_warning', 'body');
528
529
		call_integration_hook('integrate_post_ban_permissions', array(&$denied_permissions));
530
		$user_info['permissions'] = array_diff($user_info['permissions'], $denied_permissions);
531
	}
532
	// Are they absolutely under moderation?
533
	elseif (!empty($modSettings['warning_moderate']) && $modSettings['warning_moderate'] <= $user_info['warning'])
534
	{
535
		// Work out what permissions should change...
536
		$permission_change = array(
537
			'post_new' => 'post_unapproved_topics',
538
			'post_reply_own' => 'post_unapproved_replies_own',
539
			'post_reply_any' => 'post_unapproved_replies_any',
540
			'post_attachment' => 'post_unapproved_attachments',
541
		);
542
		call_integration_hook('integrate_warn_permissions', array(&$permission_change));
543 View Code Duplication
		foreach ($permission_change as $old => $new)
544
		{
545
			if (!in_array($old, $user_info['permissions']))
546
				unset($permission_change[$old]);
547
			else
548
				$user_info['permissions'][] = $new;
549
		}
550
		$user_info['permissions'] = array_diff($user_info['permissions'], array_keys($permission_change));
551
	}
552
553
	// @todo Find a better place to call this? Needs to be after permissions loaded!
554
	// Finally, some bits we cache in the session because it saves queries.
555
	if (isset($_SESSION['mc']) && $_SESSION['mc']['time'] > $modSettings['settings_updated'] && $_SESSION['mc']['id'] == $user_info['id'])
556
		$user_info['mod_cache'] = $_SESSION['mc'];
557
	else
558
	{
559
		require_once(SUBSDIR . '/Auth.subs.php');
560
		rebuildModCache();
561
	}
562
563
	// Now that we have the mod cache taken care of lets setup a cache for the number of mod reports still open
564
	if (isset($_SESSION['rc']) && $_SESSION['rc']['time'] > $modSettings['last_mod_report_action'] && $_SESSION['rc']['id'] == $user_info['id'])
565
	{
566
		$context['open_mod_reports'] = $_SESSION['rc']['reports'];
567
		if (allowedTo('admin_forum'))
568
		{
569
			$context['open_pm_reports'] = $_SESSION['rc']['pm_reports'];
570
		}
571
	}
572
	elseif ($_SESSION['mc']['bq'] != '0=1')
573
	{
574
		require_once(SUBSDIR . '/Moderation.subs.php');
575
		recountOpenReports(true, allowedTo('admin_forum'));
576
	}
577
	else
578
		$context['open_mod_reports'] = 0;
579
}
580
581
/**
582
 * Log a ban in the database.
583
 *
584
 * What it does:
585
 *
586
 * - Log the current user in the ban logs.
587
 * - Increment the hit counters for the specified ban ID's (if any.)
588
 *
589
 * @package Bans
590
 * @param int[] $ban_ids = array()
591
 * @param string|null $email = null
592
 */
593
function log_ban($ban_ids = array(), $email = null)
594
{
595
	global $user_info;
596
597
	$db = database();
598
599
	// Don't log web accelerators, it's very confusing...
600
	if (isset($_SERVER['HTTP_X_MOZ']) && $_SERVER['HTTP_X_MOZ'] == 'prefetch')
601
		return;
602
603
	$db->insert('',
604
		'{db_prefix}log_banned',
605
		array('id_member' => 'int', 'ip' => 'string-16', 'email' => 'string', 'log_time' => 'int'),
606
		array($user_info['id'], $user_info['ip'], ($email === null ? ($user_info['is_guest'] ? '' : $user_info['email']) : $email), time()),
607
		array('id_ban_log')
608
	);
609
610
	// One extra point for these bans.
611
	if (!empty($ban_ids))
612
		$db->query('', '
613
			UPDATE {db_prefix}ban_items
614
			SET hits = hits + 1
615
			WHERE id_ban IN ({array_int:ban_ids})',
616
			array(
617
				'ban_ids' => $ban_ids,
618
			)
619
		);
620
}
621
622
/**
623
 * Checks if a given email address might be banned.
624
 *
625
 * What it does:
626
 *
627
 * - Check if a given email is banned.
628
 * - Performs an immediate ban if the turns turns out positive.
629
 *
630
 * @package Bans
631
 * @param string $email
632
 * @param string $restriction
633
 * @param string $error
634
 *
635
 * @throws Elk_Exception
636
 */
637
function isBannedEmail($email, $restriction, $error)
638
{
639 1
	global $txt;
640
641 1
	$db = database();
642
643
	// Can't ban an empty email
644 1
	if (empty($email) || trim($email) == '')
645 1
		return;
646
647
	// Let's start with the bans based on your IP/hostname/memberID...
648 1
	$ban_ids = isset($_SESSION['ban'][$restriction]) ? $_SESSION['ban'][$restriction]['ids'] : array();
649 1
	$ban_reason = isset($_SESSION['ban'][$restriction]) ? $_SESSION['ban'][$restriction]['reason'] : '';
650
651
	// ...and add to that the email address you're trying to register.
652 1
	$request = $db->query('', '
653 1
		SELECT bi.id_ban, bg.' . $restriction . ', bg.cannot_access, bg.reason
654
		FROM {db_prefix}ban_items AS bi
655
			INNER JOIN {db_prefix}ban_groups AS bg ON (bg.id_ban_group = bi.id_ban_group)
656
		WHERE {string:email} LIKE bi.email_address
657 1
			AND (bg.' . $restriction . ' = {int:cannot_access} OR bg.cannot_access = {int:cannot_access})
658 1
			AND (bg.expire_time IS NULL OR bg.expire_time >= {int:now})',
659
		array(
660 1
			'email' => $email,
661 1
			'cannot_access' => 1,
662 1
			'now' => time(),
663
		)
664 1
	);
665 1
	while ($row = $db->fetch_assoc($request))
666
	{
667
		if (!empty($row['cannot_access']))
668
		{
669
			$_SESSION['ban']['cannot_access']['ids'][] = $row['id_ban'];
670
			$_SESSION['ban']['cannot_access']['reason'] = $row['reason'];
671
		}
672
		if (!empty($row[$restriction]))
673
		{
674
			$ban_ids[] = $row['id_ban'];
675
			$ban_reason = $row['reason'];
676
		}
677
	}
678 1
	$db->free_result($request);
679
680
	// You're in biiig trouble.  Banned for the rest of this session!
681 1
	if (isset($_SESSION['ban']['cannot_access']))
682 1
	{
683
		log_ban($_SESSION['ban']['cannot_access']['ids']);
684
		$_SESSION['ban']['last_checked'] = time();
685
686
		throw new Elk_Exception(sprintf($txt['your_ban'], $txt['guest_title']) . $_SESSION['ban']['cannot_access']['reason'], false);
687
	}
688
689 1
	if (!empty($ban_ids))
690 1
	{
691
		// Log this ban for future reference.
692
		log_ban($ban_ids, $email);
693
		throw new Elk_Exception($error . $ban_reason, false);
694
	}
695 1
}
696
697
/**
698
 * Make sure the user's correct session was passed, and they came from here.
699
 *
700
 * What it does:
701
 *
702
 * - Checks the current session, verifying that the person is who he or she should be.
703
 * - Also checks the referrer to make sure they didn't get sent here.
704
 * - Depends on the disableCheckUA setting, which is usually missing.
705
 * - Will check GET, POST, or REQUEST depending on the passed type.
706
 * - Also optionally checks the referring action if passed. (note that the referring action must be by GET.)
707
 *
708
 * @param string $type = 'post' (post, get, request)
709
 * @param string $from_action = ''
710
 * @param bool   $is_fatal = true
711
 *
712
 * @return string the error message if is_fatal is false.
713
 * @throws Elk_Exception
714
 */
715
function checkSession($type = 'post', $from_action = '', $is_fatal = true)
716
{
717
	global $modSettings, $boardurl;
718
719
	// We'll work out user agent checks
720
	$req = request();
721
722
	// Is it in as $_POST['sc']?
723
	if ($type == 'post')
724
	{
725
		$check = isset($_POST[$_SESSION['session_var']]) ? $_POST[$_SESSION['session_var']] : (empty($modSettings['strictSessionCheck']) && isset($_POST['sc']) ? $_POST['sc'] : null);
726
		if ($check !== $_SESSION['session_value'])
727
			$error = 'session_timeout';
728
	}
729
	// How about $_GET['sesc']?
730
	elseif ($type === 'get')
731
	{
732
		$check = isset($_GET[$_SESSION['session_var']]) ? $_GET[$_SESSION['session_var']] : (empty($modSettings['strictSessionCheck']) && isset($_GET['sesc']) ? $_GET['sesc'] : null);
733
		if ($check !== $_SESSION['session_value'])
734
			$error = 'session_verify_fail';
735
	}
736
	// Or can it be in either?
737
	elseif ($type == 'request')
738
	{
739
		$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)));
740
741
		if ($check !== $_SESSION['session_value'])
742
			$error = 'session_verify_fail';
743
	}
744
745
	// Verify that they aren't changing user agents on us - that could be bad.
746
	if ((!isset($_SESSION['USER_AGENT']) || $_SESSION['USER_AGENT'] != $req->user_agent()) && empty($modSettings['disableCheckUA']))
747
		$error = 'session_verify_fail';
748
749
	// Make sure a page with session check requirement is not being prefetched.
750
	stop_prefetching();
751
752
	// Check the referring site - it should be the same server at least!
753
	if (isset($_SESSION['request_referer']))
754
		$referrer_url = $_SESSION['request_referer'];
755
	else
756
		$referrer_url = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '';
757
758
	$referrer = @parse_url($referrer_url);
759
760
	if (!empty($referrer['host']))
761
	{
762
		if (strpos($_SERVER['HTTP_HOST'], ':') !== false)
763
			$real_host = substr($_SERVER['HTTP_HOST'], 0, strpos($_SERVER['HTTP_HOST'], ':'));
764
		else
765
			$real_host = $_SERVER['HTTP_HOST'];
766
767
		$parsed_url = parse_url($boardurl);
768
769
		// Are global cookies on? If so, let's check them ;).
770
		if (!empty($modSettings['globalCookies']))
771
		{
772
			if (preg_match('~(?:[^\.]+\.)?([^\.]{3,}\..+)\z~i', $parsed_url['host'], $parts) == 1)
773
				$parsed_url['host'] = $parts[1];
774
775
			if (preg_match('~(?:[^\.]+\.)?([^\.]{3,}\..+)\z~i', $referrer['host'], $parts) == 1)
776
				$referrer['host'] = $parts[1];
777
778
			if (preg_match('~(?:[^\.]+\.)?([^\.]{3,}\..+)\z~i', $real_host, $parts) == 1)
779
				$real_host = $parts[1];
780
		}
781
782
		// Okay: referrer must either match parsed_url or real_host.
783
		if (isset($parsed_url['host']) && strtolower($referrer['host']) != strtolower($parsed_url['host']) && strtolower($referrer['host']) != strtolower($real_host))
784
		{
785
			$error = 'verify_url_fail';
786
			$log_error = true;
787
			$sprintf = array(Util::htmlspecialchars($referrer_url));
788
		}
789
	}
790
791
	// Well, first of all, if a from_action is specified you'd better have an old_url.
792
	if (!empty($from_action) && (!isset($_SESSION['old_url']) || preg_match('~[?;&]action=' . $from_action . '([;&]|$)~', $_SESSION['old_url']) == 0))
793
	{
794
		$error = 'verify_url_fail';
795
		$log_error = true;
796
		$sprintf = array(Util::htmlspecialchars($referrer_url));
797
	}
798
799
	// Everything is ok, return an empty string.
800
	if (!isset($error))
801
		return '';
802
	// A session error occurred, show the error.
803
	elseif ($is_fatal)
804
	{
805
		if (isset($_GET['xml']) || isset($_REQUEST['api']))
806
		{
807
			@ob_end_clean();
808
			header('HTTP/1.1 403 Forbidden - Session timeout');
809
			die;
0 ignored issues
show
Coding Style Compatibility introduced by
The function checkSession() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
810
		}
811
		else
812
			throw new Elk_Exception($error, isset($log_error) ? 'user' : false, isset($sprintf) ? $sprintf : array());
813
	}
814
	// A session error occurred, return the error to the calling function.
815
	else
816
		return $error;
817
818
	// We really should never fall through here, for very important reasons.  Let's make sure.
819
	trigger_error('Hacking attempt...', E_USER_ERROR);
0 ignored issues
show
Unused Code introduced by
// We really should neve...mpt...', E_USER_ERROR); does not seem to be 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...
820
}
821
822
/**
823
 * Lets give you a token of our appreciation.
824
 *
825
 * What it does:
826
 *
827
 * - Creates a one time use form token
828
 *
829
 * @param string $action The specific site action that a token will be generated for
830
 * @param string $type = 'post' If the token will be returned via post or get
831
 *
832
 * @return string[] array of token var, time, csrf, token
833
 */
834
function createToken($action, $type = 'post')
835
{
836 6
	global $context;
837
838
	// Generate a new token token_var pair
839 6
	$tokenizer = new Token_Hash();
840 6
	$token_var = $tokenizer->generate_hash(rand(7, 12));
841 6
	$token = $tokenizer->generate_hash(32);
842
843
	// We need user agent and the client IP
844 6
	$req = request();
845 6
	$csrf_hash = hash('sha1', $token . $req->client_ip() . $req->user_agent());
846
847
	// Save the session token and make it available to the forms
848 6
	$_SESSION['token'][$type . '-' . $action] = array($token_var, $csrf_hash, time(), $token);
849 6
	$context[$action . '_token'] = $token;
850 6
	$context[$action . '_token_var'] = $token_var;
851
852 6
	return array($action . '_token_var' => $token_var, $action . '_token' => $token);
853
}
854
855
/**
856
 * Only patrons with valid tokens can ride this ride.
857
 *
858
 * What it does:
859
 *
860
 * - Validates that the received token is correct
861
 * - 1. The token exists in session.
862
 * - 2. The {$type} variable should exist.
863
 * - 3. We concatenate the variable we received with the user agent
864
 * - 4. Match that result against what is in the session.
865
 * - 5. If it matches, success, otherwise we fallout.
866
 *
867
 * @param string $action
868
 * @param string $type = 'post' (get, request, or post)
869
 * @param bool   $reset = true Reset the token on failure
870
 * @param bool   $fatal if true a fatal_lang_error is issued for invalid tokens, otherwise false is returned
871
 *
872
 * @return bool except for $action == 'login' where the token is returned
873
 * @throws Elk_Exception token_verify_fail
874
 */
875
function validateToken($action, $type = 'post', $reset = true, $fatal = true)
876
{
877 4
	$type = ($type === 'get' || $type === 'request') ? $type : 'post';
878 4
	$token_index = $type . '-' . $action;
879
880
	// Logins are special: the token is used to have the password with javascript before POST it
881 4 View Code Duplication
	if ($action == 'login')
882 4
	{
883
		if (isset($_SESSION['token'][$token_index]))
884
		{
885
			$return = $_SESSION['token'][$token_index][3];
886
			unset($_SESSION['token'][$token_index]);
887
888
			return $return;
889
		}
890
		else
891
			return '';
892
	}
893
894 4
	if (!isset($_SESSION['token'][$token_index]))
895 4
		return false;
896
897
	// We need the user agent and client IP
898
	$req = request();
899
900
	// Shortcut
901
	$passed_token_var = isset($GLOBALS['_' . strtoupper($type)][$_SESSION['token'][$token_index][0]]) ? $GLOBALS['_' . strtoupper($type)][$_SESSION['token'][$token_index][0]] : null;
902
	$csrf_hash = hash('sha1', $passed_token_var . $req->client_ip() . $req->user_agent());
903
904
	// Checked what was passed in combination with the user agent
905 View Code Duplication
	if (isset($_SESSION['token'][$token_index], $passed_token_var)
906
		&& $csrf_hash === $_SESSION['token'][$token_index][1])
907
	{
908
		// Consume the token, let them pass
909
		unset($_SESSION['token'][$token_index]);
910
911
		return true;
912
	}
913
914
	// Patrons with invalid tokens get the boot.
915
	if ($reset)
916
	{
917
		// Might as well do some cleanup on this.
918
		cleanTokens();
919
920
		// I'm back baby.
921
		createToken($action, $type);
922
923
		if ($fatal)
924
			throw new Elk_Exception('token_verify_fail', false);
925
	}
926
	// You don't get a new token
927
	else
928
	{
929
		// Explicitly remove this token
930
		unset($_SESSION['token'][$token_index]);
931
932
		// Remove older tokens.
933
		cleanTokens();
934
	}
935
936
	return false;
937
}
938
939
/**
940
 * Removes old unused tokens from session
941
 *
942
 * What it does:
943
 *
944
 * - Defaults to 3 hours before a token is considered expired
945
 * - if $complete = true will remove all tokens
946
 *
947
 * @param bool $complete = false
948
 * @param string $suffix = false
949
 */
950
function cleanTokens($complete = false, $suffix = '')
951
{
952
	// We appreciate cleaning up after yourselves.
953
	if (!isset($_SESSION['token']))
954
		return;
955
956
	// Clean up tokens, trying to give enough time still.
957
	foreach ($_SESSION['token'] as $key => $data)
958
	{
959
		if (!empty($suffix))
960
			$force = $complete || strpos($key, $suffix);
961
		else
962
			$force = $complete;
963
964
		if ($data[2] + 10800 < time() || $force)
965
			unset($_SESSION['token'][$key]);
966
	}
967
}
968
969
/**
970
 * Check whether a form has been submitted twice.
971
 *
972
 * What it does:
973
 *
974
 * - Registers a sequence number for a form.
975
 * - Checks whether a submitted sequence number is registered in the current session.
976
 * - Depending on the value of is_fatal shows an error or returns true or false.
977
 * - Frees a sequence number from the stack after it's been checked.
978
 * - Frees a sequence number without checking if action == 'free'.
979
 *
980
 * @param string $action
981
 * @param bool   $is_fatal = true
982
 *
983
 * @return bool
984
 * @throws Elk_Exception error_form_already_submitted
985
 */
986
function checkSubmitOnce($action, $is_fatal = false)
987
{
988
	global $context;
989
990
	if (!isset($_SESSION['forms']))
991
		$_SESSION['forms'] = array();
992
993
	// Register a form number and store it in the session stack. (use this on the page that has the form.)
994
	if ($action == 'register')
995
	{
996
		$tokenizer = new Token_Hash();
997
		$context['form_sequence_number'] = '';
998
		while (empty($context['form_sequence_number']) || in_array($context['form_sequence_number'], $_SESSION['forms']))
999
			$context['form_sequence_number'] = $tokenizer->generate_hash();
1000
	}
1001
	// Check whether the submitted number can be found in the session.
1002
	elseif ($action == 'check')
1003
	{
1004
		if (!isset($_REQUEST['seqnum']))
1005
			return true;
1006
		elseif (!in_array($_REQUEST['seqnum'], $_SESSION['forms']))
1007
		{
1008
			// Mark this one as used
1009
			$_SESSION['forms'][] = (string) $_REQUEST['seqnum'];
1010
			return true;
1011
		}
1012
		elseif ($is_fatal)
1013
			throw new Elk_Exception('error_form_already_submitted', false);
1014
		else
1015
			return false;
1016
	}
1017
	// Don't check, just free the stack number.
1018
	elseif ($action == 'free' && isset($_REQUEST['seqnum']) && in_array($_REQUEST['seqnum'], $_SESSION['forms']))
1019
		$_SESSION['forms'] = array_diff($_SESSION['forms'], array($_REQUEST['seqnum']));
1020
	elseif ($action != 'free')
1021
		trigger_error('checkSubmitOnce(): Invalid action \'' . $action . '\'', E_USER_WARNING);
1022
}
1023
1024
/**
1025
 * This function checks whether the user is allowed to do permission. (ie. post_new.)
1026
 *
1027
 * What it does:
1028
 *
1029
 * - If boards parameter is specified, checks those boards instead of the current one (if applicable).
1030
 * - Always returns true if the user is an administrator.
1031
 *
1032
 * @param string[]|string $permission permission
1033
 * @param int[]|int|null $boards array of board IDs, a single id or null
1034
 *
1035
 * @return boolean if the user can do the permission
1036
 */
1037
function allowedTo($permission, $boards = null)
1038
{
1039 33
	global $user_info;
1040
1041 33
	$db = database();
1042
1043
	// You're always allowed to do nothing. (unless you're a working man, MR. LAZY :P!)
1044 33
	if (empty($permission))
1045 33
		return true;
1046
1047
	// You're never allowed to do something if your data hasn't been loaded yet!
1048 33
	if (empty($user_info))
1049 33
		return false;
1050
1051
	// Administrators are supermen :P.
1052 33
	if ($user_info['is_admin'])
1053 33
		return true;
1054
1055
	// Make sure permission is a valid array
1056 10
	if (!is_array($permission))
1057 10
		$permission = array($permission);
1058
1059
	// Are we checking the _current_ board, or some other boards?
1060 10
	if ($boards === null)
1061 10
	{
1062 10
		if (empty($user_info['permissions']))
1063 10
			return false;
1064
1065
		// Check if they can do it, you aren't allowed, by default.
1066 5
		return count(array_intersect($permission, $user_info['permissions'])) !== 0 ? true : false;
1067
	}
1068
1069
	if (!is_array($boards))
1070
		$boards = array($boards);
1071
1072
	if (empty($user_info['groups']))
1073
		return false;
1074
1075
	$request = $db->query('', '
1076
		SELECT MIN(bp.add_deny) AS add_deny
1077
		FROM {db_prefix}boards AS b
1078
			INNER JOIN {db_prefix}board_permissions AS bp ON (bp.id_profile = b.id_profile)
1079
			LEFT JOIN {db_prefix}moderators AS mods ON (mods.id_board = b.id_board AND mods.id_member = {int:current_member})
1080
		WHERE b.id_board IN ({array_int:board_list})
1081
			AND bp.id_group IN ({array_int:group_list}, {int:moderator_group})
1082
			AND bp.permission IN ({array_string:permission_list})
1083
			AND (mods.id_member IS NOT NULL OR bp.id_group != {int:moderator_group})
1084
		GROUP BY b.id_board',
1085
		array(
1086
			'current_member' => $user_info['id'],
1087
			'board_list' => $boards,
1088
			'group_list' => $user_info['groups'],
1089
			'moderator_group' => 3,
1090
			'permission_list' => $permission,
1091
		)
1092
	);
1093
1094
	// Make sure they can do it on all of the boards.
1095
	if ($db->num_rows($request) != count($boards))
1096
		return false;
1097
1098
	$result = true;
1099
	while ($row = $db->fetch_assoc($request))
1100
		$result &= !empty($row['add_deny']);
1101
	$db->free_result($request);
1102
1103
	// If the query returned 1, they can do it... otherwise, they can't.
1104
	return $result;
1105
}
1106
1107
/**
1108
 * This function returns fatal error if the user doesn't have the respective permission.
1109
 *
1110
 * What it does:
1111
 *
1112
 * - Uses allowedTo() to check if the user is allowed to do permission.
1113
 * - Checks the passed boards or current board for the permission.
1114
 * - If they are not, it loads the Errors language file and shows an error using $txt['cannot_' . $permission].
1115
 * - If they are a guest and cannot do it, this calls is_not_guest().
1116
 *
1117
 * @param string[]|string $permission array of or single string, of permissions to check
1118
 * @param int[]|null      $boards = null
1119
 *
1120
 * @throws Elk_Exception
1121
 */
1122
function isAllowedTo($permission, $boards = null)
1123
{
1124 16
	global $user_info, $txt;
1125
1126
	static $heavy_permissions = array(
1127
		'admin_forum',
1128
		'manage_attachments',
1129
		'manage_smileys',
1130
		'manage_boards',
1131
		'edit_news',
1132
		'moderate_forum',
1133
		'manage_bans',
1134
		'manage_membergroups',
1135
		'manage_permissions',
1136 16
	);
1137
1138
	// Make it an array, even if a string was passed.
1139 16
	$permission = is_array($permission) ? $permission : array($permission);
1140
1141
	// Check the permission and return an error...
1142 16
	if (!allowedTo($permission, $boards))
1143 16
	{
1144
		// Pick the last array entry as the permission shown as the error.
1145
		$error_permission = array_shift($permission);
1146
1147
		// If they are a guest, show a login. (because the error might be gone if they do!)
1148
		if ($user_info['is_guest'])
1149
		{
1150
			loadLanguage('Errors');
1151
			is_not_guest($txt['cannot_' . $error_permission]);
1152
		}
1153
1154
		// Clear the action because they aren't really doing that!
1155
		$_GET['action'] = '';
1156
		$_GET['board'] = '';
1157
		$_GET['topic'] = '';
1158
		writeLog(true);
1159
1160
		throw new Elk_Exception('cannot_' . $error_permission, false);
1161
	}
1162
1163
	// If you're doing something on behalf of some "heavy" permissions, validate your session.
1164
	// (take out the heavy permissions, and if you can't do anything but those, you need a validated session.)
1165 16
	if (!allowedTo(array_diff($permission, $heavy_permissions), $boards))
1166 16
		validateSession();
1167 16
}
1168
1169
/**
1170
 * Return the boards a user has a certain (board) permission on. (array(0) if all.)
1171
 *
1172
 * What it does:
1173
 *
1174
 * - Returns a list of boards on which the user is allowed to do the specified permission.
1175
 * - Returns an array with only a 0 in it if the user has permission to do this on every board.
1176
 * - Returns an empty array if he or she cannot do this on any board.
1177
 * - If check_access is true will also make sure the group has proper access to that board.
1178
 *
1179
 * @param string[]|string $permissions array of permission names to check access against
1180
 * @param bool $check_access = true
1181
 * @param bool $simple = true
1182
 */
1183
function boardsAllowedTo($permissions, $check_access = true, $simple = true)
1184
{
1185 1
	global $user_info;
1186
1187 1
	$db = database();
1188
1189
	// Arrays are nice, most of the time.
1190 1
	if (!is_array($permissions))
1191 1
		$permissions = array($permissions);
1192
1193
	/*
1194
	 * Set $simple to true to use this function in compatibility mode
1195
	 * Otherwise, the resultant array becomes split into the multiple
1196
	 * permissions that were passed. Other than that, it's just the normal
1197
	 * state of play that you're used to.
1198
	 */
1199
1200
	// I am the master, the master of the universe!
1201 1
	if ($user_info['is_admin'])
1202 1
	{
1203
		if ($simple)
1204 1
			return array(0);
1205
		else
1206
		{
1207
			$boards = array();
1208
			foreach ($permissions as $permission)
1209
				$boards[$permission] = array(0);
1210
1211
			return $boards;
1212
		}
1213
	}
1214
1215
	// All groups the user is in except 'moderator'.
1216
	$groups = array_diff($user_info['groups'], array(3));
1217
1218
	$request = $db->query('', '
1219
		SELECT b.id_board, bp.add_deny' . ($simple ? '' : ', bp.permission') . '
1220
		FROM {db_prefix}board_permissions AS bp
1221
			INNER JOIN {db_prefix}boards AS b ON (b.id_profile = bp.id_profile)
1222
			LEFT JOIN {db_prefix}moderators AS mods ON (mods.id_board = b.id_board AND mods.id_member = {int:current_member})
1223
		WHERE bp.id_group IN ({array_int:group_list}, {int:moderator_group})
1224
			AND bp.permission IN ({array_string:permissions})
1225
			AND (mods.id_member IS NOT NULL OR bp.id_group != {int:moderator_group})' .
1226
			($check_access ? ' AND {query_see_board}' : ''),
1227
		array(
1228
			'current_member' => $user_info['id'],
1229
			'group_list' => $groups,
1230
			'moderator_group' => 3,
1231
			'permissions' => $permissions,
1232
		)
1233
	);
1234
	$boards = array();
1235
	$deny_boards = array();
1236
	while ($row = $db->fetch_assoc($request))
1237
	{
1238
		if ($simple)
1239
		{
1240
			if (empty($row['add_deny']))
1241
				$deny_boards[] = $row['id_board'];
1242
			else
1243
				$boards[] = $row['id_board'];
1244
		}
1245
		else
1246
		{
1247
			if (empty($row['add_deny']))
1248
				$deny_boards[$row['permission']][] = $row['id_board'];
1249
			else
1250
				$boards[$row['permission']][] = $row['id_board'];
1251
		}
1252
	}
1253
	$db->free_result($request);
1254
1255
	if ($simple)
1256
		$boards = array_unique(array_values(array_diff($boards, $deny_boards)));
1257
	else
1258
	{
1259
		foreach ($permissions as $permission)
1260
		{
1261
			// Never had it to start with
1262
			if (empty($boards[$permission]))
1263
				$boards[$permission] = array();
1264
			else
1265
			{
1266
				// Or it may have been removed
1267
				$deny_boards[$permission] = isset($deny_boards[$permission]) ? $deny_boards[$permission] : array();
1268
				$boards[$permission] = array_unique(array_values(array_diff($boards[$permission], $deny_boards[$permission])));
1269
			}
1270
		}
1271
	}
1272
1273
	return $boards;
1274
}
1275
1276
/**
1277
 * Returns whether an email address should be shown and how.
1278
 *
1279
 * What it does:
1280
 *
1281
 * Possible outcomes are:
1282
 * - 'yes': show the full email address
1283
 * - 'yes_permission_override': show the full email address, either you
1284
 * are a moderator or it's your own email address.
1285
 * - 'no_through_forum': don't show the email address, but do allow
1286
 * things to be mailed using the built-in forum mailer.
1287
 * - 'no': keep the email address hidden.
1288
 *
1289
 * @param bool $userProfile_hideEmail
1290
 * @param int $userProfile_id
1291
 *
1292
 * @return string (yes, yes_permission_override, no_through_forum, no)
1293
 */
1294
function showEmailAddress($userProfile_hideEmail, $userProfile_id)
1295
{
1296 1
	global $user_info;
1297
1298
	// Should this user's email address be shown?
1299
	// If you're guest: no.
1300
	// If the user is post-banned: no.
1301
	// If it's your own profile and you've not set your address hidden: yes_permission_override.
1302
	// If you're a moderator with sufficient permissions: yes_permission_override.
1303
	// If the user has set their profile to do not email me: no.
1304
	// Otherwise: no_through_forum. (don't show it but allow emailing the member)
1305
1306 1
	if ($user_info['is_guest'] || isset($_SESSION['ban']['cannot_post']))
1307 1
		return 'no';
1308 1
	elseif ((!$user_info['is_guest'] && $user_info['id'] == $userProfile_id && !$userProfile_hideEmail))
1309 1
		return 'yes_permission_override';
1310
	elseif (allowedTo('moderate_forum'))
1311
		return 'yes_permission_override';
1312
	elseif ($userProfile_hideEmail)
1313
		return 'no';
1314
	else
1315
		return 'no_through_forum';
1316
}
1317
1318
/**
1319
 * This function attempts to protect from carrying out specific actions repeatedly.
1320
 *
1321
 * What it does:
1322
 *
1323
 * - Checks if a user is trying specific actions faster than a given minimum wait threshold.
1324
 * - The time taken depends on error_type - generally uses the modSetting.
1325
 * - Generates a fatal message when triggered, suspending execution.
1326
 *
1327
 * @event integrate_spam_protection Allows to update action wait timeOverrides
1328
 * @param string  $error_type used also as a $txt index. (not an actual string.)
1329
 * @param boolean $fatal is the spam check a fatal error on failure
1330
 *
1331
 * @return bool|int|mixed
1332
 * @throws Elk_Exception
1333
 */
1334
function spamProtection($error_type, $fatal = true)
1335
{
1336
	global $modSettings, $user_info;
1337
1338
	$db = database();
1339
1340
	// Certain types take less/more time.
1341
	$timeOverrides = array(
1342
		'login' => 2,
1343
		'register' => 2,
1344
		'remind' => 30,
1345
		'contact' => 30,
1346
		'sendtopic' => $modSettings['spamWaitTime'] * 4,
1347
		'sendmail' => $modSettings['spamWaitTime'] * 5,
1348
		'reporttm' => $modSettings['spamWaitTime'] * 4,
1349
		'search' => !empty($modSettings['search_floodcontrol_time']) ? $modSettings['search_floodcontrol_time'] : 1,
1350
	);
1351
	call_integration_hook('integrate_spam_protection', array(&$timeOverrides));
1352
1353
	// Moderators are free...
1354
	if (!allowedTo('moderate_board'))
1355
		$timeLimit = isset($timeOverrides[$error_type]) ? $timeOverrides[$error_type] : $modSettings['spamWaitTime'];
1356
	else
1357
		$timeLimit = 2;
1358
1359
	// Delete old entries...
1360
	$db->query('', '
1361
		DELETE FROM {db_prefix}log_floodcontrol
1362
		WHERE log_time < {int:log_time}
1363
			AND log_type = {string:log_type}',
1364
		array(
1365
			'log_time' => time() - $timeLimit,
1366
			'log_type' => $error_type,
1367
		)
1368
	);
1369
1370
	// Add a new entry, deleting the old if necessary.
1371
	$db->insert('replace',
1372
		'{db_prefix}log_floodcontrol',
1373
		array('ip' => 'string-16', 'log_time' => 'int', 'log_type' => 'string'),
1374
		array($user_info['ip'], time(), $error_type),
1375
		array('ip', 'log_type')
1376
	);
1377
1378
	// If affected is 0 or 2, it was there already.
1379
	if ($db->affected_rows() != 1)
1380
	{
1381
		// Spammer!  You only have to wait a *few* seconds!
1382
		if ($fatal)
1383
		{
1384
			throw new Elk_Exception($error_type . '_WaitTime_broken', false, array($timeLimit));
1385
		}
1386
		else
1387
			return $timeLimit;
1388
	}
1389
1390
	// They haven't posted within the limit.
1391
	return false;
1392
}
1393
1394
/**
1395
 * A generic function to create a pair of index.php and .htaccess files in a directory
1396
 *
1397
 * @param string $path the (absolute) directory path
1398
 * @param boolean $allow_localhost if access should be allowed to localhost
1399
 * @param string $files (optional, default '*') parameter for the Files tag
1400
 *
1401
 * @return string|boolean on success error string if anything fails
1402
 */
1403
function secureDirectory($path, $allow_localhost = false, $files = '*')
1404
{
1405
	if (empty($path))
1406
		return 'empty_path';
1407
1408
	if (!is_writable($path))
1409
		return 'path_not_writable';
1410
1411
	$directoryname = basename($path);
1412
1413
	$errors = array();
1414
	$close = empty($allow_localhost) ? '
1415
</Files>' : '
1416
	Allow from localhost
1417
</Files>
1418
1419
RemoveHandler .php .php3 .phtml .cgi .fcgi .pl .fpl .shtml';
1420
1421 View Code Duplication
	if (file_exists($path . '/.htaccess'))
1422
		$errors[] = 'htaccess_exists';
1423
	else
1424
	{
1425
		$fh = @fopen($path . '/.htaccess', 'w');
1426
		if ($fh)
1427
		{
1428
			fwrite($fh, '<Files ' . $files . '>
1429
	Order Deny,Allow
1430
	Deny from all' . $close);
1431
			fclose($fh);
1432
		}
1433
		$errors[] = 'htaccess_cannot_create_file';
1434
	}
1435
1436 View Code Duplication
	if (file_exists($path . '/index.php'))
1437
		$errors[] = 'index-php_exists';
1438
	else
1439
	{
1440
		$fh = @fopen($path . '/index.php', 'w');
1441
		if ($fh)
1442
		{
1443
			fwrite($fh, '<?php
1444
1445
/**
1446
 * This file is here solely to protect your ' . $directoryname . ' directory.
1447
 */
1448
1449
// Look for Settings.php....
1450
if (file_exists(dirname(dirname(__FILE__)) . \'/Settings.php\'))
1451
{
1452
	// Found it!
1453
	require(dirname(dirname(__FILE__)) . \'/Settings.php\');
1454
	header(\'Location: \' . $boardurl);
1455
}
1456
// Can\'t find it... just forget it.
1457
else
1458
	exit;');
1459
			fclose($fh);
1460
		}
1461
		$errors[] = 'index-php_cannot_create_file';
1462
	}
1463
1464
	if (!empty($errors))
1465
		return $errors;
1466
	else
1467
		return true;
1468
}
1469
1470
/**
1471
 * Helper function that puts together a ban query for a given ip
1472
 *
1473
 * What it does:
1474
 *
1475
 * - Builds the query for ipv6, ipv4 or 255.255.255.255 depending on whats supplied
1476
 *
1477
 * @param string $fullip An IP address either IPv6 or not
1478
 *
1479
 * @return string A SQL condition
1480
 */
1481
function constructBanQueryIP($fullip)
1482
{
1483
	// First attempt a IPv6 address.
1484 1
	if (isValidIPv6($fullip))
1485 1
	{
1486
		$ip_parts = convertIPv6toInts($fullip);
1487
1488
		$ban_query = '((' . $ip_parts[0] . ' BETWEEN bi.ip_low1 AND bi.ip_high1)
1489
			AND (' . $ip_parts[1] . ' BETWEEN bi.ip_low2 AND bi.ip_high2)
1490
			AND (' . $ip_parts[2] . ' BETWEEN bi.ip_low3 AND bi.ip_high3)
1491
			AND (' . $ip_parts[3] . ' BETWEEN bi.ip_low4 AND bi.ip_high4)
1492
			AND (' . $ip_parts[4] . ' BETWEEN bi.ip_low5 AND bi.ip_high5)
1493
			AND (' . $ip_parts[5] . ' BETWEEN bi.ip_low6 AND bi.ip_high6)
1494
			AND (' . $ip_parts[6] . ' BETWEEN bi.ip_low7 AND bi.ip_high7)
1495
			AND (' . $ip_parts[7] . ' BETWEEN bi.ip_low8 AND bi.ip_high8))';
1496
	}
1497
	// Check if we have a valid IPv4 address.
1498 1
	elseif (preg_match('/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $fullip, $ip_parts) == 1)
1499 1
		$ban_query = '((' . $ip_parts[1] . ' BETWEEN bi.ip_low1 AND bi.ip_high1)
1500 1
			AND (' . $ip_parts[2] . ' BETWEEN bi.ip_low2 AND bi.ip_high2)
1501 1
			AND (' . $ip_parts[3] . ' BETWEEN bi.ip_low3 AND bi.ip_high3)
1502 1
			AND (' . $ip_parts[4] . ' BETWEEN bi.ip_low4 AND bi.ip_high4))';
1503
	// We use '255.255.255.255' for 'unknown' since it's not valid anyway.
1504
	else
1505
		$ban_query = '(bi.ip_low1 = 255 AND bi.ip_high1 = 255
1506
			AND bi.ip_low2 = 255 AND bi.ip_high2 = 255
1507
			AND bi.ip_low3 = 255 AND bi.ip_high3 = 255
1508
			AND bi.ip_low4 = 255 AND bi.ip_high4 = 255)';
1509
1510 1
	return $ban_query;
1511
}
1512
1513
/**
1514
 * Decide if we are going to enable bad behavior scanning for this user
1515
 *
1516
 * What it does:
1517
 *
1518
 * - Admins and Moderators get a free pass
1519
 * - Optionally existing users with post counts over a limit are bypassed
1520
 * - Others get a humane frisking
1521
 */
1522
function loadBadBehavior()
1523
{
1524
	global $modSettings, $user_info, $bb2_results;
1525
1526
	// Bad Behavior Enabled?
1527
	if (!empty($modSettings['badbehavior_enabled']))
1528
	{
1529
		require_once(EXTDIR . '/bad-behavior/badbehavior-plugin.php');
1530
		$bb_run = true;
1531
1532
		// We may want to give some folks a hallway pass
1533
		if (!$user_info['is_guest'])
1534
		{
1535
			if (!empty($user_info['is_moderator']) || !empty($user_info['is_admin']))
1536
				$bb_run = false;
1537
			elseif (!empty($modSettings['badbehavior_postcount_wl']) && $modSettings['badbehavior_postcount_wl'] < 0)
1538
				$bb_run = false;
1539 View Code Duplication
			elseif (!empty($modSettings['badbehavior_postcount_wl']) && $modSettings['badbehavior_postcount_wl'] > 0 && ($user_info['posts'] > $modSettings['badbehavior_postcount_wl']))
1540
				$bb_run = false;
1541
		}
1542
1543
		// Put on the sanitary gloves, its time for a patdown !
1544
		if ($bb_run === true)
1545
		{
1546
			$bb2_results = bb2_start(bb2_read_settings());
1547
			addInlineJavascript(bb2_insert_head());
1548
		}
1549
	}
1550
}
1551
1552
/**
1553
 * This protects against brute force attacks on a member's password.
1554
 *
1555
 * What it does:
1556
 *
1557
 * - Importantly, even if the password was right we DON'T TELL THEM!
1558
 * - Allows 5 attempts every 10 seconds
1559
 *
1560
 * @param int         $id_member
1561
 * @param string|bool $password_flood_value = false or string joined on |'s
1562
 * @param boolean     $was_correct = false
1563
 *
1564
 * @throws Elk_Exception no_access
1565
 */
1566
function validatePasswordFlood($id_member, $password_flood_value = false, $was_correct = false)
1567
{
1568
	global $cookiename;
1569
1570
	// As this is only brute protection, we allow 5 attempts every 10 seconds.
1571
1572
	// Destroy any session or cookie data about this member, as they validated wrong.
1573
	require_once(SUBSDIR . '/Auth.subs.php');
1574
	setLoginCookie(-3600, 0);
1575
1576
	if (isset($_SESSION['login_' . $cookiename]))
1577
		unset($_SESSION['login_' . $cookiename]);
1578
1579
	// We need a member!
1580
	if (!$id_member)
1581
	{
1582
		// Redirect back!
1583
		redirectexit();
1584
1585
		// Probably not needed, but still make sure...
1586
		throw new Elk_Exception('no_access', false);
1587
	}
1588
1589
	// Let's just initialize to something (and 0 is better than nothing)
1590
	$time_stamp = 0;
1591
	$number_tries = 0;
1592
1593
	// Right, have we got a flood value?
1594
	if ($password_flood_value !== false)
1595
		@list ($time_stamp, $number_tries) = explode('|', $password_flood_value);
1596
1597
	// Timestamp invalid or non-existent?
1598
	if (empty($number_tries) || $time_stamp < (time() - 10))
1599
	{
1600
		// If it wasn't *that* long ago, don't give them another five goes.
1601
		$number_tries = !empty($number_tries) && $time_stamp < (time() - 20) ? 2 : $number_tries;
1602
		$time_stamp = time();
1603
	}
1604
1605
	$number_tries++;
1606
1607
	// Broken the law?
1608
	if ($number_tries > 5)
1609
		throw new Elk_Exception('login_threshold_brute_fail', 'critical');
1610
1611
	// Otherwise set the members data. If they correct on their first attempt then we actually clear it, otherwise we set it!
1612
	require_once(SUBSDIR . '/Members.subs.php');
1613
	updateMemberData($id_member, array('passwd_flood' => $was_correct && $number_tries == 1 ? '' : $time_stamp . '|' . $number_tries));
1614
}
1615
1616
/**
1617
 * This sets the X-Frame-Options header.
1618
 *
1619
 * @param string|null $override the frame option, defaults to deny.
1620
 */
1621
function frameOptionsHeader($override = null)
1622
{
1623
	global $modSettings;
1624
1625
	$option = 'SAMEORIGIN';
1626
1627
	if (is_null($override) && !empty($modSettings['frame_security']))
1628
		$option = $modSettings['frame_security'];
1629
	elseif (in_array($override, array('SAMEORIGIN', 'DENY')))
1630
		$option = $override;
1631
1632
	// Don't bother setting the header if we have disabled it.
1633
	if ($option == 'DISABLE')
1634
		return;
1635
1636
	// Finally set it.
1637
	header('X-Frame-Options: ' . $option);
1638
}
1639
1640
/**
1641
 * This adds additional security headers that may prevent browsers from doing something they should not
1642
 *
1643
 * What it does:
1644
 *
1645
 * - X-XSS-Protection header - This header enables the Cross-site scripting (XSS) filter
1646
 * built into most recent web browsers. It's usually enabled by default, so the role of this
1647
 * header is to re-enable the filter for this particular website if it was disabled by the user.
1648
 * - X-Content-Type-Options header - It prevents the browser from doing MIME-type sniffing,
1649
 * only IE and Chrome are honouring this header. This reduces exposure to drive-by download attacks
1650
 * and sites serving user uploaded content that could be treated as executable or dynamic HTML files.
1651
 *
1652
 * @param boolean|null $override
1653
 */
1654
function securityOptionsHeader($override = null)
1655
{
1656
	if ($override !== true)
1657
	{
1658
		header('X-XSS-Protection: 1');
1659
		header('X-Content-Type-Options: nosniff');
1660
	}
1661
}
1662
1663
/**
1664
 * Stop some browsers pre fetching activity to reduce server load
1665
 */
1666
function stop_prefetching()
1667
{
1668 2
	if (isset($_SERVER['HTTP_X_PURPOSE']) && in_array($_SERVER['HTTP_X_PURPOSE'], array('preview', 'instant'))
1669 2
		|| (isset($_SERVER['HTTP_X_MOZ']) && $_SERVER['HTTP_X_MOZ'] === 'prefetch'))
1670 2
	{
1671
		@ob_end_clean();
1672
		header('HTTP/1.1 403 Forbidden');
1673
		die;
0 ignored issues
show
Coding Style Compatibility introduced by
The function stop_prefetching() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
1674
	}
1675 2
}
1676
1677
/**
1678
 * Check if the admin's session is active
1679
 *
1680
 * @return bool
1681
 */
1682
function isAdminSessionActive()
1683
{
1684
	global $modSettings;
1685
1686
	return empty($modSettings['securityDisable']) && (isset($_SESSION['admin_time']) && $_SESSION['admin_time'] + ($modSettings['admin_session_lifetime'] * 60) > time());
1687
}
1688
1689
/**
1690
 * Check if security files exist
1691
 *
1692
 * If files are found, populate $context['security_controls_files']:
1693
 * * 'title'	- $txt['security_risk']
1694
 * * 'errors'	- An array of strings with the key being the filename and the value an error with the filename in it
1695
 *
1696
 * @event integrate_security_files Allows to add / modify to security files array
1697
 *
1698
 * @return bool
1699
 */
1700
function checkSecurityFiles()
1701
{
1702
	global $txt, $context;
1703
1704
	$has_files = false;
1705
1706
	$securityFiles = array('install.php', 'upgrade.php', 'convert.php', 'repair_paths.php', 'repair_settings.php', 'Settings.php~', 'Settings_bak.php~');
1707
	call_integration_hook('integrate_security_files', array(&$securityFiles));
1708
1709
	foreach ($securityFiles as $securityFile)
1710
	{
1711
		if (file_exists(BOARDDIR . '/' . $securityFile))
1712
		{
1713
			$has_files = true;
1714
1715
			$context['security_controls_files']['title'] = $txt['security_risk'];
1716
			$context['security_controls_files']['errors'][$securityFile] = sprintf($txt['not_removed'], $securityFile);
1717
1718
			if ($securityFile == 'Settings.php~' || $securityFile == 'Settings_bak.php~')
1719
			{
1720
				$context['security_controls_files']['errors'][$securityFile] .= '<span class="smalltext">' . sprintf($txt['not_removed_extra'], $securityFile, substr($securityFile, 0, -1)) . '</span>';
1721
			}
1722
		}
1723
	}
1724
1725
	return $has_files;
1726
}
1727
1728
/**
1729
 * The login URL should not redirect to certain areas (attachments, js actions, etc)
1730
 * this function does these checks and return if the URL is valid or not.
1731
 *
1732
 * @param string $url - The URL to validate
1733
 * @param bool $match_board - If true tries to match board|topic in the URL as well
1734
 * @return bool
1735
 */
1736
function validLoginUrl($url, $match_board = false)
1737
{
1738
	if (empty($url))
1739
	{
1740
		return false;
1741
	}
1742
1743
	if (substr($url, 0, 7) !== 'http://' && substr($url, 0, 8) !== 'https://')
1744
	{
1745
		return false;
1746
	}
1747
1748
	$invalid_strings = array('dlattach' => '~(board|topic)[=,]~', 'jslocale' => '', 'login' => '');
1749
	call_integration_hook('integrate_validLoginUrl', array(&$invalid_strings));
1750
1751
	foreach ($invalid_strings as $invalid_string => $valid_match)
1752
	{
1753
		if (strpos($url, $invalid_string) !== false || ($match_board === true && !empty($valid_match) && preg_match($valid_match, $url) == 0))
1754
		{
1755
			return false;
1756
		}
1757
	}
1758
1759
	return true;
1760
}