Issues (1065)

Sources/Security.php (1 issue)

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 2025 Simple Machines and individual contributors
12
 * @license https://www.simplemachines.org/about/smf/license.php BSD
13
 *
14
 * @version 2.1.5
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())
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_iri($_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']))
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
	die('No direct access...');
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
		if (isset($_REQUEST['action']) && $_REQUEST['action'] == 'dlattach')
315
			die();
316
317
		// 'Log' the user out.  Can't have any funny business... (save the name!)
318
		$old_name = isset($user_info['name']) && $user_info['name'] != '' ? $user_info['name'] : $txt['guest_title'];
319
		$user_info['name'] = '';
320
		$user_info['username'] = '';
321
		$user_info['is_guest'] = true;
322
		$user_info['is_admin'] = false;
323
		$user_info['permissions'] = array();
324
		$user_info['id'] = 0;
325
		$context['user'] = array(
326
			'id' => 0,
327
			'username' => '',
328
			'name' => $txt['guest_title'],
329
			'is_guest' => true,
330
			'is_logged' => false,
331
			'is_admin' => false,
332
			'is_mod' => false,
333
			'can_mod' => false,
334
			'language' => $user_info['language'],
335
		);
336
337
		// A goodbye present.
338
		require_once($sourcedir . '/Subs-Auth.php');
339
		require_once($sourcedir . '/LogInOut.php');
340
		$cookie_url = url_parts(!empty($modSettings['localCookies']), !empty($modSettings['globalCookies']));
341
		smf_setcookie($cookiename . '_', implode(',', $_SESSION['ban']['cannot_access']['ids']), time() + 3153600, $cookie_url[1], $cookie_url[0], false, false);
342
343
		// Don't scare anyone, now.
344
		$_GET['action'] = '';
345
		$_GET['board'] = '';
346
		$_GET['topic'] = '';
347
		writeLog(true);
348
		Logout(true, false);
349
350
		// You banned, sucka!
351
		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);
352
353
		// If we get here, something's gone wrong.... but let's try anyway.
354
		die('No direct access...');
355
	}
356
	// You're not allowed to log in but yet you are. Let's fix that.
357
	elseif (isset($_SESSION['ban']['cannot_login']) && !$user_info['is_guest'])
358
	{
359
		// We don't wanna see you!
360
		$smcFunc['db_query']('', '
361
			DELETE FROM {db_prefix}log_online
362
			WHERE id_member = {int:current_member}',
363
			array(
364
				'current_member' => $user_info['id'],
365
			)
366
		);
367
368
		// 'Log' the user out.  Can't have any funny business... (save the name!)
369
		$old_name = isset($user_info['name']) && $user_info['name'] != '' ? $user_info['name'] : $txt['guest_title'];
370
		$user_info['name'] = '';
371
		$user_info['username'] = '';
372
		$user_info['is_guest'] = true;
373
		$user_info['is_admin'] = false;
374
		$user_info['permissions'] = array();
375
		$user_info['id'] = 0;
376
		$context['user'] = array(
377
			'id' => 0,
378
			'username' => '',
379
			'name' => $txt['guest_title'],
380
			'is_guest' => true,
381
			'is_logged' => false,
382
			'is_admin' => false,
383
			'is_mod' => false,
384
			'can_mod' => false,
385
			'language' => $user_info['language'],
386
		);
387
388
		// SMF's Wipe 'n Clean(r) erases all traces.
389
		$_GET['action'] = '';
390
		$_GET['board'] = '';
391
		$_GET['topic'] = '';
392
		writeLog(true);
393
394
		require_once($sourcedir . '/LogInOut.php');
395
		Logout(true, false);
396
397
		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);
398
	}
399
400
	// Fix up the banning permissions.
401
	if (isset($user_info['permissions']))
402
		banPermissions();
403
}
404
405
/**
406
 * Fix permissions according to ban status.
407
 * Applies any states of banning by removing permissions the user cannot have.
408
 */
409
function banPermissions()
410
{
411
	global $user_info, $sourcedir, $modSettings, $context;
412
413
	// Somehow they got here, at least take away all permissions...
414
	if (isset($_SESSION['ban']['cannot_access']))
415
		$user_info['permissions'] = array();
416
	// Okay, well, you can watch, but don't touch a thing.
417
	elseif (isset($_SESSION['ban']['cannot_post']) || (!empty($modSettings['warning_mute']) && $modSettings['warning_mute'] <= $user_info['warning']))
418
	{
419
		$denied_permissions = array(
420
			'pm_send',
421
			'calendar_post', 'calendar_edit_own', 'calendar_edit_any',
422
			'poll_post',
423
			'poll_add_own', 'poll_add_any',
424
			'poll_edit_own', 'poll_edit_any',
425
			'poll_lock_own', 'poll_lock_any',
426
			'poll_remove_own', 'poll_remove_any',
427
			'manage_attachments', 'manage_smileys', 'manage_boards', 'admin_forum', 'manage_permissions',
428
			'moderate_forum', 'manage_membergroups', 'manage_bans', 'send_mail', 'edit_news',
429
			'profile_identity_any', 'profile_extra_any', 'profile_title_any',
430
			'profile_forum_any', 'profile_other_any', 'profile_signature_any',
431
			'post_new', 'post_reply_own', 'post_reply_any',
432
			'delete_own', 'delete_any', 'delete_replies',
433
			'make_sticky',
434
			'merge_any', 'split_any',
435
			'modify_own', 'modify_any', 'modify_replies',
436
			'move_any',
437
			'lock_own', 'lock_any',
438
			'remove_own', 'remove_any',
439
			'post_unapproved_topics', 'post_unapproved_replies_own', 'post_unapproved_replies_any',
440
		);
441
		call_integration_hook('integrate_post_ban_permissions', array(&$denied_permissions));
442
		$user_info['permissions'] = array_diff($user_info['permissions'], $denied_permissions);
443
	}
444
	// Are they absolutely under moderation?
445
	elseif (!empty($modSettings['warning_moderate']) && $modSettings['warning_moderate'] <= $user_info['warning'])
446
	{
447
		// Work out what permissions should change...
448
		$permission_change = array(
449
			'post_new' => 'post_unapproved_topics',
450
			'post_reply_own' => 'post_unapproved_replies_own',
451
			'post_reply_any' => 'post_unapproved_replies_any',
452
			'post_attachment' => 'post_unapproved_attachments',
453
		);
454
		call_integration_hook('integrate_warn_permissions', array(&$permission_change));
455
		foreach ($permission_change as $old => $new)
456
		{
457
			if (!in_array($old, $user_info['permissions']))
458
				unset($permission_change[$old]);
459
			else
460
				$user_info['permissions'][] = $new;
461
		}
462
		$user_info['permissions'] = array_diff($user_info['permissions'], array_keys($permission_change));
463
	}
464
465
	// @todo Find a better place to call this? Needs to be after permissions loaded!
466
	// Finally, some bits we cache in the session because it saves queries.
467
	if (isset($_SESSION['mc']) && $_SESSION['mc']['time'] > $modSettings['settings_updated'] && $_SESSION['mc']['id'] == $user_info['id'])
468
		$user_info['mod_cache'] = $_SESSION['mc'];
469
	else
470
	{
471
		require_once($sourcedir . '/Subs-Auth.php');
472
		rebuildModCache();
473
	}
474
475
	// Now that we have the mod cache taken care of lets setup a cache for the number of mod reports still open
476
	if (isset($_SESSION['rc']['reports']) && isset($_SESSION['rc']['member_reports']) && $_SESSION['rc']['time'] > $modSettings['last_mod_report_action'] && $_SESSION['rc']['id'] == $user_info['id'])
477
	{
478
		$context['open_mod_reports'] = $_SESSION['rc']['reports'];
479
		$context['open_member_reports'] = $_SESSION['rc']['member_reports'];
480
	}
481
	elseif ($_SESSION['mc']['bq'] != '0=1')
482
	{
483
		require_once($sourcedir . '/Subs-ReportedContent.php');
484
		$context['open_mod_reports'] = recountOpenReports('posts');
485
		$context['open_member_reports'] = recountOpenReports('members');
486
	}
487
	else
488
	{
489
		$context['open_mod_reports'] = 0;
490
		$context['open_member_reports'] = 0;
491
	}
492
}
493
494
/**
495
 * Log a ban in the database.
496
 * Log the current user in the ban logs.
497
 * Increment the hit counters for the specified ban ID's (if any.)
498
 *
499
 * @param array $ban_ids The IDs of the bans
500
 * @param string $email The email address associated with the user that triggered this hit
501
 */
502
function log_ban($ban_ids = array(), $email = null)
503
{
504
	global $user_info, $smcFunc;
505
506
	// Don't log web accelerators, it's very confusing...
507
	if (isset($_SERVER['HTTP_X_MOZ']) && $_SERVER['HTTP_X_MOZ'] == 'prefetch')
508
		return;
509
510
	$smcFunc['db_insert']('',
511
		'{db_prefix}log_banned',
512
		array('id_member' => 'int', 'ip' => 'inet', 'email' => 'string', 'log_time' => 'int'),
513
		array($user_info['id'], $user_info['ip'], ($email === null ? ($user_info['is_guest'] ? '' : $user_info['email']) : $email), time()),
514
		array('id_ban_log')
515
	);
516
517
	// One extra point for these bans.
518
	if (!empty($ban_ids))
519
		$smcFunc['db_query']('', '
520
			UPDATE {db_prefix}ban_items
521
			SET hits = hits + 1
522
			WHERE id_ban IN ({array_int:ban_ids})',
523
			array(
524
				'ban_ids' => $ban_ids,
525
			)
526
		);
527
}
528
529
/**
530
 * Checks if a given email address might be banned.
531
 * Check if a given email is banned.
532
 * Performs an immediate ban if the turns turns out positive.
533
 *
534
 * @param string $email The email to check
535
 * @param string $restriction What type of restriction (cannot_post, cannot_register, etc.)
536
 * @param string $error The error message to display if they are indeed banned
537
 */
538
function isBannedEmail($email, $restriction, $error)
539
{
540
	global $txt, $smcFunc;
541
542
	// Can't ban an empty email
543
	if (empty($email) || trim($email) == '')
544
		return;
545
546
	// Let's start with the bans based on your IP/hostname/memberID...
547
	$ban_ids = isset($_SESSION['ban'][$restriction]) ? $_SESSION['ban'][$restriction]['ids'] : array();
548
	$ban_reason = isset($_SESSION['ban'][$restriction]) ? $_SESSION['ban'][$restriction]['reason'] : '';
549
550
	// ...and add to that the email address you're trying to register.
551
	$request = $smcFunc['db_query']('', '
552
		SELECT bi.id_ban, bg.' . $restriction . ', bg.cannot_access, bg.reason
553
		FROM {db_prefix}ban_items AS bi
554
			INNER JOIN {db_prefix}ban_groups AS bg ON (bg.id_ban_group = bi.id_ban_group)
555
		WHERE {string:email} LIKE bi.email_address
556
			AND (bg.' . $restriction . ' = {int:cannot_access} OR bg.cannot_access = {int:cannot_access})
557
			AND (bg.expire_time IS NULL OR bg.expire_time >= {int:now})',
558
		array(
559
			'email' => $email,
560
			'cannot_access' => 1,
561
			'now' => time(),
562
		)
563
	);
564
	while ($row = $smcFunc['db_fetch_assoc']($request))
565
	{
566
		if (!empty($row['cannot_access']))
567
		{
568
			$_SESSION['ban']['cannot_access']['ids'][] = $row['id_ban'];
569
			$_SESSION['ban']['cannot_access']['reason'] = $row['reason'];
570
		}
571
		if (!empty($row[$restriction]))
572
		{
573
			$ban_ids[] = $row['id_ban'];
574
			$ban_reason = $row['reason'];
575
		}
576
	}
577
	$smcFunc['db_free_result']($request);
578
579
	// You're in biiig trouble.  Banned for the rest of this session!
580
	if (isset($_SESSION['ban']['cannot_access']))
581
	{
582
		log_ban($_SESSION['ban']['cannot_access']['ids']);
583
		$_SESSION['ban']['last_checked'] = time();
584
585
		fatal_error(sprintf($txt['your_ban'], $txt['guest_title']) . $_SESSION['ban']['cannot_access']['reason'], false);
586
	}
587
588
	if (!empty($ban_ids))
589
	{
590
		// Log this ban for future reference.
591
		log_ban($ban_ids, $email);
592
		fatal_error($error . $ban_reason, false);
593
	}
594
}
595
596
/**
597
 * Make sure the user's correct session was passed, and they came from here.
598
 * Checks the current session, verifying that the person is who he or she should be.
599
 * Also checks the referrer to make sure they didn't get sent here.
600
 * Depends on the disableCheckUA setting, which is usually missing.
601
 * Will check GET, POST, or REQUEST depending on the passed type.
602
 * Also optionally checks the referring action if passed. (note that the referring action must be by GET.)
603
 *
604
 * @param string $type The type of check (post, get, request)
605
 * @param string $from_action The action this is coming from
606
 * @param bool $is_fatal Whether to die with a fatal error if the check fails
607
 * @return string The error message if is_fatal is false.
608
 */
609
function checkSession($type = 'post', $from_action = '', $is_fatal = true)
610
{
611
	global $context, $sc, $modSettings, $boardurl;
612
613
	// Is it in as $_POST['sc']?
614
	if ($type == 'post')
615
	{
616
		$check = isset($_POST[$_SESSION['session_var']]) ? $_POST[$_SESSION['session_var']] : (empty($modSettings['strictSessionCheck']) && isset($_POST['sc']) ? $_POST['sc'] : null);
617
		if ($check !== $sc)
618
			$error = 'session_timeout';
619
	}
620
621
	// How about $_GET['sesc']?
622
	elseif ($type == 'get')
623
	{
624
		$check = isset($_GET[$_SESSION['session_var']]) ? $_GET[$_SESSION['session_var']] : (empty($modSettings['strictSessionCheck']) && isset($_GET['sesc']) ? $_GET['sesc'] : null);
625
		if ($check !== $sc)
626
			$error = 'session_verify_fail';
627
	}
628
629
	// Or can it be in either?
630
	elseif ($type == 'request')
631
	{
632
		$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)));
633
634
		if ($check !== $sc)
635
			$error = 'session_verify_fail';
636
	}
637
638
	// Verify that they aren't changing user agents on us - that could be bad.
639
	if ((!isset($_SESSION['USER_AGENT']) || $_SESSION['USER_AGENT'] != $_SERVER['HTTP_USER_AGENT']) && empty($modSettings['disableCheckUA']))
640
		$error = 'session_verify_fail';
641
642
	// Make sure a page with session check requirement is not being prefetched.
643
	if (isset($_SERVER['HTTP_X_MOZ']) && $_SERVER['HTTP_X_MOZ'] == 'prefetch')
644
	{
645
		ob_end_clean();
646
		send_http_status(403);
647
		die;
648
	}
649
650
	// Check the referring site - it should be the same server at least!
651
	if (isset($_SESSION['request_referer']))
652
		$referrer = $_SESSION['request_referer'];
653
	else
654
		$referrer = isset($_SERVER['HTTP_REFERER']) ? @parse_url($_SERVER['HTTP_REFERER']) : array();
655
656
	// Check the refer but if we have CORS enabled and it came from a trusted source, we can skip this check.
657
	if (!empty($referrer['host']) && (empty($modSettings['allow_cors']) || empty($context['valid_cors_found']) || !in_array($context['valid_cors_found'], array('same', 'subdomain'))))
658
	{
659
		if (strpos($_SERVER['HTTP_HOST'], ':') !== false)
660
			$real_host = substr($_SERVER['HTTP_HOST'], 0, strpos($_SERVER['HTTP_HOST'], ':'));
661
		else
662
			$real_host = $_SERVER['HTTP_HOST'];
663
664
		$parsed_url = parse_iri($boardurl);
665
666
		// Are global cookies on?  If so, let's check them ;).
667
		if (!empty($modSettings['globalCookies']))
668
		{
669
			if (preg_match('~(?:[^\.]+\.)?([^\.]{3,}\..+)\z~i', $parsed_url['host'], $parts) == 1)
670
				$parsed_url['host'] = $parts[1];
671
672
			if (preg_match('~(?:[^\.]+\.)?([^\.]{3,}\..+)\z~i', $referrer['host'], $parts) == 1)
673
				$referrer['host'] = $parts[1];
674
675
			if (preg_match('~(?:[^\.]+\.)?([^\.]{3,}\..+)\z~i', $real_host, $parts) == 1)
676
				$real_host = $parts[1];
677
		}
678
679
		// Okay: referrer must either match parsed_url or real_host.
680
		if (isset($parsed_url['host']) && strtolower($referrer['host']) != strtolower($parsed_url['host']) && strtolower($referrer['host']) != strtolower($real_host))
681
		{
682
			$error = 'verify_url_fail';
683
			$log_error = true;
684
		}
685
	}
686
687
	// Well, first of all, if a from_action is specified you'd better have an old_url.
688
	if (!empty($from_action) && (!isset($_SESSION['old_url']) || preg_match('~[?;&]action=' . $from_action . '([;&]|$)~', $_SESSION['old_url']) == 0))
689
	{
690
		$error = 'verify_url_fail';
691
		$log_error = true;
692
	}
693
694
	if (strtolower($_SERVER['HTTP_USER_AGENT']) == 'hacker')
695
		fatal_error('Sound the alarm!  It\'s a hacker!  Close the castle gates!!', false);
696
697
	// Everything is ok, return an empty string.
698
	if (!isset($error))
699
		return '';
700
	// A session error occurred, show the error.
701
	elseif ($is_fatal)
702
	{
703
		if (isset($_GET['xml']))
704
		{
705
			ob_end_clean();
706
			send_http_status(403, 'Forbidden - Session timeout');
707
			die;
708
		}
709
		else
710
			fatal_lang_error($error, isset($log_error) ? 'user' : false);
711
	}
712
	// A session error occurred, return the error to the calling function.
713
	else
714
		return $error;
715
716
	// We really should never fall through here, for very important reasons.  Let's make sure.
717
	die('No direct access...');
718
}
719
720
/**
721
 * Check if a specific confirm parameter was given.
722
 *
723
 * @param string $action The action we want to check against
724
 * @return bool|string True if the check passed or a token
725
 */
726
function checkConfirm($action)
727
{
728
	global $modSettings, $smcFunc;
729
730
	if (isset($_GET['confirm']) && isset($_SESSION['confirm_' . $action]) && md5($_GET['confirm'] . $_SERVER['HTTP_USER_AGENT']) == $_SESSION['confirm_' . $action])
731
		return true;
732
733
	else
734
	{
735
		$token = md5($smcFunc['random_int']() . session_id() . (string) microtime() . $modSettings['rand_seed']);
736
		$_SESSION['confirm_' . $action] = md5($token . $_SERVER['HTTP_USER_AGENT']);
737
738
		return $token;
739
	}
740
}
741
742
/**
743
 * Lets give you a token of our appreciation.
744
 *
745
 * @param string $action The action to create the token for
746
 * @param string $type The type of token ('post', 'get' or 'request')
747
 * @return array An array containing the name of the token var and the actual token
748
 */
749
function createToken($action, $type = 'post')
750
{
751
	global $modSettings, $context, $smcFunc;
752
753
	$token = md5($smcFunc['random_int']() . session_id() . (string) microtime() . $modSettings['rand_seed'] . $type);
754
	$token_var = substr(preg_replace('~^\d+~', '', md5($smcFunc['random_int']() . (string) microtime() . $smcFunc['random_int']())), 0, $smcFunc['random_int'](7, 12));
755
756
	$_SESSION['token'][$type . '-' . $action] = array($token_var, md5($token . $_SERVER['HTTP_USER_AGENT']), time(), $token);
757
758
	$context[$action . '_token'] = $token;
759
	$context[$action . '_token_var'] = $token_var;
760
761
	return array($action . '_token_var' => $token_var, $action . '_token' => $token);
762
}
763
764
/**
765
 * Only patrons with valid tokens can ride this ride.
766
 *
767
 * @param string $action The action to validate the token for
768
 * @param string $type The type of request (get, request, or post)
769
 * @param bool $reset Whether to reset the token and display an error if validation fails
770
 * @return bool returns whether the validation was successful
771
 */
772
function validateToken($action, $type = 'post', $reset = true)
773
{
774
	$type = $type == 'get' || $type == 'request' ? $type : 'post';
775
776
	// This nasty piece of code validates a token.
777
	/*
778
		1. The token exists in session.
779
		2. The {$type} variable should exist.
780
		3. We concat the variable we received with the user agent
781
		4. Match that result against what is in the session.
782
		5. If it matches, success, otherwise we fallout.
783
	*/
784
	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])
785
	{
786
		// Invalidate this token now.
787
		unset($_SESSION['token'][$type . '-' . $action]);
788
789
		return true;
790
	}
791
792
	// Patrons with invalid tokens get the boot.
793
	if ($reset)
794
	{
795
		// Might as well do some cleanup on this.
796
		cleanTokens();
797
798
		// I'm back baby.
799
		createToken($action, $type);
800
801
		fatal_lang_error('token_verify_fail', false);
802
	}
803
	// Remove this token as its useless
804
	else
805
		unset($_SESSION['token'][$type . '-' . $action]);
806
807
	// Randomly check if we should remove some older tokens.
808
	if (mt_rand(0, 138) == 23)
809
		cleanTokens();
810
811
	return false;
812
}
813
814
/**
815
 * Removes old unused tokens from session
816
 * defaults to 3 hours before a token is considered expired
817
 * if $complete = true will remove all tokens
818
 *
819
 * @param bool $complete Whether to remove all tokens or only expired ones
820
 */
821
function cleanTokens($complete = false)
822
{
823
	// We appreciate cleaning up after yourselves.
824
	if (!isset($_SESSION['token']))
825
		return;
826
827
	// Clean up tokens, trying to give enough time still.
828
	foreach ($_SESSION['token'] as $key => $data)
829
		if ($data[2] + 10800 < time() || $complete)
830
			unset($_SESSION['token'][$key]);
831
}
832
833
/**
834
 * Check whether a form has been submitted twice.
835
 * Registers a sequence number for a form.
836
 * Checks whether a submitted sequence number is registered in the current session.
837
 * Depending on the value of is_fatal shows an error or returns true or false.
838
 * Frees a sequence number from the stack after it's been checked.
839
 * Frees a sequence number without checking if action == 'free'.
840
 *
841
 * @param string $action The action - can be 'register', 'check' or 'free'
842
 * @param bool $is_fatal Whether to die with a fatal error
843
 * @return void|bool If the action isn't check, returns nothing, otherwise returns whether the check was successful
844
 */
845
function checkSubmitOnce($action, $is_fatal = true)
846
{
847
	global $context, $txt;
848
849
	if (!isset($_SESSION['forms']))
850
		$_SESSION['forms'] = array();
851
852
	// Register a form number and store it in the session stack. (use this on the page that has the form.)
853
	if ($action == 'register')
854
	{
855
		$context['form_sequence_number'] = 0;
856
		while (empty($context['form_sequence_number']) || in_array($context['form_sequence_number'], $_SESSION['forms']))
857
			$context['form_sequence_number'] = mt_rand(1, 16000000);
858
	}
859
	// Check whether the submitted number can be found in the session.
860
	elseif ($action == 'check')
861
	{
862
		if (!isset($_REQUEST['seqnum']))
863
			return true;
864
		elseif (!in_array($_REQUEST['seqnum'], $_SESSION['forms']))
865
		{
866
			$_SESSION['forms'][] = (int) $_REQUEST['seqnum'];
867
			return true;
868
		}
869
		elseif ($is_fatal)
870
			fatal_lang_error('error_form_already_submitted', false);
871
		else
872
			return false;
873
	}
874
	// Don't check, just free the stack number.
875
	elseif ($action == 'free' && isset($_REQUEST['seqnum']) && in_array($_REQUEST['seqnum'], $_SESSION['forms']))
876
		$_SESSION['forms'] = array_diff($_SESSION['forms'], array($_REQUEST['seqnum']));
877
	elseif ($action != 'free')
878
	{
879
		loadLanguage('Errors');
880
		trigger_error(sprintf($txt['check_submit_once_invalid_action'], $action), E_USER_WARNING);
881
	}
882
}
883
884
/**
885
 * Check the user's permissions.
886
 * checks whether the user is allowed to do permission. (ie. post_new.)
887
 * If boards is specified, checks those boards instead of the current one.
888
 * If any is true, will return true if the user has the permission on any of the specified boards
889
 * Always returns true if the user is an administrator.
890
 *
891
 * @param string|array $permission A single permission to check or an array of permissions to check
892
 * @param int|array $boards The ID of a board or an array of board IDs if we want to check board-level permissions
893
 * @param bool $any Whether to check for permission on at least one board instead of all boards
894
 * @return bool Whether the user has the specified permission
895
 */
896
function allowedTo($permission, $boards = null, $any = false)
897
{
898
	global $user_info, $smcFunc;
899
	static $perm_cache = array();
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) || !isset($user_info['permissions']))
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
	// This should be a boolean.
917
	$any = (bool) $any;
918
919
	// Are we checking the _current_ board, or some other boards?
920
	if ($boards === null)
921
	{
922
		$user_permissions = (array) $user_info['permissions'];
923
924
		// Allow temporary overrides for general permissions?
925
		call_integration_hook('integrate_allowed_to_general', array(&$user_permissions, $permission));
926
927
		return array_intersect($permission, $user_permissions) != [];
928
	}
929
	elseif (!is_array($boards))
930
		$boards = array($boards);
931
932
	$cache_key = hash('md5', $user_info['id'] . '-' . implode(',', $permission) . '-' . implode(',', $boards) . '-' . (int) $any);
933
934
	if (isset($perm_cache[$cache_key]))
935
		return $perm_cache[$cache_key];
936
937
	$request = $smcFunc['db_query']('', '
938
		SELECT MIN(bp.add_deny) AS add_deny
939
		FROM {db_prefix}boards AS b
940
			INNER JOIN {db_prefix}board_permissions AS bp ON (bp.id_profile = b.id_profile)
941
			LEFT JOIN {db_prefix}moderators AS mods ON (mods.id_board = b.id_board AND mods.id_member = {int:current_member})
942
			LEFT JOIN {db_prefix}moderator_groups AS modgs ON (modgs.id_board = b.id_board AND modgs.id_group IN ({array_int:group_list}))
943
		WHERE b.id_board IN ({array_int:board_list})
944
			AND bp.id_group IN ({array_int:group_list}, {int:moderator_group})
945
			AND bp.permission IN ({array_string:permission_list})
946
			AND (mods.id_member IS NOT NULL OR modgs.id_group IS NOT NULL OR bp.id_group != {int:moderator_group})
947
		GROUP BY b.id_board',
948
		array(
949
			'current_member' => $user_info['id'],
950
			'board_list' => $boards,
951
			'group_list' => $user_info['groups'],
952
			'moderator_group' => 3,
953
			'permission_list' => $permission,
954
		)
955
	);
956
957
	if ($any)
958
	{
959
		$result = false;
960
		while ($row = $smcFunc['db_fetch_assoc']($request))
961
		{
962
			$result = !empty($row['add_deny']);
963
			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...
964
				break;
965
		}
966
		$smcFunc['db_free_result']($request);
967
		$return = $result;
968
	}
969
970
	// Make sure they can do it on all of the boards.
971
	elseif ($smcFunc['db_num_rows']($request) != count($boards))
972
		$return = false;
973
974
	else
975
	{
976
		$result = true;
977
		while ($row = $smcFunc['db_fetch_assoc']($request))
978
			$result &= !empty($row['add_deny']);
979
		$smcFunc['db_free_result']($request);
980
		$return = $result;
981
	}
982
983
	// Allow temporary overrides for board permissions?
984
	call_integration_hook('integrate_allowed_to_board', array(&$return, $permission, $boards, $any));
985
986
	$perm_cache[$cache_key] = $return;
987
988
	// If the query returned 1, they can do it... otherwise, they can't.
989
	return $return;
990
}
991
992
/**
993
 * Fatal error if they cannot.
994
 * Uses allowedTo() to check if the user is allowed to do permission.
995
 * Checks the passed boards or current board for the permission.
996
 * If $any is true, the user only needs permission on at least one of the boards to pass
997
 * If they are not, it loads the Errors language file and shows an error using $txt['cannot_' . $permission].
998
 * If they are a guest and cannot do it, this calls is_not_guest().
999
 *
1000
 * @param string|array $permission A single permission to check or an array of permissions to check
1001
 * @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)
1002
 * @param bool $any Whether to check for permission on at least one board instead of all boards
1003
 */
1004
function isAllowedTo($permission, $boards = null, $any = false)
1005
{
1006
	global $user_info, $txt;
1007
1008
	$heavy_permissions = array(
1009
		'admin_forum',
1010
		'manage_attachments',
1011
		'manage_smileys',
1012
		'manage_boards',
1013
		'edit_news',
1014
		'moderate_forum',
1015
		'manage_bans',
1016
		'manage_membergroups',
1017
		'manage_permissions',
1018
	);
1019
1020
	// Make it an array, even if a string was passed.
1021
	$permission = (array) $permission;
1022
1023
	call_integration_hook('integrate_heavy_permissions_session', array(&$heavy_permissions));
1024
1025
	// Check the permission and return an error...
1026
	if (!allowedTo($permission, $boards, $any))
1027
	{
1028
		// Pick the last array entry as the permission shown as the error.
1029
		$error_permission = array_shift($permission);
1030
1031
		// If they are a guest, show a login. (because the error might be gone if they do!)
1032
		if ($user_info['is_guest'])
1033
		{
1034
			loadLanguage('Errors');
1035
			is_not_guest($txt['cannot_' . $error_permission]);
1036
		}
1037
1038
		// Clear the action because they aren't really doing that!
1039
		$_GET['action'] = '';
1040
		$_GET['board'] = '';
1041
		$_GET['topic'] = '';
1042
		writeLog(true);
1043
1044
		fatal_lang_error('cannot_' . $error_permission, false);
1045
1046
		// Getting this far is a really big problem, but let's try our best to prevent any cases...
1047
		die('No direct access...');
1048
	}
1049
1050
	// If you're doing something on behalf of some "heavy" permissions, validate your session.
1051
	// (take out the heavy permissions, and if you can't do anything but those, you need a validated session.)
1052
	if (!allowedTo(array_diff($permission, $heavy_permissions), $boards))
1053
		validateSession();
1054
}
1055
1056
/**
1057
 * Return the boards a user has a certain (board) permission on. (array(0) if all.)
1058
 *  - returns a list of boards on which the user is allowed to do the specified permission.
1059
 *  - returns an array with only a 0 in it if the user has permission to do this on every board.
1060
 *  - returns an empty array if he or she cannot do this on any board.
1061
 * If check_access is true will also make sure the group has proper access to that board.
1062
 *
1063
 * @param string|array $permissions A single permission to check or an array of permissions to check
1064
 * @param bool $check_access Whether to check only the boards the user has access to
1065
 * @param bool $simple Whether to return a simple array of board IDs or one with permissions as the keys
1066
 * @return array An array of board IDs or an array containing 'permission' => 'board,board2,...' pairs
1067
 */
1068
function boardsAllowedTo($permissions, $check_access = true, $simple = true)
1069
{
1070
	global $user_info, $smcFunc;
1071
1072
	// Arrays are nice, most of the time.
1073
	$permissions = (array) $permissions;
1074
1075
	/*
1076
	 * Set $simple to true to use this function as it were in SMF 2.0.x.
1077
	 * Otherwise, the resultant array becomes split into the multiple
1078
	 * permissions that were passed. Other than that, it's just the normal
1079
	 * state of play that you're used to.
1080
	 */
1081
1082
	// Administrators are all powerful, sorry.
1083
	if ($user_info['is_admin'])
1084
	{
1085
		if ($simple)
1086
			return array(0);
1087
		else
1088
		{
1089
			$boards = array();
1090
			foreach ($permissions as $permission)
1091
				$boards[$permission] = array(0);
1092
1093
			return $boards;
1094
		}
1095
	}
1096
1097
	// All groups the user is in except 'moderator'.
1098
	$groups = array_diff($user_info['groups'], array(3));
1099
1100
	$request = $smcFunc['db_query']('', '
1101
		SELECT b.id_board, bp.add_deny' . ($simple ? '' : ', bp.permission') . '
1102
		FROM {db_prefix}board_permissions AS bp
1103
			INNER JOIN {db_prefix}boards AS b ON (b.id_profile = bp.id_profile)
1104
			LEFT JOIN {db_prefix}moderators AS mods ON (mods.id_board = b.id_board AND mods.id_member = {int:current_member})
1105
			LEFT JOIN {db_prefix}moderator_groups AS modgs ON (modgs.id_board = b.id_board AND modgs.id_group IN ({array_int:group_list}))
1106
		WHERE bp.id_group IN ({array_int:group_list}, {int:moderator_group})
1107
			AND bp.permission IN ({array_string:permissions})
1108
			AND (mods.id_member IS NOT NULL OR modgs.id_group IS NOT NULL OR bp.id_group != {int:moderator_group})' .
1109
			($check_access ? ' AND {query_see_board}' : '') . '
1110
		ORDER BY b.board_order',
1111
		array(
1112
			'current_member' => $user_info['id'],
1113
			'group_list' => $groups,
1114
			'moderator_group' => 3,
1115
			'permissions' => $permissions,
1116
		)
1117
	);
1118
	$boards = array();
1119
	$deny_boards = array();
1120
	while ($row = $smcFunc['db_fetch_assoc']($request))
1121
	{
1122
		if ($simple)
1123
		{
1124
			if (empty($row['add_deny']))
1125
				$deny_boards[] = $row['id_board'];
1126
			else
1127
				$boards[] = $row['id_board'];
1128
		}
1129
		else
1130
		{
1131
			if (empty($row['add_deny']))
1132
				$deny_boards[$row['permission']][] = $row['id_board'];
1133
			else
1134
				$boards[$row['permission']][] = $row['id_board'];
1135
		}
1136
	}
1137
	$smcFunc['db_free_result']($request);
1138
1139
	if ($simple)
1140
		$boards = array_unique(array_values(array_diff($boards, $deny_boards)));
1141
	else
1142
	{
1143
		foreach ($permissions as $permission)
1144
		{
1145
			// never had it to start with
1146
			if (empty($boards[$permission]))
1147
				$boards[$permission] = array();
1148
			else
1149
			{
1150
				// Or it may have been removed
1151
				$deny_boards[$permission] = isset($deny_boards[$permission]) ? $deny_boards[$permission] : array();
1152
				$boards[$permission] = array_unique(array_values(array_diff($boards[$permission], $deny_boards[$permission])));
1153
			}
1154
		}
1155
	}
1156
1157
	// Maybe a mod needs to tweak the list of allowed boards on the fly?
1158
	call_integration_hook('integrate_boards_allowed_to', array(&$boards, $deny_boards, $permissions, $check_access, $simple));
1159
1160
	return $boards;
1161
}
1162
1163
/**
1164
 * This function attempts to protect from spammed messages and the like.
1165
 * The time taken depends on error_type - generally uses the modSetting.
1166
 *
1167
 * @param string $error_type The error type. Also used as a $txt index (not an actual string).
1168
 * @param boolean $only_return_result Whether you want the function to die with a fatal_lang_error.
1169
 * @return bool Whether they've posted within the limit
1170
 */
1171
function spamProtection($error_type, $only_return_result = false)
1172
{
1173
	global $modSettings, $user_info, $smcFunc;
1174
1175
	// Certain types take less/more time.
1176
	$timeOverrides = array(
1177
		'login' => 2,
1178
		'register' => 2,
1179
		'remind' => 30,
1180
		'sendmail' => $modSettings['spamWaitTime'] * 5,
1181
		'reporttm' => $modSettings['spamWaitTime'] * 4,
1182
		'search' => !empty($modSettings['search_floodcontrol_time']) ? $modSettings['search_floodcontrol_time'] : 1,
1183
	);
1184
1185
	call_integration_hook('integrate_spam_protection', array(&$timeOverrides));
1186
1187
	// Moderators are free...
1188
	if (!allowedTo('moderate_board'))
1189
		$timeLimit = isset($timeOverrides[$error_type]) ? $timeOverrides[$error_type] : $modSettings['spamWaitTime'];
1190
	else
1191
		$timeLimit = 2;
1192
1193
	// Delete old entries...
1194
	$smcFunc['db_query']('', '
1195
		DELETE FROM {db_prefix}log_floodcontrol
1196
		WHERE log_time < {int:log_time}
1197
			AND log_type = {string:log_type}',
1198
		array(
1199
			'log_time' => time() - $timeLimit,
1200
			'log_type' => $error_type,
1201
		)
1202
	);
1203
1204
	// Add a new entry, deleting the old if necessary.
1205
	$smcFunc['db_insert']('replace',
1206
		'{db_prefix}log_floodcontrol',
1207
		array('ip' => 'inet', 'log_time' => 'int', 'log_type' => 'string'),
1208
		array($user_info['ip'], time(), $error_type),
1209
		array('ip', 'log_type')
1210
	);
1211
1212
	// If affected is 0 or 2, it was there already.
1213
	if ($smcFunc['db_affected_rows']() != 1)
1214
	{
1215
		// Spammer!  You only have to wait a *few* seconds!
1216
		if (!$only_return_result)
1217
			fatal_lang_error($error_type . '_WaitTime_broken', false, array($timeLimit));
1218
1219
		return true;
1220
	}
1221
1222
	// They haven't posted within the limit.
1223
	return false;
1224
}
1225
1226
/**
1227
 * A generic function to create a pair of index.php and .htaccess files in a directory
1228
 *
1229
 * @param string|array $paths The (absolute) directory path
1230
 * @param boolean $attachments Whether this is an attachment directory
1231
 * @return bool|array True on success an array of errors if anything fails
1232
 */
1233
function secureDirectory($paths, $attachments = false)
1234
{
1235
	$errors = array();
1236
1237
	// Work with arrays
1238
	$paths = (array) $paths;
1239
1240
	if (empty($paths))
1241
		$errors[] = 'empty_path';
1242
1243
	if (!empty($errors))
1244
		return $errors;
1245
1246
	foreach ($paths as $path)
1247
	{
1248
		if (!is_writable($path))
1249
		{
1250
			$errors[] = 'path_not_writable';
1251
1252
			continue;
1253
		}
1254
1255
		$directory_name = basename($path);
1256
1257
		$close = empty($attachments) ? '
1258
</Files>' : '
1259
	Allow from localhost
1260
</Files>
1261
1262
RemoveHandler .php .php3 .phtml .cgi .fcgi .pl .fpl .shtml';
1263
1264
		if (file_exists($path . '/.htaccess'))
1265
		{
1266
			$errors[] = 'htaccess_exists';
1267
1268
			continue;
1269
		}
1270
1271
		else
1272
		{
1273
			$fh = @fopen($path . '/.htaccess', 'w');
1274
1275
			if ($fh)
1276
			{
1277
				fwrite($fh, '<Files *>
1278
	Order Deny,Allow
1279
	Deny from all' . $close);
1280
				fclose($fh);
1281
			}
1282
1283
			else
1284
				$errors[] = 'htaccess_cannot_create_file';
1285
		}
1286
1287
		if (file_exists($path . '/index.php'))
1288
		{
1289
			$errors[] = 'index-php_exists';
1290
1291
			continue;
1292
		}
1293
1294
		else
1295
		{
1296
			$fh = @fopen($path . '/index.php', 'w');
1297
1298
			if ($fh)
1299
			{
1300
				fwrite($fh, '<' . '?php
1301
1302
/**
1303
 * This file is here solely to protect your ' . $directory_name . ' directory.
1304
 */
1305
1306
// Look for Settings.php....
1307
if (file_exists(dirname(dirname(__FILE__)) . \'/Settings.php\'))
1308
{
1309
	// Found it!
1310
	require(dirname(dirname(__FILE__)) . \'/Settings.php\');
1311
	header(\'location: \' . $boardurl);
1312
}
1313
// Can\'t find it... just forget it.
1314
else
1315
	exit;
1316
1317
?' . '>');
1318
				fclose($fh);
1319
			}
1320
1321
			else
1322
				$errors[] = 'index-php_cannot_create_file';
1323
		}
1324
	}
1325
1326
	if (!empty($errors))
1327
		return $errors;
1328
1329
	else
1330
		return true;
1331
}
1332
1333
/**
1334
 * This sets the X-Frame-Options header.
1335
 *
1336
 * @param string $override An option to override (either 'SAMEORIGIN' or 'DENY')
1337
 * @since 2.1
1338
 */
1339
function frameOptionsHeader($override = null)
1340
{
1341
	global $modSettings;
1342
1343
	$option = 'SAMEORIGIN';
1344
	if (is_null($override) && !empty($modSettings['frame_security']))
1345
		$option = $modSettings['frame_security'];
1346
	elseif (in_array($override, array('SAMEORIGIN', 'DENY')))
1347
		$option = $override;
1348
1349
	// Don't bother setting the header if we have disabled it.
1350
	if ($option == 'DISABLE')
1351
		return;
1352
1353
	// Finally set it.
1354
	header('x-frame-options: ' . $option);
1355
1356
	// And some other useful ones.
1357
	header('x-xss-protection: 1');
1358
	header('x-content-type-options: nosniff');
1359
}
1360
1361
/**
1362
 * This sets the Access-Control-Allow-Origin header.
1363
 * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
1364
 *
1365
 * @param bool $set_header (Default: true): When false, we will do the logic, but not send the headers.  The relevant logic is still saved in the $context and can be sent manually.
1366
 *
1367
 * @since 2.1
1368
 */
1369
function corsPolicyHeader($set_header = true)
1370
{
1371
	global $boardurl, $modSettings, $context;
1372
1373
	if (empty($modSettings['allow_cors']) || empty($_SERVER['HTTP_ORIGIN']))
1374
		return;
1375
1376
	foreach (array('origin' => $_SERVER['HTTP_ORIGIN'], 'boardurl_parts' => $boardurl) as $var => $url)
1377
	{
1378
		// Convert any Punycode to Unicode for the sake of comparision, then parse.
1379
		$$var = parse_iri(url_to_iri((string) validate_iri(normalize_iri(trim($url)))));
1380
	}
1381
1382
	// The admin wants weak security... :(
1383
	if (!empty($modSettings['cors_domains']) && $modSettings['cors_domains'] === '*')
1384
	{
1385
		$context['cors_domain'] = '*';
1386
		$context['valid_cors_found'] = 'wildcard';
1387
	}
1388
1389
	// Oh good, the admin cares about security. :)
1390
	else
1391
	{
1392
		$i = 0;
1393
1394
		// Build our list of allowed CORS origins.
1395
		$allowed_origins = array();
1396
1397
		// If subdomain-independent cookies are on, allow CORS requests from subdomains.
1398
		if (!empty($modSettings['globalCookies']) && !empty($modSettings['globalCookiesDomain']))
1399
		{
1400
			$allowed_origins[++$i] = array_merge(parse_iri('//*.' . trim($modSettings['globalCookiesDomain'])), array('type' => 'subdomain'));
1401
		}
1402
1403
		// Support forum_alias_urls as well, since those are supported by our login cookie.
1404
		if (!empty($modSettings['forum_alias_urls']))
1405
		{
1406
			foreach (explode(',', $modSettings['forum_alias_urls']) as $alias)
1407
				$allowed_origins[++$i] = array_merge(parse_iri((strpos($alias, '//') === false ? '//' : '') . trim($alias)), array('type' => 'alias'));
1408
		}
1409
1410
		// Additional CORS domains.
1411
		if (!empty($modSettings['cors_domains']))
1412
		{
1413
			foreach (explode(',', $modSettings['cors_domains']) as $cors_domain)
1414
			{
1415
				$allowed_origins[++$i] = array_merge(parse_iri((strpos($cors_domain, '//') === false ? '//' : '') . trim($cors_domain)), array('type' => 'additional'));
1416
1417
				if (strpos($allowed_origins[$i]['host'], '*') === 0)
1418
					 $allowed_origins[$i]['type'] .= '_wildcard';
1419
			}
1420
		}
1421
1422
		// Does the origin match any of our allowed domains?
1423
		foreach ($allowed_origins as $allowed_origin)
1424
		{
1425
			// If a specific scheme is required, it must match.
1426
			if (!empty($allowed_origin['scheme']) && $allowed_origin['scheme'] !== $origin['scheme'])
1427
				continue;
1428
1429
			// If a specific port is required, it must match.
1430
			if (!empty($allowed_origin['port']))
1431
			{
1432
				// Automatically supply the default port for the "special" schemes.
1433
				// See https://url.spec.whatwg.org/#special-scheme
1434
				if (empty($origin['port']))
1435
				{
1436
					switch ($origin['scheme'])
1437
					{
1438
						case 'http':
1439
						case 'ws':
1440
							$origin['port'] = 80;
1441
							break;
1442
1443
						case 'https':
1444
						case 'wss':
1445
							$origin['port'] = 443;
1446
							break;
1447
1448
						case 'ftp':
1449
							$origin['port'] = 21;
1450
							break;
1451
1452
						case 'file':
1453
						default:
1454
							$origin['port'] = null;
1455
							break;
1456
					}
1457
				}
1458
1459
				if ((int) $allowed_origin['port'] !== (int) $origin['port'])
1460
					continue;
1461
			}
1462
1463
			// Wildcard can only be the first character.
1464
			if (strrpos($allowed_origin['host'], '*') > 0)
1465
				continue;
1466
1467
			// Wildcard means allow the domain or any subdomains.
1468
			if (strpos($allowed_origin['host'], '*') === 0)
1469
				$host_regex = '(?:^|\.)' . preg_quote(ltrim($allowed_origin['host'], '*.'), '~') . '$';
1470
1471
			// No wildcard means allow the domain only.
1472
			else
1473
				$host_regex = '^' . preg_quote($allowed_origin['host'], '~') . '$';
1474
1475
			if (preg_match('~' . $host_regex . '~u', $origin['host']))
1476
			{
1477
				$context['cors_domain'] = trim($_SERVER['HTTP_ORIGIN']);
1478
				$context['valid_cors_found'] = $allowed_origin['type'];
1479
				break;
1480
			}
1481
		}
1482
	}
1483
1484
	// The default is just to place the root URL of the forum into the policy.
1485
	if (empty($context['cors_domain']))
1486
	{
1487
		$context['cors_domain'] = iri_to_url($boardurl_parts['scheme'] . '://' . $boardurl_parts['host']);
1488
1489
		// Attach the port if needed.
1490
		if (!empty($boardurl_parts['port']))
1491
			$context['cors_domain'] .= ':' . $boardurl_parts['port'];
1492
1493
		$context['valid_cors_found'] = 'same';
1494
	}
1495
1496
	$context['cors_headers'] = 'X-SMF-AJAX';
1497
1498
	// Any additional headers?
1499
	if (!empty($modSettings['cors_headers']))
1500
	{
1501
		// Cleanup any typos.
1502
		$cors_headers = explode(',', $modSettings['cors_headers']);
1503
		foreach ($cors_headers as &$ch)
1504
			$ch = str_replace(' ', '-', trim($ch));
1505
1506
		$context['cors_headers'] .= ',' . implode(',', $cors_headers);
1507
	}
1508
1509
	// Allowing Cross-Origin Resource Sharing (CORS).
1510
	if ($set_header && !empty($context['valid_cors_found']) && !empty($context['cors_domain']))
1511
	{
1512
		header('Access-Control-Allow-Origin: ' . $context['cors_domain']);
1513
		header('Access-Control-Allow-Headers: ' . $context['cors_headers']);
1514
1515
		// Be careful with this, you're allowing an external site to allow the browser to send cookies with this.
1516
		if (!empty($modSettings['allow_cors_credentials']))
1517
			header('Access-Control-Allow-Credentials: true');
1518
	}
1519
}
1520
1521
?>