Completed
Push — release-2.1 ( cb3e35...e7ba5c )
by Jeremy
10:31 queued 02:46
created

Security.php ➔ boardsAllowedTo()   C

Complexity

Conditions 14
Paths 13

Size

Total Lines 90

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 14
nc 13
nop 3
dl 0
loc 90
rs 5.2314
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 2018 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;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
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) && !httpsOn())
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;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
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;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
155
	global $sourcedir, $cookiename, $user_settings, $smcFunc;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
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, COALESCE(bg.expire_time, 0) AS expire_time
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
			$_SESSION['ban']['expire_time'] = $row['expire_time'];
285
		}
286
		$smcFunc['db_free_result']($request);
287
288
		// My mistake. Next time better.
289
		if (!isset($_SESSION['ban']['cannot_access']))
290
		{
291
			require_once($sourcedir . '/Subs-Auth.php');
292
			$cookie_url = url_parts(!empty($modSettings['localCookies']), !empty($modSettings['globalCookies']));
293
			smf_setcookie($cookiename . '_', '', time() - 3600, $cookie_url[1], $cookie_url[0], false, false);
294
		}
295
	}
296
297
	// If you're fully banned, it's end of the story for you.
298
	if (isset($_SESSION['ban']['cannot_access']))
299
	{
300
		// We don't wanna see you!
301
		if (!$user_info['is_guest'])
302
			$smcFunc['db_query']('', '
303
				DELETE FROM {db_prefix}log_online
304
				WHERE id_member = {int:current_member}',
305
				array(
306
					'current_member' => $user_info['id'],
307
				)
308
			);
309
310
		// 'Log' the user out.  Can't have any funny business... (save the name!)
311
		$old_name = isset($user_info['name']) && $user_info['name'] != '' ? $user_info['name'] : $txt['guest_title'];
312
		$user_info['name'] = '';
313
		$user_info['username'] = '';
314
		$user_info['is_guest'] = true;
315
		$user_info['is_admin'] = false;
316
		$user_info['permissions'] = array();
317
		$user_info['id'] = 0;
318
		$context['user'] = array(
319
			'id' => 0,
320
			'username' => '',
321
			'name' => $txt['guest_title'],
322
			'is_guest' => true,
323
			'is_logged' => false,
324
			'is_admin' => false,
325
			'is_mod' => false,
326
			'can_mod' => false,
327
			'language' => $user_info['language'],
328
		);
329
330
		// A goodbye present.
331
		require_once($sourcedir . '/Subs-Auth.php');
332
		require_once($sourcedir . '/LogInOut.php');
333
		$cookie_url = url_parts(!empty($modSettings['localCookies']), !empty($modSettings['globalCookies']));
334
		smf_setcookie($cookiename . '_', implode(',', $_SESSION['ban']['cannot_access']['ids']), time() + 3153600, $cookie_url[1], $cookie_url[0], false, false);
335
336
		// Don't scare anyone, now.
337
		$_GET['action'] = '';
338
		$_GET['board'] = '';
339
		$_GET['topic'] = '';
340
		writeLog(true);
341
		Logout(true, false);
342
343
		// You banned, sucka!
344
		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...
345
346
		// If we get here, something's gone wrong.... but let's try anyway.
347
		trigger_error('Hacking attempt...', E_USER_ERROR);
348
	}
349
	// You're not allowed to log in but yet you are. Let's fix that.
350
	elseif (isset($_SESSION['ban']['cannot_login']) && !$user_info['is_guest'])
351
	{
352
		// We don't wanna see you!
353
		$smcFunc['db_query']('', '
354
			DELETE FROM {db_prefix}log_online
355
			WHERE id_member = {int:current_member}',
356
			array(
357
				'current_member' => $user_info['id'],
358
			)
359
		);
360
361
		// 'Log' the user out.  Can't have any funny business... (save the name!)
362
		$old_name = isset($user_info['name']) && $user_info['name'] != '' ? $user_info['name'] : $txt['guest_title'];
363
		$user_info['name'] = '';
364
		$user_info['username'] = '';
365
		$user_info['is_guest'] = true;
366
		$user_info['is_admin'] = false;
367
		$user_info['permissions'] = array();
368
		$user_info['id'] = 0;
369
		$context['user'] = array(
370
			'id' => 0,
371
			'username' => '',
372
			'name' => $txt['guest_title'],
373
			'is_guest' => true,
374
			'is_logged' => false,
375
			'is_admin' => false,
376
			'is_mod' => false,
377
			'can_mod' => false,
378
			'language' => $user_info['language'],
379
		);
380
381
		// SMF's Wipe 'n Clean(r) erases all traces.
382
		$_GET['action'] = '';
383
		$_GET['board'] = '';
384
		$_GET['topic'] = '';
385
		writeLog(true);
386
387
		require_once($sourcedir . '/LogInOut.php');
388
		Logout(true, false);
389
390
		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...
391
	}
392
393
	// Fix up the banning permissions.
394
	if (isset($user_info['permissions']))
395
		banPermissions();
396
}
397
398
/**
399
 * Fix permissions according to ban status.
400
 * Applies any states of banning by removing permissions the user cannot have.
401
 */
402
function banPermissions()
403
{
404
	global $user_info, $sourcedir, $modSettings, $context;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
405
406
	// Somehow they got here, at least take away all permissions...
407
	if (isset($_SESSION['ban']['cannot_access']))
408
		$user_info['permissions'] = array();
409
	// Okay, well, you can watch, but don't touch a thing.
410
	elseif (isset($_SESSION['ban']['cannot_post']) || (!empty($modSettings['warning_mute']) && $modSettings['warning_mute'] <= $user_info['warning']))
411
	{
412
		$denied_permissions = array(
413
			'pm_send',
414
			'calendar_post', 'calendar_edit_own', 'calendar_edit_any',
415
			'poll_post',
416
			'poll_add_own', 'poll_add_any',
417
			'poll_edit_own', 'poll_edit_any',
418
			'poll_lock_own', 'poll_lock_any',
419
			'poll_remove_own', 'poll_remove_any',
420
			'manage_attachments', 'manage_smileys', 'manage_boards', 'admin_forum', 'manage_permissions',
421
			'moderate_forum', 'manage_membergroups', 'manage_bans', 'send_mail', 'edit_news',
422
			'profile_identity_any', 'profile_extra_any', 'profile_title_any',
423
			'profile_forum_any', 'profile_other_any', 'profile_signature_any',
424
			'post_new', 'post_reply_own', 'post_reply_any',
425
			'delete_own', 'delete_any', 'delete_replies',
426
			'make_sticky',
427
			'merge_any', 'split_any',
428
			'modify_own', 'modify_any', 'modify_replies',
429
			'move_any',
430
			'lock_own', 'lock_any',
431
			'remove_own', 'remove_any',
432
			'post_unapproved_topics', 'post_unapproved_replies_own', 'post_unapproved_replies_any',
433
		);
434
		call_integration_hook('integrate_post_ban_permissions', array(&$denied_permissions));
435
		$user_info['permissions'] = array_diff($user_info['permissions'], $denied_permissions);
436
	}
437
	// Are they absolutely under moderation?
438
	elseif (!empty($modSettings['warning_moderate']) && $modSettings['warning_moderate'] <= $user_info['warning'])
439
	{
440
		// Work out what permissions should change...
441
		$permission_change = array(
442
			'post_new' => 'post_unapproved_topics',
443
			'post_reply_own' => 'post_unapproved_replies_own',
444
			'post_reply_any' => 'post_unapproved_replies_any',
445
			'post_attachment' => 'post_unapproved_attachments',
446
		);
447
		call_integration_hook('integrate_warn_permissions', array(&$permission_change));
448
		foreach ($permission_change as $old => $new)
449
		{
450
			if (!in_array($old, $user_info['permissions']))
451
				unset($permission_change[$old]);
452
			else
453
				$user_info['permissions'][] = $new;
454
		}
455
		$user_info['permissions'] = array_diff($user_info['permissions'], array_keys($permission_change));
456
	}
457
458
	// @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...
459
	// Finally, some bits we cache in the session because it saves queries.
460
	if (isset($_SESSION['mc']) && $_SESSION['mc']['time'] > $modSettings['settings_updated'] && $_SESSION['mc']['id'] == $user_info['id'])
461
		$user_info['mod_cache'] = $_SESSION['mc'];
462
	else
463
	{
464
		require_once($sourcedir . '/Subs-Auth.php');
465
		rebuildModCache();
466
	}
467
468
	// Now that we have the mod cache taken care of lets setup a cache for the number of mod reports still open
469
	if (isset($_SESSION['rc']['reports']) && isset($_SESSION['rc']['member_reports']) && $_SESSION['rc']['time'] > $modSettings['last_mod_report_action'] && $_SESSION['rc']['id'] == $user_info['id'])
470
	{
471
		$context['open_mod_reports'] = $_SESSION['rc']['reports'];
472
		$context['open_member_reports'] = $_SESSION['rc']['member_reports'];
473
	}
474
	elseif ($_SESSION['mc']['bq'] != '0=1')
475
	{
476
		require_once($sourcedir . '/Subs-ReportedContent.php');
477
		$context['open_mod_reports'] = recountOpenReports('posts');
478
		$context['open_member_reports'] = recountOpenReports('members');
479
	}
480
	else
481
	{
482
		$context['open_mod_reports'] = 0;
483
		$context['open_member_reports'] = 0;
484
	}
485
}
486
487
/**
488
 * Log a ban in the database.
489
 * Log the current user in the ban logs.
490
 * Increment the hit counters for the specified ban ID's (if any.)
491
 *
492
 * @param array $ban_ids The IDs of the bans
493
 * @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...
494
 */
495
function log_ban($ban_ids = array(), $email = null)
496
{
497
	global $user_info, $smcFunc;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
498
499
	// Don't log web accelerators, it's very confusing...
500
	if (isset($_SERVER['HTTP_X_MOZ']) && $_SERVER['HTTP_X_MOZ'] == 'prefetch')
501
		return;
502
503
	$smcFunc['db_insert']('',
504
		'{db_prefix}log_banned',
505
		array('id_member' => 'int', 'ip' => 'inet', 'email' => 'string', 'log_time' => 'int'),
506
		array($user_info['id'], $user_info['ip'], ($email === null ? ($user_info['is_guest'] ? '' : $user_info['email']) : $email), time()),
507
		array('id_ban_log')
508
	);
509
510
	// One extra point for these bans.
511
	if (!empty($ban_ids))
512
		$smcFunc['db_query']('', '
513
			UPDATE {db_prefix}ban_items
514
			SET hits = hits + 1
515
			WHERE id_ban IN ({array_int:ban_ids})',
516
			array(
517
				'ban_ids' => $ban_ids,
518
			)
519
		);
520
}
521
522
/**
523
 * Checks if a given email address might be banned.
524
 * Check if a given email is banned.
525
 * Performs an immediate ban if the turns turns out positive.
526
 *
527
 * @param string $email The email to check
528
 * @param string $restriction What type of restriction (cannot_post, cannot_register, etc.)
529
 * @param string $error The error message to display if they are indeed banned
530
 */
531
function isBannedEmail($email, $restriction, $error)
532
{
533
	global $txt, $smcFunc;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
534
535
	// Can't ban an empty email
536
	if (empty($email) || trim($email) == '')
537
		return;
538
539
	// Let's start with the bans based on your IP/hostname/memberID...
540
	$ban_ids = isset($_SESSION['ban'][$restriction]) ? $_SESSION['ban'][$restriction]['ids'] : array();
541
	$ban_reason = isset($_SESSION['ban'][$restriction]) ? $_SESSION['ban'][$restriction]['reason'] : '';
542
543
	// ...and add to that the email address you're trying to register.
544
	$request = $smcFunc['db_query']('', '
545
		SELECT bi.id_ban, bg.' . $restriction . ', bg.cannot_access, bg.reason
546
		FROM {db_prefix}ban_items AS bi
547
			INNER JOIN {db_prefix}ban_groups AS bg ON (bg.id_ban_group = bi.id_ban_group)
548
		WHERE {string:email} LIKE bi.email_address
549
			AND (bg.' . $restriction . ' = {int:cannot_access} OR bg.cannot_access = {int:cannot_access})
550
			AND (bg.expire_time IS NULL OR bg.expire_time >= {int:now})',
551
		array(
552
			'email' => $email,
553
			'cannot_access' => 1,
554
			'now' => time(),
555
		)
556
	);
557
	while ($row = $smcFunc['db_fetch_assoc']($request))
558
	{
559
		if (!empty($row['cannot_access']))
560
		{
561
			$_SESSION['ban']['cannot_access']['ids'][] = $row['id_ban'];
562
			$_SESSION['ban']['cannot_access']['reason'] = $row['reason'];
563
		}
564
		if (!empty($row[$restriction]))
565
		{
566
			$ban_ids[] = $row['id_ban'];
567
			$ban_reason = $row['reason'];
568
		}
569
	}
570
	$smcFunc['db_free_result']($request);
571
572
	// You're in biiig trouble.  Banned for the rest of this session!
573
	if (isset($_SESSION['ban']['cannot_access']))
574
	{
575
		log_ban($_SESSION['ban']['cannot_access']['ids']);
576
		$_SESSION['ban']['last_checked'] = time();
577
578
		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...
579
	}
580
581
	if (!empty($ban_ids))
582
	{
583
		// Log this ban for future reference.
584
		log_ban($ban_ids, $email);
585
		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...
586
	}
587
}
588
589
/**
590
 * Make sure the user's correct session was passed, and they came from here.
591
 * Checks the current session, verifying that the person is who he or she should be.
592
 * Also checks the referrer to make sure they didn't get sent here.
593
 * Depends on the disableCheckUA setting, which is usually missing.
594
 * Will check GET, POST, or REQUEST depending on the passed type.
595
 * Also optionally checks the referring action if passed. (note that the referring action must be by GET.)
596
 *
597
 * @param string $type The type of check (post, get, request)
598
 * @param string $from_action The action this is coming from
599
 * @param bool $is_fatal Whether to die with a fatal error if the check fails
600
 * @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...
601
 */
602
function checkSession($type = 'post', $from_action = '', $is_fatal = true)
603
{
604
	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...
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
605
606
	// 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...
607
	if ($type == 'post')
608
	{
609
		$check = isset($_POST[$_SESSION['session_var']]) ? $_POST[$_SESSION['session_var']] : (empty($modSettings['strictSessionCheck']) && isset($_POST['sc']) ? $_POST['sc'] : null);
610
		if ($check !== $sc)
611
			$error = 'session_timeout';
612
	}
613
614
	// 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...
615
	elseif ($type == 'get')
616
	{
617
		$check = isset($_GET[$_SESSION['session_var']]) ? $_GET[$_SESSION['session_var']] : (empty($modSettings['strictSessionCheck']) && isset($_GET['sesc']) ? $_GET['sesc'] : null);
618
		if ($check !== $sc)
619
			$error = 'session_verify_fail';
620
	}
621
622
	// Or can it be in either?
623
	elseif ($type == 'request')
624
	{
625
		$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)));
626
627
		if ($check !== $sc)
628
			$error = 'session_verify_fail';
629
	}
630
631
	// Verify that they aren't changing user agents on us - that could be bad.
632
	if ((!isset($_SESSION['USER_AGENT']) || $_SESSION['USER_AGENT'] != $_SERVER['HTTP_USER_AGENT']) && empty($modSettings['disableCheckUA']))
633
		$error = 'session_verify_fail';
634
635
	// Make sure a page with session check requirement is not being prefetched.
636 View Code Duplication
	if (isset($_SERVER['HTTP_X_MOZ']) && $_SERVER['HTTP_X_MOZ'] == 'prefetch')
637
	{
638
		ob_end_clean();
639
		header('HTTP/1.1 403 Forbidden');
640
		die;
641
	}
642
643
	// Check the referring site - it should be the same server at least!
644
	if (isset($_SESSION['request_referer']))
645
		$referrer = $_SESSION['request_referer'];
646
	else
647
		$referrer = isset($_SERVER['HTTP_REFERER']) ? @parse_url($_SERVER['HTTP_REFERER']) : array();
648
	if (!empty($referrer['host']))
649
	{
650
		if (strpos($_SERVER['HTTP_HOST'], ':') !== false)
651
			$real_host = substr($_SERVER['HTTP_HOST'], 0, strpos($_SERVER['HTTP_HOST'], ':'));
652
		else
653
			$real_host = $_SERVER['HTTP_HOST'];
654
655
		$parsed_url = parse_url($boardurl);
656
657
		// Are global cookies on?  If so, let's check them ;).
658
		if (!empty($modSettings['globalCookies']))
659
		{
660
			if (preg_match('~(?:[^\.]+\.)?([^\.]{3,}\..+)\z~i', $parsed_url['host'], $parts) == 1)
661
				$parsed_url['host'] = $parts[1];
662
663
			if (preg_match('~(?:[^\.]+\.)?([^\.]{3,}\..+)\z~i', $referrer['host'], $parts) == 1)
664
				$referrer['host'] = $parts[1];
665
666
			if (preg_match('~(?:[^\.]+\.)?([^\.]{3,}\..+)\z~i', $real_host, $parts) == 1)
667
				$real_host = $parts[1];
668
		}
669
670
		// Okay: referrer must either match parsed_url or real_host.
671
		if (isset($parsed_url['host']) && strtolower($referrer['host']) != strtolower($parsed_url['host']) && strtolower($referrer['host']) != strtolower($real_host))
672
		{
673
			$error = 'verify_url_fail';
674
			$log_error = true;
675
		}
676
	}
677
678
	// Well, first of all, if a from_action is specified you'd better have an old_url.
679
	if (!empty($from_action) && (!isset($_SESSION['old_url']) || preg_match('~[?;&]action=' . $from_action . '([;&]|$)~', $_SESSION['old_url']) == 0))
680
	{
681
		$error = 'verify_url_fail';
682
		$log_error = true;
683
	}
684
685
	if (strtolower($_SERVER['HTTP_USER_AGENT']) == 'hacker')
686
		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...
687
688
	// Everything is ok, return an empty string.
689
	if (!isset($error))
690
		return '';
691
	// A session error occurred, show the error.
692
	elseif ($is_fatal)
693
	{
694
		if (isset($_GET['xml']))
695
		{
696
			ob_end_clean();
697
			header('HTTP/1.1 403 Forbidden - Session timeout');
698
			die;
699
		}
700
		else
701
			fatal_lang_error($error, isset($log_error) ? 'user' : false);
702
	}
703
	// A session error occurred, return the error to the calling function.
704
	else
705
		return $error;
706
707
	// We really should never fall through here, for very important reasons.  Let's make sure.
708
	trigger_error('Hacking attempt...', E_USER_ERROR);
709
}
710
711
/**
712
 * Check if a specific confirm parameter was given.
713
 *
714
 * @param string $action The action we want to check against
715
 * @return bool|string True if the check passed or a token
716
 */
717
function checkConfirm($action)
718
{
719
	global $modSettings;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
720
721
	if (isset($_GET['confirm']) && isset($_SESSION['confirm_' . $action]) && md5($_GET['confirm'] . $_SERVER['HTTP_USER_AGENT']) == $_SESSION['confirm_' . $action])
722
		return true;
723
724
	else
725
	{
726
		$token = md5(mt_rand() . session_id() . (string) microtime() . $modSettings['rand_seed']);
727
		$_SESSION['confirm_' . $action] = md5($token . $_SERVER['HTTP_USER_AGENT']);
728
729
		return $token;
730
	}
731
}
732
733
/**
734
 * Lets give you a token of our appreciation.
735
 *
736
 * @param string $action The action to create the token for
737
 * @param string $type The type of token ('post', 'get' or 'request')
738
 * @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...
739
 */
740
function createToken($action, $type = 'post')
741
{
742
	global $modSettings, $context;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
743
744
	$token = md5(mt_rand() . session_id() . (string) microtime() . $modSettings['rand_seed'] . $type);
745
	$token_var = substr(preg_replace('~^\d+~', '', md5(mt_rand() . (string) microtime() . mt_rand())), 0, mt_rand(7, 12));
746
747
	$_SESSION['token'][$type . '-' . $action] = array($token_var, md5($token . $_SERVER['HTTP_USER_AGENT']), time(), $token);
748
749
	$context[$action . '_token'] = $token;
750
	$context[$action . '_token_var'] = $token_var;
751
752
	return array($action . '_token_var' => $token_var, $action . '_token' => $token);
753
}
754
755
/**
756
 * Only patrons with valid tokens can ride this ride.
757
 *
758
 * @param string $action The action to validate the token for
759
 * @param string $type The type of request (get, request, or post)
760
 * @param bool $reset Whether to reset the token and display an error if validation fails
761
 * @return bool|string If the action is login, returns the token for the action, otherwise returns whether the validation was successful
762
 */
763
function validateToken($action, $type = 'post', $reset = true)
764
{
765
	$type = $type == 'get' || $type == 'request' ? $type : 'post';
766
767
	// Logins are special: the token is used to has the password with javascript before POST it
768
	if ($action == 'login')
769
	{
770
		if (isset($_SESSION['token'][$type . '-' . $action]))
771
		{
772
			$return = $_SESSION['token'][$type . '-' . $action][3];
773
			unset($_SESSION['token'][$type . '-' . $action]);
774
			return $return;
775
		}
776
		else
777
			return '';
778
	}
779
780
	// This nasty piece of code validates a token.
781
	/*
782
		1. The token exists in session.
783
		2. The {$type} variable should exist.
784
		3. We concat the variable we received with the user agent
785
		4. Match that result against what is in the session.
786
		5. If it matches, success, otherwise we fallout.
787
	*/
788
	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])
789
	{
790
		// Invalidate this token now.
791
		unset($_SESSION['token'][$type . '-' . $action]);
792
793
		return true;
794
	}
795
796
	// Patrons with invalid tokens get the boot.
797
	if ($reset)
798
	{
799
		// Might as well do some cleanup on this.
800
		cleanTokens();
801
802
		// I'm back baby.
803
		createToken($action, $type);
804
805
		fatal_lang_error('token_verify_fail', false);
806
	}
807
	// Remove this token as its useless
808
	else
809
		unset($_SESSION['token'][$type . '-' . $action]);
810
811
	// Randomly check if we should remove some older tokens.
812
	if (mt_rand(0, 138) == 23)
813
		cleanTokens();
814
815
	return false;
816
}
817
818
/**
819
 * Removes old unused tokens from session
820
 * defaults to 3 hours before a token is considered expired
821
 * if $complete = true will remove all tokens
822
 *
823
 * @param bool $complete Whether to remove all tokens or only expired ones
824
 */
825
function cleanTokens($complete = false)
826
{
827
	// We appreciate cleaning up after yourselves.
828
	if (!isset($_SESSION['token']))
829
		return;
830
831
	// Clean up tokens, trying to give enough time still.
832
	foreach ($_SESSION['token'] as $key => $data)
833
		if ($data[2] + 10800 < time() || $complete)
834
			unset($_SESSION['token'][$key]);
835
}
836
837
/**
838
 * Check whether a form has been submitted twice.
839
 * Registers a sequence number for a form.
840
 * Checks whether a submitted sequence number is registered in the current session.
841
 * Depending on the value of is_fatal shows an error or returns true or false.
842
 * Frees a sequence number from the stack after it's been checked.
843
 * Frees a sequence number without checking if action == 'free'.
844
 *
845
 * @param string $action The action - can be 'register', 'check' or 'free'
846
 * @param bool $is_fatal Whether to die with a fatal error
847
 * @return void|bool If the action isn't check, returns nothing, otherwise returns whether the check was successful
848
 */
849
function checkSubmitOnce($action, $is_fatal = true)
850
{
851
	global $context;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
852
853
	if (!isset($_SESSION['forms']))
854
		$_SESSION['forms'] = array();
855
856
	// Register a form number and store it in the session stack. (use this on the page that has the form.)
857
	if ($action == 'register')
858
	{
859
		$context['form_sequence_number'] = 0;
860
		while (empty($context['form_sequence_number']) || in_array($context['form_sequence_number'], $_SESSION['forms']))
861
			$context['form_sequence_number'] = mt_rand(1, 16000000);
862
	}
863
	// Check whether the submitted number can be found in the session.
864
	elseif ($action == 'check')
865
	{
866
		if (!isset($_REQUEST['seqnum']))
867
			return true;
868
		elseif (!in_array($_REQUEST['seqnum'], $_SESSION['forms']))
869
		{
870
			$_SESSION['forms'][] = (int) $_REQUEST['seqnum'];
871
			return true;
872
		}
873
		elseif ($is_fatal)
874
			fatal_lang_error('error_form_already_submitted', false);
875
		else
876
			return false;
877
	}
878
	// Don't check, just free the stack number.
879
	elseif ($action == 'free' && isset($_REQUEST['seqnum']) && in_array($_REQUEST['seqnum'], $_SESSION['forms']))
880
		$_SESSION['forms'] = array_diff($_SESSION['forms'], array($_REQUEST['seqnum']));
881
	elseif ($action != 'free')
882
		trigger_error('checkSubmitOnce(): Invalid action \'' . $action . '\'', E_USER_WARNING);
883
}
884
885
/**
886
 * Check the user's permissions.
887
 * checks whether the user is allowed to do permission. (ie. post_new.)
888
 * If boards is specified, checks those boards instead of the current one.
889
 * If any is true, will return true if the user has the permission on any of the specified boards
890
 * Always returns true if the user is an administrator.
891
 *
892
 * @param string|array $permission A single permission to check or an array of permissions to check
893
 * @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...
894
 * @param bool $any Whether to check for permission on at least one board instead of all boards
895
 * @return bool Whether the user has the specified permission
896
 */
897
function allowedTo($permission, $boards = null, $any = false)
898
{
899
	global $user_info, $smcFunc;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
900
901
	// You're always allowed to do nothing. (unless you're a working man, MR. LAZY :P!)
902
	if (empty($permission))
903
		return true;
904
905
	// You're never allowed to do something if your data hasn't been loaded yet!
906
	if (empty($user_info))
907
		return false;
908
909
	// Administrators are supermen :P.
910
	if ($user_info['is_admin'])
911
		return true;
912
913
	// Let's ensure this is an array.
914
	$permission = (array) $permission;
915
916
	// Are we checking the _current_ board, or some other boards?
917
	if ($boards === null)
918
	{
919
		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...
920
			return true;
921
		// You aren't allowed, by default.
922
		else
923
			return false;
924
	}
925
	elseif (!is_array($boards))
926
		$boards = array($boards);
927
928
	$request = $smcFunc['db_query']('', '
929
		SELECT MIN(bp.add_deny) AS add_deny
930
		FROM {db_prefix}boards AS b
931
			INNER JOIN {db_prefix}board_permissions AS bp ON (bp.id_profile = b.id_profile)
932
			LEFT JOIN {db_prefix}moderators AS mods ON (mods.id_board = b.id_board AND mods.id_member = {int:current_member})
933
			LEFT JOIN {db_prefix}moderator_groups AS modgs ON (modgs.id_board = b.id_board AND modgs.id_group IN ({array_int:group_list}))
934
		WHERE b.id_board IN ({array_int:board_list})
935
			AND bp.id_group IN ({array_int:group_list}, {int:moderator_group})
936
			AND bp.permission IN ({array_string:permission_list})
937
			AND (mods.id_member IS NOT NULL OR modgs.id_group IS NOT NULL OR bp.id_group != {int:moderator_group})
938
		GROUP BY b.id_board',
939
		array(
940
			'current_member' => $user_info['id'],
941
			'board_list' => $boards,
942
			'group_list' => $user_info['groups'],
943
			'moderator_group' => 3,
944
			'permission_list' => $permission,
945
		)
946
	);
947
948
	if ($any)
949
	{
950
		$result = false;
951
		while ($row = $smcFunc['db_fetch_assoc']($request))
952
		{
953
			$result = !empty($row['add_deny']);
954
			if ($result == true)
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
955
				break;
956
		}
957
		$smcFunc['db_free_result']($request);
958
		return $result;
959
	}
960
961
	// Make sure they can do it on all of the boards.
962
	if ($smcFunc['db_num_rows']($request) != count($boards))
963
		return false;
964
965
	$result = true;
966
	while ($row = $smcFunc['db_fetch_assoc']($request))
967
		$result &= !empty($row['add_deny']);
968
	$smcFunc['db_free_result']($request);
969
970
	// If the query returned 1, they can do it... otherwise, they can't.
971
	return $result;
972
}
973
974
/**
975
 * Fatal error if they cannot.
976
 * Uses allowedTo() to check if the user is allowed to do permission.
977
 * Checks the passed boards or current board for the permission.
978
 * If $any is true, the user only needs permission on at least one of the boards to pass
979
 * If they are not, it loads the Errors language file and shows an error using $txt['cannot_' . $permission].
980
 * If they are a guest and cannot do it, this calls is_not_guest().
981
 *
982
 * @param string|array $permission A single permission to check or an array of permissions to check
983
 * @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...
984
 * @param bool $any Whether to check for permission on at least one board instead of all boards
985
 */
986
function isAllowedTo($permission, $boards = null, $any = false)
987
{
988
	global $user_info, $txt;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
989
990
	$heavy_permissions = array(
991
		'admin_forum',
992
		'manage_attachments',
993
		'manage_smileys',
994
		'manage_boards',
995
		'edit_news',
996
		'moderate_forum',
997
		'manage_bans',
998
		'manage_membergroups',
999
		'manage_permissions',
1000
	);
1001
1002
	// Make it an array, even if a string was passed.
1003
	$permission = (array) $permission;
1004
1005
	call_integration_hook('integrate_heavy_permissions_session', array(&$heavy_permissions));
1006
1007
	// Check the permission and return an error...
1008
	if (!allowedTo($permission, $boards, $any))
1009
	{
1010
		// Pick the last array entry as the permission shown as the error.
1011
		$error_permission = array_shift($permission);
1012
1013
		// If they are a guest, show a login. (because the error might be gone if they do!)
1014
		if ($user_info['is_guest'])
1015
		{
1016
			loadLanguage('Errors');
1017
			is_not_guest($txt['cannot_' . $error_permission]);
1018
		}
1019
1020
		// Clear the action because they aren't really doing that!
1021
		$_GET['action'] = '';
1022
		$_GET['board'] = '';
1023
		$_GET['topic'] = '';
1024
		writeLog(true);
1025
1026
		fatal_lang_error('cannot_' . $error_permission, false);
1027
1028
		// Getting this far is a really big problem, but let's try our best to prevent any cases...
1029
		trigger_error('Hacking attempt...', E_USER_ERROR);
1030
	}
1031
1032
	// If you're doing something on behalf of some "heavy" permissions, validate your session.
1033
	// (take out the heavy permissions, and if you can't do anything but those, you need a validated session.)
1034
	if (!allowedTo(array_diff($permission, $heavy_permissions), $boards))
1035
		validateSession();
1036
}
1037
1038
/**
1039
 * Return the boards a user has a certain (board) permission on. (array(0) if all.)
1040
 *  - returns a list of boards on which the user is allowed to do the specified permission.
1041
 *  - returns an array with only a 0 in it if the user has permission to do this on every board.
1042
 *  - returns an empty array if he or she cannot do this on any board.
1043
 * If check_access is true will also make sure the group has proper access to that board.
1044
 *
1045
 * @param string|array $permissions A single permission to check or an array of permissions to check
1046
 * @param bool $check_access Whether to check only the boards the user has access to
1047
 * @param bool $simple Whether to return a simple array of board IDs or one with permissions as the keys
1048
 * @return array An array of board IDs or an array containing 'permission' => 'board,board2,...' pairs
1049
 */
1050
function boardsAllowedTo($permissions, $check_access = true, $simple = true)
1051
{
1052
	global $user_info, $smcFunc;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
1053
1054
	// Arrays are nice, most of the time.
1055
	$permissions = (array) $permissions;
1056
1057
	/*
1058
	 * Set $simple to true to use this function as it were in SMF 2.0.x.
1059
	 * Otherwise, the resultant array becomes split into the multiple
1060
	 * permissions that were passed. Other than that, it's just the normal
1061
	 * state of play that you're used to.
1062
	 */
1063
1064
	// Administrators are all powerful, sorry.
1065
	if ($user_info['is_admin'])
1066
	{
1067
		if ($simple)
1068
			return array(0);
1069
		else
1070
		{
1071
			$boards = array();
1072
			foreach ($permissions as $permission)
1073
				$boards[$permission] = array(0);
1074
1075
			return $boards;
1076
		}
1077
	}
1078
1079
	// All groups the user is in except 'moderator'.
1080
	$groups = array_diff($user_info['groups'], array(3));
1081
1082
	$request = $smcFunc['db_query']('', '
1083
		SELECT b.id_board, bp.add_deny' . ($simple ? '' : ', bp.permission') . '
1084
		FROM {db_prefix}board_permissions AS bp
1085
			INNER JOIN {db_prefix}boards AS b ON (b.id_profile = bp.id_profile)
1086
			LEFT JOIN {db_prefix}moderators AS mods ON (mods.id_board = b.id_board AND mods.id_member = {int:current_member})
1087
			LEFT JOIN {db_prefix}moderator_groups AS modgs ON (modgs.id_board = b.id_board AND modgs.id_group IN ({array_int:group_list}))
1088
		WHERE bp.id_group IN ({array_int:group_list}, {int:moderator_group})
1089
			AND bp.permission IN ({array_string:permissions})
1090
			AND (mods.id_member IS NOT NULL OR modgs.id_group IS NOT NULL OR bp.id_group != {int:moderator_group})' .
1091
			($check_access ? ' AND {query_see_board}' : ''),
1092
		array(
1093
			'current_member' => $user_info['id'],
1094
			'group_list' => $groups,
1095
			'moderator_group' => 3,
1096
			'permissions' => $permissions,
1097
		)
1098
	);
1099
	$boards = array();
1100
	$deny_boards = array();
1101
	while ($row = $smcFunc['db_fetch_assoc']($request))
1102
	{
1103
		if ($simple)
1104
		{
1105
			if (empty($row['add_deny']))
1106
				$deny_boards[] = $row['id_board'];
1107
			else
1108
				$boards[] = $row['id_board'];
1109
		}
1110
		else
1111
		{
1112
			if (empty($row['add_deny']))
1113
				$deny_boards[$row['permission']][] = $row['id_board'];
1114
			else
1115
				$boards[$row['permission']][] = $row['id_board'];
1116
		}
1117
	}
1118
	$smcFunc['db_free_result']($request);
1119
1120
	if ($simple)
1121
		$boards = array_unique(array_values(array_diff($boards, $deny_boards)));
1122
	else
1123
	{
1124
		foreach ($permissions as $permission)
1125
		{
1126
			// never had it to start with
1127
			if (empty($boards[$permission]))
1128
				$boards[$permission] = array();
1129
			else
1130
			{
1131
				// Or it may have been removed
1132
				$deny_boards[$permission] = isset($deny_boards[$permission]) ? $deny_boards[$permission] : array();
1133
				$boards[$permission] = array_unique(array_values(array_diff($boards[$permission], $deny_boards[$permission])));
1134
			}
1135
		}
1136
	}
1137
1138
	return $boards;
1139
}
1140
1141
/**
1142
 * This function attempts to protect from spammed messages and the like.
1143
 * The time taken depends on error_type - generally uses the modSetting.
1144
 *
1145
 * @param string $error_type The error type. Also used as a $txt index (not an actual string).
1146
 * @param boolean $only_return_result Whether you want the function to die with a fatal_lang_error.
1147
 * @return bool Whether they've posted within the limit
1148
 */
1149
function spamProtection($error_type, $only_return_result = false)
1150
{
1151
	global $modSettings, $user_info, $smcFunc;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
1152
1153
	// Certain types take less/more time.
1154
	$timeOverrides = array(
1155
		'login' => 2,
1156
		'register' => 2,
1157
		'remind' => 30,
1158
		'sendmail' => $modSettings['spamWaitTime'] * 5,
1159
		'reporttm' => $modSettings['spamWaitTime'] * 4,
1160
		'search' => !empty($modSettings['search_floodcontrol_time']) ? $modSettings['search_floodcontrol_time'] : 1,
1161
	);
1162
1163
1164
	// Moderators are free...
1165
	if (!allowedTo('moderate_board'))
1166
		$timeLimit = isset($timeOverrides[$error_type]) ? $timeOverrides[$error_type] : $modSettings['spamWaitTime'];
1167
	else
1168
		$timeLimit = 2;
1169
1170
	call_integration_hook('integrate_spam_protection', array(&$timeOverrides, &$timeLimit));
1171
1172
	// Delete old entries...
1173
	$smcFunc['db_query']('', '
1174
		DELETE FROM {db_prefix}log_floodcontrol
1175
		WHERE log_time < {int:log_time}
1176
			AND log_type = {string:log_type}',
1177
		array(
1178
			'log_time' => time() - $timeLimit,
1179
			'log_type' => $error_type,
1180
		)
1181
	);
1182
1183
	// Add a new entry, deleting the old if necessary.
1184
	$smcFunc['db_insert']('replace',
1185
		'{db_prefix}log_floodcontrol',
1186
		array('ip' => 'inet', 'log_time' => 'int', 'log_type' => 'string'),
1187
		array($user_info['ip'], time(), $error_type),
1188
		array('ip', 'log_type')
1189
	);
1190
1191
	// If affected is 0 or 2, it was there already.
1192
	if ($smcFunc['db_affected_rows']() != 1)
1193
	{
1194
		// Spammer!  You only have to wait a *few* seconds!
1195
		if (!$only_return_result)
1196
			fatal_lang_error($error_type . '_WaitTime_broken', false, array($timeLimit));
1197
1198
		return true;
1199
	}
1200
1201
	// They haven't posted within the limit.
1202
	return false;
1203
}
1204
1205
/**
1206
 * A generic function to create a pair of index.php and .htaccess files in a directory
1207
 *
1208
 * @param string $path The (absolute) directory path
1209
 * @param boolean $attachments Whether this is an attachment directory
1210
 * @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...
1211
 */
1212
function secureDirectory($path, $attachments = false)
1213
{
1214
	if (empty($path))
1215
		return 'empty_path';
1216
1217
	if (!is_writable($path))
1218
		return 'path_not_writable';
1219
1220
	$directoryname = basename($path);
1221
1222
	$errors = array();
1223
	$close = empty($attachments) ? '
1224
</Files>' : '
1225
	Allow from localhost
1226
</Files>
1227
1228
RemoveHandler .php .php3 .phtml .cgi .fcgi .pl .fpl .shtml';
1229
1230
	if (file_exists($path . '/.htaccess'))
1231
		$errors[] = 'htaccess_exists';
1232
	else
1233
	{
1234
		$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...
1235
		if ($fh)
1236
		{
1237
			fwrite($fh, '<Files *>
1238
	Order Deny,Allow
1239
	Deny from all' . $close);
1240
			fclose($fh);
1241
		}
1242
		$errors[] = 'htaccess_cannot_create_file';
1243
	}
1244
1245
	if (file_exists($path . '/index.php'))
1246
		$errors[] = 'index-php_exists';
1247
	else
1248
	{
1249
		$fh = @fopen($path . '/index.php', 'w');
1250
		if ($fh)
1251
		{
1252
			fwrite($fh, '<' . '?php
1253
1254
/**
1255
 * This file is here solely to protect your ' . $directoryname . ' directory.
1256
 */
1257
1258
// Look for Settings.php....
1259
if (file_exists(dirname(dirname(__FILE__)) . \'/Settings.php\'))
1260
{
1261
	// Found it!
1262
	require(dirname(dirname(__FILE__)) . \'/Settings.php\');
1263
	header(\'location: \' . $boardurl);
1264
}
1265
// Can\'t find it... just forget it.
1266
else
1267
	exit;
1268
1269
?'. '>');
1270
			fclose($fh);
1271
		}
1272
		$errors[] = 'index-php_cannot_create_file';
1273
	}
1274
1275
	if (!empty($errors))
1276
		return $errors;
1277
	else
1278
		return true;
1279
}
1280
1281
/**
1282
* This sets the X-Frame-Options header.
1283
*
1284
* @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...
1285
* @since 2.1
1286
*/
1287
function frameOptionsHeader($override = null)
1288
{
1289
	global $modSettings;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
1290
1291
	$option = 'SAMEORIGIN';
1292
	if (is_null($override) && !empty($modSettings['frame_security']))
1293
		$option = $modSettings['frame_security'];
1294
	elseif (in_array($override, array('SAMEORIGIN', 'DENY')))
1295
		$option = $override;
1296
1297
	// Don't bother setting the header if we have disabled it.
1298
	if ($option == 'DISABLE')
1299
		return;
1300
1301
	// Finally set it.
1302
	header('x-frame-options: ' . $option);
1303
1304
	// And some other useful ones.
1305
	header('x-xss-protection: 1');
1306
	header('x-content-type-options: nosniff');
1307
}
1308
1309
?>