Passed
Pull Request — release-2.1 (#6592)
by Jeremy
06:46 queued 01:14
created

corsPolicyHeader()   F

Complexity

Conditions 33
Paths 1441

Size

Total Lines 96
Code Lines 44

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 33
eloc 44
nc 1441
nop 1
dl 0
loc 96
rs 0
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 https://www.simplemachines.org
11
 * @copyright 2021 Simple Machines and individual contributors
12
 * @license https://www.simplemachines.org/about/smf/license.php BSD
13
 *
14
 * @version 2.1 RC3
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
 * @param string $force When true, require a password even if we normally wouldn't
28
 * @return void|string Returns 'session_verify_fail' if verification failed
29
 */
30
function validateSession($type = 'admin', $force = false)
31
{
32
	global $modSettings, $sourcedir, $user_info;
33
34
	// We don't care if the option is off, because Guests should NEVER get past here.
35
	is_not_guest();
36
37
	// Validate what type of session check this is.
38
	$types = array();
39
	call_integration_hook('integrate_validateSession', array(&$types));
40
	$type = in_array($type, $types) || $type == 'moderate' ? $type : 'admin';
41
42
	// If we're using XML give an additional ten minutes grace as an admin can't log on in XML mode.
43
	$refreshTime = isset($_GET['xml']) ? 4200 : 3600;
44
45
	if (empty($force))
46
	{
47
		// Is the security option off?
48
		if (!empty($modSettings['securityDisable' . ($type != 'admin' ? '_' . $type : '')]))
49
			return;
50
51
		// Or are they already logged in?, Moderator or admin session is need for this area
52
		if ((!empty($_SESSION[$type . '_time']) && $_SESSION[$type . '_time'] + $refreshTime >= time()) || (!empty($_SESSION['admin_time']) && $_SESSION['admin_time'] + $refreshTime >= time()))
53
			return;
54
	}
55
56
	require_once($sourcedir . '/Subs-Auth.php');
57
58
	// Posting the password... check it.
59
	if (isset($_POST[$type . '_pass']))
60
	{
61
		// Check to ensure we're forcing SSL for authentication
62
		if (!empty($modSettings['force_ssl']) && empty($maintenance) && !httpsOn())
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $maintenance seems to never exist and therefore empty should always be true.
Loading history...
63
			fatal_lang_error('login_ssl_required');
64
65
		checkSession();
66
67
		$good_password = in_array(true, call_integration_hook('integrate_verify_password', array($user_info['username'], $_POST[$type . '_pass'], false)), true);
68
69
		// Password correct?
70
		if ($good_password || hash_verify_password($user_info['username'], $_POST[$type . '_pass'], $user_info['passwd']))
71
		{
72
			$_SESSION[$type . '_time'] = time();
73
			unset($_SESSION['request_referer']);
74
			return;
75
		}
76
	}
77
78
	// Better be sure to remember the real referer
79
	if (empty($_SESSION['request_referer']))
80
		$_SESSION['request_referer'] = isset($_SERVER['HTTP_REFERER']) ? @parse_url($_SERVER['HTTP_REFERER']) : array();
81
	elseif (empty($_POST))
82
		unset($_SESSION['request_referer']);
83
84
	// Need to type in a password for that, man.
85
	if (!isset($_GET['xml']))
86
		adminLogin($type);
87
	else
88
		return 'session_verify_fail';
89
}
90
91
/**
92
 * Require a user who is logged in. (not a guest.)
93
 * Checks if the user is currently a guest, and if so asks them to login with a message telling them why.
94
 * Message is what to tell them when asking them to login.
95
 *
96
 * @param string $message The message to display to the guest
97
 */
98
function is_not_guest($message = '')
99
{
100
	global $user_info, $txt, $context, $scripturl, $modSettings;
101
102
	// Luckily, this person isn't a guest.
103
	if (!$user_info['is_guest'])
104
		return;
105
106
	// Log what they were trying to do didn't work)
107
	if (!empty($modSettings['who_enabled']))
108
		$_GET['error'] = 'guest_login';
109
	writeLog(true);
110
111
	// Just die.
112
	if (isset($_REQUEST['xml']))
113
		obExit(false);
114
115
	// Attempt to detect if they came from dlattach.
116
	if (SMF != 'SSI' && empty($context['theme_loaded']))
0 ignored issues
show
introduced by
The condition SMF != 'SSI' is always false.
Loading history...
117
		loadTheme();
118
119
	// Never redirect to an attachment
120
	if (strpos($_SERVER['REQUEST_URL'], 'dlattach') === false)
121
		$_SESSION['login_url'] = $_SERVER['REQUEST_URL'];
122
123
	// Load the Login template and language file.
124
	loadLanguage('Login');
125
126
	// Apparently we're not in a position to handle this now. Let's go to a safer location for now.
127
	if (empty($context['template_layers']))
128
	{
129
		$_SESSION['login_url'] = $scripturl . '?' . $_SERVER['QUERY_STRING'];
130
		redirectexit('action=login');
131
	}
132
	else
133
	{
134
		loadTemplate('Login');
135
		$context['sub_template'] = 'kick_guest';
136
		$context['robot_no_index'] = true;
137
	}
138
139
	// Use the kick_guest sub template...
140
	$context['kick_message'] = $message;
141
	$context['page_title'] = $txt['login'];
142
143
	obExit();
144
145
	// We should never get to this point, but if we did we wouldn't know the user isn't a guest.
146
	trigger_error('Hacking attempt...', E_USER_ERROR);
147
}
148
149
/**
150
 * Do banning related stuff.  (ie. disallow access....)
151
 * Checks if the user is banned, and if so dies with an error.
152
 * Caches this information for optimization purposes.
153
 *
154
 * @param bool $forceCheck Whether to force a recheck
155
 */
156
function is_not_banned($forceCheck = false)
157
{
158
	global $txt, $modSettings, $context, $user_info;
159
	global $sourcedir, $cookiename, $user_settings, $smcFunc;
160
161
	// You cannot be banned if you are an admin - doesn't help if you log out.
162
	if ($user_info['is_admin'])
163
		return;
164
165
	// Only check the ban every so often. (to reduce load.)
166
	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']))
167
	{
168
		// Innocent until proven guilty.  (but we know you are! :P)
169
		$_SESSION['ban'] = array(
170
			'last_checked' => time(),
171
			'id_member' => $user_info['id'],
172
			'ip' => $user_info['ip'],
173
			'ip2' => $user_info['ip2'],
174
			'email' => $user_info['email'],
175
		);
176
177
		$ban_query = array();
178
		$ban_query_vars = array('current_time' => time());
179
		$flag_is_activated = false;
180
181
		// Check both IP addresses.
182
		foreach (array('ip', 'ip2') as $ip_number)
183
		{
184
			if ($ip_number == 'ip2' && $user_info['ip2'] == $user_info['ip'])
185
				continue;
186
			$ban_query[] = ' {inet:' . $ip_number . '} BETWEEN bi.ip_low and bi.ip_high';
187
			$ban_query_vars[$ip_number] = $user_info[$ip_number];
188
			// IP was valid, maybe there's also a hostname...
189
			if (empty($modSettings['disableHostnameLookup']) && $user_info[$ip_number] != 'unknown')
190
			{
191
				$hostname = host_from_ip($user_info[$ip_number]);
192
				if (strlen($hostname) > 0)
193
				{
194
					$ban_query[] = '({string:hostname' . $ip_number . '} LIKE bi.hostname)';
195
					$ban_query_vars['hostname' . $ip_number] = $hostname;
196
				}
197
			}
198
		}
199
200
		// Is their email address banned?
201
		if (strlen($user_info['email']) != 0)
202
		{
203
			$ban_query[] = '({string:email} LIKE bi.email_address)';
204
			$ban_query_vars['email'] = $user_info['email'];
205
		}
206
207
		// How about this user?
208
		if (!$user_info['is_guest'] && !empty($user_info['id']))
209
		{
210
			$ban_query[] = 'bi.id_member = {int:id_member}';
211
			$ban_query_vars['id_member'] = $user_info['id'];
212
		}
213
214
		// Check the ban, if there's information.
215
		if (!empty($ban_query))
216
		{
217
			$restrictions = array(
218
				'cannot_access',
219
				'cannot_login',
220
				'cannot_post',
221
				'cannot_register',
222
			);
223
			$request = $smcFunc['db_query']('', '
224
				SELECT bi.id_ban, bi.email_address, bi.id_member, bg.cannot_access, bg.cannot_register,
225
					bg.cannot_post, bg.cannot_login, bg.reason, COALESCE(bg.expire_time, 0) AS expire_time
226
				FROM {db_prefix}ban_items AS bi
227
					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}))
228
				WHERE
229
					(' . implode(' OR ', $ban_query) . ')',
230
				$ban_query_vars
231
			);
232
			// Store every type of ban that applies to you in your session.
233
			while ($row = $smcFunc['db_fetch_assoc']($request))
234
			{
235
				foreach ($restrictions as $restriction)
236
					if (!empty($row[$restriction]))
237
					{
238
						$_SESSION['ban'][$restriction]['reason'] = $row['reason'];
239
						$_SESSION['ban'][$restriction]['ids'][] = $row['id_ban'];
240
						if (!isset($_SESSION['ban']['expire_time']) || ($_SESSION['ban']['expire_time'] != 0 && ($row['expire_time'] == 0 || $row['expire_time'] > $_SESSION['ban']['expire_time'])))
241
							$_SESSION['ban']['expire_time'] = $row['expire_time'];
242
243
						if (!$user_info['is_guest'] && $restriction == 'cannot_access' && ($row['id_member'] == $user_info['id'] || $row['email_address'] == $user_info['email']))
244
							$flag_is_activated = true;
245
					}
246
			}
247
			$smcFunc['db_free_result']($request);
248
		}
249
250
		// Mark the cannot_access and cannot_post bans as being 'hit'.
251
		if (isset($_SESSION['ban']['cannot_access']) || isset($_SESSION['ban']['cannot_post']) || isset($_SESSION['ban']['cannot_login']))
252
			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()));
253
254
		// If for whatever reason the is_activated flag seems wrong, do a little work to clear it up.
255
		if ($user_info['id'] && (($user_settings['is_activated'] >= 10 && !$flag_is_activated)
256
			|| ($user_settings['is_activated'] < 10 && $flag_is_activated)))
257
		{
258
			require_once($sourcedir . '/ManageBans.php');
259
			updateBanMembers();
260
		}
261
	}
262
263
	// Hey, I know you! You're ehm...
264
	if (!isset($_SESSION['ban']['cannot_access']) && !empty($_COOKIE[$cookiename . '_']))
265
	{
266
		$bans = explode(',', $_COOKIE[$cookiename . '_']);
267
		foreach ($bans as $key => $value)
268
			$bans[$key] = (int) $value;
269
		$request = $smcFunc['db_query']('', '
270
			SELECT bi.id_ban, bg.reason, COALESCE(bg.expire_time, 0) AS expire_time
271
			FROM {db_prefix}ban_items AS bi
272
				INNER JOIN {db_prefix}ban_groups AS bg ON (bg.id_ban_group = bi.id_ban_group)
273
			WHERE bi.id_ban IN ({array_int:ban_list})
274
				AND (bg.expire_time IS NULL OR bg.expire_time > {int:current_time})
275
				AND bg.cannot_access = {int:cannot_access}
276
			LIMIT {int:limit}',
277
			array(
278
				'cannot_access' => 1,
279
				'ban_list' => $bans,
280
				'current_time' => time(),
281
				'limit' => count($bans),
282
			)
283
		);
284
		while ($row = $smcFunc['db_fetch_assoc']($request))
285
		{
286
			$_SESSION['ban']['cannot_access']['ids'][] = $row['id_ban'];
287
			$_SESSION['ban']['cannot_access']['reason'] = $row['reason'];
288
			$_SESSION['ban']['expire_time'] = $row['expire_time'];
289
		}
290
		$smcFunc['db_free_result']($request);
291
292
		// My mistake. Next time better.
293
		if (!isset($_SESSION['ban']['cannot_access']))
294
		{
295
			require_once($sourcedir . '/Subs-Auth.php');
296
			$cookie_url = url_parts(!empty($modSettings['localCookies']), !empty($modSettings['globalCookies']));
297
			smf_setcookie($cookiename . '_', '', time() - 3600, $cookie_url[1], $cookie_url[0], false, false);
298
		}
299
	}
300
301
	// If you're fully banned, it's end of the story for you.
302
	if (isset($_SESSION['ban']['cannot_access']))
303
	{
304
		// We don't wanna see you!
305
		if (!$user_info['is_guest'])
306
			$smcFunc['db_query']('', '
307
				DELETE FROM {db_prefix}log_online
308
				WHERE id_member = {int:current_member}',
309
				array(
310
					'current_member' => $user_info['id'],
311
				)
312
			);
313
314
		// 'Log' the user out.  Can't have any funny business... (save the name!)
315
		$old_name = isset($user_info['name']) && $user_info['name'] != '' ? $user_info['name'] : $txt['guest_title'];
316
		$user_info['name'] = '';
317
		$user_info['username'] = '';
318
		$user_info['is_guest'] = true;
319
		$user_info['is_admin'] = false;
320
		$user_info['permissions'] = array();
321
		$user_info['id'] = 0;
322
		$context['user'] = array(
323
			'id' => 0,
324
			'username' => '',
325
			'name' => $txt['guest_title'],
326
			'is_guest' => true,
327
			'is_logged' => false,
328
			'is_admin' => false,
329
			'is_mod' => false,
330
			'can_mod' => false,
331
			'language' => $user_info['language'],
332
		);
333
334
		// A goodbye present.
335
		require_once($sourcedir . '/Subs-Auth.php');
336
		require_once($sourcedir . '/LogInOut.php');
337
		$cookie_url = url_parts(!empty($modSettings['localCookies']), !empty($modSettings['globalCookies']));
338
		smf_setcookie($cookiename . '_', implode(',', $_SESSION['ban']['cannot_access']['ids']), time() + 3153600, $cookie_url[1], $cookie_url[0], false, false);
339
340
		// Don't scare anyone, now.
341
		$_GET['action'] = '';
342
		$_GET['board'] = '';
343
		$_GET['topic'] = '';
344
		writeLog(true);
345
		Logout(true, false);
346
347
		// You banned, sucka!
348
		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']), false, 403);
349
350
		// If we get here, something's gone wrong.... but let's try anyway.
351
		trigger_error('Hacking attempt...', E_USER_ERROR);
352
	}
353
	// You're not allowed to log in but yet you are. Let's fix that.
354
	elseif (isset($_SESSION['ban']['cannot_login']) && !$user_info['is_guest'])
355
	{
356
		// We don't wanna see you!
357
		$smcFunc['db_query']('', '
358
			DELETE FROM {db_prefix}log_online
359
			WHERE id_member = {int:current_member}',
360
			array(
361
				'current_member' => $user_info['id'],
362
			)
363
		);
364
365
		// 'Log' the user out.  Can't have any funny business... (save the name!)
366
		$old_name = isset($user_info['name']) && $user_info['name'] != '' ? $user_info['name'] : $txt['guest_title'];
367
		$user_info['name'] = '';
368
		$user_info['username'] = '';
369
		$user_info['is_guest'] = true;
370
		$user_info['is_admin'] = false;
371
		$user_info['permissions'] = array();
372
		$user_info['id'] = 0;
373
		$context['user'] = array(
374
			'id' => 0,
375
			'username' => '',
376
			'name' => $txt['guest_title'],
377
			'is_guest' => true,
378
			'is_logged' => false,
379
			'is_admin' => false,
380
			'is_mod' => false,
381
			'can_mod' => false,
382
			'language' => $user_info['language'],
383
		);
384
385
		// SMF's Wipe 'n Clean(r) erases all traces.
386
		$_GET['action'] = '';
387
		$_GET['board'] = '';
388
		$_GET['topic'] = '';
389
		writeLog(true);
390
391
		require_once($sourcedir . '/LogInOut.php');
392
		Logout(true, false);
393
394
		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'], false, 403);
395
	}
396
397
	// Fix up the banning permissions.
398
	if (isset($user_info['permissions']))
399
		banPermissions();
400
}
401
402
/**
403
 * Fix permissions according to ban status.
404
 * Applies any states of banning by removing permissions the user cannot have.
405
 */
406
function banPermissions()
407
{
408
	global $user_info, $sourcedir, $modSettings, $context;
409
410
	// Somehow they got here, at least take away all permissions...
411
	if (isset($_SESSION['ban']['cannot_access']))
412
		$user_info['permissions'] = array();
413
	// Okay, well, you can watch, but don't touch a thing.
414
	elseif (isset($_SESSION['ban']['cannot_post']) || (!empty($modSettings['warning_mute']) && $modSettings['warning_mute'] <= $user_info['warning']))
415
	{
416
		$denied_permissions = array(
417
			'pm_send',
418
			'calendar_post', 'calendar_edit_own', 'calendar_edit_any',
419
			'poll_post',
420
			'poll_add_own', 'poll_add_any',
421
			'poll_edit_own', 'poll_edit_any',
422
			'poll_lock_own', 'poll_lock_any',
423
			'poll_remove_own', 'poll_remove_any',
424
			'manage_attachments', 'manage_smileys', 'manage_boards', 'admin_forum', 'manage_permissions',
425
			'moderate_forum', 'manage_membergroups', 'manage_bans', 'send_mail', 'edit_news',
426
			'profile_identity_any', 'profile_extra_any', 'profile_title_any',
427
			'profile_forum_any', 'profile_other_any', 'profile_signature_any',
428
			'post_new', 'post_reply_own', 'post_reply_any',
429
			'delete_own', 'delete_any', 'delete_replies',
430
			'make_sticky',
431
			'merge_any', 'split_any',
432
			'modify_own', 'modify_any', 'modify_replies',
433
			'move_any',
434
			'lock_own', 'lock_any',
435
			'remove_own', 'remove_any',
436
			'post_unapproved_topics', 'post_unapproved_replies_own', 'post_unapproved_replies_any',
437
		);
438
		call_integration_hook('integrate_post_ban_permissions', array(&$denied_permissions));
439
		$user_info['permissions'] = array_diff($user_info['permissions'], $denied_permissions);
440
	}
441
	// Are they absolutely under moderation?
442
	elseif (!empty($modSettings['warning_moderate']) && $modSettings['warning_moderate'] <= $user_info['warning'])
443
	{
444
		// Work out what permissions should change...
445
		$permission_change = array(
446
			'post_new' => 'post_unapproved_topics',
447
			'post_reply_own' => 'post_unapproved_replies_own',
448
			'post_reply_any' => 'post_unapproved_replies_any',
449
			'post_attachment' => 'post_unapproved_attachments',
450
		);
451
		call_integration_hook('integrate_warn_permissions', array(&$permission_change));
452
		foreach ($permission_change as $old => $new)
453
		{
454
			if (!in_array($old, $user_info['permissions']))
455
				unset($permission_change[$old]);
456
			else
457
				$user_info['permissions'][] = $new;
458
		}
459
		$user_info['permissions'] = array_diff($user_info['permissions'], array_keys($permission_change));
460
	}
461
462
	// @todo Find a better place to call this? Needs to be after permissions loaded!
463
	// Finally, some bits we cache in the session because it saves queries.
464
	if (isset($_SESSION['mc']) && $_SESSION['mc']['time'] > $modSettings['settings_updated'] && $_SESSION['mc']['id'] == $user_info['id'])
465
		$user_info['mod_cache'] = $_SESSION['mc'];
466
	else
467
	{
468
		require_once($sourcedir . '/Subs-Auth.php');
469
		rebuildModCache();
470
	}
471
472
	// Now that we have the mod cache taken care of lets setup a cache for the number of mod reports still open
473
	if (isset($_SESSION['rc']['reports']) && isset($_SESSION['rc']['member_reports']) && $_SESSION['rc']['time'] > $modSettings['last_mod_report_action'] && $_SESSION['rc']['id'] == $user_info['id'])
474
	{
475
		$context['open_mod_reports'] = $_SESSION['rc']['reports'];
476
		$context['open_member_reports'] = $_SESSION['rc']['member_reports'];
477
	}
478
	elseif ($_SESSION['mc']['bq'] != '0=1')
479
	{
480
		require_once($sourcedir . '/Subs-ReportedContent.php');
481
		$context['open_mod_reports'] = recountOpenReports('posts');
482
		$context['open_member_reports'] = recountOpenReports('members');
483
	}
484
	else
485
	{
486
		$context['open_mod_reports'] = 0;
487
		$context['open_member_reports'] = 0;
488
	}
489
}
490
491
/**
492
 * Log a ban in the database.
493
 * Log the current user in the ban logs.
494
 * Increment the hit counters for the specified ban ID's (if any.)
495
 *
496
 * @param array $ban_ids The IDs of the bans
497
 * @param string $email The email address associated with the user that triggered this hit
498
 */
499
function log_ban($ban_ids = array(), $email = null)
500
{
501
	global $user_info, $smcFunc;
502
503
	// Don't log web accelerators, it's very confusing...
504
	if (isset($_SERVER['HTTP_X_MOZ']) && $_SERVER['HTTP_X_MOZ'] == 'prefetch')
505
		return;
506
507
	$smcFunc['db_insert']('',
508
		'{db_prefix}log_banned',
509
		array('id_member' => 'int', 'ip' => 'inet', 'email' => 'string', 'log_time' => 'int'),
510
		array($user_info['id'], $user_info['ip'], ($email === null ? ($user_info['is_guest'] ? '' : $user_info['email']) : $email), time()),
511
		array('id_ban_log')
512
	);
513
514
	// One extra point for these bans.
515
	if (!empty($ban_ids))
516
		$smcFunc['db_query']('', '
517
			UPDATE {db_prefix}ban_items
518
			SET hits = hits + 1
519
			WHERE id_ban IN ({array_int:ban_ids})',
520
			array(
521
				'ban_ids' => $ban_ids,
522
			)
523
		);
524
}
525
526
/**
527
 * Checks if a given email address might be banned.
528
 * Check if a given email is banned.
529
 * Performs an immediate ban if the turns turns out positive.
530
 *
531
 * @param string $email The email to check
532
 * @param string $restriction What type of restriction (cannot_post, cannot_register, etc.)
533
 * @param string $error The error message to display if they are indeed banned
534
 */
535
function isBannedEmail($email, $restriction, $error)
536
{
537
	global $txt, $smcFunc;
538
539
	// Can't ban an empty email
540
	if (empty($email) || trim($email) == '')
541
		return;
542
543
	// Let's start with the bans based on your IP/hostname/memberID...
544
	$ban_ids = isset($_SESSION['ban'][$restriction]) ? $_SESSION['ban'][$restriction]['ids'] : array();
545
	$ban_reason = isset($_SESSION['ban'][$restriction]) ? $_SESSION['ban'][$restriction]['reason'] : '';
546
547
	// ...and add to that the email address you're trying to register.
548
	$request = $smcFunc['db_query']('', '
549
		SELECT bi.id_ban, bg.' . $restriction . ', bg.cannot_access, bg.reason
550
		FROM {db_prefix}ban_items AS bi
551
			INNER JOIN {db_prefix}ban_groups AS bg ON (bg.id_ban_group = bi.id_ban_group)
552
		WHERE {string:email} LIKE bi.email_address
553
			AND (bg.' . $restriction . ' = {int:cannot_access} OR bg.cannot_access = {int:cannot_access})
554
			AND (bg.expire_time IS NULL OR bg.expire_time >= {int:now})',
555
		array(
556
			'email' => $email,
557
			'cannot_access' => 1,
558
			'now' => time(),
559
		)
560
	);
561
	while ($row = $smcFunc['db_fetch_assoc']($request))
562
	{
563
		if (!empty($row['cannot_access']))
564
		{
565
			$_SESSION['ban']['cannot_access']['ids'][] = $row['id_ban'];
566
			$_SESSION['ban']['cannot_access']['reason'] = $row['reason'];
567
		}
568
		if (!empty($row[$restriction]))
569
		{
570
			$ban_ids[] = $row['id_ban'];
571
			$ban_reason = $row['reason'];
572
		}
573
	}
574
	$smcFunc['db_free_result']($request);
575
576
	// You're in biiig trouble.  Banned for the rest of this session!
577
	if (isset($_SESSION['ban']['cannot_access']))
578
	{
579
		log_ban($_SESSION['ban']['cannot_access']['ids']);
580
		$_SESSION['ban']['last_checked'] = time();
581
582
		fatal_error(sprintf($txt['your_ban'], $txt['guest_title']) . $_SESSION['ban']['cannot_access']['reason'], false);
583
	}
584
585
	if (!empty($ban_ids))
586
	{
587
		// Log this ban for future reference.
588
		log_ban($ban_ids, $email);
589
		fatal_error($error . $ban_reason, false);
590
	}
591
}
592
593
/**
594
 * Make sure the user's correct session was passed, and they came from here.
595
 * Checks the current session, verifying that the person is who he or she should be.
596
 * Also checks the referrer to make sure they didn't get sent here.
597
 * Depends on the disableCheckUA setting, which is usually missing.
598
 * Will check GET, POST, or REQUEST depending on the passed type.
599
 * Also optionally checks the referring action if passed. (note that the referring action must be by GET.)
600
 *
601
 * @param string $type The type of check (post, get, request)
602
 * @param string $from_action The action this is coming from
603
 * @param bool $is_fatal Whether to die with a fatal error if the check fails
604
 * @return string The error message if is_fatal is false.
605
 */
606
function checkSession($type = 'post', $from_action = '', $is_fatal = true)
607
{
608
	global $context, $sc, $modSettings, $boardurl;
609
610
	// Is it in as $_POST['sc']?
611
	if ($type == 'post')
612
	{
613
		$check = isset($_POST[$_SESSION['session_var']]) ? $_POST[$_SESSION['session_var']] : (empty($modSettings['strictSessionCheck']) && isset($_POST['sc']) ? $_POST['sc'] : null);
614
		if ($check !== $sc)
615
			$error = 'session_timeout';
616
	}
617
618
	// How about $_GET['sesc']?
619
	elseif ($type == 'get')
620
	{
621
		$check = isset($_GET[$_SESSION['session_var']]) ? $_GET[$_SESSION['session_var']] : (empty($modSettings['strictSessionCheck']) && isset($_GET['sesc']) ? $_GET['sesc'] : null);
622
		if ($check !== $sc)
623
			$error = 'session_verify_fail';
624
	}
625
626
	// Or can it be in either?
627
	elseif ($type == 'request')
628
	{
629
		$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)));
630
631
		if ($check !== $sc)
632
			$error = 'session_verify_fail';
633
	}
634
635
	// Verify that they aren't changing user agents on us - that could be bad.
636
	if ((!isset($_SESSION['USER_AGENT']) || $_SESSION['USER_AGENT'] != $_SERVER['HTTP_USER_AGENT']) && empty($modSettings['disableCheckUA']))
637
		$error = 'session_verify_fail';
638
639
	// Make sure a page with session check requirement is not being prefetched.
640
	if (isset($_SERVER['HTTP_X_MOZ']) && $_SERVER['HTTP_X_MOZ'] == 'prefetch')
641
	{
642
		ob_end_clean();
643
		send_http_status(403);
644
		die;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
645
	}
646
647
	// Check the referring site - it should be the same server at least!
648
	if (isset($_SESSION['request_referer']))
649
		$referrer = $_SESSION['request_referer'];
650
	else
651
		$referrer = isset($_SERVER['HTTP_REFERER']) ? @parse_url($_SERVER['HTTP_REFERER']) : array();
652
653
	// Check the refer but if we have CORS enabled and it came from a trusted source, we can skip this check.
654
	if (!empty($referrer['host']) && (empty($modSettings['allow_cors']) || empty($context['valid_cors_found']) || !in_array($context['valid_cors_found'], array('same', 'subdomain'))))
655
	{
656
		if (strpos($_SERVER['HTTP_HOST'], ':') !== false)
657
			$real_host = substr($_SERVER['HTTP_HOST'], 0, strpos($_SERVER['HTTP_HOST'], ':'));
658
		else
659
			$real_host = $_SERVER['HTTP_HOST'];
660
661
		$parsed_url = parse_url($boardurl);
662
663
		// Are global cookies on?  If so, let's check them ;).
664
		if (!empty($modSettings['globalCookies']))
665
		{
666
			if (preg_match('~(?:[^\.]+\.)?([^\.]{3,}\..+)\z~i', $parsed_url['host'], $parts) == 1)
667
				$parsed_url['host'] = $parts[1];
668
669
			if (preg_match('~(?:[^\.]+\.)?([^\.]{3,}\..+)\z~i', $referrer['host'], $parts) == 1)
670
				$referrer['host'] = $parts[1];
671
672
			if (preg_match('~(?:[^\.]+\.)?([^\.]{3,}\..+)\z~i', $real_host, $parts) == 1)
673
				$real_host = $parts[1];
674
		}
675
676
		// Okay: referrer must either match parsed_url or real_host.
677
		if (isset($parsed_url['host']) && strtolower($referrer['host']) != strtolower($parsed_url['host']) && strtolower($referrer['host']) != strtolower($real_host))
678
		{
679
			$error = 'verify_url_fail';
680
			$log_error = true;
681
		}
682
	}
683
684
	// Well, first of all, if a from_action is specified you'd better have an old_url.
685
	if (!empty($from_action) && (!isset($_SESSION['old_url']) || preg_match('~[?;&]action=' . $from_action . '([;&]|$)~', $_SESSION['old_url']) == 0))
686
	{
687
		$error = 'verify_url_fail';
688
		$log_error = true;
689
	}
690
691
	if (strtolower($_SERVER['HTTP_USER_AGENT']) == 'hacker')
692
		fatal_error('Sound the alarm!  It\'s a hacker!  Close the castle gates!!', false);
693
694
	// Everything is ok, return an empty string.
695
	if (!isset($error))
696
		return '';
697
	// A session error occurred, show the error.
698
	elseif ($is_fatal)
699
	{
700
		if (isset($_GET['xml']))
701
		{
702
			ob_end_clean();
703
			send_http_status(403, 'Forbidden - Session timeout');
704
			die;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
705
		}
706
		else
707
			fatal_lang_error($error, isset($log_error) ? 'user' : false);
708
	}
709
	// A session error occurred, return the error to the calling function.
710
	else
711
		return $error;
712
713
	// We really should never fall through here, for very important reasons.  Let's make sure.
714
	trigger_error('Hacking attempt...', E_USER_ERROR);
715
}
716
717
/**
718
 * Check if a specific confirm parameter was given.
719
 *
720
 * @param string $action The action we want to check against
721
 * @return bool|string True if the check passed or a token
722
 */
723
function checkConfirm($action)
724
{
725
	global $modSettings, $smcFunc;
726
727
	if (isset($_GET['confirm']) && isset($_SESSION['confirm_' . $action]) && md5($_GET['confirm'] . $_SERVER['HTTP_USER_AGENT']) == $_SESSION['confirm_' . $action])
728
		return true;
729
730
	else
731
	{
732
		$token = md5($smcFunc['random_int']() . session_id() . (string) microtime() . $modSettings['rand_seed']);
733
		$_SESSION['confirm_' . $action] = md5($token . $_SERVER['HTTP_USER_AGENT']);
734
735
		return $token;
736
	}
737
}
738
739
/**
740
 * Lets give you a token of our appreciation.
741
 *
742
 * @param string $action The action to create the token for
743
 * @param string $type The type of token ('post', 'get' or 'request')
744
 * @return array An array containing the name of the token var and the actual token
745
 */
746
function createToken($action, $type = 'post')
747
{
748
	global $modSettings, $context, $smcFunc;
749
750
	$token = md5($smcFunc['random_int']() . session_id() . (string) microtime() . $modSettings['rand_seed'] . $type);
751
	$token_var = substr(preg_replace('~^\d+~', '', md5($smcFunc['random_int']() . (string) microtime() . $smcFunc['random_int']())), 0, $smcFunc['random_int'](7, 12));
752
753
	$_SESSION['token'][$type . '-' . $action] = array($token_var, md5($token . $_SERVER['HTTP_USER_AGENT']), time(), $token);
754
755
	$context[$action . '_token'] = $token;
756
	$context[$action . '_token_var'] = $token_var;
757
758
	return array($action . '_token_var' => $token_var, $action . '_token' => $token);
759
}
760
761
/**
762
 * Only patrons with valid tokens can ride this ride.
763
 *
764
 * @param string $action The action to validate the token for
765
 * @param string $type The type of request (get, request, or post)
766
 * @param bool $reset Whether to reset the token and display an error if validation fails
767
 * @return bool returns whether the validation was successful
768
 */
769
function validateToken($action, $type = 'post', $reset = true)
770
{
771
	$type = $type == 'get' || $type == 'request' ? $type : 'post';
772
773
	// This nasty piece of code validates a token.
774
	/*
775
		1. The token exists in session.
776
		2. The {$type} variable should exist.
777
		3. We concat the variable we received with the user agent
778
		4. Match that result against what is in the session.
779
		5. If it matches, success, otherwise we fallout.
780
	*/
781
	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])
782
	{
783
		// Invalidate this token now.
784
		unset($_SESSION['token'][$type . '-' . $action]);
785
786
		return true;
787
	}
788
789
	// Patrons with invalid tokens get the boot.
790
	if ($reset)
791
	{
792
		// Might as well do some cleanup on this.
793
		cleanTokens();
794
795
		// I'm back baby.
796
		createToken($action, $type);
797
798
		fatal_lang_error('token_verify_fail', false);
799
	}
800
	// Remove this token as its useless
801
	else
802
		unset($_SESSION['token'][$type . '-' . $action]);
803
804
	// Randomly check if we should remove some older tokens.
805
	if (mt_rand(0, 138) == 23)
806
		cleanTokens();
807
808
	return false;
809
}
810
811
/**
812
 * Removes old unused tokens from session
813
 * defaults to 3 hours before a token is considered expired
814
 * if $complete = true will remove all tokens
815
 *
816
 * @param bool $complete Whether to remove all tokens or only expired ones
817
 */
818
function cleanTokens($complete = false)
819
{
820
	// We appreciate cleaning up after yourselves.
821
	if (!isset($_SESSION['token']))
822
		return;
823
824
	// Clean up tokens, trying to give enough time still.
825
	foreach ($_SESSION['token'] as $key => $data)
826
		if ($data[2] + 10800 < time() || $complete)
827
			unset($_SESSION['token'][$key]);
828
}
829
830
/**
831
 * Check whether a form has been submitted twice.
832
 * Registers a sequence number for a form.
833
 * Checks whether a submitted sequence number is registered in the current session.
834
 * Depending on the value of is_fatal shows an error or returns true or false.
835
 * Frees a sequence number from the stack after it's been checked.
836
 * Frees a sequence number without checking if action == 'free'.
837
 *
838
 * @param string $action The action - can be 'register', 'check' or 'free'
839
 * @param bool $is_fatal Whether to die with a fatal error
840
 * @return void|bool If the action isn't check, returns nothing, otherwise returns whether the check was successful
841
 */
842
function checkSubmitOnce($action, $is_fatal = true)
843
{
844
	global $context, $txt;
845
846
	if (!isset($_SESSION['forms']))
847
		$_SESSION['forms'] = array();
848
849
	// Register a form number and store it in the session stack. (use this on the page that has the form.)
850
	if ($action == 'register')
851
	{
852
		$context['form_sequence_number'] = 0;
853
		while (empty($context['form_sequence_number']) || in_array($context['form_sequence_number'], $_SESSION['forms']))
854
			$context['form_sequence_number'] = mt_rand(1, 16000000);
855
	}
856
	// Check whether the submitted number can be found in the session.
857
	elseif ($action == 'check')
858
	{
859
		if (!isset($_REQUEST['seqnum']))
860
			return true;
861
		elseif (!in_array($_REQUEST['seqnum'], $_SESSION['forms']))
862
		{
863
			$_SESSION['forms'][] = (int) $_REQUEST['seqnum'];
864
			return true;
865
		}
866
		elseif ($is_fatal)
867
			fatal_lang_error('error_form_already_submitted', false);
868
		else
869
			return false;
870
	}
871
	// Don't check, just free the stack number.
872
	elseif ($action == 'free' && isset($_REQUEST['seqnum']) && in_array($_REQUEST['seqnum'], $_SESSION['forms']))
873
		$_SESSION['forms'] = array_diff($_SESSION['forms'], array($_REQUEST['seqnum']));
874
	elseif ($action != 'free')
875
	{
876
		loadLanguage('Errors');
877
		trigger_error(sprintf($txt['check_submit_once_invalid_action'], $action), E_USER_WARNING);
878
	}
879
}
880
881
/**
882
 * Check the user's permissions.
883
 * checks whether the user is allowed to do permission. (ie. post_new.)
884
 * If boards is specified, checks those boards instead of the current one.
885
 * If any is true, will return true if the user has the permission on any of the specified boards
886
 * Always returns true if the user is an administrator.
887
 *
888
 * @param string|array $permission A single permission to check or an array of permissions to check
889
 * @param int|array $boards The ID of a board or an array of board IDs if we want to check board-level permissions
890
 * @param bool $any Whether to check for permission on at least one board instead of all boards
891
 * @return bool Whether the user has the specified permission
892
 */
893
function allowedTo($permission, $boards = null, $any = false)
894
{
895
	global $user_info, $smcFunc;
896
	static $perm_cache = array();
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) || !isset($user_info['permissions']))
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)
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
	$cache_key = hash('md5', $user_info['id'] . '-' . implode(',', $permission) . '-' . implode(',', $boards) . '-' . $any);
926
927
	if (isset($perm_cache[$cache_key]))
928
		return $perm_cache[$cache_key];
929
930
	$request = $smcFunc['db_query']('', '
931
		SELECT MIN(bp.add_deny) AS add_deny
932
		FROM {db_prefix}boards AS b
933
			INNER JOIN {db_prefix}board_permissions AS bp ON (bp.id_profile = b.id_profile)
934
			LEFT JOIN {db_prefix}moderators AS mods ON (mods.id_board = b.id_board AND mods.id_member = {int:current_member})
935
			LEFT JOIN {db_prefix}moderator_groups AS modgs ON (modgs.id_board = b.id_board AND modgs.id_group IN ({array_int:group_list}))
936
		WHERE b.id_board IN ({array_int:board_list})
937
			AND bp.id_group IN ({array_int:group_list}, {int:moderator_group})
938
			AND bp.permission IN ({array_string:permission_list})
939
			AND (mods.id_member IS NOT NULL OR modgs.id_group IS NOT NULL OR bp.id_group != {int:moderator_group})
940
		GROUP BY b.id_board',
941
		array(
942
			'current_member' => $user_info['id'],
943
			'board_list' => $boards,
944
			'group_list' => $user_info['groups'],
945
			'moderator_group' => 3,
946
			'permission_list' => $permission,
947
		)
948
	);
949
950
	if ($any)
951
	{
952
		$result = false;
953
		while ($row = $smcFunc['db_fetch_assoc']($request))
954
		{
955
			$result = !empty($row['add_deny']);
956
			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...
957
				break;
958
		}
959
		$smcFunc['db_free_result']($request);
960
		$return = $result;
961
	}
962
963
	// Make sure they can do it on all of the boards.
964
	elseif ($smcFunc['db_num_rows']($request) != count($boards))
965
		$return = false;
966
967
	else
968
	{
969
		$result = true;
970
		while ($row = $smcFunc['db_fetch_assoc']($request))
971
			$result &= !empty($row['add_deny']);
972
		$smcFunc['db_free_result']($request);
973
		$return = $result;
974
	}
975
976
	$perm_cache[$cache_key] = $return;
977
978
	// If the query returned 1, they can do it... otherwise, they can't.
979
	return $return;
980
}
981
982
/**
983
 * Fatal error if they cannot.
984
 * Uses allowedTo() to check if the user is allowed to do permission.
985
 * Checks the passed boards or current board for the permission.
986
 * If $any is true, the user only needs permission on at least one of the boards to pass
987
 * If they are not, it loads the Errors language file and shows an error using $txt['cannot_' . $permission].
988
 * If they are a guest and cannot do it, this calls is_not_guest().
989
 *
990
 * @param string|array $permission A single permission to check or an array of permissions to check
991
 * @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)
992
 * @param bool $any Whether to check for permission on at least one board instead of all boards
993
 */
994
function isAllowedTo($permission, $boards = null, $any = false)
995
{
996
	global $user_info, $txt;
997
998
	$heavy_permissions = array(
999
		'admin_forum',
1000
		'manage_attachments',
1001
		'manage_smileys',
1002
		'manage_boards',
1003
		'edit_news',
1004
		'moderate_forum',
1005
		'manage_bans',
1006
		'manage_membergroups',
1007
		'manage_permissions',
1008
	);
1009
1010
	// Make it an array, even if a string was passed.
1011
	$permission = (array) $permission;
1012
1013
	call_integration_hook('integrate_heavy_permissions_session', array(&$heavy_permissions));
1014
1015
	// Check the permission and return an error...
1016
	if (!allowedTo($permission, $boards, $any))
1017
	{
1018
		// Pick the last array entry as the permission shown as the error.
1019
		$error_permission = array_shift($permission);
1020
1021
		// If they are a guest, show a login. (because the error might be gone if they do!)
1022
		if ($user_info['is_guest'])
1023
		{
1024
			loadLanguage('Errors');
1025
			is_not_guest($txt['cannot_' . $error_permission]);
1026
		}
1027
1028
		// Clear the action because they aren't really doing that!
1029
		$_GET['action'] = '';
1030
		$_GET['board'] = '';
1031
		$_GET['topic'] = '';
1032
		writeLog(true);
1033
1034
		fatal_lang_error('cannot_' . $error_permission, false);
1035
1036
		// Getting this far is a really big problem, but let's try our best to prevent any cases...
1037
		trigger_error('Hacking attempt...', E_USER_ERROR);
1038
	}
1039
1040
	// If you're doing something on behalf of some "heavy" permissions, validate your session.
1041
	// (take out the heavy permissions, and if you can't do anything but those, you need a validated session.)
1042
	if (!allowedTo(array_diff($permission, $heavy_permissions), $boards))
1043
		validateSession();
1044
}
1045
1046
/**
1047
 * Return the boards a user has a certain (board) permission on. (array(0) if all.)
1048
 *  - returns a list of boards on which the user is allowed to do the specified permission.
1049
 *  - returns an array with only a 0 in it if the user has permission to do this on every board.
1050
 *  - returns an empty array if he or she cannot do this on any board.
1051
 * If check_access is true will also make sure the group has proper access to that board.
1052
 *
1053
 * @param string|array $permissions A single permission to check or an array of permissions to check
1054
 * @param bool $check_access Whether to check only the boards the user has access to
1055
 * @param bool $simple Whether to return a simple array of board IDs or one with permissions as the keys
1056
 * @return array An array of board IDs or an array containing 'permission' => 'board,board2,...' pairs
1057
 */
1058
function boardsAllowedTo($permissions, $check_access = true, $simple = true)
1059
{
1060
	global $user_info, $smcFunc;
1061
1062
	// Arrays are nice, most of the time.
1063
	$permissions = (array) $permissions;
1064
1065
	/*
1066
	 * Set $simple to true to use this function as it were in SMF 2.0.x.
1067
	 * Otherwise, the resultant array becomes split into the multiple
1068
	 * permissions that were passed. Other than that, it's just the normal
1069
	 * state of play that you're used to.
1070
	 */
1071
1072
	// Administrators are all powerful, sorry.
1073
	if ($user_info['is_admin'])
1074
	{
1075
		if ($simple)
1076
			return array(0);
1077
		else
1078
		{
1079
			$boards = array();
1080
			foreach ($permissions as $permission)
1081
				$boards[$permission] = array(0);
1082
1083
			return $boards;
1084
		}
1085
	}
1086
1087
	// All groups the user is in except 'moderator'.
1088
	$groups = array_diff($user_info['groups'], array(3));
1089
1090
	$request = $smcFunc['db_query']('', '
1091
		SELECT b.id_board, bp.add_deny' . ($simple ? '' : ', bp.permission') . '
1092
		FROM {db_prefix}board_permissions AS bp
1093
			INNER JOIN {db_prefix}boards AS b ON (b.id_profile = bp.id_profile)
1094
			LEFT JOIN {db_prefix}moderators AS mods ON (mods.id_board = b.id_board AND mods.id_member = {int:current_member})
1095
			LEFT JOIN {db_prefix}moderator_groups AS modgs ON (modgs.id_board = b.id_board AND modgs.id_group IN ({array_int:group_list}))
1096
		WHERE bp.id_group IN ({array_int:group_list}, {int:moderator_group})
1097
			AND bp.permission IN ({array_string:permissions})
1098
			AND (mods.id_member IS NOT NULL OR modgs.id_group IS NOT NULL OR bp.id_group != {int:moderator_group})' .
1099
			($check_access ? ' AND {query_see_board}' : ''),
1100
		array(
1101
			'current_member' => $user_info['id'],
1102
			'group_list' => $groups,
1103
			'moderator_group' => 3,
1104
			'permissions' => $permissions,
1105
		)
1106
	);
1107
	$boards = array();
1108
	$deny_boards = array();
1109
	while ($row = $smcFunc['db_fetch_assoc']($request))
1110
	{
1111
		if ($simple)
1112
		{
1113
			if (empty($row['add_deny']))
1114
				$deny_boards[] = $row['id_board'];
1115
			else
1116
				$boards[] = $row['id_board'];
1117
		}
1118
		else
1119
		{
1120
			if (empty($row['add_deny']))
1121
				$deny_boards[$row['permission']][] = $row['id_board'];
1122
			else
1123
				$boards[$row['permission']][] = $row['id_board'];
1124
		}
1125
	}
1126
	$smcFunc['db_free_result']($request);
1127
1128
	if ($simple)
1129
		$boards = array_unique(array_values(array_diff($boards, $deny_boards)));
1130
	else
1131
	{
1132
		foreach ($permissions as $permission)
1133
		{
1134
			// never had it to start with
1135
			if (empty($boards[$permission]))
1136
				$boards[$permission] = array();
1137
			else
1138
			{
1139
				// Or it may have been removed
1140
				$deny_boards[$permission] = isset($deny_boards[$permission]) ? $deny_boards[$permission] : array();
1141
				$boards[$permission] = array_unique(array_values(array_diff($boards[$permission], $deny_boards[$permission])));
1142
			}
1143
		}
1144
	}
1145
1146
	return $boards;
1147
}
1148
1149
/**
1150
 * This function attempts to protect from spammed messages and the like.
1151
 * The time taken depends on error_type - generally uses the modSetting.
1152
 *
1153
 * @param string $error_type The error type. Also used as a $txt index (not an actual string).
1154
 * @param boolean $only_return_result Whether you want the function to die with a fatal_lang_error.
1155
 * @return bool Whether they've posted within the limit
1156
 */
1157
function spamProtection($error_type, $only_return_result = false)
1158
{
1159
	global $modSettings, $user_info, $smcFunc;
1160
1161
	// Certain types take less/more time.
1162
	$timeOverrides = array(
1163
		'login' => 2,
1164
		'register' => 2,
1165
		'remind' => 30,
1166
		'sendmail' => $modSettings['spamWaitTime'] * 5,
1167
		'reporttm' => $modSettings['spamWaitTime'] * 4,
1168
		'search' => !empty($modSettings['search_floodcontrol_time']) ? $modSettings['search_floodcontrol_time'] : 1,
1169
	);
1170
1171
	call_integration_hook('integrate_spam_protection', array(&$timeOverrides));
1172
1173
	// Moderators are free...
1174
	if (!allowedTo('moderate_board'))
1175
		$timeLimit = isset($timeOverrides[$error_type]) ? $timeOverrides[$error_type] : $modSettings['spamWaitTime'];
1176
	else
1177
		$timeLimit = 2;
1178
1179
	// Delete old entries...
1180
	$smcFunc['db_query']('', '
1181
		DELETE FROM {db_prefix}log_floodcontrol
1182
		WHERE log_time < {int:log_time}
1183
			AND log_type = {string:log_type}',
1184
		array(
1185
			'log_time' => time() - $timeLimit,
1186
			'log_type' => $error_type,
1187
		)
1188
	);
1189
1190
	// Add a new entry, deleting the old if necessary.
1191
	$smcFunc['db_insert']('replace',
1192
		'{db_prefix}log_floodcontrol',
1193
		array('ip' => 'inet', 'log_time' => 'int', 'log_type' => 'string'),
1194
		array($user_info['ip'], time(), $error_type),
1195
		array('ip', 'log_type')
1196
	);
1197
1198
	// If affected is 0 or 2, it was there already.
1199
	if ($smcFunc['db_affected_rows']() != 1)
1200
	{
1201
		// Spammer!  You only have to wait a *few* seconds!
1202
		if (!$only_return_result)
1203
			fatal_lang_error($error_type . '_WaitTime_broken', false, array($timeLimit));
1204
1205
		return true;
1206
	}
1207
1208
	// They haven't posted within the limit.
1209
	return false;
1210
}
1211
1212
/**
1213
 * A generic function to create a pair of index.php and .htaccess files in a directory
1214
 *
1215
 * @param string|array $paths The (absolute) directory path
1216
 * @param boolean $attachments Whether this is an attachment directory
1217
 * @return bool|array True on success an array of errors if anything fails
1218
 */
1219
function secureDirectory($paths, $attachments = false)
1220
{
1221
	$errors = array();
1222
1223
	// Work with arrays
1224
	$paths = (array) $paths;
1225
1226
	if (empty($paths))
1227
		$errors[] = 'empty_path';
1228
1229
	if (!empty($errors))
1230
		return $errors;
1231
1232
	foreach ($paths as $path)
1233
	{
1234
		if (!is_writable($path))
1235
		{
1236
			$errors[] = 'path_not_writable';
1237
1238
			continue;
1239
		}
1240
1241
		$directory_name = basename($path);
1242
1243
		$close = empty($attachments) ? '
1244
</Files>' : '
1245
	Allow from localhost
1246
</Files>
1247
1248
RemoveHandler .php .php3 .phtml .cgi .fcgi .pl .fpl .shtml';
1249
1250
		if (file_exists($path . '/.htaccess'))
1251
		{
1252
			$errors[] = 'htaccess_exists';
1253
1254
			continue;
1255
		}
1256
1257
		else
1258
		{
1259
			$fh = @fopen($path . '/.htaccess', 'w');
1260
1261
			if ($fh)
1262
			{
1263
				fwrite($fh, '<Files *>
1264
	Order Deny,Allow
1265
	Deny from all' . $close);
1266
				fclose($fh);
1267
			}
1268
1269
			else
1270
				$errors[] = 'htaccess_cannot_create_file';
1271
		}
1272
1273
		if (file_exists($path . '/index.php'))
1274
		{
1275
			$errors[] = 'index-php_exists';
1276
1277
			continue;
1278
		}
1279
1280
		else
1281
		{
1282
			$fh = @fopen($path . '/index.php', 'w');
1283
1284
			if ($fh)
1285
			{
1286
				fwrite($fh, '<' . '?php
1287
1288
/**
1289
 * This file is here solely to protect your ' . $directory_name . ' directory.
1290
 */
1291
1292
// Look for Settings.php....
1293
if (file_exists(dirname(dirname(__FILE__)) . \'/Settings.php\'))
1294
{
1295
	// Found it!
1296
	require(dirname(dirname(__FILE__)) . \'/Settings.php\');
1297
	header(\'location: \' . $boardurl);
1298
}
1299
// Can\'t find it... just forget it.
1300
else
1301
	exit;
1302
1303
?' . '>');
1304
				fclose($fh);
1305
			}
1306
1307
			else
1308
				$errors[] = 'index-php_cannot_create_file';
1309
		}
1310
	}
1311
1312
	if (!empty($errors))
1313
		return $errors;
1314
1315
	else
1316
		return true;
1317
}
1318
1319
/**
1320
 * This sets the X-Frame-Options header.
1321
 *
1322
 * @param string $override An option to override (either 'SAMEORIGIN' or 'DENY')
1323
 * @since 2.1
1324
 */
1325
function frameOptionsHeader($override = null)
1326
{
1327
	global $modSettings;
1328
1329
	$option = 'SAMEORIGIN';
1330
	if (is_null($override) && !empty($modSettings['frame_security']))
1331
		$option = $modSettings['frame_security'];
1332
	elseif (in_array($override, array('SAMEORIGIN', 'DENY')))
1333
		$option = $override;
1334
1335
	// Don't bother setting the header if we have disabled it.
1336
	if ($option == 'DISABLE')
1337
		return;
1338
1339
	// Finally set it.
1340
	header('x-frame-options: ' . $option);
1341
1342
	// And some other useful ones.
1343
	header('x-xss-protection: 1');
1344
	header('x-content-type-options: nosniff');
1345
}
1346
1347
/**
1348
 * This sets the Access-Control-Allow-Origin header.
1349
 *
1350
 * @since 2.1
1351
 */
1352
function corsPolicyHeader($set_header = true)
1353
{
1354
	global $boardurl, $modSettings, $context;
1355
1356
	if (empty($modSettings['allow_cors']))
1357
		return;
1358
1359
	// We want weak security.
1360
	if (!empty($modSettings['cors_domains']) && $modSettings['cors_domains'] === '*')
1361
		$context['cors_domain'] = '*';
1362
1363
	// If subdomain-independent cookies are on, allow sub domains. This will have issues if the forum is at forum.domain.tld and the origin we are at is another.sub.domain.tld.
1364
	if (empty($context['cors_domain']) && !empty($modSettings['globalCookies']) && !empty($_SERVER['HTTP_ORIGIN']) && filter_var($_SERVER['HTTP_ORIGIN'], FILTER_VALIDATE_URL) !== false && FindCorsBaseUrl($boardurl, true) === FindCorsBaseUrl($_SERVER['HTTP_ORIGIN'], true))
1365
	{
1366
		$context['cors_domain'] = $_SERVER['HTTP_ORIGIN'];
1367
		$context['valid_cors_found'] = 'subdomain';
1368
	}
1369
1370
	// Support forum_alias_urls as well, which is supported by our login cookie, so we implant it similar here.
1371
	if (empty($context['cors_domain']) && !empty($modSettings['forum_alias_urls']) && !empty($_SERVER['HTTP_ORIGIN']) && filter_var($_SERVER['HTTP_ORIGIN'], FILTER_VALIDATE_URL) !== false)
1372
	{
1373
		$aliases = explode(',', $modSettings['forum_alias_urls']);
1374
1375
		foreach ($aliases as $alias)
1376
		{
1377
			if ($alias === $_SERVER['HTTP_ORIGIN'])
1378
			{
1379
				$context['cors_domain'] = $_SERVER['HTTP_ORIGIN'];
1380
				$context['valid_cors_found'] = 'alias';
1381
				break;
1382
			}
1383
		}
1384
	}
1385
1386
	// Additional CORS domains but its just a wildcard for everything.
1387
	if (empty($context['cors_domain']) && !empty($modSettings['cors_domains']) && !empty($_SERVER['HTTP_ORIGIN']) && filter_var($_SERVER['HTTP_ORIGIN'], FILTER_VALIDATE_URL) !== false)
1388
	{
1389
		$cors_domains = explode(',', $modSettings['cors_domains']);
1390
		$origin_base = FindCorsBaseUrl($_SERVER['HTTP_ORIGIN']);
1391
1392
		foreach ($cors_domains as $domain)
1393
		{
1394
			if ($domain === $_SERVER['HTTP_ORIGIN'])
1395
			{
1396
				$context['cors_domain'] = $_SERVER['HTTP_ORIGIN'];
1397
				$context['valid_cors_found'] = 'additional';
1398
				break;
1399
			}
1400
1401
			// If we find a * in the host, then its a wildcard and lets allow it.  This will have issues if the forum is at forum.domain.tld and the origin we are at is more.sub.domain.tld.
1402
			if ('*' === implode('.', array_slice(explode('.', parse_url($domain, PHP_URL_HOST)), 0, 1)) && FindCorsBaseUrl($domain, true) === $origin_base)
1403
			{
1404
				$context['cors_domain'] = $_SERVER['HTTP_ORIGIN'];
1405
				$context['valid_cors_found'] = 'additional_wildcard';
1406
				break;
1407
			}
1408
		}
1409
	}
1410
1411
	// The default is just to place the domain of the boardurl into the policy.
1412
	if (empty($context['cors_domain']) || empty($context['valid_cors_found']))
1413
	{
1414
		$boardurl_parts = parse_url($boardurl);
1415
1416
		// Default the CORS to the current domain
1417
		$context['cors_domain'] = $boardurl_parts['scheme'] . '://' . $boardurl_parts['host'];
1418
1419
		// Attach the port if needed.
1420
		if (!empty($boardurl_parts['port']))
1421
			$context['cors_domain'] .= ':' . $boardurl_parts['port'];
1422
1423
		$context['valid_cors_found'] = 'same';
1424
	}
1425
1426
	$context['cors_headers'] = 'X-SMF-AJAX';
1427
1428
	// Any additional headers?
1429
	if (!empty($modSettings['cors_headers']))
1430
	{
1431
		// Cleanup any typos.
1432
		$cors_headers = explode(',', $modSettings['cors_headers']);
1433
		foreach ($cors_headers as &$ch)
1434
			$ch = trim(str_replace(' ', '-', $ch));
1435
1436
		$context['cors_headers'] += implode(',', $cors_headers);
1437
	}
1438
1439
	// Allowing Cross-Origin Resource Sharing (CORS).
1440
	if ($set_header && !empty($modSettings['allow_cors']) && !empty($context['valid_cors_found']) && !empty($context['cors_domain']))
1441
	{
1442
		header('Access-Control-Allow-Origin: ' . $context['cors_domain']);
1443
		header('Access-Control-Allow-Headers: ' . $context['cors_headers']);
1444
1445
		// Be careful with this, your allowing a external site to allow the browser to send cookies with this.
1446
		if (!empty($modSettings['allow_cors_credentials']))
1447
			header('Access-Control-Allow-Credentials: true');
1448
	}
1449
}
1450
1451
/**
1452
 * Helper function to figure out a urls base domain.
1453
 *
1454
 * @since 2.1
1455
 */
1456
function FindCorsBaseUrl($url, $sub_domain = false)
1457
{
1458
	global $modSettings;
1459
1460
	$base_domain = parse_url($url, PHP_URL_HOST);
1461
1462
	// Are we saying this is a sub domain and to remove another domain level?
1463
	if ($sub_domain)
1464
		$base_domain = implode('.', array_slice(explode('.', parse_url($domain, PHP_URL_HOST)), 1));
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $domain does not exist. Did you maybe mean $sub_domain?
Loading history...
1465
1466
	// If we find www, pop it out.
1467
	else if ('www' === array_slice(explode('.', $base_domain), 0, 1))
1468
		$base_domain = implode('.', array_slice(explode('.', $base_domain, 1)), 1);
0 ignored issues
show
Bug introduced by
The call to array_slice() has too few arguments starting with offset. ( Ignorable by Annotation )

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

1468
		$base_domain = implode('.', /** @scrutinizer ignore-call */ array_slice(explode('.', $base_domain, 1)), 1);

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
Unused Code introduced by
The call to implode() has too many arguments starting with 1. ( Ignorable by Annotation )

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

1468
		$base_domain = /** @scrutinizer ignore-call */ implode('.', array_slice(explode('.', $base_domain, 1)), 1);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
1469
1470
	/*	Note, we do have a TLD regex for the autolinker, however it can not reliably be used here due to
1471
		Country Code Second Level Domains (example: co.uk) as they are not TLDs.  As these are also
1472
		controlled by their respective countries, there is no uniformity or standards on these domains.
1473
		If this wasn't true, we could use this tld regex to just find the next domain level
1474
		after the TLD and that would be our "truest" base.
1475
	*/
1476
1477
	return $base_domain;
1478
}
1479
1480
?>