validateTriggers()   D
last analyzed

Complexity

Conditions 22
Paths 66

Size

Total Lines 162
Code Lines 72

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 506

Importance

Changes 0
Metric Value
cc 22
eloc 72
nc 66
nop 1
dl 0
loc 162
ccs 0
cts 63
cp 0
crap 506
rs 4.1666
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * This file contains functions that are specifically done by administrators.
5
 *
6
 * @package   ElkArte Forum
7
 * @copyright ElkArte Forum contributors
8
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause (see accompanying LICENSE.txt file)
9
 *
10
 * This file contains code covered by:
11
 * copyright: 2011 Simple Machines (http://www.simplemachines.org)
12
 *
13
 * @version 2.0 dev
14
 *
15
 */
16
17
use ElkArte\Errors\ErrorContext;
18
use ElkArte\Helper\Util;
19
use ElkArte\Languages\Txt;
20
use ElkArte\MembersList;
21
22
/**
23
 * Saves one or more ban triggers into a ban item: according to the suggestions
24
 *
25
 * What it does:
26
 *
27
 * - Checks the $_POST variable to verify if the trigger is present
28
 * - Load triggers in to an array for validation
29
 * - Validates and saves/updates the triggers for a given ban
30
 *
31
 * @param array $suggestions A bit messy array, it should look something like:
32
 *                 array(
33
 *                   'main_ip' => '123.123.123.123',
34
 *                   'hostname' => 'hostname.tld',
35
 *                   'email' => '[email protected]',
36
 *                   'ban_suggestions' => array(
37
 *                     'main_ip',     // <= these two are those that will be
38
 *                     'hostname',    // <= used for the ban, so no email
39
 *                     'other_ips' => array(
40
 *                       'ips_in_messages' => array(...),
41
 *                       'ips_in_errors' => array(...),
42
 *                       'other_custom' => array(...),
43
 *                     )
44
 *                   )
45
 *                 )
46
 * @param int $ban_group
47
 * @param int $member
48
 * @param int $trigger_id
49
 * @return mixed array with the saved triggers or false on failure
50
 * @package Bans
51
 */
52
function saveTriggers($suggestions, $ban_group, $member = 0, $trigger_id = 0)
53
{
54
	$triggers = [
55
		'main_ip' => '',
56
		'hostname' => '',
57
		'email' => '',
58
		'member' => [
59
			'id' => $member,
60
		]
61
	];
62
63
	$ban_errors = ErrorContext::context('ban', 1);
64
65
	if (!is_array($suggestions))
0 ignored issues
show
introduced by
The condition is_array($suggestions) is always true.
Loading history...
66
	{
67
		return false;
68
	}
69
70
	// What triggers are we adding (like ip, host, email, etc)
71
	foreach ($suggestions['ban_suggestions'] as $key => $value)
72
	{
73
		if (is_array($value))
74
		{
75
			$triggers[$key] = $value;
76
		}
77
		else
78
		{
79
			$triggers[$value] = !empty($suggestions[$value]) ? $suggestions[$value] : '';
80
		}
81
	}
82
83
	// Make sure the triggers for this ban are valid
84
	$ban_triggers = validateTriggers($triggers);
85
86
	// Time to save or update!
87
	if (!empty($ban_triggers['ban_triggers']) && !$ban_errors->hasErrors())
88
	{
89
		if (empty($trigger_id))
90
		{
91
			addTriggers($ban_group, $ban_triggers['ban_triggers'], $ban_triggers['log_info']);
92
		}
93
		else
94
		{
95
			updateTriggers($trigger_id, $ban_group, array_shift($ban_triggers['ban_triggers']), $ban_triggers['log_info']);
96
		}
97
	}
98
99
	// No errors, then return the ban triggers
100
	if ($ban_errors->hasErrors())
101
	{
102
		return $triggers;
103
	}
104
105
	return false;
106
}
107
108
/**
109
 * This function removes a batch of triggers based on ids
110
 *
111
 * What it does:
112
 *
113
 * - Doesn't clean the inputs, expects valid input
114
 * - Removes the ban triggers by id or group
115
 *
116
 * @param int[]|int $items_ids
117
 * @param int|bool $group_id
118
 * @return bool
119
 * @package Bans
120
 */
121
function removeBanTriggers($items_ids = [], $group_id = false)
122
{
123
	$db = database();
124
125
	if ($group_id !== false)
126
	{
127
		$group_id = (int) $group_id;
128
	}
129
130
	if (empty($group_id) && empty($items_ids))
131
	{
132
		return false;
133
	}
134
135
	if (!is_array($items_ids))
136
	{
137
		$items_ids = [$items_ids];
138
	}
139
140
	// Log the ban removals so others know
141
	$log_info = banLogItems(banDetails($items_ids, $group_id));
142
	logTriggersUpdates($log_info, 'remove');
143
144
	// Remove the ban triggers by id's or groups
145
	if ($group_id !== false)
146
	{
147
		$db->query('', '
148
			DELETE FROM {db_prefix}ban_items
149
			WHERE id_ban IN ({array_int:ban_list})
150
				AND id_ban_group = {int:ban_group}',
151
			[
152
				'ban_list' => $items_ids,
153
				'ban_group' => $group_id,
154
			]
155
		);
156
	}
157
	elseif (!empty($items_ids))
158
	{
159
		$db->query('', '
160
			DELETE FROM {db_prefix}ban_items
161
			WHERE id_ban IN ({array_int:ban_list})',
162
			[
163
				'ban_list' => $items_ids,
164
			]
165
		);
166
	}
167
168
	return true;
169
}
170
171
/**
172
 * This function removes a batch of ban groups based on ids
173
 *
174
 * What it does:
175
 *
176
 * - Doesn't clean the inputs
177
 * - Removes entries from the ban group list, one or many
178
 *
179
 * @param int[]|int $group_ids
180
 * @return bool
181
 * @package Bans
182
 */
183
function removeBanGroups($group_ids)
184
{
185
	$db = database();
186
187
	if (!is_array($group_ids))
188
	{
189
		$group_ids = [$group_ids];
190
	}
191
192
	$group_ids = array_unique($group_ids);
193
194
	if (empty($group_ids))
195
	{
196
		return false;
197
	}
198
199
	$db->query('', '
200
		DELETE FROM {db_prefix}ban_groups
201
		WHERE id_ban_group IN ({array_int:ban_list})',
202
		[
203
			'ban_list' => $group_ids,
204
		]
205
	);
206
207
	return true;
208
}
209
210
/**
211
 * Removes ban logs
212
 *
213
 * What it does:
214
 *
215
 * - By default (no id's passed) truncate the table
216
 * - Doesn't clean the inputs
217
 *
218
 * @param int[]|int|null $ids (optional)
219
 * @return bool
220
 * @package Bans
221
 */
222
function removeBanLogs($ids = [])
223
{
224
	$db = database();
225
226
	// No specific id's passed, we truncate the entire table
227
	if (empty($ids))
228
	{
229
		$db->truncate('{db_prefix}log_banned');
230
	}
231
	else
232
	{
233
		if (!is_array($ids))
234
		{
235
			$ids = [$ids];
236
		}
237
238
		// Can only remove it once
239
		$ids = array_unique($ids);
0 ignored issues
show
Bug introduced by
It seems like $ids can also be of type integer and null; however, parameter $array of array_unique() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

239
		$ids = array_unique(/** @scrutinizer ignore-type */ $ids);
Loading history...
240
241
		if (empty($ids))
242
		{
243
			return false;
244
		}
245
246
		// Remove this grouping
247
		$db->query('', '
248
			DELETE FROM {db_prefix}log_banned
249
			WHERE id_ban_log IN ({array_int:ban_list})',
250
			[
251
				'ban_list' => $ids,
252
			]
253
		);
254
	}
255
256
	return true;
257
}
258
259
/**
260
 * This function validates the ban triggers
261
 *
262
 * @param array $triggers
263
 *
264
 * @return array
265
 * @package Bans
266
 *
267
 */
268
function validateTriggers(&$triggers)
269
{
270
	$db = database();
271
272
	$ban_errors = ErrorContext::context('ban', 1);
273
	if (empty($triggers))
274
	{
275
		$ban_errors->addError('ban_empty_triggers');
276
	}
277
278
	$ban_triggers = [];
279
	$log_info = [];
280
281
	// Go through each trigger and make sure its valid
282
	foreach ($triggers as $key => $value)
283
	{
284
		if (!empty($value))
285
		{
286
			if ($key === 'member')
287
			{
288
				continue;
289
			}
290
291
			if ($key === 'main_ip')
292
			{
293
				$value = trim($value);
294
				$ip_parts = ip2range($value);
295
				$ban_trigger = validateIPBan($ip_parts, $value);
296
				if (empty($ban_trigger['error']))
297
				{
298
					$ban_triggers['main_ip'] = $ban_trigger;
299
				}
300
				else
301
				{
302
					$ban_errors->addError($ban_trigger['error']);
303
				}
304
			}
305
			elseif ($key === 'hostname')
306
			{
307
				if (preg_match('/[^\w.\-*]/', $value) == 1)
308
				{
309
					$ban_errors->addError('invalid_hostname');
310
				}
311
				else
312
				{
313
					// Replace the * wildcard by a MySQL wildcard %.
314
					$value = substr(str_replace('*', '%', $value), 0, 255);
315
316
					$ban_triggers['hostname']['hostname'] = $value;
317
				}
318
			}
319
			elseif ($key === 'email')
320
			{
321
				if (preg_match('/[^\w.\-\+*@]/', $value) == 1)
322
				{
323
					$ban_errors->addError('invalid_email');
324
				}
325
326
				// Check the user is not banning an admin.
327
				$request = $db->query('', '
328
					SELECT 
329
						id_member
330
					FROM {db_prefix}members
331
					WHERE (id_group = {int:admin_group} OR FIND_IN_SET({int:admin_group}, additional_groups) != 0)
332
						AND email_address LIKE {string:email}
333
					LIMIT 1',
334
					[
335
						'admin_group' => 1,
336
						'email' => $value,
337
					]
338
				);
339
				if ($request->num_rows() !== 0)
340
				{
341
					$ban_errors->addError('no_ban_admin');
342
				}
343
				$request->free_result();
344
345
				$value = substr(strtolower(str_replace('*', '%', $value)), 0, 255);
346
347
				$ban_triggers['email']['email_address'] = $value;
348
			}
349
			elseif ($key == 'user')
350
			{
351
				$user = preg_replace('~&amp;#(\d{4,5}|[2-9]\d{2,4}|1[2-9]\d);~', '&#$1;', Util::htmlspecialchars($value, ENT_QUOTES));
352
353
				$request = $db->query('', '
354
					SELECT 
355
						id_member, (id_group = {int:admin_group} OR FIND_IN_SET({int:admin_group}, additional_groups) != 0) AS isAdmin
356
					FROM {db_prefix}members
357
					WHERE member_name = {string:username} OR real_name = {string:username}
358
					LIMIT 1',
359
					[
360
						'admin_group' => 1,
361
						'username' => $user,
362
					]
363
				);
364
				if ($request->num_rows() === 0)
365
				{
366
					$ban_errors->addError('invalid_username');
367
				}
368
				list ($value, $isAdmin) = $request->fetch_row();
369
				$request->free_result();
370
371
				if ($isAdmin && strtolower($isAdmin) !== 'f')
372
				{
373
					unset($value);
374
					$ban_errors->addError('no_ban_admin');
375
				}
376
				else
377
				{
378
					$ban_triggers['user']['id_member'] = $value;
379
				}
380
			}
381
			elseif (in_array($key, ['ips_in_messages', 'ips_in_errors']))
382
			{
383
				// Special case, those two are arrays themselves
384
				$values = array_unique($value);
385
386
				// Don't add the main IP again.
387
				if (isset($triggers['main_ip']))
388
				{
389
					$values = array_diff($values, [$triggers['main_ip']]);
390
				}
391
392
				unset($value);
393
				foreach ($values as $val)
394
				{
395
					$val = trim($val);
396
					$ip_parts = ip2range($val);
397
					$ban_trigger = validateIPBan($ip_parts, $val);
398
399
					if (empty($ban_trigger['error']))
400
					{
401
						$ban_triggers[$key][] = $ban_trigger;
402
403
						$log_info[] = [
404
							'value' => $val,
405
							'bantype' => 'ip_range',
406
						];
407
					}
408
					else
409
					{
410
						$ban_errors->addError($ban_trigger['error']);
411
					}
412
				}
413
			}
414
			else
415
			{
416
				$ban_errors->addError('no_bantype_selected');
417
			}
418
419
			if (isset($value) && !is_array($value))
420
			{
421
				$log_info[] = [
422
					'value' => $value,
423
					'bantype' => $key,
424
				];
425
			}
426
		}
427
	}
428
429
	return ['ban_triggers' => $ban_triggers, 'log_info' => $log_info];
430
}
431
432
/**
433
 * This function actually inserts the ban triggers into the database
434
 *
435
 * @param int $group_id
436
 * @param array $triggers associative array of trigger keys and the values
437
 * @param array $logs
438
 * @return bool
439
 * @package Bans
440
 */
441
function addTriggers($group_id = 0, $triggers = [], $logs = [])
442
{
443
	$db = database();
444
445
	$ban_errors = ErrorContext::context('ban', 1);
446
447
	if (empty($group_id))
448
	{
449
		$ban_errors->addError('ban_not_found');
450
	}
451
452
	// Preset all values that are required.
453
	$values = [
454
		'id_ban_group' => $group_id,
455
		'hostname' => '',
456
		'email_address' => '',
457
		'id_member' => 0,
458
		'ip_low1' => 0,
459
		'ip_high1' => 0,
460
		'ip_low2' => 0,
461
		'ip_high2' => 0,
462
		'ip_low3' => 0,
463
		'ip_high3' => 0,
464
		'ip_low4' => 0,
465
		'ip_high4' => 0,
466
		'ip_low5' => 0,
467
		'ip_high5' => 0,
468
		'ip_low6' => 0,
469
		'ip_high6' => 0,
470
		'ip_low7' => 0,
471
		'ip_high7' => 0,
472
		'ip_low8' => 0,
473
		'ip_high8' => 0,
474
	];
475
476
	$insertKeys = [
477
		'id_ban_group' => 'int',
478
		'hostname' => 'string',
479
		'email_address' => 'string',
480
		'id_member' => 'int',
481
		'ip_low1' => 'int',
482
		'ip_high1' => 'int',
483
		'ip_low2' => 'int',
484
		'ip_high2' => 'int',
485
		'ip_low3' => 'int',
486
		'ip_high3' => 'int',
487
		'ip_low4' => 'int',
488
		'ip_high4' => 'int',
489
		'ip_low5' => 'int',
490
		'ip_high5' => 'int',
491
		'ip_low6' => 'int',
492
		'ip_high6' => 'int',
493
		'ip_low7' => 'int',
494
		'ip_high7' => 'int',
495
		'ip_low8' => 'int',
496
		'ip_high8' => 'int',
497
	];
498
499
	$insertTriggers = [];
500
	foreach ($triggers as $key => $trigger)
501
	{
502
		// Exceptions, exceptions, exceptions...always exceptions... :P
503
		if (in_array($key, ['ips_in_messages', 'ips_in_errors']))
504
		{
505
			foreach ($trigger as $real_trigger)
506
			{
507
				$insertTriggers[] = array_merge($values, $real_trigger);
508
			}
509
		}
510
		else
511
		{
512
			$insertTriggers[] = array_merge($values, $trigger);
513
		}
514
	}
515
516
	if (empty($insertTriggers))
517
	{
518
		$ban_errors->addError('ban_no_triggers');
519
	}
520
521
	if ($ban_errors->hasErrors())
522
	{
523
		return false;
524
	}
525
526
	$db->insert('ignore',
527
		'{db_prefix}ban_items',
528
		$insertKeys,
529
		$insertTriggers,
530
		['id_ban']
531
	);
532
533
	logTriggersUpdates($logs, true);
534
535
	return true;
536
}
537
538
/**
539
 * This function updates an existing ban trigger into the database
540
 *
541
 * @param int $ban_item
542
 * @param int $group_id
543
 * @param array $trigger associative array of ban trigger => value
544
 * @param array $logs
545
 * @package Bans
546
 */
547
function updateTriggers($ban_item = 0, $group_id = 0, $trigger = [], $logs = [])
548
{
549
	$db = database();
550
551
	$ban_errors = ErrorContext::context('ban', 1);
552
553
	if (empty($ban_item))
554
	{
555
		$ban_errors->addError('ban_ban_item_empty');
556
	}
557
558
	if (empty($group_id))
559
	{
560
		$ban_errors->addError('ban_not_found');
561
	}
562
563
	if (empty($trigger))
564
	{
565
		$ban_errors->addError('ban_no_triggers');
566
	}
567
568
	// Any errors then we are not updating it
569
	if ($ban_errors->hasErrors())
570
	{
571
		return;
572
	}
573
574
	// Preset all values that are required.
575
	$values = [
576
		'id_ban_group' => $group_id,
577
		'hostname' => '',
578
		'email_address' => '',
579
		'id_member' => 0,
580
		'ip_low1' => 0,
581
		'ip_high1' => 0,
582
		'ip_low2' => 0,
583
		'ip_high2' => 0,
584
		'ip_low3' => 0,
585
		'ip_high3' => 0,
586
		'ip_low4' => 0,
587
		'ip_high4' => 0,
588
		'ip_low5' => 0,
589
		'ip_high5' => 0,
590
		'ip_low6' => 0,
591
		'ip_high6' => 0,
592
		'ip_low7' => 0,
593
		'ip_high7' => 0,
594
		'ip_low8' => 0,
595
		'ip_high8' => 0,
596
	];
597
598
	$trigger = array_merge($values, $trigger);
599
600
	$db->query('', '
601
		UPDATE {db_prefix}ban_items
602
		SET
603
			hostname = {string:hostname}, email_address = {string:email_address}, id_member = {int:id_member},
604
			ip_low1 = {int:ip_low1}, ip_high1 = {int:ip_high1},
605
			ip_low2 = {int:ip_low2}, ip_high2 = {int:ip_high2},
606
			ip_low3 = {int:ip_low3}, ip_high3 = {int:ip_high3},
607
			ip_low4 = {int:ip_low4}, ip_high4 = {int:ip_high4},
608
			ip_low5 = {int:ip_low5}, ip_high5 = {int:ip_high5},
609
			ip_low6 = {int:ip_low6}, ip_high6 = {int:ip_high6},
610
			ip_low7 = {int:ip_low7}, ip_high7 = {int:ip_high7},
611
			ip_low8 = {int:ip_low8}, ip_high8 = {int:ip_high8}
612
		WHERE id_ban = {int:ban_item}
613
			AND id_ban_group = {int:id_ban_group}',
614
		array_merge($trigger, [
615
			'id_ban_group' => $group_id,
616
			'ban_item' => $ban_item,
617
		])
618
	);
619
620
	logTriggersUpdates($logs, false);
621
}
622
623
/**
624
 * A small function to unify logging of triggers (updates and new)
625
 *
626
 * @param array $logs an array of logs, each log contains the following keys:
627
 * - bantype: a known type of ban (ip_range, hostname, email, user, main_ip)
628
 * - value: the value of the bantype (e.g. the IP or the email address banned)
629
 * @param bool|string $new type of trigger
630
 * - if the trigger is new (true), an update (false), or a removal ('remove') of an existing one
631
 * @package Bans
632
 */
633
function logTriggersUpdates($logs, $new = true)
634
{
635
	if (empty($logs))
636
	{
637
		return;
638
	}
639
640
	$log_name_map = [
641
		'main_ip' => 'ip_range',
642
		'hostname' => 'hostname',
643
		'email' => 'email',
644
		'user' => 'member',
645
		'ip_range' => 'ip_range',
646
	];
647
648
	// Log the addition of the ban entries into the moderation log.
649
	foreach ($logs as $log)
650
	{
651
		logAction('ban', [
652
			$log_name_map[$log['bantype']] => $log['value'],
653
			'new' => empty($new) ? 0 : ($new === true ? 1 : -1),
654
			'type' => $log['bantype'],
655
		]);
656
	}
657
}
658
659
/**
660
 * Updates an existing ban group
661
 *
662
 * - If the name doesn't exists a new one is created
663
 *
664
 * @param array $ban_info
665
 * @return int|bool
666
 * @package Bans
667
 */
668
function updateBanGroup($ban_info = [])
669
{
670
	$db = database();
671
672
	// Lets check for errors first
673
	$ban_errors = ErrorContext::context('ban', 1);
674
675
	if (empty($ban_info['name']))
676
	{
677
		$ban_errors->addError('ban_name_empty');
678
	}
679
680
	if (empty($ban_info['id']))
681
	{
682
		$ban_errors->addError('ban_id_empty');
683
	}
684
685
	if ($ban_errors->hasErrors())
686
	{
687
		return false;
688
	}
689
690
	// No problems found, so lets add this to the ban list
691
	$request = $db->query('', '
692
		SELECT id_ban_group
693
		FROM {db_prefix}ban_groups
694
		WHERE name = {string:new_ban_name}
695
			AND id_ban_group = {int:ban_group}
696
		LIMIT 1',
697
		[
698
			'ban_group' => $ban_info['id'],
699
			'new_ban_name' => $ban_info['name'],
700
		]
701
	);
702
	if ($request->num_rows() === 0)
703
	{
704
		return insertBanGroup($ban_info);
705
	}
706
	$request->free_result();
707
708
	$db->query('', '
709
		UPDATE {db_prefix}ban_groups
710
		SET
711
			name = {string:ban_name},
712
			reason = {string:reason},
713
			notes = {string:notes},
714
			expire_time = {raw:expiration},
715
			cannot_access = {int:cannot_access},
716
			cannot_post = {int:cannot_post},
717
			cannot_register = {int:cannot_register},
718
			cannot_login = {int:cannot_login}
719
		WHERE id_ban_group = {int:id_ban_group}',
720
		[
721
			'expiration' => $ban_info['db_expiration'],
722
			'cannot_access' => $ban_info['cannot']['access'],
723
			'cannot_post' => $ban_info['cannot']['post'],
724
			'cannot_register' => $ban_info['cannot']['register'],
725
			'cannot_login' => $ban_info['cannot']['login'],
726
			'id_ban_group' => $ban_info['id'],
727
			'ban_name' => $ban_info['name'],
728
			'reason' => $ban_info['reason'],
729
			'notes' => $ban_info['notes'],
730
		]
731
	);
732
733
	return $ban_info['id'];
734
}
735
736
/**
737
 * Creates a new ban group
738
 *
739
 * What it does:
740
 *
741
 * - If a ban group with the same name already exists or the group s successfully created the ID is returned
742
 * - On error the error code is returned or false
743
 *
744
 * @param array $ban_info
745
 * @return int the ban group's ID
746
 * @package Bans
747
 */
748
function insertBanGroup($ban_info = [])
749
{
750
	$db = database();
751
752
	$ban_errors = ErrorContext::context('ban', 1);
753
754
	if (empty($ban_info['name']))
755
	{
756
		$ban_errors->addError('ban_name_empty');
757
	}
758
759
	if (empty($ban_info['cannot']['access']) && empty($ban_info['cannot']['register']) && empty($ban_info['cannot']['post']) && empty($ban_info['cannot']['login']))
760
	{
761
		$ban_errors->addError('ban_unknown_restriction_type');
762
	}
763
764
	if ($ban_errors->hasErrors())
765
	{
766
		return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type integer.
Loading history...
767
	}
768
769
	// Check whether a ban with this name already exists.
770
	$request = $db->query('', '
771
		SELECT 
772
			id_ban_group
773
		FROM {db_prefix}ban_groups
774
		WHERE name = {string:new_ban_name}
775
		LIMIT 1',
776
		[
777
			'new_ban_name' => $ban_info['name'],
778
		]
779
	);
780
781
	// @todo shouldn't be an error here?
782
	if ($request->num_rows() === 1)
783
	{
784
		list ($id_ban) = $request->fetch_row();
785
		$request->free_result();
786
787
		return $id_ban;
788
	}
789
	$request->free_result();
790
791
	// Yes yes, we're ready to add now.
792
	$db->insert('',
793
		'{db_prefix}ban_groups',
794
		[
795
			'name' => 'string-20', 'ban_time' => 'int', 'expire_time' => 'raw', 'cannot_access' => 'int', 'cannot_register' => 'int',
796
			'cannot_post' => 'int', 'cannot_login' => 'int', 'reason' => 'string-255', 'notes' => 'string-65534',
797
		],
798
		[
799
			$ban_info['name'], time(), $ban_info['db_expiration'], $ban_info['cannot']['access'], $ban_info['cannot']['register'],
800
			$ban_info['cannot']['post'], $ban_info['cannot']['login'], $ban_info['reason'], $ban_info['notes'],
801
		],
802
		['id_ban_group']
803
	);
804
	$ban_info['id'] = $db->insert_id('{db_prefix}ban_groups');
805
806
	if (empty($ban_info['id']))
807
	{
808
		$ban_errors->addError('impossible_insert_new_bangroup');
809
	}
810
811
	return $ban_info['id'];
812
}
813
814
/**
815
 * Convert a range of given IP number into a single string.
816
 *
817
 * - It's practically the reverse function of ip2range().
818
 *
819
 * @param int[] $low IPv4 format
820
 * @param int[] $high IPv4 format
821
 * @return string
822
 * @package Bans
823
 * @example
824
 * range2ip(array(10, 10, 10, 0), array(10, 10, 20, 255)) returns '10.10.10-20.*
825
 */
826
function range2ip($low, $high)
827
{
828
	$ip = [];
829
830
	// IPv6 check.
831
	if (!empty($high[4]) || !empty($high[5]) || !empty($high[6]) || !empty($high[7]))
832
	{
833
		if (count($low) !== 8 || count($high) !== 8)
834
		{
835
			return '';
836
		}
837
838
		for ($i = 0; $i < 8; $i++)
839
		{
840
			if ($low[$i] === $high[$i])
841
			{
842
				$ip[$i] = dechex($low[$i]);
843
			}
844
			elseif ($low[$i] == '0' && $high[$i] == '255')
845
			{
846
				$ip[$i] = '*';
847
			}
848
			else
849
			{
850
				$ip[$i] = dechex($low[$i]) . '-' . dechex($high[$i]);
851
			}
852
		}
853
854
		return implode(':', $ip);
855
	}
856
857
	// Legacy IPv4 stuff.
858
	// (count($low) != 4 || count($high) != 4) would not work because $low and $high always contain 8 elements!
859
	if ((count($low) !== 4 || count($high) !== 4) && (count($low) !== 8 || count($high) !== 8))
860
	{
861
		return '';
862
	}
863
864
	for ($i = 0; $i < 4; $i++)
865
	{
866
		if ($low[$i] === $high[$i])
867
		{
868
			$ip[$i] = $low[$i];
869
		}
870
		elseif ($low[$i] == '0' && $high[$i] == '255')
871
		{
872
			$ip[$i] = '*';
873
		}
874
		else
875
		{
876
			$ip[$i] = $low[$i] . '-' . $high[$i];
877
		}
878
	}
879
880
	// Pretending is fun... the IP can't be this, so use it for 'unknown'.
881
	if ($ip == [255, 255, 255, 255])
882
	{
883
		return 'unknown';
884
	}
885
886
	return implode('.', $ip);
887
}
888
889
/**
890
 * Checks whether a given IP range already exists in the trigger list.
891
 *
892
 * What it does:
893
 *
894
 * - If yes, it returns an error message.
895
 * - Otherwise, it returns an array
896
 * - optimized for the database.
897
 *
898
 * @param int[] $ip_array array of ip array ints
899
 * @param string $fullip
900
 *
901
 * @return array
902
 * @package Bans
903
 *
904
 */
905
function validateIPBan($ip_array, $fullip = '')
906
{
907
	global $scripturl;
908
909
	$db = database();
910
911
	if (count($ip_array) === 4 || count($ip_array) === 8)
912
	{
913
		$values = [
914
			'ip_low1' => $ip_array[0]['low'],
915
			'ip_high1' => $ip_array[0]['high'],
916
			'ip_low2' => $ip_array[1]['low'],
917
			'ip_high2' => $ip_array[1]['high'],
918
			'ip_low3' => $ip_array[2]['low'],
919
			'ip_high3' => $ip_array[2]['high'],
920
			'ip_low4' => $ip_array[3]['low'],
921
			'ip_high4' => $ip_array[3]['high'],
922
			'ip_low5' => $ip_array[4]['low'],
923
			'ip_high5' => $ip_array[4]['high'],
924
			'ip_low6' => $ip_array[5]['low'],
925
			'ip_high6' => $ip_array[5]['high'],
926
			'ip_low7' => $ip_array[6]['low'],
927
			'ip_high7' => $ip_array[6]['high'],
928
			'ip_low8' => $ip_array[7]['low'],
929
			'ip_high8' => $ip_array[7]['high'],
930
		];
931
	}
932
	else
933
	{
934
		$values = ['error' => 'invalid_ip'];
935
	}
936
937
	$request = $db->query('', '
938
		SELECT 
939
			bg.id_ban_group, bg.name
940
		FROM {db_prefix}ban_groups AS bg
941
		INNER JOIN {db_prefix}ban_items AS bi ON
942
			(bi.id_ban_group = bg.id_ban_group)
943
			AND ip_low1 = {int:ip_low1} AND ip_high1 = {int:ip_high1}
944
			AND ip_low2 = {int:ip_low2} AND ip_high2 = {int:ip_high2}
945
			AND ip_low3 = {int:ip_low3} AND ip_high3 = {int:ip_high3}
946
			AND ip_low4 = {int:ip_low4} AND ip_high4 = {int:ip_high4}
947
			AND ip_low5 = {int:ip_low5} AND ip_high5 = {int:ip_high5}
948
			AND ip_low6 = {int:ip_low6} AND ip_high6 = {int:ip_high6}
949
			AND ip_low7 = {int:ip_low7} AND ip_high7 = {int:ip_high7}
950
			AND ip_low8 = {int:ip_low8} AND ip_high8 = {int:ip_high8}
951
		LIMIT 1',
952
		$values
953
	);
954
	if ($request->num_rows() !== 0)
955
	{
956
		list ($error_id_ban, $error_ban_name) = $request->fetch_row();
957
		$values = ['error' => ['ban_trigger_already_exists', [
958
			$fullip,
959
			'<a href="' . $scripturl . '?action=admin;area=ban;sa=edit;bg=' . $error_id_ban . '">' . $error_ban_name . '</a>',
960
		]]];
961
	}
962
	$request->free_result();
963
964
	return $values;
965
}
966
967
/**
968
 * As it says... this tries to review the list of banned members, to match new bans.
969
 *
970
 * - Note: is_activated >= 10: a member is banned.
971
 *
972
 * @package Bans
973
 */
974
function updateBanMembers()
975
{
976
	$db = database();
977
978
	$updates = [];
979
	$allMembers = [];
980
	$newMembers = [];
981
	$memberIDs = [];
982
	$memberEmails = [];
983
	$memberEmailWild = [];
984
985
	// Start by getting all active bans - it's quicker doing this in parts...
986
	$db->fetchQuery('
987
		SELECT 
988
			bi.id_member, bi.email_address
989
		FROM {db_prefix}ban_items AS bi
990
			INNER JOIN {db_prefix}ban_groups AS bg ON (bg.id_ban_group = bi.id_ban_group)
991
		WHERE (bi.id_member > {int:no_member} OR bi.email_address != {string:blank_string})
992
			AND bg.cannot_access = {int:cannot_access_on}
993
			AND (bg.expire_time IS NULL OR bg.expire_time > {int:current_time})',
994
		[
995
			'no_member' => 0,
996
			'cannot_access_on' => 1,
997
			'current_time' => time(),
998
			'blank_string' => '',
999
		]
1000
	)->fetch_callback(
1001
		function ($row) use (&$memberIDs, &$memberEmails, &$memberEmailWild) {
1002
			if ($row['id_member'])
1003
			{
1004
				$memberIDs[$row['id_member']] = $row['id_member'];
1005
			}
1006
			if ($row['email_address'])
1007
			{
1008
				// Does it have a wildcard - if so we can't do a IN on it.
1009
				if (str_contains($row['email_address'], '%'))
1010
				{
1011
					$memberEmailWild[$row['email_address']] = $row['email_address'];
1012
				}
1013
				else
1014
				{
1015
					$memberEmails[$row['email_address']] = $row['email_address'];
1016
				}
1017
			}
1018
		}
1019
	);
1020
1021
	// Build up the query.
1022
	$queryPart = [];
1023
	$queryValues = [];
1024
	if (!empty($memberIDs))
1025
	{
1026
		$queryPart[] = 'mem.id_member IN ({array_string:member_ids})';
1027
		$queryValues['member_ids'] = $memberIDs;
1028
	}
1029
1030
	if (!empty($memberEmails))
1031
	{
1032
		$queryPart[] = 'mem.email_address IN ({array_string:member_emails})';
1033
		$queryValues['member_emails'] = $memberEmails;
1034
	}
1035
1036
	$count = 0;
1037
	foreach ($memberEmailWild as $email)
1038
	{
1039
		$queryPart[] = 'mem.email_address LIKE {string:wild_' . $count . '}';
1040
		$queryValues['wild_' . ($count++)] = $email;
1041
	}
1042
1043
	// Find all banned members.
1044
	if (!empty($queryPart))
1045
	{
1046
		$db->fetchQuery('
1047
			SELECT 
1048
				mem.id_member, mem.is_activated
1049
			FROM {db_prefix}members AS mem
1050
			WHERE ' . implode(' OR ', $queryPart),
1051
			$queryValues
1052
		)->fetch_callback(
1053
			function ($row) use (&$allMembers, &$updates, &$newMembers) {
1054
				if (!in_array($row['id_member'], $allMembers))
1055
				{
1056
					$allMembers[] = $row['id_member'];
1057
					// Do they need an update?
1058
					if ($row['is_activated'] < 10)
1059
					{
1060
						$updates[($row['is_activated'] + 10)][] = $row['id_member'];
1061
						$newMembers[] = $row['id_member'];
1062
					}
1063
				}
1064
			}
1065
		);
1066
	}
1067
1068
	// We welcome our new members in the realm of the banned.
1069
	if (!empty($newMembers))
1070
	{
1071
		require_once(SUBSDIR . '/Logging.subs.php');
1072
		logOnline($newMembers, false);
1073
	}
1074
1075
	// Find members that are wrongfully marked as banned.
1076
	$db->fetchQuery('
1077
		SELECT mem.id_member, mem.is_activated - 10 AS new_value
1078
		FROM {db_prefix}members AS mem
1079
			LEFT JOIN {db_prefix}ban_items AS bi ON (bi.id_member = mem.id_member OR mem.email_address LIKE bi.email_address)
1080
			LEFT JOIN {db_prefix}ban_groups AS bg ON (bg.id_ban_group = bi.id_ban_group AND bg.cannot_access = {int:cannot_access_activated} AND (bg.expire_time IS NULL OR bg.expire_time > {int:current_time}))
1081
		WHERE (bi.id_ban IS NULL OR bg.id_ban_group IS NULL)
1082
			AND mem.is_activated >= {int:ban_flag}',
1083
		[
1084
			'cannot_access_activated' => 1,
1085
			'current_time' => time(),
1086
			'ban_flag' => 10,
1087
		]
1088
	)->fetch_callback(
1089
		function ($row) use (&$allMembers, &$updates) {
1090
			// Don't do this twice!
1091
			if (!in_array($row['id_member'], $allMembers))
1092
			{
1093
				$updates[$row['new_value']][] = $row['id_member'];
1094
				$allMembers[] = $row['id_member'];
1095
			}
1096
		}
1097
	);
1098
1099
	if (!empty($updates))
1100
	{
1101
		require_once(SUBSDIR . '/Members.subs.php');
1102
		foreach ($updates as $newStatus => $members)
1103
		{
1104
			updateMemberData($members, ['is_activated' => $newStatus]);
1105
		}
1106
	}
1107
1108
	// Update the latest member and our total members as banning may change them.
1109
	require_once(SUBSDIR . '/Members.subs.php');
1110
	updateMemberStats();
1111
}
1112
1113
/**
1114
 * Returns member data for a given member id in a suggestion format used by bans
1115
 *
1116
 * @param int $id
1117
 *
1118
 * @return array
1119
 * @package Bans
1120
 * @uses getBasicMemberData
1121
 *
1122
 */
1123
function getMemberData($id)
1124
{
1125
	$suggestions = [];
1126
1127
	require_once(SUBSDIR . '/Members.subs.php');
1128
1129
	$result = getBasicMemberData($id, ['moderation' => true]);
1130
	if (!empty($result))
1131
	{
1132
		$suggestions = [
1133
			'member' => [
1134
				'id' => $result['id_member'],
1135
				'name' => $result['real_name'],
1136
			],
1137
			'main_ip' => $result['member_ip'],
1138
			'email' => $result['email_address'],
1139
		];
1140
	}
1141
1142
	return $suggestions;
1143
}
1144
1145
/**
1146
 * Get ban triggers for the given parameters.
1147
 *
1148
 * @param int $start The item to start with (for pagination purposes)
1149
 * @param int $items_per_page The number of items to show per page
1150
 * @param string $sort A string indicating how to sort the results
1151
 * @param string $trigger_type
1152
 * @return array
1153
 * @package Bans
1154
 */
1155
function list_getBanTriggers($start, $items_per_page, $sort, $trigger_type)
1156
{
1157
	$db = database();
1158
1159
	$where = [
1160
		'ip' => 'bi.ip_low1 > 0',
1161
		'hostname' => 'bi.hostname != {string:blank_string}',
1162
		'email' => 'bi.email_address != {string:blank_string}',
1163
	];
1164
1165
	return $db->fetchQuery('
1166
		SELECT
1167
			bi.id_ban, bi.ip_low1, bi.ip_high1, bi.ip_low2, bi.ip_high2, bi.ip_low3, bi.ip_high3, bi.ip_low4, bi.ip_high4, bi.ip_low5, bi.ip_high5, bi.ip_low6, bi.ip_high6, bi.ip_low7, bi.ip_high7, bi.ip_low8, bi.ip_high8, bi.hostname, bi.email_address, bi.hits,
1168
			bg.id_ban_group, bg.name' . ($trigger_type === 'member' ? ',
1169
			mem.id_member, mem.real_name' : '') . '
1170
		FROM {db_prefix}ban_items AS bi
1171
			INNER JOIN {db_prefix}ban_groups AS bg ON (bg.id_ban_group = bi.id_ban_group)' . ($trigger_type === 'member' ? '
1172
			INNER JOIN {db_prefix}members AS mem ON (mem.id_member = bi.id_member)' : '
1173
		WHERE ' . $where[$trigger_type]) . '
1174
		ORDER BY ' . $sort . '
1175
		LIMIT ' . $items_per_page . '  OFFSET ' . $start,
1176
		[
1177
			'blank_string' => '',
1178
		]
1179
	)->fetch_all();
1180
}
1181
1182
/**
1183
 * Used to see if a user is banned
1184
 *
1185
 * - Checks banning by ip, hostname, email or member id
1186
 *
1187
 * @param int $memID
1188
 * @param string $hostname
1189
 * @param string $email
1190
 *
1191
 * @return array
1192
 * @package Bans
1193
 *
1194
 */
1195
function BanCheckUser($memID, $hostname = '', $email = '')
1196
{
1197
	global $scripturl, $txt;
1198
1199
	$db = database();
1200
	$bans = [];
1201
	$member = MembersList::get($memID);
1202
	$member->loadContext();
1203
1204
	// This is a valid member id, we at least need that
1205
	if (!$member->isEmpty())
1206
	{
1207
		$ban_query = [];
1208
		$ban_query_vars = [
1209
			'time' => time(),
1210
		];
1211
1212
		// Member id and ip
1213 2
		$ban_query[] = 'id_member = ' . $memID;
1214
		require_once(SOURCEDIR . '/Security.php');
1215 2
		$ban_query[] = constructBanQueryIP($member['ip']);
1216 2
1217 2
		// Do we have a hostname?
1218 2
		if (!empty($hostname))
1219
		{
1220
			$ban_query[] = '({string:hostname} LIKE hostname)';
1221 2
			$ban_query_vars['hostname'] = $hostname;
1222
		}
1223 2
1224
		// Check their email as well...
1225 2
		if ($email !== '')
1226
		{
1227
			$ban_query[] = '({string:email} LIKE bi.email_address)';
1228
			$ban_query_vars['email'] = $email;
1229 2
		}
1230 2
1231 2
		// So... are they banned?  Dying to know!
1232
		$request = $db->query('', '
1233
			SELECT bg.id_ban_group, bg.name, bg.cannot_access, bg.cannot_post, bg.cannot_register,
1234 2
				bg.cannot_login, bg.reason
1235
			FROM {db_prefix}ban_items AS bi
1236
				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:time}))
1237
			WHERE (' . implode(' OR ', $ban_query) . ')',
1238
			$ban_query_vars
1239
		);
1240
		$bans = [];
1241 2
		while (($row = $request->fetch_assoc()))
1242
		{
1243 2
			// Work out what restrictions we actually have.
1244 2
			$ban_restrictions = [];
1245
			foreach (['access', 'register', 'login', 'post'] as $type)
1246
			{
1247
				if ($row['cannot_' . $type])
1248 2
				{
1249
					$ban_restrictions[] = $txt['ban_type_' . $type];
1250
				}
1251
			}
1252
1253 2
			// No actual ban in place?
1254 1
			if (empty($ban_restrictions))
1255
			{
1256 2
				continue;
1257 2
			}
1258
1259
			// Prepare the link for context.
1260
			$ban_explanation = sprintf($txt['user_cannot_due_to'], implode(', ', $ban_restrictions), '<a href="' . $scripturl . '?action=admin;area=ban;sa=edit;bg=' . $row['id_ban_group'] . '">' . $row['name'] . '</a>');
1261
1262
			$bans[$row['id_ban_group']] = [
1263
				'reason' => empty($row['reason']) ? '' : '<br /><br /><strong>' . $txt['ban_reason'] . ':</strong> ' . $row['reason'],
1264
				'cannot' => [
1265
					'access' => !empty($row['cannot_access']),
1266
					'register' => !empty($row['cannot_register']),
1267
					'post' => !empty($row['cannot_post']),
1268
					'login' => !empty($row['cannot_login']),
1269
				],
1270
				'explanation' => $ban_explanation,
1271
			];
1272
		}
1273
		$request->free_result();
1274
	}
1275
1276
	return $bans;
1277
}
1278
1279
/**
1280
 * This returns the total number of ban triggers of the given type.
1281
 *
1282
 * @param string $trigger_type
1283
 * @return int
1284
 * @package Bans
1285
 */
1286
function list_getNumBanTriggers($trigger_type)
1287
{
1288
	$db = database();
1289 2
1290
	$where = [
1291
		'ip' => 'bi.ip_low1 > 0',
1292 2
		'hostname' => 'bi.hostname != {string:blank_string}',
1293
		'email' => 'bi.email_address != {string:blank_string}',
1294
	];
1295
1296
	$request = $db->query('', '
1297
		SELECT COUNT(*)
1298
		FROM {db_prefix}ban_items AS bi' . ($trigger_type === 'member' ? '
1299
			INNER JOIN {db_prefix}members AS mem ON (mem.id_member = bi.id_member)' : '
1300
		WHERE ' . $where[$trigger_type]),
1301
		[
1302
			'blank_string' => '',
1303
		]
1304
	);
1305
	list ($num_triggers) = $request->fetch_row();
1306
	$request->free_result();
1307
1308
	return $num_triggers;
1309
}
1310
1311
/**
1312
 * Load a list of ban log entries from the database.
1313
 *
1314
 * - no permissions checks are done
1315
 *
1316
 * @param int $start The item to start with (for pagination purposes)
1317
 * @param int $items_per_page The number of items to show per page
1318
 * @param string $sort A string indicating how to sort the results
1319
 *
1320
 * @return array
1321
 * @package Bans
1322
 *
1323
 */
1324
function list_getBanLogEntries($start, $items_per_page, $sort)
1325
{
1326
	$db = database();
1327
1328
	return $db->fetchQuery('
1329
		SELECT lb.id_ban_log, lb.id_member, COALESCE(lb.ip, {string:dash}) AS ip, COALESCE(lb.email, {string:dash}) AS email, lb.log_time, COALESCE(mem.real_name, {string:blank_string}) AS real_name
1330
		FROM {db_prefix}log_banned AS lb
1331
			LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = lb.id_member)
1332
		ORDER BY ' . $sort . '
1333
		LIMIT ' . $items_per_page . '  OFFSET ' . $start,
1334
		[
1335
			'blank_string' => '',
1336
			'dash' => '-',
1337
		]
1338
	)->fetch_all();
1339
}
1340
1341
/**
1342
 * This returns the total count of ban log entries.
1343
 *
1344
 * @package Bans
1345
 */
1346
function list_getNumBanLogEntries()
1347
{
1348
	$db = database();
1349
1350
	$request = $db->query('', '
1351
		SELECT COUNT(*)
1352
		FROM {db_prefix}log_banned AS lb',
1353
		[]
1354
	);
1355
	list ($num_entries) = $request->fetch_row();
1356
	$request->free_result();
1357
1358
	return $num_entries;
1359
}
1360
1361
/**
1362
 * Get the total number of ban from the ban group table
1363
 *
1364
 * @return int
1365
 * @package Bans
1366
 */
1367
function list_getNumBans()
1368
{
1369
	$db = database();
1370
1371
	$request = $db->query('', '
1372
		SELECT COUNT(*) AS num_bans
1373
		FROM {db_prefix}ban_groups',
1374
		[]
1375
	);
1376
	list ($numBans) = $request->fetch_row();
1377
	$request->free_result();
1378
1379
	return $numBans;
1380
}
1381
1382
/**
1383
 * Retrieves all the ban items belonging to a certain ban group
1384
 *
1385
 * @param int $start The item to start with (for pagination purposes)
1386
 * @param int $items_per_page The number of items to show per page
1387
 * @param int $sort A string indicating how to sort the results
1388
 * @param int $ban_group_id
1389
 *
1390
 * @return array
1391
 * @throws \ElkArte\Exceptions\Exception ban_not_found
1392
 * @package Bans
1393
 *
1394
 */
1395
function list_getBanItems($start = 0, $items_per_page = 0, $sort = 0, $ban_group_id = 0)
1396
{
1397
	global $context, $scripturl;
1398
1399
	$db = database();
1400
1401
	$ban_items = [];
1402
	$request = $db->query('', '
1403
		SELECT
1404
			bi.id_ban, bi.hostname, bi.email_address, bi.id_member, bi.hits,
1405
			bi.ip_low1, bi.ip_high1, bi.ip_low2, bi.ip_high2, bi.ip_low3, bi.ip_high3, bi.ip_low4, bi.ip_high4,
1406
			bi.ip_low5, bi.ip_high5, bi.ip_low6, bi.ip_high6, bi.ip_low7, bi.ip_high7, bi.ip_low8, bi.ip_high8,
1407
			bg.id_ban_group, bg.name, bg.ban_time, bg.expire_time, bg.reason, bg.notes, bg.cannot_access, bg.cannot_register, bg.cannot_login, bg.cannot_post,
1408
			COALESCE(mem.id_member, 0) AS id_member, mem.member_name, mem.real_name
1409
		FROM {db_prefix}ban_groups AS bg
1410
			LEFT JOIN {db_prefix}ban_items AS bi ON (bi.id_ban_group = bg.id_ban_group)
1411
			LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = bi.id_member)
1412
		WHERE bg.id_ban_group = {int:current_ban}
1413
		LIMIT {int:start}, {int:items_per_page}',
1414
		[
1415
			'current_ban' => $ban_group_id,
1416
			'start' => $start,
1417
			'items_per_page' => $items_per_page,
1418
		]
1419
	);
1420
	if ($request->num_rows() === 0)
1421
	{
1422
		throw new \ElkArte\Exceptions\Exception('ban_not_found', false);
1423
	}
1424
	while (($row = $request->fetch_assoc()))
1425
	{
1426
		if (!isset($context['ban']))
1427
		{
1428
			$context['ban'] = [
1429
				'id' => $row['id_ban_group'],
1430
				'name' => $row['name'],
1431
				'expiration' => [
1432
					'status' => $row['expire_time'] === null ? 'never' : ($row['expire_time'] < time() ? 'expired' : 'one_day'),
1433
					'days' => $row['expire_time'] > time() ? floor(($row['expire_time'] - time()) / 86400) : 0
1434
				],
1435
				'reason' => $row['reason'],
1436
				'notes' => $row['notes'],
1437
				'cannot' => [
1438
					'access' => !empty($row['cannot_access']),
1439
					'post' => !empty($row['cannot_post']),
1440
					'register' => !empty($row['cannot_register']),
1441
					'login' => !empty($row['cannot_login']),
1442
				],
1443
				'is_new' => false,
1444
				'hostname' => '',
1445
				'email' => '',
1446
			];
1447
		}
1448
1449
		if (!empty($row['id_ban']))
1450
		{
1451
			$ban_items[$row['id_ban']] = [
1452
				'id' => $row['id_ban'],
1453
				'hits' => $row['hits'],
1454
			];
1455
			if (!empty($row['ip_high1']))
1456
			{
1457
				$ban_items[$row['id_ban']]['type'] = 'ip';
1458
				$ban_items[$row['id_ban']]['ip'] = range2ip([$row['ip_low1'], $row['ip_low2'], $row['ip_low3'], $row['ip_low4'], $row['ip_low5'], $row['ip_low6'], $row['ip_low7'], $row['ip_low8']], [$row['ip_high1'], $row['ip_high2'], $row['ip_high3'], $row['ip_high4'], $row['ip_high5'], $row['ip_high6'], $row['ip_high7'], $row['ip_high8']]);
1459
			}
1460
			elseif (!empty($row['hostname']))
1461
			{
1462
				$ban_items[$row['id_ban']]['type'] = 'hostname';
1463
				$ban_items[$row['id_ban']]['hostname'] = str_replace('%', '*', $row['hostname']);
1464
			}
1465
			elseif (!empty($row['email_address']))
1466
			{
1467
				$ban_items[$row['id_ban']]['type'] = 'email';
1468
				$ban_items[$row['id_ban']]['email'] = str_replace('%', '*', $row['email_address']);
1469
			}
1470
			elseif (!empty($row['id_member']))
1471
			{
1472
				$ban_items[$row['id_ban']]['type'] = 'user';
1473
				$ban_items[$row['id_ban']]['user'] = [
1474
					'id' => $row['id_member'],
1475
					'name' => $row['real_name'],
1476
					'href' => $scripturl . '?action=profile;u=' . $row['id_member'],
1477
					'link' => '<a href="' . $scripturl . '?action=profile;u=' . $row['id_member'] . '">' . $row['real_name'] . '</a>',
1478
				];
1479
			}
1480
			// Invalid ban (member probably doesn't exist anymore).
1481
			else
1482
			{
1483
				unset($ban_items[$row['id_ban']]);
1484
				removeBanTriggers($row['id_ban']);
1485
			}
1486
		}
1487
	}
1488
	$request->free_result();
1489
1490
	return $ban_items;
1491
}
1492
1493
/**
1494
 * Get bans, what else? For the given options.
1495
 *
1496
 * @param int $start The item to start with (for pagination purposes)
1497
 * @param int $items_per_page The number of items to show per page
1498
 * @param string $sort A string indicating how to sort the results
1499
 * @return array
1500
 * @package Bans
1501
 */
1502
function list_getBans($start, $items_per_page, $sort)
1503
{
1504
	$db = database();
1505
1506
	return $db->fetchQuery('
1507
		SELECT bg.id_ban_group, bg.name, bg.ban_time, bg.expire_time, bg.reason, bg.notes, COUNT(bi.id_ban) AS num_triggers
1508
		FROM {db_prefix}ban_groups AS bg
1509
			LEFT JOIN {db_prefix}ban_items AS bi ON (bi.id_ban_group = bg.id_ban_group)
1510
		GROUP BY bg.id_ban_group, bg.name, bg.ban_time, bg.expire_time, bg.reason, bg.notes
1511
		ORDER BY {raw:sort}
1512
		LIMIT {int:limit} OFFSET {int:offset} ',
1513
		[
1514
			'sort' => $sort,
1515
			'offset' => $start,
1516
			'limit' => $items_per_page,
1517
		]
1518
	)->fetch_all();
1519
}
1520
1521
/**
1522
 * Gets the number of ban items belonging to a certain ban group
1523
 *
1524
 * @param int $ban_group_id
1525
 * @return int
1526
 * @package Bans
1527
 */
1528
function list_getNumBanItems($ban_group_id = 0)
1529
{
1530
	$db = database();
1531
1532
	$ban_group_id = (int) $ban_group_id;
1533
1534
	$request = $db->query('', '
1535
		SELECT 
1536
			COUNT(bi.id_ban)
1537
		FROM {db_prefix}ban_groups AS bg
1538
			LEFT JOIN {db_prefix}ban_items AS bi ON (bi.id_ban_group = bg.id_ban_group)
1539
			LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = bi.id_member)
1540
		WHERE bg.id_ban_group = {int:current_ban}',
1541
		[
1542
			'current_ban' => $ban_group_id,
1543
		]
1544
	);
1545
	list ($banNumber) = $request->fetch_row();
1546
	$request->free_result();
1547
1548
	return $banNumber;
1549
}
1550
1551
/**
1552
 * Load other IPs the given member has used on forum while posting.
1553
 *
1554
 * @param int $member_id
1555
 *
1556
 * @return array
1557
 * @package Bans
1558
 *
1559
 */
1560
function banLoadAdditionalIPsMember($member_id)
1561
{
1562
	$db = database();
1563
1564
	// Find some additional IP's used by this member.
1565
	$request = $db->fetchQuery('
1566
		SELECT DISTINCT poster_ip
1567
		FROM {db_prefix}messages
1568
		WHERE id_member = {int:current_user}
1569
			AND poster_ip != {string:empty}
1570
		ORDER BY poster_ip',
1571
		[
1572
			'current_user' => $member_id,
1573
			'empty' => '',
1574
		]
1575
	);
1576
	$message_ips = $request->fetch_callback(function($row) {
1577
		return $row['poster_ip'];
1578
	});
1579
	$request->free_result();
1580
1581
	return $message_ips;
1582
}
1583
1584
/**
1585
 * Load other IPs the given member has received errors logged while they were using them.
1586
 *
1587
 * @param int $member_id
1588
 *
1589
 * @return array
1590
 * @package Bans
1591
 *
1592
 */
1593
function banLoadAdditionalIPsError($member_id)
1594
{
1595
	$db = database();
1596
1597
	$request = $db->fetchQuery('
1598
		SELECT DISTINCT ip
1599
		FROM {db_prefix}log_errors
1600
		WHERE id_member = {int:current_user}
1601
			AND ip != {string:empty}
1602
		ORDER BY ip',
1603
		[
1604
			'current_user' => $member_id,
1605
			'empty' => '',
1606
		]
1607
	);
1608
	$error_ips = $request->fetch_callback(function ($row) {
1609
		return $row['ip'];
1610
	});
1611
	$request->free_result();
1612
1613
	return $error_ips;
1614
}
1615
1616
/**
1617
 * Finds additional IPs related to a certain user
1618
 *
1619
 * @param int $member_id
1620
 * @return array
1621
 * @package Bans
1622
 */
1623
function banLoadAdditionalIPs($member_id)
1624
{
1625
	// Borrowing a few language strings from profile.
1626
	Txt::load('Profile');
1627
1628
	$search_list = [];
1629
	call_integration_hook('integrate_load_additional_ip_ban', [&$search_list]);
1630
	$search_list += ['ips_in_messages' => 'banLoadAdditionalIPsMember', 'ips_in_errors' => 'banLoadAdditionalIPsError'];
1631
1632
	$return = [];
1633
	foreach ($search_list as $key => $callable)
1634
	{
1635
		if (is_callable($callable))
1636
		{
1637
			$return[$key] = call_user_func($callable, $member_id);
1638
		}
1639
	}
1640
1641
	return $return;
1642
}
1643
1644
/**
1645
 * Fetches ban details
1646
 *
1647
 * @param int[]|int $ban_ids
1648
 * @param int|bool $ban_group
1649
 *
1650
 * @return array
1651
 * @package Bans
1652
 *
1653
 */
1654
function banDetails($ban_ids, $ban_group = false)
1655
{
1656
	$db = database();
1657
1658
	if (!is_array($ban_ids))
1659
	{
1660
		$ban_ids = [$ban_ids];
1661
	}
1662
1663
	$details = [];
1664
	$db->fetchQuery('
1665
		SELECT
1666
			bi.id_ban, bi.id_ban_group, bi.hostname, bi.email_address, bi.id_member,
1667
			bi.ip_low1, bi.ip_high1, bi.ip_low2, bi.ip_high2, bi.ip_low3, bi.ip_high3, bi.ip_low4, bi.ip_high4,
1668
			bi.ip_low5, bi.ip_high5, bi.ip_low6, bi.ip_high6, bi.ip_low7, bi.ip_high7, bi.ip_low8, bi.ip_high8,
1669
			mem.member_name, mem.real_name
1670
		FROM {db_prefix}ban_items AS bi
1671
			LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = bi.id_member)
1672
		WHERE bi.id_ban IN ({array_int:ban_items})' . ($ban_group !== false ? '
1673
			AND bi.id_ban_group = {int:ban_group}' : ''),
1674
		[
1675
			'ban_items' => $ban_ids,
1676
			'ban_group' => $ban_group,
1677
		]
1678
	)->fetch_callback(
1679
		function ($row) use (&$details) {
1680
			$details[$row['id_ban']] = $row;
1681
		}
1682
	);
1683
1684
	return $details;
1685
}
1686
1687
/**
1688
 * When removing a ban trigger, this will return the specifics of whats being
1689
 * removed so it can be logged
1690
 *
1691
 * @param array $ban_details
1692
 *
1693
 * @return array
1694
 * @package Bans
1695
 *
1696
 */
1697
function banLogItems($ban_details)
1698
{
1699
	$log_info = [];
1700
1701
	// For each ban, get the details for logging
1702
	foreach ($ban_details as $row)
1703
	{
1704
		// An ip ban
1705
		if (!empty($row['ip_high1']))
1706
		{
1707
			$ip = range2ip([$row['ip_low1'], $row['ip_low2'], $row['ip_low3'], $row['ip_low4'], $row['ip_low5'], $row['ip_low6'], $row['ip_low7'], $row['ip_low8']], [$row['ip_high1'], $row['ip_high2'], $row['ip_high3'], $row['ip_high4'], $row['ip_high5'], $row['ip_high6'], $row['ip_high7'], $row['ip_high8']]);
1708
			$is_range = (str_contains($ip, '-') || str_contains($ip, '*'));
1709
1710
			$log_info[] = [
1711
				'bantype' => ($is_range ? 'ip_range' : 'main_ip'),
1712
				'value' => $ip,
1713
			];
1714
		}
1715
		// Hostname
1716
		elseif (!empty($row['hostname']))
1717
		{
1718
			$log_info[] = [
1719
				'bantype' => 'hostname',
1720
				'value' => $row['hostname'],
1721
			];
1722
		}
1723
		// Email Address
1724
		elseif (!empty($row['email_address']))
1725
		{
1726
			$log_info[] = [
1727
				'bantype' => 'email',
1728
				'value' => str_replace('%', '*', $row['email_address']),
1729
			];
1730
		}
1731
		// Member ID
1732
		elseif (!empty($row['id_member']))
1733
		{
1734
			$log_info[] = [
1735
				'bantype' => 'user',
1736
				'value' => $row['id_member'],
1737
			];
1738
		}
1739
	}
1740
1741
	return $log_info;
1742
}
1743