Issues (1686)

sources/Security.php (2 issues)

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;
0 ignored issues
show
Using exit here is not recommended.

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

Loading history...
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);
0 ignored issues
show
trigger_error('Hacking attempt...', E_USER_ERROR) is not reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
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]);
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