Failed Conditions
Branch release-2.1 (4e22cf)
by Rick
06:39
created

Security.php ➔ boardsAllowedTo()   D

Complexity

Conditions 15
Paths 26

Size

Total Lines 91
Code Lines 45

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 15
eloc 45
nc 26
nop 3
dl 0
loc 91
rs 4.9121
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
 * Simple Machines Forum (SMF)
8
 *
9
 * @package SMF
10
 * @author Simple Machines http://www.simplemachines.org
11
 * @copyright 2017 Simple Machines and individual contributors
12
 * @license http://www.simplemachines.org/about/smf/license.php BSD
13
 *
14
 * @version 2.1 Beta 4
15
 */
16
17
if (!defined('SMF'))
18
	die('No direct access...');
19
20
/**
21
 * Check if the user is who he/she says he is
22
 * Makes sure the user is who they claim to be by requiring a password to be typed in every hour.
23
 * Is turned on and off by the securityDisable setting.
24
 * Uses the adminLogin() function of Subs-Auth.php if they need to login, which saves all request (post and get) data.
25
 *
26
 * @param string $type What type of session this is
27
 * @return void|string Returns 'session_verify_fail' if verification failed
28
 */
29
function validateSession($type = 'admin')
30
{
31
	global $modSettings, $sourcedir, $user_info;
32
33
	// We don't care if the option is off, because Guests should NEVER get past here.
34
	is_not_guest();
35
36
	// Validate what type of session check this is.
37
	$types = array();
38
	call_integration_hook('integrate_validateSession', array(&$types));
39
	$type = in_array($type, $types) || $type == 'moderate' ? $type : 'admin';
40
41
	// If we're using XML give an additional ten minutes grace as an admin can't log on in XML mode.
42
	$refreshTime = isset($_GET['xml']) ? 4200 : 3600;
43
44
	// Is the security option off?
45
	if (!empty($modSettings['securityDisable' . ($type != 'admin' ? '_' . $type : '')]))
46
		return;
47
48
	// Or are they already logged in?, Moderator or admin session is need for this area
49
	if ((!empty($_SESSION[$type . '_time']) && $_SESSION[$type . '_time'] + $refreshTime >= time()) || (!empty($_SESSION['admin_time']) && $_SESSION['admin_time'] + $refreshTime >= time()))
50
		return;
51
52
	require_once($sourcedir . '/Subs-Auth.php');
53
54
	// Posting the password... check it.
55
	if (isset($_POST[$type . '_pass']))
56
	{
57
		// Check to ensure we're forcing SSL for authentication
58 View Code Duplication
		if (!empty($modSettings['force_ssl']) && empty($maintenance) && (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] != 'on'))
0 ignored issues
show
Bug introduced by
The variable $maintenance seems to never exist, and therefore empty should always return true. Did you maybe rename this variable?

This check looks for calls to isset(...) or empty() on variables that are yet undefined. These calls will always produce the same result and can be removed.

This is most likely caused by the renaming of a variable or the removal of a function/method parameter.

Loading history...
59
			fatal_lang_error('login_ssl_required');
60
61
		checkSession();
62
63
		$good_password = in_array(true, call_integration_hook('integrate_verify_password', array($user_info['username'], $_POST[$type . '_pass'], false)), true);
64
65
		// Password correct?
66
		if ($good_password || hash_verify_password($user_info['username'], $_POST[$type . '_pass'], $user_info['passwd']))
67
		{
68
			$_SESSION[$type . '_time'] = time();
69
			unset($_SESSION['request_referer']);
70
			return;
71
		}
72
	}
73
74
	// Better be sure to remember the real referer
75
	if (empty($_SESSION['request_referer']))
76
		$_SESSION['request_referer'] = isset($_SERVER['HTTP_REFERER']) ? @parse_url($_SERVER['HTTP_REFERER']) : array();
77
	elseif (empty($_POST))
78
		unset($_SESSION['request_referer']);
79
80
	// Need to type in a password for that, man.
81
	if (!isset($_GET['xml']))
82
		adminLogin($type);
83
	else
84
		return 'session_verify_fail';
85
}
86
87
/**
88
 * Require a user who is logged in. (not a guest.)
89
 * Checks if the user is currently a guest, and if so asks them to login with a message telling them why.
90
 * Message is what to tell them when asking them to login.
91
 *
92
 * @param string $message The message to display to the guest
93
 */
94
function is_not_guest($message = '')
95
{
96
	global $user_info, $txt, $context, $scripturl, $modSettings;
97
98
	// Luckily, this person isn't a guest.
99
	if (!$user_info['is_guest'])
100
		return;
101
102
	// Log what they were trying to do didn't work)
103
	if (!empty($modSettings['who_enabled']))
104
		$_GET['error'] = 'guest_login';
105
	writeLog(true);
106
107
	// Just die.
108
	if (isset($_REQUEST['xml']))
109
		obExit(false);
110
111
	// Attempt to detect if they came from dlattach.
112
	if (SMF != 'SSI' && empty($context['theme_loaded']))
113
		loadTheme();
114
115
	// Never redirect to an attachment
116 View Code Duplication
	if (strpos($_SERVER['REQUEST_URL'], 'dlattach') === false)
117
		$_SESSION['login_url'] = $_SERVER['REQUEST_URL'];
118
119
	// Load the Login template and language file.
120
	loadLanguage('Login');
121
122
	// Apparently we're not in a position to handle this now. Let's go to a safer location for now.
123
	if (empty($context['template_layers']))
124
	{
125
		$_SESSION['login_url'] = $scripturl . '?' . $_SERVER['QUERY_STRING'];
126
		redirectexit('action=login');
127
	}
128
	else
129
	{
130
		loadTemplate('Login');
131
		$context['sub_template'] = 'kick_guest';
132
		$context['robot_no_index'] = true;
133
	}
134
135
	// Use the kick_guest sub template...
136
	$context['kick_message'] = $message;
137
	$context['page_title'] = $txt['login'];
138
139
	obExit();
140
141
	// We should never get to this point, but if we did we wouldn't know the user isn't a guest.
142
	trigger_error('Hacking attempt...', E_USER_ERROR);
143
}
144
145
/**
146
 * Do banning related stuff.  (ie. disallow access....)
147
 * Checks if the user is banned, and if so dies with an error.
148
 * Caches this information for optimization purposes.
149
 *
150
 * @param bool $forceCheck Whether to force a recheck
151
 */
152
function is_not_banned($forceCheck = false)
153
{
154
	global $txt, $modSettings, $context, $user_info;
155
	global $sourcedir, $cookiename, $user_settings, $smcFunc;
156
157
	// You cannot be banned if you are an admin - doesn't help if you log out.
158
	if ($user_info['is_admin'])
159
		return;
160
161
	// Only check the ban every so often. (to reduce load.)
162
	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']))
163
	{
164
		// Innocent until proven guilty.  (but we know you are! :P)
165
		$_SESSION['ban'] = array(
166
			'last_checked' => time(),
167
			'id_member' => $user_info['id'],
168
			'ip' => $user_info['ip'],
169
			'ip2' => $user_info['ip2'],
170
			'email' => $user_info['email'],
171
		);
172
173
		$ban_query = array();
174
		$ban_query_vars = array('current_time' => time());
175
		$flag_is_activated = false;
176
177
		// Check both IP addresses.
178
		foreach (array('ip', 'ip2') as $ip_number)
179
		{
180
			if ($ip_number == 'ip2' && $user_info['ip2'] == $user_info['ip'])
181
				continue;
182
			$ban_query[] = ' {inet:' . $ip_number . '} BETWEEN bi.ip_low and bi.ip_high';
183
			$ban_query_vars[$ip_number] = $user_info[$ip_number];
184
			// IP was valid, maybe there's also a hostname...
185
			if (empty($modSettings['disableHostnameLookup']) && $user_info[$ip_number] != 'unknown')
186
			{
187
				$hostname = host_from_ip($user_info[$ip_number]);
188
				if (strlen($hostname) > 0)
189
				{
190
					$ban_query[] = '({string:hostname' . $ip_number . '} LIKE bi.hostname)';
191
					$ban_query_vars['hostname' . $ip_number] = $hostname;
192
				}
193
			}
194
		}
195
196
		// Is their email address banned?
197
		if (strlen($user_info['email']) != 0)
198
		{
199
			$ban_query[] = '({string:email} LIKE bi.email_address)';
200
			$ban_query_vars['email'] = $user_info['email'];
201
		}
202
203
		// How about this user?
204
		if (!$user_info['is_guest'] && !empty($user_info['id']))
205
		{
206
			$ban_query[] = 'bi.id_member = {int:id_member}';
207
			$ban_query_vars['id_member'] = $user_info['id'];
208
		}
209
210
		// Check the ban, if there's information.
211
		if (!empty($ban_query))
212
		{
213
			$restrictions = array(
214
				'cannot_access',
215
				'cannot_login',
216
				'cannot_post',
217
				'cannot_register',
218
			);
219
			$request = $smcFunc['db_query']('', '
220
				SELECT bi.id_ban, bi.email_address, bi.id_member, bg.cannot_access, bg.cannot_register,
221
					bg.cannot_post, bg.cannot_login, bg.reason, COALESCE(bg.expire_time, 0) AS expire_time
222
				FROM {db_prefix}ban_items AS bi
223
					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}))
224
				WHERE
225
					(' . implode(' OR ', $ban_query) . ')',
226
				$ban_query_vars
227
			);
228
			// Store every type of ban that applies to you in your session.
229
			while ($row = $smcFunc['db_fetch_assoc']($request))
230
			{
231
				foreach ($restrictions as $restriction)
232
					if (!empty($row[$restriction]))
233
					{
234
						$_SESSION['ban'][$restriction]['reason'] = $row['reason'];
235
						$_SESSION['ban'][$restriction]['ids'][] = $row['id_ban'];
236
						if (!isset($_SESSION['ban']['expire_time']) || ($_SESSION['ban']['expire_time'] != 0 && ($row['expire_time'] == 0 || $row['expire_time'] > $_SESSION['ban']['expire_time'])))
237
							$_SESSION['ban']['expire_time'] = $row['expire_time'];
238
239
						if (!$user_info['is_guest'] && $restriction == 'cannot_access' && ($row['id_member'] == $user_info['id'] || $row['email_address'] == $user_info['email']))
240
							$flag_is_activated = true;
241
					}
242
			}
243
			$smcFunc['db_free_result']($request);
244
		}
245
246
		// Mark the cannot_access and cannot_post bans as being 'hit'.
247
		if (isset($_SESSION['ban']['cannot_access']) || isset($_SESSION['ban']['cannot_post']) || isset($_SESSION['ban']['cannot_login']))
248
			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()));
249
250
		// If for whatever reason the is_activated flag seems wrong, do a little work to clear it up.
251
		if ($user_info['id'] && (($user_settings['is_activated'] >= 10 && !$flag_is_activated)
252
			|| ($user_settings['is_activated'] < 10 && $flag_is_activated)))
253
		{
254
			require_once($sourcedir . '/ManageBans.php');
255
			updateBanMembers();
256
		}
257
	}
258
259
	// Hey, I know you! You're ehm...
260
	if (!isset($_SESSION['ban']['cannot_access']) && !empty($_COOKIE[$cookiename . '_']))
261
	{
262
		$bans = explode(',', $_COOKIE[$cookiename . '_']);
263
		foreach ($bans as $key => $value)
264
			$bans[$key] = (int) $value;
265
		$request = $smcFunc['db_query']('', '
266
			SELECT bi.id_ban, bg.reason
267
			FROM {db_prefix}ban_items AS bi
268
				INNER JOIN {db_prefix}ban_groups AS bg ON (bg.id_ban_group = bi.id_ban_group)
269
			WHERE bi.id_ban IN ({array_int:ban_list})
270
				AND (bg.expire_time IS NULL OR bg.expire_time > {int:current_time})
271
				AND bg.cannot_access = {int:cannot_access}
272
			LIMIT {int:limit}',
273
			array(
274
				'cannot_access' => 1,
275
				'ban_list' => $bans,
276
				'current_time' => time(),
277
				'limit' => count($bans),
278
			)
279
		);
280
		while ($row = $smcFunc['db_fetch_assoc']($request))
281
		{
282
			$_SESSION['ban']['cannot_access']['ids'][] = $row['id_ban'];
283
			$_SESSION['ban']['cannot_access']['reason'] = $row['reason'];
284
		}
285
		$smcFunc['db_free_result']($request);
286
287
		// My mistake. Next time better.
288
		if (!isset($_SESSION['ban']['cannot_access']))
289
		{
290
			require_once($sourcedir . '/Subs-Auth.php');
291
			$cookie_url = url_parts(!empty($modSettings['localCookies']), !empty($modSettings['globalCookies']));
292
			smf_setcookie($cookiename . '_', '', time() - 3600, $cookie_url[1], $cookie_url[0], false, false);
293
		}
294
	}
295
296
	// If you're fully banned, it's end of the story for you.
297
	if (isset($_SESSION['ban']['cannot_access']))
298
	{
299
		// We don't wanna see you!
300
		if (!$user_info['is_guest'])
301
			$smcFunc['db_query']('', '
302
				DELETE FROM {db_prefix}log_online
303
				WHERE id_member = {int:current_member}',
304
				array(
305
					'current_member' => $user_info['id'],
306
				)
307
			);
308
309
		// 'Log' the user out.  Can't have any funny business... (save the name!)
310
		$old_name = isset($user_info['name']) && $user_info['name'] != '' ? $user_info['name'] : $txt['guest_title'];
311
		$user_info['name'] = '';
312
		$user_info['username'] = '';
313
		$user_info['is_guest'] = true;
314
		$user_info['is_admin'] = false;
315
		$user_info['permissions'] = array();
316
		$user_info['id'] = 0;
317
		$context['user'] = array(
318
			'id' => 0,
319
			'username' => '',
320
			'name' => $txt['guest_title'],
321
			'is_guest' => true,
322
			'is_logged' => false,
323
			'is_admin' => false,
324
			'is_mod' => false,
325
			'can_mod' => false,
326
			'language' => $user_info['language'],
327
		);
328
329
		// A goodbye present.
330
		require_once($sourcedir . '/Subs-Auth.php');
331
		require_once($sourcedir . '/LogInOut.php');
332
		$cookie_url = url_parts(!empty($modSettings['localCookies']), !empty($modSettings['globalCookies']));
333
		smf_setcookie($cookiename . '_', implode(',', $_SESSION['ban']['cannot_access']['ids']), time() + 3153600, $cookie_url[1], $cookie_url[0], false, false);
334
335
		// Don't scare anyone, now.
336
		$_GET['action'] = '';
337
		$_GET['board'] = '';
338
		$_GET['topic'] = '';
339
		writeLog(true);
340
		Logout(true, false);
341
342
		// You banned, sucka!
343
		fatal_error(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'], timeformat($_SESSION['ban']['expire_time'], false)) : $txt['your_ban_expires_never']), !empty($modSettings['log_ban_hits']) ? 'ban' : false);
0 ignored issues
show
Security Bug introduced by
It seems like !empty($modSettings['log...hits']) ? 'ban' : false can also be of type false; however, fatal_error() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
344
345
		// If we get here, something's gone wrong.... but let's try anyway.
346
		trigger_error('Hacking attempt...', E_USER_ERROR);
347
	}
348
	// You're not allowed to log in but yet you are. Let's fix that.
349
	elseif (isset($_SESSION['ban']['cannot_login']) && !$user_info['is_guest'])
350
	{
351
		// We don't wanna see you!
352
		$smcFunc['db_query']('', '
353
			DELETE FROM {db_prefix}log_online
354
			WHERE id_member = {int:current_member}',
355
			array(
356
				'current_member' => $user_info['id'],
357
			)
358
		);
359
360
		// 'Log' the user out.  Can't have any funny business... (save the name!)
361
		$old_name = isset($user_info['name']) && $user_info['name'] != '' ? $user_info['name'] : $txt['guest_title'];
362
		$user_info['name'] = '';
363
		$user_info['username'] = '';
364
		$user_info['is_guest'] = true;
365
		$user_info['is_admin'] = false;
366
		$user_info['permissions'] = array();
367
		$user_info['id'] = 0;
368
		$context['user'] = array(
369
			'id' => 0,
370
			'username' => '',
371
			'name' => $txt['guest_title'],
372
			'is_guest' => true,
373
			'is_logged' => false,
374
			'is_admin' => false,
375
			'is_mod' => false,
376
			'can_mod' => false,
377
			'language' => $user_info['language'],
378
		);
379
380
		// SMF's Wipe 'n Clean(r) erases all traces.
381
		$_GET['action'] = '';
382
		$_GET['board'] = '';
383
		$_GET['topic'] = '';
384
		writeLog(true);
385
386
		require_once($sourcedir . '/LogInOut.php');
387
		Logout(true, false);
388
389
		fatal_error(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'], timeformat($_SESSION['ban']['expire_time'], false)) : $txt['your_ban_expires_never']) . '<br>' . $txt['ban_continue_browse'], !empty($modSettings['log_ban_hits']) ? 'ban' : false);
0 ignored issues
show
Security Bug introduced by
It seems like !empty($modSettings['log...hits']) ? 'ban' : false can also be of type false; however, fatal_error() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
390
	}
391
392
	// Fix up the banning permissions.
393
	if (isset($user_info['permissions']))
394
		banPermissions();
395
}
396
397
/**
398
 * Fix permissions according to ban status.
399
 * Applies any states of banning by removing permissions the user cannot have.
400
 */
401
function banPermissions()
402
{
403
	global $user_info, $sourcedir, $modSettings, $context;
404
405
	// Somehow they got here, at least take away all permissions...
406
	if (isset($_SESSION['ban']['cannot_access']))
407
		$user_info['permissions'] = array();
408
	// Okay, well, you can watch, but don't touch a thing.
409
	elseif (isset($_SESSION['ban']['cannot_post']) || (!empty($modSettings['warning_mute']) && $modSettings['warning_mute'] <= $user_info['warning']))
410
	{
411
		$denied_permissions = array(
412
			'pm_send',
413
			'calendar_post', 'calendar_edit_own', 'calendar_edit_any',
414
			'poll_post',
415
			'poll_add_own', 'poll_add_any',
416
			'poll_edit_own', 'poll_edit_any',
417
			'poll_lock_own', 'poll_lock_any',
418
			'poll_remove_own', 'poll_remove_any',
419
			'manage_attachments', 'manage_smileys', 'manage_boards', 'admin_forum', 'manage_permissions',
420
			'moderate_forum', 'manage_membergroups', 'manage_bans', 'send_mail', 'edit_news',
421
			'profile_identity_any', 'profile_extra_any', 'profile_title_any',
422
			'profile_forum_any', 'profile_other_any', 'profile_signature_any',
423
			'post_new', 'post_reply_own', 'post_reply_any',
424
			'delete_own', 'delete_any', 'delete_replies',
425
			'make_sticky',
426
			'merge_any', 'split_any',
427
			'modify_own', 'modify_any', 'modify_replies',
428
			'move_any',
429
			'lock_own', 'lock_any',
430
			'remove_own', 'remove_any',
431
			'post_unapproved_topics', 'post_unapproved_replies_own', 'post_unapproved_replies_any',
432
		);
433
		call_integration_hook('integrate_post_ban_permissions', array(&$denied_permissions));
434
		$user_info['permissions'] = array_diff($user_info['permissions'], $denied_permissions);
435
	}
436
	// Are they absolutely under moderation?
437
	elseif (!empty($modSettings['warning_moderate']) && $modSettings['warning_moderate'] <= $user_info['warning'])
438
	{
439
		// Work out what permissions should change...
440
		$permission_change = array(
441
			'post_new' => 'post_unapproved_topics',
442
			'post_reply_own' => 'post_unapproved_replies_own',
443
			'post_reply_any' => 'post_unapproved_replies_any',
444
			'post_attachment' => 'post_unapproved_attachments',
445
		);
446
		call_integration_hook('integrate_warn_permissions', array(&$permission_change));
447
		foreach ($permission_change as $old => $new)
448
		{
449
			if (!in_array($old, $user_info['permissions']))
450
				unset($permission_change[$old]);
451
			else
452
				$user_info['permissions'][] = $new;
453
		}
454
		$user_info['permissions'] = array_diff($user_info['permissions'], array_keys($permission_change));
455
	}
456
457
	// @todo Find a better place to call this? Needs to be after permissions loaded!
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
458
	// Finally, some bits we cache in the session because it saves queries.
459
	if (isset($_SESSION['mc']) && $_SESSION['mc']['time'] > $modSettings['settings_updated'] && $_SESSION['mc']['id'] == $user_info['id'])
460
		$user_info['mod_cache'] = $_SESSION['mc'];
461
	else
462
	{
463
		require_once($sourcedir . '/Subs-Auth.php');
464
		rebuildModCache();
465
	}
466
467
	// Now that we have the mod cache taken care of lets setup a cache for the number of mod reports still open
468
	if (isset($_SESSION['rc']['reports']) && isset($_SESSION['rc']['member_reports']) && $_SESSION['rc']['time'] > $modSettings['last_mod_report_action'] && $_SESSION['rc']['id'] == $user_info['id'])
469
	{
470
		$context['open_mod_reports'] = $_SESSION['rc']['reports'];
471
		$context['open_member_reports'] = $_SESSION['rc']['member_reports'];
472
	}
473
	elseif ($_SESSION['mc']['bq'] != '0=1')
474
	{
475
		require_once($sourcedir . '/Subs-ReportedContent.php');
476
		$context['open_mod_reports'] = recountOpenReports('posts');
477
		$context['open_member_reports'] = recountOpenReports('members');
478
	}
479
	else
480
	{
481
		$context['open_mod_reports'] = 0;
482
		$context['open_member_reports'] = 0;
483
	}
484
}
485
486
/**
487
 * Log a ban in the database.
488
 * Log the current user in the ban logs.
489
 * Increment the hit counters for the specified ban ID's (if any.)
490
 *
491
 * @param array $ban_ids The IDs of the bans
492
 * @param string $email The email address associated with the user that triggered this hit
0 ignored issues
show
Documentation introduced by
Should the type for parameter $email not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
493
 */
494
function log_ban($ban_ids = array(), $email = null)
495
{
496
	global $user_info, $smcFunc;
497
498
	// Don't log web accelerators, it's very confusing...
499
	if (isset($_SERVER['HTTP_X_MOZ']) && $_SERVER['HTTP_X_MOZ'] == 'prefetch')
500
		return;
501
502
	$smcFunc['db_insert']('',
503
		'{db_prefix}log_banned',
504
		array('id_member' => 'int', 'ip' => 'inet', 'email' => 'string', 'log_time' => 'int'),
505
		array($user_info['id'], $user_info['ip'], ($email === null ? ($user_info['is_guest'] ? '' : $user_info['email']) : $email), time()),
506
		array('id_ban_log')
507
	);
508
509
	// One extra point for these bans.
510
	if (!empty($ban_ids))
511
		$smcFunc['db_query']('', '
512
			UPDATE {db_prefix}ban_items
513
			SET hits = hits + 1
514
			WHERE id_ban IN ({array_int:ban_ids})',
515
			array(
516
				'ban_ids' => $ban_ids,
517
			)
518
		);
519
}
520
521
/**
522
 * Checks if a given email address might be banned.
523
 * Check if a given email is banned.
524
 * Performs an immediate ban if the turns turns out positive.
525
 *
526
 * @param string $email The email to check
527
 * @param string $restriction What type of restriction (cannot_post, cannot_register, etc.)
528
 * @param string $error The error message to display if they are indeed banned
529
 */
530
function isBannedEmail($email, $restriction, $error)
531
{
532
	global $txt, $smcFunc;
533
534
	// Can't ban an empty email
535
	if (empty($email) || trim($email) == '')
536
		return;
537
538
	// Let's start with the bans based on your IP/hostname/memberID...
539
	$ban_ids = isset($_SESSION['ban'][$restriction]) ? $_SESSION['ban'][$restriction]['ids'] : array();
540
	$ban_reason = isset($_SESSION['ban'][$restriction]) ? $_SESSION['ban'][$restriction]['reason'] : '';
541
542
	// ...and add to that the email address you're trying to register.
543
	$request = $smcFunc['db_query']('', '
544
		SELECT bi.id_ban, bg.' . $restriction . ', bg.cannot_access, bg.reason
545
		FROM {db_prefix}ban_items AS bi
546
			INNER JOIN {db_prefix}ban_groups AS bg ON (bg.id_ban_group = bi.id_ban_group)
547
		WHERE {string:email} LIKE bi.email_address
548
			AND (bg.' . $restriction . ' = {int:cannot_access} OR bg.cannot_access = {int:cannot_access})
549
			AND (bg.expire_time IS NULL OR bg.expire_time >= {int:now})',
550
		array(
551
			'email' => $email,
552
			'cannot_access' => 1,
553
			'now' => time(),
554
		)
555
	);
556
	while ($row = $smcFunc['db_fetch_assoc']($request))
557
	{
558
		if (!empty($row['cannot_access']))
559
		{
560
			$_SESSION['ban']['cannot_access']['ids'][] = $row['id_ban'];
561
			$_SESSION['ban']['cannot_access']['reason'] = $row['reason'];
562
		}
563
		if (!empty($row[$restriction]))
564
		{
565
			$ban_ids[] = $row['id_ban'];
566
			$ban_reason = $row['reason'];
567
		}
568
	}
569
	$smcFunc['db_free_result']($request);
570
571
	// You're in biiig trouble.  Banned for the rest of this session!
572
	if (isset($_SESSION['ban']['cannot_access']))
573
	{
574
		log_ban($_SESSION['ban']['cannot_access']['ids']);
575
		$_SESSION['ban']['last_checked'] = time();
576
577
		fatal_error(sprintf($txt['your_ban'], $txt['guest_title']) . $_SESSION['ban']['cannot_access']['reason'], false);
0 ignored issues
show
Documentation introduced by
false is of type boolean, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
578
	}
579
580
	if (!empty($ban_ids))
581
	{
582
		// Log this ban for future reference.
583
		log_ban($ban_ids, $email);
584
		fatal_error($error . $ban_reason, false);
0 ignored issues
show
Documentation introduced by
false is of type boolean, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
585
	}
586
}
587
588
/**
589
 * Make sure the user's correct session was passed, and they came from here.
590
 * Checks the current session, verifying that the person is who he or she should be.
591
 * Also checks the referrer to make sure they didn't get sent here.
592
 * Depends on the disableCheckUA setting, which is usually missing.
593
 * Will check GET, POST, or REQUEST depending on the passed type.
594
 * Also optionally checks the referring action if passed. (note that the referring action must be by GET.)
595
 *
596
 * @param string $type The type of check (post, get, request)
597
 * @param string $from_action The action this is coming from
598
 * @param bool $is_fatal Whether to die with a fatal error if the check fails
599
 * @return string The error message if is_fatal is false.
0 ignored issues
show
Documentation introduced by
Should the return type not be null|string?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
600
 */
601
function checkSession($type = 'post', $from_action = '', $is_fatal = true)
602
{
603
	global $sc, $modSettings, $boardurl;
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $sc. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
604
605
	// Is it in as $_POST['sc']?
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
606
	if ($type == 'post')
607
	{
608
		$check = isset($_POST[$_SESSION['session_var']]) ? $_POST[$_SESSION['session_var']] : (empty($modSettings['strictSessionCheck']) && isset($_POST['sc']) ? $_POST['sc'] : null);
609
		if ($check !== $sc)
610
			$error = 'session_timeout';
611
	}
612
613
	// How about $_GET['sesc']?
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
614
	elseif ($type == 'get')
615
	{
616
		$check = isset($_GET[$_SESSION['session_var']]) ? $_GET[$_SESSION['session_var']] : (empty($modSettings['strictSessionCheck']) && isset($_GET['sesc']) ? $_GET['sesc'] : null);
617
		if ($check !== $sc)
618
			$error = 'session_verify_fail';
619
	}
620
621
	// Or can it be in either?
622
	elseif ($type == 'request')
623
	{
624
		$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)));
625
626
		if ($check !== $sc)
627
			$error = 'session_verify_fail';
628
	}
629
630
	// Verify that they aren't changing user agents on us - that could be bad.
631
	if ((!isset($_SESSION['USER_AGENT']) || $_SESSION['USER_AGENT'] != $_SERVER['HTTP_USER_AGENT']) && empty($modSettings['disableCheckUA']))
632
		$error = 'session_verify_fail';
633
634
	// Make sure a page with session check requirement is not being prefetched.
635 View Code Duplication
	if (isset($_SERVER['HTTP_X_MOZ']) && $_SERVER['HTTP_X_MOZ'] == 'prefetch')
636
	{
637
		ob_end_clean();
638
		header('HTTP/1.1 403 Forbidden');
639
		die;
640
	}
641
642
	// Check the referring site - it should be the same server at least!
643
	if (isset($_SESSION['request_referer']))
644
		$referrer = $_SESSION['request_referer'];
645
	else
646
		$referrer = isset($_SERVER['HTTP_REFERER']) ? @parse_url($_SERVER['HTTP_REFERER']) : array();
647
	if (!empty($referrer['host']))
648
	{
649
		if (strpos($_SERVER['HTTP_HOST'], ':') !== false)
650
			$real_host = substr($_SERVER['HTTP_HOST'], 0, strpos($_SERVER['HTTP_HOST'], ':'));
651
		else
652
			$real_host = $_SERVER['HTTP_HOST'];
653
654
		$parsed_url = parse_url($boardurl);
655
656
		// Are global cookies on?  If so, let's check them ;).
657
		if (!empty($modSettings['globalCookies']))
658
		{
659
			if (preg_match('~(?:[^\.]+\.)?([^\.]{3,}\..+)\z~i', $parsed_url['host'], $parts) == 1)
660
				$parsed_url['host'] = $parts[1];
661
662
			if (preg_match('~(?:[^\.]+\.)?([^\.]{3,}\..+)\z~i', $referrer['host'], $parts) == 1)
663
				$referrer['host'] = $parts[1];
664
665
			if (preg_match('~(?:[^\.]+\.)?([^\.]{3,}\..+)\z~i', $real_host, $parts) == 1)
666
				$real_host = $parts[1];
667
		}
668
669
		// Okay: referrer must either match parsed_url or real_host.
670
		if (isset($parsed_url['host']) && strtolower($referrer['host']) != strtolower($parsed_url['host']) && strtolower($referrer['host']) != strtolower($real_host))
671
		{
672
			$error = 'verify_url_fail';
673
			$log_error = true;
674
		}
675
	}
676
677
	// Well, first of all, if a from_action is specified you'd better have an old_url.
678
	if (!empty($from_action) && (!isset($_SESSION['old_url']) || preg_match('~[?;&]action=' . $from_action . '([;&]|$)~', $_SESSION['old_url']) == 0))
679
	{
680
		$error = 'verify_url_fail';
681
		$log_error = true;
682
	}
683
684
	if (strtolower($_SERVER['HTTP_USER_AGENT']) == 'hacker')
685
		fatal_error('Sound the alarm!  It\'s a hacker!  Close the castle gates!!', false);
0 ignored issues
show
Documentation introduced by
false is of type boolean, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
686
687
	// Everything is ok, return an empty string.
688
	if (!isset($error))
689
		return '';
690
	// A session error occurred, show the error.
691
	elseif ($is_fatal)
692
	{
693
		if (isset($_GET['xml']))
694
		{
695
			ob_end_clean();
696
			header('HTTP/1.1 403 Forbidden - Session timeout');
697
			die;
698
		}
699
		else
700
			fatal_lang_error($error, isset($log_error) ? 'user' : false);
701
	}
702
	// A session error occurred, return the error to the calling function.
703
	else
704
		return $error;
705
706
	// We really should never fall through here, for very important reasons.  Let's make sure.
707
	trigger_error('Hacking attempt...', E_USER_ERROR);
708
}
709
710
/**
711
 * Check if a specific confirm parameter was given.
712
 *
713
 * @param string $action The action we want to check against
714
 * @return bool|string True if the check passed or a token
715
 */
716
function checkConfirm($action)
717
{
718
	global $modSettings;
719
720
	if (isset($_GET['confirm']) && isset($_SESSION['confirm_' . $action]) && md5($_GET['confirm'] . $_SERVER['HTTP_USER_AGENT']) == $_SESSION['confirm_' . $action])
721
		return true;
722
723
	else
724
	{
725
		$token = md5(mt_rand() . session_id() . (string) microtime() . $modSettings['rand_seed']);
726
		$_SESSION['confirm_' . $action] = md5($token . $_SERVER['HTTP_USER_AGENT']);
727
728
		return $token;
729
	}
730
}
731
732
/**
733
 * Lets give you a token of our appreciation.
734
 *
735
 * @param string $action The action to create the token for
736
 * @param string $type The type of token ('post', 'get' or 'request')
737
 * @return array An array containing the name of the token var and the actual token
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use array<string,string>.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
738
 */
739
function createToken($action, $type = 'post')
740
{
741
	global $modSettings, $context;
742
743
	$token = md5(mt_rand() . session_id() . (string) microtime() . $modSettings['rand_seed'] . $type);
744
	$token_var = substr(preg_replace('~^\d+~', '', md5(mt_rand() . (string) microtime() . mt_rand())), 0, mt_rand(7, 12));
745
746
	$_SESSION['token'][$type . '-' . $action] = array($token_var, md5($token . $_SERVER['HTTP_USER_AGENT']), time(), $token);
747
748
	$context[$action . '_token'] = $token;
749
	$context[$action . '_token_var'] = $token_var;
750
751
	return array($action . '_token_var' => $token_var, $action . '_token' => $token);
752
}
753
754
/**
755
 * Only patrons with valid tokens can ride this ride.
756
 *
757
 * @param string $action The action to validate the token for
758
 * @param string $type The type of request (get, request, or post)
759
 * @param bool $reset Whether to reset the token and display an error if validation fails
760
 * @return bool|string If the action is login, returns the token for the action, otherwise returns whether the validation was successful
761
 */
762
function validateToken($action, $type = 'post', $reset = true)
763
{
764
	$type = $type == 'get' || $type == 'request' ? $type : 'post';
765
766
	// Logins are special: the token is used to has the password with javascript before POST it
767
	if ($action == 'login')
768
	{
769
		if (isset($_SESSION['token'][$type . '-' . $action]))
770
		{
771
			$return = $_SESSION['token'][$type . '-' . $action][3];
772
			unset($_SESSION['token'][$type . '-' . $action]);
773
			return $return;
774
		}
775
		else
776
			return '';
777
	}
778
779
	// This nasty piece of code validates a token.
780
	/*
781
		1. The token exists in session.
782
		2. The {$type} variable should exist.
783
		3. We concat the variable we received with the user agent
784
		4. Match that result against what is in the session.
785
		5. If it matches, success, otherwise we fallout.
786
	*/
787
	if (isset($_SESSION['token'][$type . '-' . $action], $GLOBALS['_' . strtoupper($type)][$_SESSION['token'][$type . '-' . $action][0]]) && md5($GLOBALS['_' . strtoupper($type)][$_SESSION['token'][$type . '-' . $action][0]] . $_SERVER['HTTP_USER_AGENT']) == $_SESSION['token'][$type . '-' . $action][1])
788
	{
789
		// Invalidate this token now.
790
		unset($_SESSION['token'][$type . '-' . $action]);
791
792
		return true;
793
	}
794
795
	// Patrons with invalid tokens get the boot.
796
	if ($reset)
797
	{
798
		// Might as well do some cleanup on this.
799
		cleanTokens();
800
801
		// I'm back baby.
802
		createToken($action, $type);
803
804
		fatal_lang_error('token_verify_fail', false);
805
	}
806
	// Remove this token as its useless
807
	else
808
		unset($_SESSION['token'][$type . '-' . $action]);
809
810
	// Randomly check if we should remove some older tokens.
811
	if (mt_rand(0, 138) == 23)
812
		cleanTokens();
813
814
	return false;
815
}
816
817
/**
818
 * Removes old unused tokens from session
819
 * defaults to 3 hours before a token is considered expired
820
 * if $complete = true will remove all tokens
821
 *
822
 * @param bool $complete Whether to remove all tokens or only expired ones
823
 */
824
function cleanTokens($complete = false)
825
{
826
	// We appreciate cleaning up after yourselves.
827
	if (!isset($_SESSION['token']))
828
		return;
829
830
	// Clean up tokens, trying to give enough time still.
831
	foreach ($_SESSION['token'] as $key => $data)
832
		if ($data[2] + 10800 < time() || $complete)
833
			unset($_SESSION['token'][$key]);
834
}
835
836
/**
837
 * Check whether a form has been submitted twice.
838
 * Registers a sequence number for a form.
839
 * Checks whether a submitted sequence number is registered in the current session.
840
 * Depending on the value of is_fatal shows an error or returns true or false.
841
 * Frees a sequence number from the stack after it's been checked.
842
 * Frees a sequence number without checking if action == 'free'.
843
 *
844
 * @param string $action The action - can be 'register', 'check' or 'free'
845
 * @param bool $is_fatal Whether to die with a fatal error
846
 * @return void|bool If the action isn't check, returns nothing, otherwise returns whether the check was successful
847
 */
848
function checkSubmitOnce($action, $is_fatal = true)
849
{
850
	global $context;
851
852
	if (!isset($_SESSION['forms']))
853
		$_SESSION['forms'] = array();
854
855
	// Register a form number and store it in the session stack. (use this on the page that has the form.)
856
	if ($action == 'register')
857
	{
858
		$context['form_sequence_number'] = 0;
859
		while (empty($context['form_sequence_number']) || in_array($context['form_sequence_number'], $_SESSION['forms']))
860
			$context['form_sequence_number'] = mt_rand(1, 16000000);
861
	}
862
	// Check whether the submitted number can be found in the session.
863
	elseif ($action == 'check')
864
	{
865
		if (!isset($_REQUEST['seqnum']))
866
			return true;
867
		elseif (!in_array($_REQUEST['seqnum'], $_SESSION['forms']))
868
		{
869
			$_SESSION['forms'][] = (int) $_REQUEST['seqnum'];
870
			return true;
871
		}
872
		elseif ($is_fatal)
873
			fatal_lang_error('error_form_already_submitted', false);
874
		else
875
			return false;
876
	}
877
	// Don't check, just free the stack number.
878
	elseif ($action == 'free' && isset($_REQUEST['seqnum']) && in_array($_REQUEST['seqnum'], $_SESSION['forms']))
879
		$_SESSION['forms'] = array_diff($_SESSION['forms'], array($_REQUEST['seqnum']));
880
	elseif ($action != 'free')
881
		trigger_error('checkSubmitOnce(): Invalid action \'' . $action . '\'', E_USER_WARNING);
882
}
883
884
/**
885
 * Check the user's permissions.
886
 * checks whether the user is allowed to do permission. (ie. post_new.)
887
 * If boards is specified, checks those boards instead of the current one.
888
 * Always returns true if the user is an administrator.
889
 *
890
 * @param string|array $permission A single permission to check or an array of permissions to check
891
 * @param int|array $boards The ID of a board or an array of board IDs if we want to check board-level permissions
0 ignored issues
show
Documentation introduced by
Should the type for parameter $boards not be integer|array|null? Also, consider making the array more specific, something like array<String>, or String[].

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive. In addition it looks for parameters that have the generic type array and suggests a stricter type like array<String>.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
892
 * @return bool Whether the user has the specified permission
893
 */
894
function allowedTo($permission, $boards = null)
895
{
896
	global $user_info, $smcFunc;
897
898
	// You're always allowed to do nothing. (unless you're a working man, MR. LAZY :P!)
899
	if (empty($permission))
900
		return true;
901
902
	// You're never allowed to do something if your data hasn't been loaded yet!
903
	if (empty($user_info))
904
		return false;
905
906
	// Administrators are supermen :P.
907
	if ($user_info['is_admin'])
908
		return true;
909
910
	// Let's ensure this is an array.
911
	$permission = (array) $permission;
912
913
	// Are we checking the _current_ board, or some other boards?
914
	if ($boards === null)
915
	{
916
		if (count(array_intersect($permission, $user_info['permissions'])) != 0)
0 ignored issues
show
Coding Style introduced by
The if-else statement can be simplified to return count(array_inter...['permissions'])) != 0;.
Loading history...
917
			return true;
918
		// You aren't allowed, by default.
919
		else
920
			return false;
921
	}
922
	elseif (!is_array($boards))
923
		$boards = array($boards);
924
925
	$request = $smcFunc['db_query']('', '
926
		SELECT MIN(bp.add_deny) AS add_deny
927
		FROM {db_prefix}boards AS b
928
			INNER JOIN {db_prefix}board_permissions AS bp ON (bp.id_profile = b.id_profile)
929
			LEFT JOIN {db_prefix}moderators AS mods ON (mods.id_board = b.id_board AND mods.id_member = {int:current_member})
930
			LEFT JOIN {db_prefix}moderator_groups AS modgs ON (modgs.id_board = b.id_board AND modgs.id_group IN ({array_int:group_list}))
931
		WHERE b.id_board IN ({array_int:board_list})
932
			AND bp.id_group IN ({array_int:group_list}, {int:moderator_group})
933
			AND bp.permission IN ({array_string:permission_list})
934
			AND (mods.id_member IS NOT NULL OR modgs.id_group IS NOT NULL OR bp.id_group != {int:moderator_group})
935
		GROUP BY b.id_board',
936
		array(
937
			'current_member' => $user_info['id'],
938
			'board_list' => $boards,
939
			'group_list' => $user_info['groups'],
940
			'moderator_group' => 3,
941
			'permission_list' => $permission,
942
		)
943
	);
944
945
	// Make sure they can do it on all of the boards.
946
	if ($smcFunc['db_num_rows']($request) != count($boards))
947
		return false;
948
949
	$result = true;
950
	while ($row = $smcFunc['db_fetch_assoc']($request))
951
		$result &= !empty($row['add_deny']);
952
	$smcFunc['db_free_result']($request);
953
954
	// If the query returned 1, they can do it... otherwise, they can't.
955
	return $result;
956
}
957
958
/**
959
 * Fatal error if they cannot.
960
 * Uses allowedTo() to check if the user is allowed to do permission.
961
 * Checks the passed boards or current board for the permission.
962
 * If they are not, it loads the Errors language file and shows an error using $txt['cannot_' . $permission].
963
 * If they are a guest and cannot do it, this calls is_not_guest().
964
 *
965
 * @param string|array $permission A single permission to check or an array of permissions to check
966
 * @param int|array $boards The ID of a single board or an array of board IDs if we're checking board-level permissions (null otherwise)
0 ignored issues
show
Documentation introduced by
Should the type for parameter $boards not be integer|array|null? Also, consider making the array more specific, something like array<String>, or String[].

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive. In addition it looks for parameters that have the generic type array and suggests a stricter type like array<String>.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
967
 */
968
function isAllowedTo($permission, $boards = null)
969
{
970
	global $user_info, $txt;
971
972
	$heavy_permissions = array(
973
		'admin_forum',
974
		'manage_attachments',
975
		'manage_smileys',
976
		'manage_boards',
977
		'edit_news',
978
		'moderate_forum',
979
		'manage_bans',
980
		'manage_membergroups',
981
		'manage_permissions',
982
	);
983
984
	// Make it an array, even if a string was passed.
985
	$permission = (array) $permission;
986
987
	call_integration_hook('integrate_heavy_permissions_session', array(&$heavy_permissions));
988
989
	// Check the permission and return an error...
990
	if (!allowedTo($permission, $boards))
991
	{
992
		// Pick the last array entry as the permission shown as the error.
993
		$error_permission = array_shift($permission);
994
995
		// If they are a guest, show a login. (because the error might be gone if they do!)
996
		if ($user_info['is_guest'])
997
		{
998
			loadLanguage('Errors');
999
			is_not_guest($txt['cannot_' . $error_permission]);
1000
		}
1001
1002
		// Clear the action because they aren't really doing that!
1003
		$_GET['action'] = '';
1004
		$_GET['board'] = '';
1005
		$_GET['topic'] = '';
1006
		writeLog(true);
1007
1008
		fatal_lang_error('cannot_' . $error_permission, false);
1009
1010
		// Getting this far is a really big problem, but let's try our best to prevent any cases...
1011
		trigger_error('Hacking attempt...', E_USER_ERROR);
1012
	}
1013
1014
	// If you're doing something on behalf of some "heavy" permissions, validate your session.
1015
	// (take out the heavy permissions, and if you can't do anything but those, you need a validated session.)
1016
	if (!allowedTo(array_diff($permission, $heavy_permissions), $boards))
1017
		validateSession();
1018
}
1019
1020
/**
1021
 * Return the boards a user has a certain (board) permission on. (array(0) if all.)
1022
 *  - returns a list of boards on which the user is allowed to do the specified permission.
1023
 *  - returns an array with only a 0 in it if the user has permission to do this on every board.
1024
 *  - returns an empty array if he or she cannot do this on any board.
1025
 * If check_access is true will also make sure the group has proper access to that board.
1026
 *
1027
 * @param string|array $permissions A single permission to check or an array of permissions to check
1028
 * @param bool $check_access Whether to check only the boards the user has access to
1029
 * @param bool $simple Whether to return a simple array of board IDs or one with permissions as the keys
1030
 * @return array An array of board IDs or an array containing 'permission' => 'board,board2,...' pairs
1031
 */
1032
function boardsAllowedTo($permissions, $check_access = true, $simple = true)
1033
{
1034
	global $user_info, $smcFunc;
1035
1036
	// Arrays are nice, most of the time.
1037
	if (!is_array($permissions))
1038
		$permissions = array($permissions);
1039
1040
	/*
1041
	 * Set $simple to true to use this function as it were in SMF 2.0.x.
1042
	 * Otherwise, the resultant array becomes split into the multiple
1043
	 * permissions that were passed. Other than that, it's just the normal
1044
	 * state of play that you're used to.
1045
	 */
1046
1047
	// Administrators are all powerful, sorry.
1048
	if ($user_info['is_admin'])
1049
	{
1050
		if ($simple)
1051
			return array(0);
1052
		else
1053
		{
1054
			$boards = array();
1055
			foreach ($permissions as $permission)
1056
				$boards[$permission] = array(0);
1057
1058
			return $boards;
1059
		}
1060
	}
1061
1062
	// All groups the user is in except 'moderator'.
1063
	$groups = array_diff($user_info['groups'], array(3));
1064
1065
	$request = $smcFunc['db_query']('', '
1066
		SELECT b.id_board, bp.add_deny' . ($simple ? '' : ', bp.permission') . '
1067
		FROM {db_prefix}board_permissions AS bp
1068
			INNER JOIN {db_prefix}boards AS b ON (b.id_profile = bp.id_profile)
1069
			LEFT JOIN {db_prefix}moderators AS mods ON (mods.id_board = b.id_board AND mods.id_member = {int:current_member})
1070
			LEFT JOIN {db_prefix}moderator_groups AS modgs ON (modgs.id_board = b.id_board AND modgs.id_group IN ({array_int:group_list}))
1071
		WHERE bp.id_group IN ({array_int:group_list}, {int:moderator_group})
1072
			AND bp.permission IN ({array_string:permissions})
1073
			AND (mods.id_member IS NOT NULL OR modgs.id_group IS NOT NULL OR bp.id_group != {int:moderator_group})' .
1074
			($check_access ? ' AND {query_see_board}' : ''),
1075
		array(
1076
			'current_member' => $user_info['id'],
1077
			'group_list' => $groups,
1078
			'moderator_group' => 3,
1079
			'permissions' => $permissions,
1080
		)
1081
	);
1082
	$boards = array();
1083
	$deny_boards = array();
1084
	while ($row = $smcFunc['db_fetch_assoc']($request))
1085
	{
1086
		if ($simple)
1087
		{
1088
			if (empty($row['add_deny']))
1089
				$deny_boards[] = $row['id_board'];
1090
			else
1091
				$boards[] = $row['id_board'];
1092
		}
1093
		else
1094
		{
1095
			if (empty($row['add_deny']))
1096
				$deny_boards[$row['permission']][] = $row['id_board'];
1097
			else
1098
				$boards[$row['permission']][] = $row['id_board'];
1099
		}
1100
	}
1101
	$smcFunc['db_free_result']($request);
1102
1103
	if ($simple)
1104
		$boards = array_unique(array_values(array_diff($boards, $deny_boards)));
1105
	else
1106
	{
1107
		foreach ($permissions as $permission)
1108
		{
1109
			// never had it to start with
1110
			if (empty($boards[$permission]))
1111
				$boards[$permission] = array();
1112
			else
1113
			{
1114
				// Or it may have been removed
1115
				$deny_boards[$permission] = isset($deny_boards[$permission]) ? $deny_boards[$permission] : array();
1116
				$boards[$permission] = array_unique(array_values(array_diff($boards[$permission], $deny_boards[$permission])));
1117
			}
1118
		}
1119
	}
1120
1121
	return $boards;
1122
}
1123
1124
/**
1125
 * This function attempts to protect from spammed messages and the like.
1126
 * The time taken depends on error_type - generally uses the modSetting.
1127
 *
1128
 * @param string $error_type The error type. Also used as a $txt index (not an actual string).
1129
 * @param boolean $only_return_result Whether you want the function to die with a fatal_lang_error.
1130
 * @return bool Whether they've posted within the limit
1131
 */
1132
function spamProtection($error_type, $only_return_result = false)
1133
{
1134
	global $modSettings, $user_info, $smcFunc;
1135
1136
	// Certain types take less/more time.
1137
	$timeOverrides = array(
1138
		'login' => 2,
1139
		'register' => 2,
1140
		'remind' => 30,
1141
		'sendmail' => $modSettings['spamWaitTime'] * 5,
1142
		'reporttm' => $modSettings['spamWaitTime'] * 4,
1143
		'search' => !empty($modSettings['search_floodcontrol_time']) ? $modSettings['search_floodcontrol_time'] : 1,
1144
	);
1145
1146
1147
	// Moderators are free...
1148
	if (!allowedTo('moderate_board'))
1149
		$timeLimit = isset($timeOverrides[$error_type]) ? $timeOverrides[$error_type] : $modSettings['spamWaitTime'];
1150
	else
1151
		$timeLimit = 2;
1152
1153
	call_integration_hook('integrate_spam_protection', array(&$timeOverrides, &$timeLimit));
1154
1155
	// Delete old entries...
1156
	$smcFunc['db_query']('', '
1157
		DELETE FROM {db_prefix}log_floodcontrol
1158
		WHERE log_time < {int:log_time}
1159
			AND log_type = {string:log_type}',
1160
		array(
1161
			'log_time' => time() - $timeLimit,
1162
			'log_type' => $error_type,
1163
		)
1164
	);
1165
1166
	// Add a new entry, deleting the old if necessary.
1167
	$smcFunc['db_insert']('replace',
1168
		'{db_prefix}log_floodcontrol',
1169
		array('ip' => 'inet', 'log_time' => 'int', 'log_type' => 'string'),
1170
		array($user_info['ip'], time(), $error_type),
1171
		array('ip', 'log_type')
1172
	);
1173
1174
	// If affected is 0 or 2, it was there already.
1175
	if ($smcFunc['db_affected_rows']() != 1)
1176
	{
1177
		// Spammer!  You only have to wait a *few* seconds!
1178
		if (!$only_return_result)
1179
			fatal_lang_error($error_type . '_WaitTime_broken', false, array($timeLimit));
1180
1181
		return true;
1182
	}
1183
1184
	// They haven't posted within the limit.
1185
	return false;
1186
}
1187
1188
/**
1189
 * A generic function to create a pair of index.php and .htaccess files in a directory
1190
 *
1191
 * @param string $path The (absolute) directory path
1192
 * @param boolean $attachments Whether this is an attachment directory
1193
 * @return bool|string True on success error or a string if anything fails
0 ignored issues
show
Documentation introduced by
Should the return type not be string|string[]|boolean?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
1194
 */
1195
function secureDirectory($path, $attachments = false)
1196
{
1197
	if (empty($path))
1198
		return 'empty_path';
1199
1200
	if (!is_writable($path))
1201
		return 'path_not_writable';
1202
1203
	$directoryname = basename($path);
1204
1205
	$errors = array();
1206
	$close = empty($attachments) ? '
1207
</Files>' : '
1208
	Allow from localhost
1209
</Files>
1210
1211
RemoveHandler .php .php3 .phtml .cgi .fcgi .pl .fpl .shtml';
1212
1213
	if (file_exists($path . '/.htaccess'))
1214
		$errors[] = 'htaccess_exists';
1215
	else
1216
	{
1217
		$fh = @fopen($path . '/.htaccess', 'w');
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $fh. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
1218
		if ($fh) {
1219
			fwrite($fh, '<Files *>
1220
	Order Deny,Allow
1221
	Deny from all' . $close);
1222
			fclose($fh);
1223
		}
1224
		$errors[] = 'htaccess_cannot_create_file';
1225
	}
1226
1227
	if (file_exists($path . '/index.php'))
1228
		$errors[] = 'index-php_exists';
1229
	else
1230
	{
1231
		$fh = @fopen($path . '/index.php', 'w');
1232
		if ($fh) {
1233
			fwrite($fh, '<' . '?php
1234
1235
/**
1236
 * This file is here solely to protect your ' . $directoryname . ' directory.
1237
 */
1238
1239
// Look for Settings.php....
1240
if (file_exists(dirname(dirname(__FILE__)) . \'/Settings.php\'))
1241
{
1242
	// Found it!
1243
	require(dirname(dirname(__FILE__)) . \'/Settings.php\');
1244
	header(\'Location: \' . $boardurl);
1245
}
1246
// Can\'t find it... just forget it.
1247
else
1248
	exit;
1249
1250
?'. '>');
1251
			fclose($fh);
1252
		}
1253
		$errors[] = 'index-php_cannot_create_file';
1254
	}
1255
1256
	if (!empty($errors))
1257
		return $errors;
1258
	else
1259
		return true;
1260
}
1261
1262
/**
1263
* This sets the X-Frame-Options header.
1264
*
1265
* @param string $override An option to override (either 'SAMEORIGIN' or 'DENY')
0 ignored issues
show
Documentation introduced by
Should the type for parameter $override not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
1266
* @since 2.1
1267
*/
1268
function frameOptionsHeader($override = null)
1269
{
1270
	global $modSettings;
1271
1272
	$option = 'SAMEORIGIN';
1273
	if (is_null($override) && !empty($modSettings['frame_security']))
1274
		$option = $modSettings['frame_security'];
1275
	elseif (in_array($override, array('SAMEORIGIN', 'DENY')))
1276
		$option = $override;
1277
1278
	// Don't bother setting the header if we have disabled it.
1279
	if ($option == 'DISABLE')
1280
		return;
1281
1282
	// Finally set it.
1283
	header('X-Frame-Options: ' . $option);
1284
1285
	// And some other useful ones.
1286
	header('X-XSS-Protection: 1');
1287
	header('X-Content-Type-Options: nosniff');
1288
}
1289
1290
?>