Issues (1686)

sources/subs/Bans.subs.php (3 issues)

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 mixed[] $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 = array(
55
		'main_ip' => '',
56
		'hostname' => '',
57
		'email' => '',
58
		'member' => array(
59
			'id' => $member,
60
		)
61
	);
62
63
	$ban_errors = ErrorContext::context('ban', 1);
64
65
	if (!is_array($suggestions))
0 ignored issues
show
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
	else
105
	{
106
		return false;
107
	}
108
}
109
110
/**
111
 * This function removes a batch of triggers based on ids
112
 *
113
 * What it does:
114
 *
115
 * - Doesn't clean the inputs, expects valid input
116
 * - Removes the ban triggers by id or group
117
 *
118
 * @param int[]|int $items_ids
119
 * @param int|bool $group_id
120
 * @return bool
121
 * @package Bans
122
 */
123
function removeBanTriggers($items_ids = array(), $group_id = false)
124
{
125
	$db = database();
126
127
	if ($group_id !== false)
128
	{
129
		$group_id = (int) $group_id;
130
	}
131
132
	if (empty($group_id) && empty($items_ids))
133
	{
134
		return false;
135
	}
136
137
	if (!is_array($items_ids))
138
	{
139
		$items_ids = array($items_ids);
140
	}
141
142
	// Log the ban removals so others know
143
	$log_info = banLogItems(banDetails($items_ids, $group_id));
144
	logTriggersUpdates($log_info, 'remove');
145
146
	// Remove the ban triggers by id's or groups
147
	if ($group_id !== false)
148
	{
149
		$db->query('', '
150
			DELETE FROM {db_prefix}ban_items
151
			WHERE id_ban IN ({array_int:ban_list})
152
				AND id_ban_group = {int:ban_group}',
153
			array(
154
				'ban_list' => $items_ids,
155
				'ban_group' => $group_id,
156
			)
157
		);
158
	}
159
	elseif (!empty($items_ids))
160
	{
161
		$db->query('', '
162
			DELETE FROM {db_prefix}ban_items
163
			WHERE id_ban IN ({array_int:ban_list})',
164
			array(
165
				'ban_list' => $items_ids,
166
			)
167
		);
168
	}
169
170
	return true;
171
}
172
173
/**
174
 * This function removes a batch of ban groups based on ids
175
 *
176
 * What it does:
177
 *
178
 * - Doesn't clean the inputs
179
 * - Removes entries from the ban group list, one or many
180
 *
181
 * @param int[]|int $group_ids
182
 * @return bool
183
 * @package Bans
184
 */
185
function removeBanGroups($group_ids)
186
{
187
	$db = database();
188
189
	if (!is_array($group_ids))
190
	{
191
		$group_ids = array($group_ids);
192
	}
193
194
	$group_ids = array_unique($group_ids);
195
196
	if (empty($group_ids))
197
	{
198
		return false;
199
	}
200
201
	$db->query('', '
202
		DELETE FROM {db_prefix}ban_groups
203
		WHERE id_ban_group IN ({array_int:ban_list})',
204
		array(
205
			'ban_list' => $group_ids,
206
		)
207
	);
208
209
	return true;
210
}
211
212
/**
213
 * Removes ban logs
214
 *
215
 * What it does:
216
 *
217
 * - By default (no id's passed) truncate the table
218
 * - Doesn't clean the inputs
219
 *
220
 * @param int[]|int|null $ids (optional)
221
 * @return bool
222
 * @package Bans
223
 */
224
function removeBanLogs($ids = array())
225
{
226
	$db = database();
227
228
	// No specific id's passed, we truncate the entire table
229
	if (empty($ids))
230
	{
231
		$db->truncate('{db_prefix}log_banned');
232
	}
233
	else
234
	{
235
		if (!is_array($ids))
236
		{
237
			$ids = array($ids);
238
		}
239
240
		// Can only remove it once
241
		$ids = array_unique($ids);
0 ignored issues
show
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

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