Issues (1691)

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