Issues (1686)

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
 * @package   ElkArte Forum
8
 * @copyright ElkArte Forum contributors
9
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause (see accompanying LICENSE.txt file)
10
 *
11
 * This file contains code covered by:
12
 * copyright: 2011 Simple Machines (http://www.simplemachines.org)
13
 *
14
 * @version 2.0 dev
15
 *
16
 */
17
18
use ElkArte\Cache\Cache;
19
use ElkArte\Controller\Auth;
20
use ElkArte\EventManager;
21
use ElkArte\Helper\FileFunctions;
22
use ElkArte\Helper\TokenHash;
23
use ElkArte\Helper\Util;
24
use ElkArte\Http\Headers;
25
use ElkArte\Languages\Txt;
26
use ElkArte\Request;
27
use ElkArte\User;
28
29
/**
30
 * Check if the user is who they say they are.
31
 *
32
 * What it does:
33
 *
34
 * - This function makes sure the user is who they claim to be by requiring a
35
 * password to be typed in every hour.
36
 * - This check can be turned on and off by the securityDisable setting.
37
 * - Uses the adminLogin() function of subs/Auth.subs.php if they need to login,
38
 * which saves all request (POST and GET) data.
39
 *
40
 * @event integrate_validateSession Called at start of validateSession
41
 *
42
 * @param string $type = admin
43
 *
44
 * @return bool|string
45 8
 */
46
function validateSession($type = 'admin')
47
{
48 8
	global $modSettings;
49
50
	// Guests are not welcome here.
51 8
	is_not_guest();
52 8
53 8
	// Validate what type of session check this is.
54
	$types = [];
55
	call_integration_hook('integrate_validateSession', [&$types]);
56 8
	$type = in_array($type, $types, true) || $type === 'moderate' ? $type : 'admin';
57
58 8
	// Set the lifetime for our admin session. Default is ten minutes.
59
	$refreshTime = 10;
60
61 8
	if (isset($modSettings['admin_session_lifetime']))
62
	{
63
		// Maybe someone is paranoid or mistakenly misconfigured the param? Give them at least 5 minutes.
64
		if ($modSettings['admin_session_lifetime'] < 5)
65
		{
66
			$refreshTime = 5;
67 8
		}
68
69
		// A whole day should be more than enough..
70
		elseif ($modSettings['admin_session_lifetime'] > 14400)
71
		{
72
			$refreshTime = 14400;
73
		}
74
75 8
		// We are between our internal min and max. Let's keep the board owner's value.
76
		else
77
		{
78
			$refreshTime = $modSettings['admin_session_lifetime'];
79
		}
80 8
	}
81
82
	// If we're using XML give an additional ten minutes grace as an admin can't log on in XML mode.
83
	if (isset($_GET['api']) && $_GET['api'] === 'xml')
84
	{
85 8
		$refreshTime += 10;
86
	}
87
88
	$refreshTime *= 60;
89 8
90
	// Is the security option off?
91 6
	// @todo remove the exception (means update the db as well)
92
	if (!empty($modSettings['securityDisable' . ($type !== 'admin' ? '_' . $type : '')]))
93
	{
94
		return true;
95 2
	}
96
97 2
	// If their admin or moderator session hasn't expired yet, let it pass, let the admin session trump a moderation one as well
98
	if ((!empty($_SESSION[$type . '_time']) && $_SESSION[$type . '_time'] + $refreshTime >= time()) || (!empty($_SESSION['admin_time']) && $_SESSION['admin_time'] + $refreshTime >= time()))
99
	{
100
		return true;
101
	}
102
103
	require_once(SUBSDIR . '/Auth.subs.php');
104
105
	// Coming from the login screen
106
	if (isset($_POST[$type . '_pass']) || isset($_POST[$type . '_hash_pass']))
107
	{
108
		checkSession();
109
		validateToken('admin-login');
110
111
		// Hashed password, ahoy!
112
		if (isset($_POST[$type . '_hash_pass']) && strlen($_POST[$type . '_hash_pass']) === 64
113
			&& checkPassword($type, true))
114
		{
115
			return true;
116
		}
117
118
		// Posting the password... check it.
119
		if (isset($_POST[$type . '_pass']) && str_replace('*', '', $_POST[$type . '_pass']) !== '' && checkPassword($type))
120
		{
121
			return true;
122
		}
123
	}
124
125
	// Better be sure to remember the real referer
126
	if (empty($_SESSION['request_referer']))
127
	{
128
		$_SESSION['request_referer'] = $_SERVER['HTTP_REFERER'] ?? '';
129
	}
130
	elseif (empty($_POST))
131
	{
132
		unset($_SESSION['request_referer']);
133
	}
134
135
	// Need to type in a password for that, man.
136
	if (!isset($_GET['api']))
137
	{
138
		adminLogin($type);
139
	}
140
141
	return 'session_verify_fail';
142
}
143
144
/**
145
 * Validates a supplied password is correct
146
 *
147
 * What it does:
148
 *
149
 * - Uses integration function to verify password is enabled
150
 * - Uses validateLoginPassword to check using standard ElkArte methods
151
 *
152
 * @event integrate_verify_password allows integration to verify the password
153
 * @param string $type
154
 * @param bool $hash if the supplied password is in _hash_pass
155
 *
156
 * @return bool
157
 */
158
function checkPassword($type, $hash = false)
159
{
160
	$password = $_POST[$type . ($hash ? '_hash_pass' : '_pass')];
161
162
	// Allow integration to verify the password
163
	$good_password = in_array(true, call_integration_hook('integrate_verify_password', [User::$info->username, $password, $hash]), true);
164
165
	// Password correct?
166
	if ($good_password || validateLoginPassword($password, User::$info->passwd, $hash ? '' : User::$info->username))
167
	{
168
		$_SESSION[$type . '_time'] = time();
169
		unset($_SESSION['request_referer']);
170
171
		return true;
172
	}
173
174
	return false;
175
}
176
177
/**
178
 * Require a user who is logged in. (not a guest.)
179
 *
180
 * What it does:
181
 *
182
 * - Checks if the user is currently a guest, and if so asks them to login with a message telling them why.
183
 * - Message is what to tell them when asking them to login.
184
 *
185
 * @param string $message = ''
186
 * @param bool $is_fatal = true
187
 *
188
 * @return bool
189
 */
190
function is_not_guest($message = '', $is_fatal = true)
191
{
192
	global $txt, $context, $scripturl;
193
194
	// Luckily, this person isn't a guest.
195
	if (isset(User::$info->is_guest) && User::$info->is_guest === false)
196
	{
197
		return true;
198
	}
199
200
	// People always worry when they see people doing things they aren't actually doing...
201
	$_GET['action'] = '';
202
	$_GET['board'] = '';
203
	$_GET['topic'] = '';
204
	writeLog(true);
205
206
	// Just die.
207 14
	if ((isset($_REQUEST['api']) && $_REQUEST['api'] === 'xml') || !$is_fatal)
208
	{
209
		obExit(false);
210 14
	}
211
212 14
	// Attempt to detect if they came from dlattach.
213
	if (ELK !== 'SSI' && empty($context['theme_loaded']))
214
	{
215
		new ElkArte\Themes\ThemeLoader();
216
	}
217
218
	// Never redirect to an attachment
219
	if (validLoginUrl($_SERVER['REQUEST_URL']))
220
	{
221
		$_SESSION['login_url'] = $_SERVER['REQUEST_URL'];
222
	}
223
224
	// Load the Login template and language file.
225
	Txt::load('Login');
226
227
	// Apparently we're not in a position to handle this now. Let's go to a safer location for now.
228
	if (!theme()->getLayers()->hasLayers())
229
	{
230
		$_SESSION['login_url'] = $scripturl . '?' . $_SERVER['QUERY_STRING'];
231
		redirectexit('action=login');
232
	}
233
	elseif (isset($_GET['api']))
234
	{
235
		return false;
236
	}
237
	else
238
	{
239
		theme()->getTemplates()->load('Login');
240
		createToken('login');
241
		$context['sub_template'] = 'kick_guest';
242
		$context['robot_no_index'] = true;
243
	}
244
245
	// Use the kick_guest sub template...
246
	$context['kick_message'] = $message;
247
	$context['page_title'] = $txt['login'];
248
	$context['default_password'] = '';
249
250
	obExit();
251
252
	// We should never get to this point, but if we did we wouldn't know the user isn't a guest.
253
	trigger_error('Hacking attempt...', E_USER_ERROR);
254
}
255
256
/**
257
 * Apply restrictions for banned users. For example, disallow access.
258
 *
259
 * What it does:
260
 *
261
 * - If the user is banned, it dies with an error.
262
 * - Caches this information for optimization purposes.
263
 * - Forces a recheck if force_check is true.
264
 *
265
 * @param bool $forceCheck = false
266
 *
267
 * @throws \ElkArte\Exceptions\Exception
268
 */
269
function is_not_banned($forceCheck = false)
270
{
271
	global $txt, $modSettings, $cookiename;
272
273
	$db = database();
274
275
	// You cannot be banned if you are an admin - doesn't help if you log out.
276
	if (User::$info->is_admin)
277
	{
278
		return;
279
	}
280
281
	// Only check the ban every so often. (to reduce load.)
282
	if ($forceCheck
283
		|| !isset($_SESSION['ban'])
284
		|| empty($modSettings['banLastUpdated'])
285
		|| ($_SESSION['ban']['last_checked'] < $modSettings['banLastUpdated'])
286
		|| $_SESSION['ban']['id_member'] !== User::$info->id
287
		|| $_SESSION['ban']['ip'] !== User::$info->ip
288
		|| $_SESSION['ban']['ip2'] !== User::$info->ip2
289
		|| (isset(User::$info->email) && $_SESSION['ban']['email'] !== User::$info->email))
290
	{
291
		// Innocent until proven guilty.  (but we know you are! :P)
292
		$_SESSION['ban'] = [
293
			'last_checked' => time(),
294
			'id_member' => User::$info->id,
295
			'ip' => User::$info->ip,
296
			'ip2' => User::$info->ip2,
297
			'email' => User::$info->email,
298
		];
299
300
		$ban_query = [];
301
		$ban_query_vars = ['current_time' => time()];
302
		$flag_is_activated = false;
303
304
		// Check both IP addresses.
305
		foreach (['ip', 'ip2'] as $ip_number)
306
		{
307
			if ($ip_number === 'ip2' && User::$info->ip2 === User::$info->ip)
308
			{
309
				continue;
310
			}
311
312
			$ban_query[] = constructBanQueryIP(User::$info->{$ip_number});
313
314
			// IP was valid, maybe there's also a hostname...
315
			if (empty($modSettings['disableHostnameLookup']) && User::$info->{$ip_number} !== 'unknown')
316
			{
317
				$hostname = host_from_ip(User::$info->{$ip_number});
318
				if ($hostname !== '')
319
				{
320
					$ban_query[] = '({string:hostname} LIKE bi.hostname)';
321
					$ban_query_vars['hostname'] = $hostname;
322
				}
323
			}
324
		}
325
326
		// Is their email address banned?
327
		if (User::$info->email !== '')
328
		{
329
			$ban_query[] = '({string:email} LIKE bi.email_address)';
330
			$ban_query_vars['email'] = User::$info->email;
331
		}
332
333
		// How about this user?
334
		if (User::$info->is_guest === false && !empty(User::$info->id))
335
		{
336
			$ban_query[] = 'bi.id_member = {int:id_member}';
337
			$ban_query_vars['id_member'] = User::$info->id;
338
		}
339
340
		// Check the ban, if there's information.
341
		if (!empty($ban_query))
342
		{
343
			$restrictions = [
344
				'cannot_access',
345
				'cannot_login',
346
				'cannot_post',
347
				'cannot_register',
348
			];
349
			$db->fetchQuery('
350
				SELECT 
351
					bi.id_ban, bi.email_address, bi.id_member, bg.cannot_access, bg.cannot_register,
352
					bg.cannot_post, bg.cannot_login, bg.reason, COALESCE(bg.expire_time, 0) AS expire_time
353
				FROM {db_prefix}ban_items AS bi
354
					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}))
355
				WHERE
356
					(' . implode(' OR ', $ban_query) . ')',
357
				$ban_query_vars
358
			)->fetch_callback(
359
				static function ($row) use ($restrictions, &$flag_is_activated) {
360
					// Store every type of ban that applies to you in your session.
361
					foreach ($restrictions as $restriction)
362
					{
363
						if (!empty($row[$restriction]))
364
						{
365
							$_SESSION['ban'][$restriction]['reason'] = $row['reason'];
366
							$_SESSION['ban'][$restriction]['ids'][] = $row['id_ban'];
367
							if (!isset($_SESSION['ban']['expire_time']) || ($_SESSION['ban']['expire_time'] != 0 && ($row['expire_time'] == 0 || $row['expire_time'] > $_SESSION['ban']['expire_time'])))
368
							{
369
								$_SESSION['ban']['expire_time'] = $row['expire_time'];
370
							}
371
372
							if (User::$info->is_guest === false && $restriction === 'cannot_access' && ($row['id_member'] == User::$info->id || $row['email_address'] === User::$info->email))
373
							{
374
								$flag_is_activated = true;
375
							}
376
						}
377
					}
378
				}
379
			);
380
		}
381
382
		// Mark the cannot_access and cannot_post bans as being 'hit'.
383
		if (isset($_SESSION['ban']['cannot_access']) || isset($_SESSION['ban']['cannot_post']) || isset($_SESSION['ban']['cannot_login']))
384
		{
385
			log_ban(array_merge(isset($_SESSION['ban']['cannot_access']) ? $_SESSION['ban']['cannot_access']['ids'] : [], isset($_SESSION['ban']['cannot_post']) ? $_SESSION['ban']['cannot_post']['ids'] : [], isset($_SESSION['ban']['cannot_login']) ? $_SESSION['ban']['cannot_login']['ids'] : []));
386
		}
387
388
		// If for whatever reason the is_activated flag seems wrong, do a little work to clear it up.
389
		if (User::$info->id && ((User::$settings['is_activated'] >= 10 && !$flag_is_activated)
390
				|| (User::$settings['is_activated'] < 10 && $flag_is_activated)))
391
		{
392
			require_once(SUBSDIR . '/Bans.subs.php');
393
			updateBanMembers();
394
		}
395
	}
396
397
	// Hey, I know you! You're ehm...
398
	if (!isset($_SESSION['ban']['cannot_access']) && !empty($_COOKIE[$cookiename . '_']))
399
	{
400
		$bans = explode(',', $_COOKIE[$cookiename . '_']);
401
		foreach ($bans as $key => $value)
402
		{
403
			$bans[$key] = (int) $value;
404
		}
405
406
		$db->fetchQuery('
407
			SELECT 
408
				bi.id_ban, bg.reason
409
			FROM {db_prefix}ban_items AS bi
410
				INNER JOIN {db_prefix}ban_groups AS bg ON (bg.id_ban_group = bi.id_ban_group)
411
			WHERE bi.id_ban IN ({array_int:ban_list})
412
				AND (bg.expire_time IS NULL OR bg.expire_time > {int:current_time})
413
				AND bg.cannot_access = {int:cannot_access}
414
			LIMIT ' . count($bans),
415
			[
416
				'cannot_access' => 1,
417
				'ban_list' => $bans,
418
				'current_time' => time(),
419
			]
420
		)->fetch_callback(
421
			static function ($row) {
422
				$_SESSION['ban']['cannot_access']['ids'][] = $row['id_ban'];
423
				$_SESSION['ban']['cannot_access']['reason'] = $row['reason'];
424
			}
425
		);
426
427
		// My mistake. Next time better.
428
		if (!isset($_SESSION['ban']['cannot_access']))
429
		{
430
			require_once(SUBSDIR . '/Auth.subs.php');
431
			$cookie_url = url_parts(!empty($modSettings['localCookies']), !empty($modSettings['globalCookies']));
432
			elk_setcookie($cookiename . '_', '', time() - 3600, $cookie_url[1], $cookie_url[0], false, false);
433
		}
434
	}
435
436
	// If you're fully banned, it's end of the story for you.
437
	if (isset($_SESSION['ban']['cannot_access']))
438
	{
439
		require_once(SUBSDIR . '/Auth.subs.php');
440
441
		// We don't wanna see you!
442
		if (User::$info->is_guest === false)
443
		{
444
			$controller = new Auth(new EventManager());
445
			$controller->setUser(User::$info);
446
			$controller->action_logout(true, false);
447
		}
448
449
		// 'Log' the user out.  Can't have any funny business... (save the name!)
450
		$old_name = (string) User::$info->name !== '' ? User::$info->name : $txt['guest_title'];
451
		User::logOutUser(true);
452
		loadUserContext();
453
454
		// A goodbye present.
455
		$cookie_url = url_parts(!empty($modSettings['localCookies']), !empty($modSettings['globalCookies']));
456
		elk_setcookie($cookiename . '_', implode(',', $_SESSION['ban']['cannot_access']['ids']), time() + 3153600, $cookie_url[1], $cookie_url[0], false, false);
457
458
		// Don't scare anyone, now.
459
		$_GET['action'] = '';
460
		$_GET['board'] = '';
461
		$_GET['topic'] = '';
462
		writeLog(true);
463
464
		// You banned, sucka!
465
		throw new \ElkArte\Exceptions\Exception(sprintf($txt['your_ban'], $old_name) . (empty($_SESSION['ban']['cannot_access']['reason']) ? '' : '<br />' . $_SESSION['ban']['cannot_access']['reason']) . '<br />' . (empty($_SESSION['ban']['expire_time']) ? $txt['your_ban_expires_never'] : sprintf($txt['your_ban_expires'], standardTime($_SESSION['ban']['expire_time'], false))), 'user');
466
	}
467
468
	// You're not allowed to log in but yet you are. Let's fix that.
469
	if (isset($_SESSION['ban']['cannot_login']) && User::$info->is_guest === false)
470
	{
471
		// We don't wanna see you!
472
		require_once(SUBSDIR . '/Logging.subs.php');
473
		deleteMemberLogOnline();
474
475
		// 'Log' the user out.  Can't have any funny business... (save the name!)
476
		$old_name = (string) User::$info->name !== '' ? User::$info->name : $txt['guest_title'];
477
		User::logOutUser(true);
478
		loadUserContext();
479
480
		// Wipe 'n Clean(r) erases all traces.
481
		$_GET['action'] = '';
482
		$_GET['board'] = '';
483
		$_GET['topic'] = '';
484
		writeLog(true);
485
486
		// Log them out
487
		$controller = new Auth(new EventManager());
488
		$controller->setUser(User::$info);
489
		$controller->action_logout(true, false);
490
491
		// Tell them thanks
492
		throw new \ElkArte\Exceptions\Exception(sprintf($txt['your_ban'], $old_name) . (empty($_SESSION['ban']['cannot_login']['reason']) ? '' : '<br />' . $_SESSION['ban']['cannot_login']['reason']) . '<br />' . (empty($_SESSION['ban']['expire_time']) ? $txt['your_ban_expires_never'] : sprintf($txt['your_ban_expires'], standardTime($_SESSION['ban']['expire_time'], false))) . '<br />' . $txt['ban_continue_browse'], 'user');
493
	}
494
495
	// Fix up the banning permissions.
496
	if (!property_exists(User::$info, 'permissions'))
497
	{
498
		return;
499
	}
500
501
	if (User::$info->permissions === null)
502
	{
503
		return;
504
	}
505
506
	banPermissions();
507
}
508
509
/**
510
 * Fix permissions according to ban status.
511
 *
512
 * What it does:
513
 *
514
 * - Applies any states of banning by removing permissions the user cannot have.
515
 *
516
 * @event integrate_post_ban_permissions Allows to update denied permissions
517
 * @event integrate_warn_permissions Allows changing of permissions for users on warning moderate
518
 * @package Bans
519
 */
520
function banPermissions()
521 1
{
522
	global $modSettings, $context;
523
524 1
	// Somehow they got here, at least take away all permissions...
525
	if (isset($_SESSION['ban']['cannot_access']))
526
	{
527
		User::$info->permissions = [];
528
	}
529 1
	// Okay, well, you can watch, but don't touch a thing.
530
	elseif (isset($_SESSION['ban']['cannot_post']) || (!empty($modSettings['warning_mute']) && $modSettings['warning_mute'] <= User::$info->warning))
531
	{
532
		$denied_permissions = [
533
			'pm_send',
534
			'calendar_post', 'calendar_edit_own', 'calendar_edit_any',
535
			'poll_post',
536
			'poll_add_own', 'poll_add_any',
537
			'poll_edit_own', 'poll_edit_any',
538
			'poll_lock_own', 'poll_lock_any',
539
			'poll_remove_own', 'poll_remove_any',
540
			'manage_attachments', 'manage_smileys', 'manage_boards', 'admin_forum', 'manage_permissions',
541
			'moderate_forum', 'manage_membergroups', 'manage_bans', 'send_mail', 'edit_news',
542
			'profile_identity_any', 'profile_extra_any', 'profile_title_any',
543
			'post_new', 'post_reply_own', 'post_reply_any',
544
			'delete_own', 'delete_any', 'delete_replies',
545
			'make_sticky',
546
			'merge_any', 'split_any',
547
			'modify_own', 'modify_any', 'modify_replies',
548
			'move_any',
549
			'lock_own', 'lock_any',
550
			'remove_own', 'remove_any',
551
			'post_unapproved_topics', 'post_unapproved_replies_own', 'post_unapproved_replies_any',
552
		];
553
		theme()->getLayers()->addAfter('admin_warning', 'body');
554
555
		call_integration_hook('integrate_post_ban_permissions', [&$denied_permissions]);
556
		User::$info->permissions = array_diff(User::$info->permissions, $denied_permissions);
557
	}
558
	// Are they absolutely under moderation?
559 1
	elseif (!empty($modSettings['warning_moderate']) && $modSettings['warning_moderate'] <= User::$info->warning)
560
	{
561
		// Work out what permissions should change...
562
		$permission_change = [
563
			'post_new' => 'post_unapproved_topics',
564
			'post_reply_own' => 'post_unapproved_replies_own',
565
			'post_reply_any' => 'post_unapproved_replies_any',
566
			'post_attachment' => 'post_unapproved_attachments',
567
		];
568
		call_integration_hook('integrate_warn_permissions', [&$permission_change]);
569
		foreach ($permission_change as $old => $new)
570
		{
571
			if (!in_array($old, User::$info->permissions))
572
			{
573
				unset($permission_change[$old]);
574
			}
575
			else
576
			{
577
				User::$info->permissions = array_merge((array) User::$info->permissions, $new);
578
			}
579
		}
580
581
		User::$info->permissions = array_diff(User::$info->permissions, array_keys($permission_change));
582
	}
583
584
	// @todo Find a better place to call this? Needs to be after permissions loaded!
585 1
	// Finally, some bits we cache in the session because it saves queries.
586
	if (isset($_SESSION['mc']) && $_SESSION['mc']['time'] > $modSettings['settings_updated'] && $_SESSION['mc']['id'] == User::$info->id)
587
	{
588
		User::$info->mod_cache = $_SESSION['mc'];
589
	}
590
	else
591 1
	{
592 1
		require_once(SUBSDIR . '/Auth.subs.php');
593
		rebuildModCache();
594
	}
595
596 1
	// Now that we have the mod cache taken care of lets setup a cache for the number of mod reports still open
597
	if (isset($_SESSION['rc']) && $_SESSION['rc']['time'] > $modSettings['last_mod_report_action'] && $_SESSION['rc']['id'] == User::$info->id)
598
	{
599
		$context['open_mod_reports'] = $_SESSION['rc']['reports'];
600
		if (allowedTo('admin_forum'))
601
		{
602
			$context['open_pm_reports'] = $_SESSION['rc']['pm_reports'];
603
		}
604 1
	}
605
	elseif ($_SESSION['mc']['bq'] != '0=1')
606
	{
607
		require_once(SUBSDIR . '/Moderation.subs.php');
608
		recountOpenReports(true, allowedTo('admin_forum'));
609
	}
610
	else
611 1
	{
612
		$context['open_mod_reports'] = 0;
613 1
	}
614
}
615
616
/**
617
 * Log a ban in the database.
618
 *
619
 * What it does:
620
 *
621
 * - Log the current user in the ban logs.
622
 * - Increment the hit counters for the specified ban ID's (if any.)
623
 *
624
 * @param int[] $ban_ids = array()
625
 * @param string|null $email = null
626
 * @package Bans
627
 */
628
function log_ban($ban_ids = [], $email = null)
629
{
630
	$db = database();
631
632
	// Don't log web accelerators, it's very confusing...
633
	if (isset($_SERVER['HTTP_X_MOZ']) && $_SERVER['HTTP_X_MOZ'] === 'prefetch')
634
	{
635
		return;
636
	}
637
638
	$db->insert('',
639
		'{db_prefix}log_banned',
640
		[
641
			'id_member' => 'int',
642
			'ip' => 'string-16',
643
			'email' => 'string',
644
			'log_time' => 'int'
645
		],
646
		[
647
			User::$info->id,
648
			User::$info->ip,
649
			$email ?? (string) User::$info->email,
650
			time()
651
		],
652
		['id_ban_log']
653
	);
654
655
	// One extra point for these bans.
656
	if (!empty($ban_ids))
657
	{
658
		$db->query('', '
659
			UPDATE {db_prefix}ban_items
660
			SET hits = hits + 1
661
			WHERE id_ban IN ({array_int:ban_ids})',
662
			[
663
				'ban_ids' => $ban_ids,
664
			]
665
		);
666
	}
667
}
668
669
/**
670
 * Checks if a given email address might be banned.
671
 *
672
 * What it does:
673
 *
674
 * - Check if a given email is banned.
675
 * - Performs an immediate ban if the turns turns out positive.
676
 *
677
 * @param string $email
678
 * @param string $restriction
679
 * @param string $error
680
 *
681
 * @throws \ElkArte\Exceptions\Exception
682
 * @package Bans
683
 */
684
function isBannedEmail($email, $restriction, $error)
685
{
686 2
	global $txt;
687
688 2
	$db = database();
689
690
	// Can't ban an empty email
691 2
	if (empty($email) || trim($email) === '')
692
	{
693
		return;
694
	}
695
696
	// Let's start with the bans based on your IP/hostname/memberID...
697 2
	$ban_ids = isset($_SESSION['ban'][$restriction]) ? $_SESSION['ban'][$restriction]['ids'] : [];
698 2
	$ban_reason = isset($_SESSION['ban'][$restriction]) ? $_SESSION['ban'][$restriction]['reason'] : '';
699
700
	// ...and add to that the email address you're trying to register.
701 2
	$db->fetchQuery('
702
		SELECT 
703 2
			bi.id_ban, bg.' . $restriction . ', bg.cannot_access, bg.reason
704
		FROM {db_prefix}ban_items AS bi
705
			INNER JOIN {db_prefix}ban_groups AS bg ON (bg.id_ban_group = bi.id_ban_group)
706
		WHERE {string:email} LIKE bi.email_address
707 2
			AND (bg.' . $restriction . ' = {int:cannot_access} OR bg.cannot_access = {int:cannot_access})
708
			AND (bg.expire_time IS NULL OR bg.expire_time >= {int:now})',
709
		[
710 2
			'email' => $email,
711 2
			'cannot_access' => 1,
712 2
			'now' => time(),
713
		]
714 2
	)->fetch_callback(
715
		static function ($row) use (&$ban_ids, &$ban_reason, $restriction) {
716
			if (!empty($row['cannot_access']))
717
			{
718
				$_SESSION['ban']['cannot_access']['ids'][] = $row['id_ban'];
719
				$_SESSION['ban']['cannot_access']['reason'] = $row['reason'];
720
			}
721
722
			if (!empty($row[$restriction]))
723
			{
724
				$ban_ids[] = $row['id_ban'];
725
				$ban_reason = $row['reason'];
726
			}
727 2
		}
728
	);
729
730
	// You're in biiig trouble.  Banned for the rest of this session!
731 2
	if (isset($_SESSION['ban']['cannot_access']))
732
	{
733
		log_ban($_SESSION['ban']['cannot_access']['ids']);
734
		$_SESSION['ban']['last_checked'] = time();
735
736
		throw new \ElkArte\Exceptions\Exception(sprintf($txt['your_ban'], $txt['guest_title']) . $_SESSION['ban']['cannot_access']['reason'], false);
737
	}
738
739 2
	if (!empty($ban_ids))
740
	{
741
		// Log this ban for future reference.
742
		log_ban($ban_ids, $email);
743
		throw new \ElkArte\Exceptions\Exception($error . $ban_reason, false);
744
	}
745 2
}
746
747
/**
748
 * Make sure the user's correct session was passed, and they came from here.
749
 *
750
 * What it does:
751
 *
752
 * - Checks the current session, verifying that the person is who he or she should be.
753
 * - Also checks the referrer to make sure they didn't get sent here.
754
 * - Depends on the disableCheckUA setting, which is usually missing.
755
 * - Will check GET, POST, or REQUEST depending on the passed type.
756
 * - Also optionally checks the referring action if passed. (note that the referring action must be by GET.)
757
 *
758
 * @param string $type = 'post' (post, get, request)
759
 * @param string $from_action = ''
760
 * @param bool $is_fatal = true
761
 *
762
 * @return string the error message if is_fatal is false.
763
 */
764
function checkSession($type = 'post', $from_action = '', $is_fatal = true)
765
{
766
	global $modSettings, $boardurl;
767 18
768
	// We'll work out user agent checks
769
	$req = Request::instance();
770 18
771
	// Is it in as $_POST['sc']?
772
	if ($type === 'post')
773 18
	{
774
		$check = $_POST[$_SESSION['session_var']] ?? (empty($modSettings['strictSessionCheck']) && isset($_POST['sc']) ? $_POST['sc'] : null);
775 14
		if ($check !== $_SESSION['session_value'])
776 14
		{
777
			$error = 'session_timeout';
778 14
		}
779
	}
780
	// How about $_GET['sesc']?
781
	elseif ($type === 'get')
782 4
	{
783
		$check = $_GET[$_SESSION['session_var']] ?? (empty($modSettings['strictSessionCheck']) && isset($_GET['sesc']) ? $_GET['sesc'] : null);
784 2
		if ($check !== $_SESSION['session_value'])
785 2
		{
786
			$error = 'session_verify_fail';
787 2
		}
788
	}
789
	// Or can it be in either?
790
	elseif ($type === 'request')
791 2
	{
792
		$check = null;
793 2
		if (isset($_GET[$_SESSION['session_var']]))
794
		{
795 2
			$check = $_GET[$_SESSION['session_var']];
796
		}
797
		elseif (empty($modSettings['strictSessionCheck']) && isset($_GET['sesc']))
798
		{
799
			$check = $_GET['sesc'];
800
		}
801
		elseif (isset($_POST[$_SESSION['session_var']]))
802 18
		{
803
			$check = $_POST[$_SESSION['session_var']];
804
		}
805
		elseif (empty($modSettings['strictSessionCheck']) && isset($_POST['sc']))
806
		{
807
			$check = $_POST['sc'];
808 18
		}
809
810
		if ($check !== $_SESSION['session_value'])
811 18
		{
812
			$error = 'session_verify_fail';
813
		}
814
	}
815
816
	// Verify that they aren't changing user agents on us - that could be bad.
817 18
	if ((!isset($_SESSION['USER_AGENT']) || $_SESSION['USER_AGENT'] !== $req->user_agent()) && empty($modSettings['disableCheckUA']))
818
	{
819
		$error = 'session_verify_fail';
820 18
	}
821
822 18
	// Make sure a page with session check requirement is not being prefetched.
823
	stop_prefetching();
824
825
	// Check the referring site - it should be the same server at least!
826
827
	$referrer_url = $_SESSION['request_referer'] ?? ($_SERVER['HTTP_REFERER'] ?? '');
828
829
	$referrer = @parse_url($referrer_url);
830
	if (!empty($referrer['host']))
831
	{
832
		if (strpos($_SERVER['HTTP_HOST'], ':') !== false)
833
		{
834
			$real_host = substr($_SERVER['HTTP_HOST'], 0, strpos($_SERVER['HTTP_HOST'], ':'));
835
		}
836
		else
837
		{
838
			$real_host = $_SERVER['HTTP_HOST'];
839
		}
840
841
		$parsed_url = parse_url($boardurl);
842
843
		// Are global cookies on? If so, let's check them ;).
844
		if (!empty($modSettings['globalCookies']))
845
		{
846
			if (preg_match('~(?:[^\.]+\.)?([^\.]{3,}\..+)\z~i', $parsed_url['host'], $parts) == 1)
847
			{
848
				$parsed_url['host'] = $parts[1];
849
			}
850
851
			if (preg_match('~(?:[^\.]+\.)?([^\.]{3,}\..+)\z~i', $referrer['host'], $parts) == 1)
852
			{
853
				$referrer['host'] = $parts[1];
854
			}
855
856
			if (preg_match('~(?:[^\.]+\.)?([^\.]{3,}\..+)\z~i', $real_host, $parts) == 1)
857
			{
858
				$real_host = $parts[1];
859
			}
860
		}
861
862
		// Okay: referrer must either match parsed_url or real_host.
863
		if (isset($parsed_url['host']) && strtolower($referrer['host']) !== strtolower($parsed_url['host']) && strtolower($referrer['host']) !== strtolower($real_host))
864 18
		{
865
			$error = 'verify_url_fail';
866
			$log_error = true;
867
			$sprintf = [Util::htmlspecialchars($referrer_url)];
868
		}
869
	}
870
871
	// Well, first of all, if a from_action is specified you'd better have an old_url.
872 18
	if (!empty($from_action) && (!isset($_SESSION['old_url']) || preg_match('~[?;&]action=' . $from_action . '([;&]|$)~', $_SESSION['old_url']) !== 1))
873
	{
874 18
		$error = 'verify_url_fail';
875
		$log_error = true;
876
		$sprintf = [Util::htmlspecialchars($referrer_url)];
877
	}
878
879
	// Everything is ok, return an empty string.
880
	if (!isset($error))
881
	{
882
		return '';
883
	}
884
885
	// A session error occurred, show the error.
886
	if ($is_fatal)
887
	{
888
		if (isset($_REQUEST['api']))
889
		{
890
			@ob_end_clean();
891
			Headers::instance()
892
				->removeHeader('all')
893
				->headerSpecial('HTTP/1.1 403 Forbidden - Session timeout')
894
				->sendHeaders();
895
			die;
896
		}
897
		throw new \ElkArte\Exceptions\Exception($error, isset($log_error) ? 'user' : false, $sprintf ?? []);
898
	}
899
	// A session error occurred, return the error to the calling function.
900
	else
901
	{
902
		return $error;
903
	}
904
905
	// We really should never fall through here, for very important reasons.  Let's make sure.
906
	trigger_error('Hacking attempt...', E_USER_ERROR);
907
}
908
909
/**
910
 * Let's give you a token of our appreciation.
911
 *
912
 * What it does:
913
 *
914 13
 * - Creates a one time use form token
915
 *
916
 * @param string $action The specific site action that a token will be generated for
917 13
 * @param string $type = 'post' If the token will be returned via post or get
918 13
 *
919 13
 * @return string[] array of token var, time, csrf, token
920
 */
921
function createToken($action, $type = 'post')
922 13
{
923 13
	global $context;
924
925
	// Generate a new token token_var pair
926 13
	$tokenizer = new TokenHash();
927 13
	$token_var = $tokenizer->generate_hash(rand(7, 12));
928 13
	$token = $tokenizer->generate_hash(32);
929
930 13
	// We need user agent and the client IP
931
	$req = Request::instance();
932
	$csrf_hash = hash('sha1', $token . $req->client_ip() . $req->user_agent());
933
934
	// Save the session token and make it available to the forms
935
	$_SESSION['token'][$type . '-' . $action] = [$token_var, $csrf_hash, time(), $token];
936
	$context[$action . '_token'] = $token;
937
	$context[$action . '_token_var'] = $token_var;
938
939
	return [$action . '_token_var' => $token_var, $action . '_token' => $token];
940
}
941
942
/**
943
 * Only patrons with valid tokens can ride this ride.
944
 *
945
 * What it does:
946
 *
947
 * Validates that the received token is correct
948
 *  1. The token exists in session.
949
 *  2. The {$type} variable should exist.
950
 *  3. We concatenate the variable we received with the user agent
951
 *  4. Match that result against what is in the session.
952
 *  5. If it matches, success, otherwise we fallout.
953
 *
954
 * @param string $action
955 8
 * @param string $type = 'post' (get, request, or post)
956 8
 * @param bool $reset = true Reset the token on failure
957
 * @param bool $fatal if true a fatal_lang_error is issued for invalid tokens, otherwise false is returned
958
 *
959 8
 * @return bool|string except for $action == 'login' where the token is returned
960
 * @throws \ElkArte\Exceptions\Exception token_verify_fail
961 2
 */
962
function validateToken($action, $type = 'post', $reset = true, $fatal = true)
963 2
{
964 2
	$type = ($type === 'get' || $type === 'request') ? $type : 'post';
965
	$token_index = $type . '-' . $action;
966 2
967
	// Logins are special: the token is used to have the password with javascript before POST it
968
	if ($action === 'login')
969
	{
970
		if (isset($_SESSION['token'][$token_index]))
971
		{
972 6
			$return = $_SESSION['token'][$token_index][3];
973
			unset($_SESSION['token'][$token_index]);
974 6
975
			return $return;
976
		}
977
978
		return '';
979
	}
980
981
	if (!isset($_SESSION['token'][$token_index]))
982
	{
983
		return false;
984
	}
985
986
	// We need the user agent and client IP
987
	$req = Request::instance();
988
989
	// Shortcut
990
	$passed_token_var = $GLOBALS['_' . strtoupper($type)][$_SESSION['token'][$token_index][0]] ?? null;
991
	$csrf_hash = hash('sha1', $passed_token_var . $req->client_ip() . $req->user_agent());
992
993
	// Checked what was passed in combination with the user agent
994
	if (isset($passed_token_var)
995
		&& $csrf_hash === $_SESSION['token'][$token_index][1])
996
	{
997
		// Consume the token, let them pass
998
		unset($_SESSION['token'][$token_index]);
999
1000
		return true;
1001
	}
1002
1003
	// Patrons with invalid tokens get the boot.
1004
	if ($reset)
1005
	{
1006
		// Might as well do some cleanup on this.
1007
		cleanTokens();
1008
1009
		// I'm back baby.
1010
		createToken($action, $type);
1011
1012
		if ($fatal)
1013
		{
1014
			throw new \ElkArte\Exceptions\Exception('token_verify_fail', false);
1015
		}
1016
	}
1017
	// You don't get a new token
1018
	else
1019
	{
1020
		// Explicitly remove this token
1021
		unset($_SESSION['token'][$token_index]);
1022
1023
		// Remove older tokens.
1024
		cleanTokens();
1025
	}
1026
1027
	return false;
1028
}
1029
1030
/**
1031
 * Removes old unused tokens from session
1032
 *
1033
 * What it does:
1034
 *
1035 1
 * - Defaults to 3 hours before a token is considered expired
1036
 * - if $complete = true will remove all tokens
1037
 *
1038
 * @param bool $complete = false
1039
 * @param string $suffix = false
1040
 */
1041 1
function cleanTokens($complete = false, $suffix = '')
1042
{
1043 1
	// We appreciate cleaning up after yourselves.
1044
	if (!isset($_SESSION['token']))
1045
	{
1046
		return;
1047
	}
1048
1049 1
	// Clean up tokens, trying to give enough time still.
1050
	foreach ($_SESSION['token'] as $key => $data)
1051
	{
1052 1
		$force = empty($suffix) ? $complete : $complete || strpos($key, $suffix);
1053
1054
		if ($data[2] + 10800 < time() || $force)
1055
		{
1056
			unset($_SESSION['token'][$key]);
1057 1
		}
1058
	}
1059
}
1060
1061
/**
1062
 * Check whether a form has been submitted twice.
1063
 *
1064
 * What it does:
1065
 *
1066
 * - Registers a sequence number for a form.
1067
 * - Checks whether a submitted sequence number is registered in the current session.
1068
 * - Depending on the value of is_fatal shows an error or returns true or false.
1069
 * - Frees a sequence number from the stack after it's been checked.
1070
 * - Frees a sequence number without checking if action == 'free'.
1071
 *
1072
 * @param string $action
1073
 * @param bool $is_fatal = true
1074
 *
1075
 * @return bool|void
1076
 * @throws \ElkArte\Exceptions\Exception error_form_already_submitted
1077
 */
1078 12
function checkSubmitOnce($action, $is_fatal = false)
1079
{
1080 12
	global $context;
1081
1082 12
	if (!isset($_SESSION['forms']))
1083
	{
1084
		$_SESSION['forms'] = [];
1085
	}
1086 12
1087
	// Register a form number and store it in the session stack. (use this on the page that has the form.)
1088 6
	if ($action === 'register')
1089 6
	{
1090 6
		$tokenizer = new TokenHash();
1091
		$context['form_sequence_number'] = '';
1092 6
		while (empty($context['form_sequence_number']) || in_array($context['form_sequence_number'], $_SESSION['forms'], true))
1093
		{
1094
			$context['form_sequence_number'] = $tokenizer->generate_hash();
1095
		}
1096 8
	}
1097
	// Check whether the submitted number can be found in the session.
1098 8
	elseif ($action === 'check')
1099
	{
1100 8
		if (!isset($_REQUEST['seqnum']))
1101
		{
1102
			return true;
1103
		}
1104
1105
		if (!in_array($_REQUEST['seqnum'], $_SESSION['forms'], true))
1106
		{
1107
			// Mark this one as used
1108
			$_SESSION['forms'][] = (string) $_REQUEST['seqnum'];
1109
			return true;
1110
		}
1111
1112
		if ($is_fatal)
1113
		{
1114
			throw new \ElkArte\Exceptions\Exception('error_form_already_submitted', false);
1115
		}
1116
		else
1117
		{
1118
			return false;
1119
		}
1120
	}
1121
	// Don't check, just free the stack number.
1122
	elseif ($action === 'free' && isset($_REQUEST['seqnum']) && in_array($_REQUEST['seqnum'], $_SESSION['forms'], true))
1123
	{
1124
		$_SESSION['forms'] = array_diff($_SESSION['forms'], [$_REQUEST['seqnum']]);
1125
	}
1126
	elseif ($action !== 'free')
1127 6
	{
1128
		trigger_error("checkSubmitOnce(): Invalid action '" . $action . "'", E_USER_WARNING);
1129
	}
1130
}
1131
1132
/**
1133
 * This function checks whether the user is allowed to do permission. (ie. post_new.)
1134
 *
1135
 * What it does:
1136
 *
1137
 * - If boards parameter is specified, checks those boards instead of the current one (if applicable).
1138
 * - Always returns true if the user is an administrator.
1139
 *
1140
 * @param string[]|string $permission permission
1141
 * @param int[]|int|null $boards array of board IDs, a single id or null
1142
 *
1143
 * @return bool if the user can do the permission
1144
 */
1145 330
function allowedTo($permission, $boards = null)
1146
{
1147
	$db = database();
1148 330
1149
	// You're always allowed to do nothing. (unless you're a working man, MR. LAZY :P!)
1150 12
	if (empty($permission))
1151
	{
1152
		return true;
1153
	}
1154 330
1155
	// You're never allowed to do something if your data hasn't been loaded yet!
1156
	if (empty(User::$info) || !isset(User::$info['permissions']))
1157
	{
1158
		return false;
1159
	}
1160 330
1161
	// Administrators are supermen :P.
1162 306
	if (User::$info->is_admin)
1163
	{
1164
		return true;
1165
	}
1166 24
1167
	// Make sure permission is a valid array
1168 22
	if (!is_array($permission))
1169
	{
1170
		$permission = [$permission];
1171
	}
1172 24
1173
	// Are we checking the _current_ board, or some other boards?
1174 24
	if ($boards === null)
1175
	{
1176 12
		if (empty(User::$info->permissions))
1177
		{
1178
			return false;
1179
		}
1180 12
1181
		// Check if they can do it, you aren't allowed, by default.
1182
		return array_intersect($permission, User::$info->permissions) !== [];
1183
	}
1184
1185
	if (!is_array($boards))
1186
	{
1187
		$boards = [$boards];
1188
	}
1189
1190
	if (empty(User::$info->groups))
1191
	{
1192
		return false;
1193
	}
1194
1195
	$request = $db->query('', '
1196
		SELECT 
1197
			MIN(bp.add_deny) AS add_deny
1198
		FROM {db_prefix}boards AS b
1199
			INNER JOIN {db_prefix}board_permissions AS bp ON (bp.id_profile = b.id_profile)
1200
			LEFT JOIN {db_prefix}moderators AS mods ON (mods.id_board = b.id_board AND mods.id_member = {int:current_member})
1201
		WHERE b.id_board IN ({array_int:board_list})
1202
			AND bp.id_group IN ({array_int:group_list}, {int:moderator_group})
1203
			AND bp.permission IN ({array_string:permission_list})
1204
			AND (mods.id_member IS NOT NULL OR bp.id_group != {int:moderator_group})
1205
		GROUP BY b.id_board',
1206
		[
1207
			'current_member' => User::$info->id,
1208
			'board_list' => $boards,
1209
			'group_list' => User::$info->groups,
1210
			'moderator_group' => 3,
1211
			'permission_list' => $permission,
1212
		]
1213
	);
1214
1215
	// Make sure they can do it on all the boards.
1216
	if ($request->num_rows() !== count($boards))
1217
	{
1218
		return false;
1219
	}
1220
1221
	$result = true;
1222
	while (($row = $request->fetch_assoc()))
1223
	{
1224
		$result = $result && !empty($row['add_deny']);
1225
	}
1226
1227
	$request->free_result();
1228
1229
	// If the query returned 1, they can do it... otherwise, they can't.
1230
	return $result;
1231
}
1232
1233
/**
1234
 * This function returns fatal error if the user doesn't have the respective permission.
1235
 *
1236
 * What it does:
1237
 *
1238
 * - Uses allowedTo() to check if the user is allowed to do permission.
1239
 * - Checks the passed boards or current board for the permission.
1240
 * - If they are not, it loads the Errors language file and shows an error using $txt['cannot_' . $permission].
1241
 * - If they are a guest and cannot do it, this calls is_not_guest().
1242
 *
1243
 * @param string[]|string $permission array of or single string, of permissions to check
1244
 * @param int[]|null $boards = null
1245
 *
1246
 * @throws \ElkArte\Exceptions\Exception cannot_xyz where xyz is the permission
1247 68
 */
1248
function isAllowedTo($permission, $boards = null)
1249 68
{
1250
	global $txt;
1251
1252
	static $heavy_permissions = [
1253
		'admin_forum',
1254
		'manage_attachments',
1255
		'manage_smileys',
1256
		'manage_boards',
1257
		'edit_news',
1258
		'moderate_forum',
1259
		'manage_bans',
1260
		'manage_membergroups',
1261
		'manage_permissions',
1262 68
	];
1263
1264
	// Make it an array, even if a string was passed.
1265 68
	$permission = is_array($permission) ? $permission : [$permission];
1266
1267
	// Check the permission and return an error...
1268
	if (!allowedTo($permission, $boards))
1269
	{
1270
		// Pick the last array entry as the permission shown as the error.
1271
		$error_permission = array_shift($permission);
1272
1273
		// If they are a guest, show a login. (because the error might be gone if they do!)
1274
		if (User::$info->is_guest)
1275
		{
1276
			Txt::load('Errors');
1277
			is_not_guest($txt['cannot_' . $error_permission]);
1278
		}
1279
1280
		// Clear the action because they aren't really doing that!
1281
		$_GET['action'] = '';
1282
		$_GET['board'] = '';
1283
		$_GET['topic'] = '';
1284
		writeLog(true);
1285
1286
		throw new \ElkArte\Exceptions\Exception('cannot_' . $error_permission, false);
1287
	}
1288 68
1289
	// If you're doing something on behalf of some "heavy" permissions, validate your session.
1290
	// (take out the heavy permissions, and if you can't do anything but those, you need a validated session.)
1291
	if (!allowedTo(array_diff($permission, $heavy_permissions), $boards))
1292 68
	{
1293
		validateSession();
1294
	}
1295
}
1296
1297
/**
1298
 * Return the boards a user has a certain (board) permission on. (array(0) if all.)
1299
 *
1300
 * What it does:
1301
 *
1302
 * - Returns a list of boards on which the user is allowed to do the specified permission.
1303
 * - Returns an array with only a 0 in it if the user has permission to do this on every board.
1304
 * - Returns an empty array if he or she cannot do this on any board.
1305
 * - If check_access is true will also make sure the group has proper access to that board.
1306
 *
1307
 * @param string[]|string $permissions array of permission names to check access against
1308
 * @param bool $check_access = true
1309
 * @param bool $simple = true Set $simple to true to use this function in compatibility mode
1310
 *             otherwise, the resultant array becomes split into the multiple
1311
 *             permissions that were passed. Other than that, it's just the normal
1312
 *             state of play that you're used to.
1313
 *
1314
 * @return int[]
1315
 * @throws \ElkArte\Exceptions\Exception
1316 3
 */
1317
function boardsAllowedTo($permissions, $check_access = true, $simple = true)
1318
{
1319 3
	$db = database();
1320
1321 3
	// Arrays are nice, most of the time.
1322
	if (!is_array($permissions))
1323
	{
1324
		$permissions = [$permissions];
1325 3
	}
1326
1327 2
	// I am the master, the master of the universe!
1328
	if (User::$info->is_admin)
1329 2
	{
1330
		if ($simple)
1331
		{
1332
			return [0];
1333
		}
1334
1335
		$boards = [];
1336
		foreach ($permissions as $permission)
1337
		{
1338
			$boards[$permission] = [0];
1339
		}
1340
1341
		return $boards;
1342
	}
1343
1344 1
	// All groups the user is in except 'moderator'.
1345
	$groups = array_diff(User::$info->groups, [3]);
0 ignored issues
show
Bug Best Practice introduced by
The property groups does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
1346 1
1347 1
	$boards = [];
1348 1
	$deny_boards = [];
1349
	$db->fetchQuery('
1350 1
		SELECT 
1351
			b.id_board, bp.add_deny' . ($simple ? '' : ', bp.permission') . '
1352
		FROM {db_prefix}board_permissions AS bp
1353
			INNER JOIN {db_prefix}boards AS b ON (b.id_profile = bp.id_profile)
1354
			LEFT JOIN {db_prefix}moderators AS mods ON (mods.id_board = b.id_board AND mods.id_member = {int:current_member})
1355
		WHERE bp.id_group IN ({array_int:group_list}, {int:moderator_group})
1356
			AND bp.permission IN ({array_string:permissions})
1357 1
			AND (mods.id_member IS NOT NULL OR bp.id_group != {int:moderator_group})' .
1358
		($check_access ? ' AND {query_see_board}' : ''),
1359 1
		[
1360 1
			'current_member' => User::$info->id,
1361 1
			'group_list' => $groups,
1362 1
			'moderator_group' => 3,
1363
			'permissions' => $permissions,
1364 1
		]
1365
	)->fetch_callback(
1366
		static function ($row) use ($simple, &$deny_boards, &$boards) {
1367
			if ($simple)
1368
			{
1369
				if (empty($row['add_deny']))
1370
				{
1371
					$deny_boards[] = (int) $row['id_board'];
1372
				}
1373
				else
1374
				{
1375
					$boards[] = (int) $row['id_board'];
1376
				}
1377
			}
1378
			elseif (empty($row['add_deny']))
1379
			{
1380
				$deny_boards[$row['permission']][] = (int) $row['id_board'];
1381
			}
1382
			else
1383
			{
1384
				$boards[$row['permission']][] = (int) $row['id_board'];
1385 1
			}
1386
		}
1387
	);
1388 1
1389
	if ($simple)
1390 1
	{
1391
		$boards = array_unique(array_values(array_diff($boards, $deny_boards)));
1392
	}
1393
	else
1394
	{
1395
		foreach ($permissions as $permission)
1396
		{
1397
			// Never had it to start with
1398
			if (empty($boards[$permission]))
1399
			{
1400
				$boards[$permission] = [];
1401
			}
1402
			else
1403
			{
1404
				// Or it may have been removed
1405
				$deny_boards[$permission] = $deny_boards[$permission] ?? [];
1406
				$boards[$permission] = array_unique(array_values(array_diff($boards[$permission], $deny_boards[$permission])));
1407
			}
1408
		}
1409
	}
1410 1
1411
	return $boards;
1412
}
1413
1414
/**
1415
 * Returns whether an email address should be shown and how.
1416
 *
1417
 * What it does:
1418
 *
1419
 * Possible outcomes are:
1420
 *  If it's your own profile yes.
1421
 *  If you're a moderator with sufficient permissions: yes.
1422
 *  Otherwise: no
1423
 *
1424
 * @param int $userProfile_id
1425
 *
1426
 * @return bool
1427
 */
1428
function showEmailAddress($userProfile_id)
1429
{
1430
	// Should this user's email address be shown?
1431
	if ((User::$info->is_guest === false && User::$info->id === (int) $userProfile_id))
1432
	{
1433
		return true;
1434
	}
1435
1436
	if (allowedTo('moderate_forum'))
1437
	{
1438
		return true;
1439
	}
1440
1441 8
	return false;
1442
}
1443
1444
/**
1445
 * This function attempts to protect from carrying out specific actions repeatedly.
1446 8
 *
1447
 * What it does:
1448 6
 *
1449
 * - Checks if a user is trying specific actions faster than a given minimum wait threshold.
1450
 * - The time taken depends on error_type - generally uses the modSetting.
1451 2
 * - Generates a fatal message when triggered, suspending execution.
1452
 *
1453 2
 * @event integrate_spam_protection Allows updating action wait timeOverrides
1454
 * @param string $error_type used also as a $txt index. (not an actual string.)
1455
 * @param bool $fatal is the spam check a fatal error on failure
1456
 *
1457
 * @return bool|int|mixed
1458
 * @throws \ElkArte\Exceptions\Exception
1459
 */
1460
function spamProtection($error_type, $fatal = true)
1461
{
1462
	global $modSettings;
1463
1464
	$db = database();
1465
1466
	// Certain types take less/more time.
1467
	$timeOverrides = [
1468
		'login' => 2,
1469
		'register' => 2,
1470
		'remind' => 30,
1471
		'contact' => 30,
1472
		'sendmail' => $modSettings['spamWaitTime'] * 5,
1473
		'reporttm' => $modSettings['spamWaitTime'] * 4,
1474
		'search' => empty($modSettings['search_floodcontrol_time']) ? 1 : $modSettings['search_floodcontrol_time'],
1475
	];
1476
	call_integration_hook('integrate_spam_protection', [&$timeOverrides]);
1477
1478
	// Moderators are free...
1479
	$timeLimit = allowedTo('moderate_board') ? 2 : $timeOverrides[$error_type] ?? $modSettings['spamWaitTime'];
1480
1481
	// Delete old entries...
1482 12
	$db->query('', '
1483
		DELETE FROM {db_prefix}log_floodcontrol
1484 12
		WHERE log_time < {int:log_time}
1485
			AND log_type = {string:log_type}',
1486
		[
1487
			'log_time' => time() - $timeLimit,
1488 12
			'log_type' => $error_type,
1489 12
		]
1490 12
	);
1491 12
1492 12
	// Add a new entry, deleting the old if necessary.
1493 12
	$request = $db->replace(
1494 12
		'{db_prefix}log_floodcontrol',
1495 12
		['ip' => 'string-16', 'log_time' => 'int', 'log_type' => 'string'],
1496
		[User::$info->ip, time(), $error_type],
1497 12
		['ip', 'log_type']
1498
	);
1499
1500 12
	// If affected is 0 or 2, it was there already.
1501
	if ($request->affected_rows() != 1)
1502
	{
1503
		// Spammer!  You only have to wait a *few* seconds!
1504
		if ($fatal)
1505
		{
1506 12
			throw new \ElkArte\Exceptions\Exception($error_type . '_WaitTime_broken', false, [$timeLimit]);
1507
		}
1508
1509
		return $timeLimit;
1510 12
	}
1511
1512
	// They haven't posted within the limit.
1513
	return false;
1514
}
1515 12
1516 12
/**
1517
 * A generic function to create a pair of index.php and .htaccess files in a directory
1518
 *
1519
 * @param string $path the (absolute) directory path
1520
 * @param bool $allow_localhost if access should be allowed to localhost
1521 12
 * @param string $files (optional, default '*') parameter for the Files tag
1522 12
 *
1523 12
 * @return string[]|string|bool on success error string if anything fails
1524 12
 */
1525 12
function secureDirectory($path, $allow_localhost = false, $files = '*')
1526
{
1527
	if (empty($path))
1528
	{
1529 12
		return 'empty_path';
1530
	}
1531
1532
	if (!FileFunctions::instance()->isWritable($path))
1533
	{
1534
		return 'path_not_writable';
1535
	}
1536
1537
	$directoryname = basename($path);
1538
1539
	// How deep is this from our boarddir
1540
	$tree = explode(DIRECTORY_SEPARATOR, $path);
1541
	$root = explode(DIRECTORY_SEPARATOR, BOARDDIR);
1542
	$count = max(count($tree) - count($root), 0);
1543 12
1544
	$errors = [];
1545
1546
	if (file_exists($path . '/.htaccess'))
1547
	{
1548
		$errors[] = 'htaccess_exists';
1549
	}
1550
	else
1551
	{
1552
		$fh = @fopen($path . '/.htaccess', 'wb');
1553
		if ($fh)
1554
		{
1555
			fwrite($fh, '# Apache 2.4
1556
<IfModule mod_authz_core.c>
1557
	Require all denied
1558
	<Files ' . ($files === '*' ? $files : '~ ' . $files) . '>
1559
		<RequireAll>
1560
			Require all granted
1561
			Require not env blockAccess' . (empty($allow_localhost) ? '
1562
		</RequireAll>
1563
	</Files>' : '
1564
		Require host localhost
1565
		</RequireAll>
1566
	</Files>
1567
1568
	RemoveHandler .php .php3 .phtml .cgi .fcgi .pl .fpl .shtml') . '
1569
</IfModule>
1570
1571
# Apache 2.2
1572
<IfModule !mod_authz_core.c>
1573
	Order Deny,Allow
1574
	Deny from all
1575
1576
	<Files ' . $files . '>
1577
		Allow from all' . (empty($allow_localhost) ? '
1578
	</Files>' : '
1579
		Allow from localhost
1580
	</Files>
1581
1582
	RemoveHandler .php .php3 .phtml .cgi .fcgi .pl .fpl .shtml') . '
1583
</IfModule>');
1584
			fclose($fh);
1585
		}
1586
1587
		$errors[] = 'htaccess_cannot_create_file';
1588
	}
1589
1590
	if (file_exists($path . '/index.php'))
1591
	{
1592
		$errors[] = 'index-php_exists';
1593
	}
1594
	else
1595
	{
1596
		$fh = @fopen($path . '/index.php', 'wb');
1597
		if ($fh)
1598
		{
1599
			fwrite($fh, '<?php
1600
1601
/**
1602
 * This file is here solely to protect your ' . $directoryname . ' directory.
1603
 */
1604
1605
// Look for Settings.php....
1606
if (file_exists(dirname(__FILE__, ' . ($count + 1) . ') . \'/Settings.php\'))
1607
{
1608
	// Found it!
1609
	require(dirname(__FILE__, ' . ($count + 1) . ') . \'/Settings.php\');
1610
	header(\'Location: \' . $boardurl);
1611
}
1612
// Can\'t find it... just forget it.
1613
else
1614
	exit;');
1615
			fclose($fh);
1616
		}
1617
1618
		$errors[] = 'index-php_cannot_create_file';
1619
	}
1620
1621
	if (!empty($errors))
1622
	{
1623
		return $errors;
1624
	}
1625
1626
	return true;
1627
}
1628
1629
/**
1630
 * Helper function that puts together a ban query for a given ip
1631
 *
1632
 * What it does:
1633
 *
1634
 * - Builds the query for ipv6, ipv4 or 255.255.255.255 depending on what's supplied
1635
 *
1636
 * @param string $fullip An IP address either IPv6 or not
1637
 *
1638
 * @return string A SQL condition
1639
 */
1640
function constructBanQueryIP($fullip)
1641
{
1642
	// First attempt a IPv6 address.
1643
	if (isValidIPv6($fullip))
1644
	{
1645
		$ip_parts = convertIPv6toInts($fullip);
1646
1647
		$ban_query = '((' . $ip_parts[0] . ' BETWEEN bi.ip_low1 AND bi.ip_high1)
1648
			AND (' . $ip_parts[1] . ' BETWEEN bi.ip_low2 AND bi.ip_high2)
1649
			AND (' . $ip_parts[2] . ' BETWEEN bi.ip_low3 AND bi.ip_high3)
1650
			AND (' . $ip_parts[3] . ' BETWEEN bi.ip_low4 AND bi.ip_high4)
1651
			AND (' . $ip_parts[4] . ' BETWEEN bi.ip_low5 AND bi.ip_high5)
1652
			AND (' . $ip_parts[5] . ' BETWEEN bi.ip_low6 AND bi.ip_high6)
1653
			AND (' . $ip_parts[6] . ' BETWEEN bi.ip_low7 AND bi.ip_high7)
1654
			AND (' . $ip_parts[7] . ' BETWEEN bi.ip_low8 AND bi.ip_high8))';
1655
	}
1656
	// Check if we have a valid IPv4 address.
1657
	elseif (preg_match('/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $fullip, $ip_parts) == 1)
1658
	{
1659
		$ban_query = '((' . $ip_parts[1] . ' BETWEEN bi.ip_low1 AND bi.ip_high1)
1660
			AND (' . $ip_parts[2] . ' BETWEEN bi.ip_low2 AND bi.ip_high2)
1661
			AND (' . $ip_parts[3] . ' BETWEEN bi.ip_low3 AND bi.ip_high3)
1662
			AND (' . $ip_parts[4] . ' BETWEEN bi.ip_low4 AND bi.ip_high4))';
1663
	}
1664
	// We use '255.255.255.255' for 'unknown' since it's not valid anyway.
1665
	else
1666
	{
1667
		$ban_query = '(bi.ip_low1 = 255 AND bi.ip_high1 = 255
1668 2
			AND bi.ip_low2 = 255 AND bi.ip_high2 = 255
1669
			AND bi.ip_low3 = 255 AND bi.ip_high3 = 255
1670
			AND bi.ip_low4 = 255 AND bi.ip_high4 = 255)';
1671
	}
1672
1673
	return $ban_query;
1674
}
1675
1676
/**
1677
 * Decide if we are going to do any "bad behavior" scanning for this user
1678
 *
1679
 * What it does:
1680
 *
1681
 * - Admins and Moderators get a free pass
1682 2
 * - Returns true if Accept header is missing
1683
 * - Check with project Honey Pot for known miscreants
1684 2
 *
1685 2
 * @return bool true if bad, false otherwise
1686 2
 */
1687 2
function runBadBehavior()
1688
{
1689
	global $modSettings;
1690
1691
	// Admins and Mods get a free pass
1692
	if (!empty(User::$info->is_moderator) || !empty(User::$info->is_admin))
1693
	{
1694
		return false;
1695
	}
1696
1697
	// Clients will have an "Accept" header, generally only bots or scrappers don't
1698 2
	if (!empty($modSettings['badbehavior_accept_header']) && !array_key_exists('HTTP_ACCEPT', $_SERVER))
1699
	{
1700
		return true;
1701
	}
1702
1703
	// Do not block private IP ranges 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 or 127.0.0.0/8
1704
	if (preg_match('~^((10|172\.(1[6-9]|2\d|3[01])|192\.168|127)\.)~', $_SERVER['REMOTE_ADDR']) === 1)
1705
	{
1706
		return false;
1707
	}
1708
1709
	// Project honey pot blacklist check [Your Access Key] [Octet-Reversed IP] [List-Specific Domain]
1710
	if (empty($modSettings['badbehavior_httpbl_key']) || empty($_SERVER['REMOTE_ADDR']))
1711
	{
1712
		return false;
1713
	}
1714
1715
	// Try to load it from the cache first
1716
	$cache = Cache::instance();
1717
	$dnsQuery = $modSettings['badbehavior_httpbl_key'] . '.' . implode('.', array_reverse(explode('.', $_SERVER['REMOTE_ADDR']))) . '.dnsbl.httpbl.org';
1718
	if (!$cache->getVar($dnsResult, 'dnsQuery-' . $_SERVER['REMOTE_ADDR'], 240))
1719
	{
1720
		$dnsResult = gethostbyname($dnsQuery);
1721
		$cache->put('dnsQuery-' . $_SERVER['REMOTE_ADDR'], $dnsResult, 240);
1722
	}
1723
1724
	if (!empty($dnsResult) && $dnsResult !== $dnsQuery)
1725
	{
1726
		$result = explode('.', $dnsResult);
1727
		$result = array_map('intval', $result);
1728
		if ($result[0] === 127 // Valid Response
1729
			&& ($result[3] & 3 || $result[3] & 5) // Listed as Suspicious + Harvester || Suspicious + Comment Spammer
1730
			&& $result[2] >= $modSettings['badbehavior_httpbl_threat'] // Level
1731
			&& $result[1] <= $modSettings['badbehavior_httpbl_maxage']) // Age
1732
		{
1733
			return true;
1734
		}
1735
	}
1736
1737
	return false;
1738
}
1739
1740
/**
1741
 * This protects against brute force attacks on a member's password.
1742
 *
1743
 * What it does:
1744
 *
1745
 * - Importantly, even if the password was right we DON'T TELL THEM!
1746
 * - Allows 5 attempts every 10 seconds
1747
 *
1748
 * @param int $id_member
1749
 * @param string|bool $password_flood_value = false or string joined on |'s
1750
 * @param bool $was_correct = false
1751
 *
1752
 * @throws \ElkArte\Exceptions\Exception no_access
1753
 */
1754
function validatePasswordFlood($id_member, $password_flood_value = false, $was_correct = false)
1755
{
1756
	global $cookiename;
1757
1758
	// As this is only brute protection, we allow 5 attempts every 10 seconds.
1759
1760
	// Destroy any session or cookie data about this member, as they validated wrong.
1761
	require_once(SUBSDIR . '/Auth.subs.php');
1762
	setLoginCookie(-3600, 0);
1763
1764
	if (isset($_SESSION['login_' . $cookiename]))
1765
	{
1766
		unset($_SESSION['login_' . $cookiename]);
1767
	}
1768
1769
	// We need a member!
1770
	if ($id_member === 0)
1771
	{
1772
		// Redirect back!
1773
		redirectexit();
1774
1775
		// Probably not needed, but still make sure...
1776
		throw new \ElkArte\Exceptions\Exception('no_access', false);
1777
	}
1778
1779
	// Let's just initialize to something (and 0 is better than nothing)
1780
	$time_stamp = 0;
1781
	$number_tries = 0;
1782
1783
	// Right, have we got a flood value?
1784
	if ($password_flood_value !== false)
1785
	{
1786
		@[$time_stamp, $number_tries] = explode('|', $password_flood_value);
1787
	}
1788
1789
	// Timestamp invalid or non-existent?
1790
	if (empty($number_tries) || $time_stamp < (time() - 10))
1791
	{
1792
		// If it wasn't *that* long ago, don't give them another five goes.
1793
		$number_tries = !empty($number_tries) && $time_stamp < (time() - 20) ? 2 : $number_tries;
1794
		$time_stamp = time();
1795
	}
1796
1797
	$number_tries++;
1798
1799
	// Broken the law?
1800
	if ($number_tries > 5)
1801
	{
1802
		throw new \ElkArte\Exceptions\Exception('login_threshold_brute_fail', 'critical');
1803
	}
1804
1805
	// Otherwise set the members data. If they correct on their first attempt then we actually clear it, otherwise we set it!
1806
	require_once(SUBSDIR . '/Members.subs.php');
1807
	updateMemberData($id_member, ['passwd_flood' => $was_correct && $number_tries == 1 ? '' : $time_stamp . '|' . $number_tries]);
1808
}
1809
1810
/**
1811
 * This sets the X-Frame-Options header.
1812
 *
1813
 * @param string|null $override the frame option, defaults to deny.
1814
 */
1815
function frameOptionsHeader($override = null)
1816
{
1817
	global $modSettings;
1818
1819
	$option = 'SAMEORIGIN';
1820
1821
	if (is_null($override) && !empty($modSettings['frame_security']))
1822
	{
1823
		$option = $modSettings['frame_security'];
1824
	}
1825
	elseif (in_array($override, ['SAMEORIGIN', 'DENY']))
1826
	{
1827
		$option = $override;
1828
	}
1829
1830
	// Don't bother setting the header if we have disabled it.
1831
	if ($option === 'DISABLE')
1832
	{
1833
		return;
1834
	}
1835
1836
	// Finally set it.
1837
	Headers::instance()->header('X-Frame-Options', $option);
1838
}
1839
1840
/**
1841
 * This adds additional security headers that may prevent browsers from doing something they should not
1842
 *
1843
 * What it does:
1844
 *
1845
 * - X-XSS-Protection header - This header enables the Cross-site scripting (XSS) filter
1846
 * built into most recent web browsers. It's usually enabled by default, so the role of this
1847
 * header is to re-enable the filter for this particular website if it was disabled by the user.
1848
 * - X-Content-Type-Options header - It prevents the browser from doing MIME-type sniffing,
1849
 * only IE and Chrome are honouring this header. This reduces exposure to drive-by download attacks
1850
 * and sites serving user uploaded content that could be treated as executable or dynamic HTML files.
1851
 *
1852
 * @param bool|null $override
1853
 */
1854
function securityOptionsHeader($override = null)
1855
{
1856
	if ($override !== true)
1857
	{
1858
		Headers::instance()
1859
			->header('X-XSS-Protection', '1')
1860
			->header('X-Content-Type-Options', 'nosniff');
1861
	}
1862
}
1863
1864
/**
1865
 * Stop some browsers pre fetching activity to reduce server load
1866
 */
1867
function stop_prefetching()
1868
{
1869
	if ((isset($_SERVER['HTTP_PURPOSE']) && $_SERVER['HTTP_PURPOSE'] === 'prefetch')
1870
		|| (isset($_SERVER['HTTP_X_MOZ']) && $_SERVER['HTTP_X_MOZ'] === 'prefetch'))
1871
	{
1872
		@ob_end_clean();
1873
		Headers::instance()
1874
			->removeHeader('all')
1875
			->headerSpecial('HTTP/1.1 403 Prefetch Forbidden')
1876
			->sendHeaders();
1877
		die;
1878
	}
1879
}
1880 24
1881 24
/**
1882
 * Check if the admin's session is active
1883
 *
1884
 * @return bool
1885
 */
1886
function isAdminSessionActive()
1887 24
{
1888
	global $modSettings;
1889
1890
	return empty($modSettings['securityDisable']) && (isset($_SESSION['admin_time']) && $_SESSION['admin_time'] + ($modSettings['admin_session_lifetime'] * 60) > time());
1891
}
1892
1893
/**
1894
 * Check if security files exist
1895
 *
1896
 * If files are found, populate $context['security_controls_files']:
1897
 * * 'title'    - $txt['security_risk']
1898
 * * 'errors'    - An array of strings with the key being the filename and the value an error with the filename in it
1899
 *
1900
 * @event integrate_security_files Allows adding / modifying security files array
1901
 *
1902
 * @return bool
1903
 */
1904
function checkSecurityFiles()
1905
{
1906
	global $txt, $context;
1907
1908
	$has_files = false;
1909
1910
	$securityFiles = ['install.php', 'upgrade.php', 'convert.php', 'repair_paths.php', 'repair_settings.php', 'Settings.php~', 'Settings_bak.php~'];
1911
	call_integration_hook('integrate_security_files', [&$securityFiles]);
1912
1913
	foreach ($securityFiles as $securityFile)
1914
	{
1915
		if (file_exists(BOARDDIR . '/' . $securityFile))
1916
		{
1917
			$has_files = true;
1918
1919
			$context['security_controls_files']['title'] = $txt['security_risk'];
1920
			$context['security_controls_files']['errors'][$securityFile] = sprintf($txt['not_removed'], $securityFile);
1921
1922
			if ($securityFile === 'Settings.php~' || $securityFile === 'Settings_bak.php~')
1923
			{
1924
				$context['security_controls_files']['errors'][$securityFile] .= '<span class="smalltext">' . sprintf($txt['not_removed_extra'], $securityFile, substr($securityFile, 0, -1)) . '</span>';
1925
			}
1926
		}
1927
	}
1928
1929
	return $has_files;
1930
}
1931
1932
/**
1933
 * The login URL should not redirect to certain areas (attachments, js actions, etc)
1934
 * this function does these checks and return if the URL is valid or not.
1935
 *
1936
 * @param string $url - The URL to validate
1937
 * @param bool $match_board - If true tries to match board|topic in the URL as well
1938
 * @return bool
1939
 */
1940
function validLoginUrl($url, $match_board = false)
1941
{
1942
	if (empty($url))
1943
	{
1944
		return false;
1945
	}
1946
1947
	if (strpos($url, 'http://') !== 0 && strpos($url, 'https://') !== 0)
1948
	{
1949
		return false;
1950 2
	}
1951
1952
	$invalid_strings = ['dlattach' => '~(board|topic)[=,]~', 'jslocale' => '', 'login' => ''];
1953
	call_integration_hook('integrate_validLoginUrl', [&$invalid_strings]);
1954
1955 2
	foreach ($invalid_strings as $invalid_string => $valid_match)
1956
	{
1957
		if (strpos($url, $invalid_string) !== false
1958
			|| ($match_board === true && !empty($valid_match) && preg_match($valid_match, $url) !== 1))
1959
		{
1960 2
			return false;
1961 2
		}
1962
	}
1963 2
1964
	return true;
1965
}
1966