Issues (1027)

Sources/ManageBans.php (9 issues)

1
<?php
2
3
/**
4
 * This file contains all the functions used for the ban center.
5
 *
6
 * @todo refactor as controller-model
7
 *
8
 * Simple Machines Forum (SMF)
9
 *
10
 * @package SMF
11
 * @author Simple Machines http://www.simplemachines.org
12
 * @copyright 2019 Simple Machines and individual contributors
13
 * @license http://www.simplemachines.org/about/smf/license.php BSD
14
 *
15
 * @version 2.1 RC2
16
 */
17
18
if (!defined('SMF'))
19
	die('No direct access...');
20
21
/**
22
 * Ban center. The main entrance point for all ban center functions.
23
 * It is accesssed by ?action=admin;area=ban.
24
 * It choses a function based on the 'sa' parameter, like many others.
25
 * The default sub-action is BanList().
26
 * It requires the ban_members permission.
27
 * It initializes the admin tabs.
28
 *
29
 * @uses ManageBans template.
30
 */
31
function Ban()
32
{
33
	global $context, $txt, $scripturl;
34
35
	isAllowedTo('manage_bans');
36
37
	loadTemplate('ManageBans');
38
39
	$subActions = array(
40
		'add' => 'BanEdit',
41
		'browse' => 'BanBrowseTriggers',
42
		'edittrigger' => 'BanEditTrigger',
43
		'edit' => 'BanEdit',
44
		'list' => 'BanList',
45
		'log' => 'BanLog',
46
	);
47
48
	// Default the sub-action to 'view ban list'.
49
	$_REQUEST['sa'] = isset($_REQUEST['sa']) && isset($subActions[$_REQUEST['sa']]) ? $_REQUEST['sa'] : 'list';
50
51
	$context['page_title'] = $txt['ban_title'];
52
	$context['sub_action'] = $_REQUEST['sa'];
53
54
	// Tabs for browsing the different ban functions.
55
	$context[$context['admin_menu_name']]['tab_data'] = array(
56
		'title' => $txt['ban_title'],
57
		'help' => 'ban_members',
58
		'description' => $txt['ban_description'],
59
		'tabs' => array(
60
			'list' => array(
61
				'description' => $txt['ban_description'],
62
				'href' => $scripturl . '?action=admin;area=ban;sa=list',
63
				'is_selected' => $_REQUEST['sa'] == 'list' || $_REQUEST['sa'] == 'edit' || $_REQUEST['sa'] == 'edittrigger',
64
			),
65
			'add' => array(
66
				'description' => $txt['ban_description'],
67
				'href' => $scripturl . '?action=admin;area=ban;sa=add',
68
				'is_selected' => $_REQUEST['sa'] == 'add',
69
			),
70
			'browse' => array(
71
				'description' => $txt['ban_trigger_browse_description'],
72
				'href' => $scripturl . '?action=admin;area=ban;sa=browse',
73
				'is_selected' => $_REQUEST['sa'] == 'browse',
74
			),
75
			'log' => array(
76
				'description' => $txt['ban_log_description'],
77
				'href' => $scripturl . '?action=admin;area=ban;sa=log',
78
				'is_selected' => $_REQUEST['sa'] == 'log',
79
				'is_last' => true,
80
			),
81
		),
82
	);
83
84
	call_integration_hook('integrate_manage_bans', array(&$subActions));
85
86
	// Call the right function for this sub-action.
87
	call_helper($subActions[$_REQUEST['sa']]);
88
}
89
90
/**
91
 * Shows a list of bans currently set.
92
 * It is accessed by ?action=admin;area=ban;sa=list.
93
 * It removes expired bans.
94
 * It allows sorting on different criteria.
95
 * It also handles removal of selected ban items.
96
 *
97
 * @uses the main ManageBans template.
98
 */
99
function BanList()
100
{
101
	global $txt, $context, $scripturl;
102
	global $user_info, $sourcedir, $modSettings;
103
104
	// User pressed the 'remove selection button'.
105
	if (!empty($_POST['removeBans']) && !empty($_POST['remove']) && is_array($_POST['remove']))
106
	{
107
		checkSession();
108
109
		// Make sure every entry is a proper integer.
110
		array_map('intval', $_POST['remove']);
111
112
		// Unban them all!
113
		removeBanGroups($_POST['remove']);
114
		removeBanTriggers($_POST['remove']);
115
116
		// No more caching this ban!
117
		updateSettings(array('banLastUpdated' => time()));
118
119
		// Some members might be unbanned now. Update the members table.
120
		updateBanMembers();
121
	}
122
123
	// Create a date string so we don't overload them with date info.
124
	if (preg_match('~%[AaBbCcDdeGghjmuYy](?:[^%]*%[AaBbCcDdeGghjmuYy])*~', $user_info['time_format'], $matches) == 0 || empty($matches[0]))
125
		$context['ban_time_format'] = $user_info['time_format'];
126
	else
127
		$context['ban_time_format'] = $matches[0];
128
129
	$listOptions = array(
130
		'id' => 'ban_list',
131
		'title' => $txt['ban_title'],
132
		'items_per_page' => $modSettings['defaultMaxListItems'],
133
		'base_href' => $scripturl . '?action=admin;area=ban;sa=list',
134
		'default_sort_col' => 'added',
135
		'default_sort_dir' => 'desc',
136
		'get_items' => array(
137
			'function' => 'list_getBans',
138
		),
139
		'get_count' => array(
140
			'function' => 'list_getNumBans',
141
		),
142
		'no_items_label' => $txt['ban_no_entries'],
143
		'columns' => array(
144
			'name' => array(
145
				'header' => array(
146
					'value' => $txt['ban_name'],
147
				),
148
				'data' => array(
149
					'db' => 'name',
150
				),
151
				'sort' => array(
152
					'default' => 'bg.name',
153
					'reverse' => 'bg.name DESC',
154
				),
155
			),
156
			'notes' => array(
157
				'header' => array(
158
					'value' => $txt['ban_notes'],
159
				),
160
				'data' => array(
161
					'db' => 'notes',
162
					'class' => 'smalltext',
163
				),
164
				'sort' => array(
165
					'default' => 'LENGTH(bg.notes) > 0 DESC, bg.notes',
166
					'reverse' => 'LENGTH(bg.notes) > 0, bg.notes DESC',
167
				),
168
			),
169
			'reason' => array(
170
				'header' => array(
171
					'value' => $txt['ban_reason'],
172
				),
173
				'data' => array(
174
					'db' => 'reason',
175
					'class' => 'smalltext',
176
				),
177
				'sort' => array(
178
					'default' => 'LENGTH(bg.reason) > 0 DESC, bg.reason',
179
					'reverse' => 'LENGTH(bg.reason) > 0, bg.reason DESC',
180
				),
181
			),
182
			'added' => array(
183
				'header' => array(
184
					'value' => $txt['ban_added'],
185
				),
186
				'data' => array(
187
					'function' => function($rowData) use ($context)
188
					{
189
						return timeformat($rowData['ban_time'], empty($context['ban_time_format']) ? true : $context['ban_time_format']);
190
					},
191
				),
192
				'sort' => array(
193
					'default' => 'bg.ban_time',
194
					'reverse' => 'bg.ban_time DESC',
195
				),
196
			),
197
			'expires' => array(
198
				'header' => array(
199
					'value' => $txt['ban_expires'],
200
				),
201
				'data' => array(
202
					'function' => function($rowData) use ($txt)
203
					{
204
						// This ban never expires...whahaha.
205
						if ($rowData['expire_time'] === null)
206
							return $txt['never'];
207
208
						// This ban has already expired.
209
						elseif ($rowData['expire_time'] < time())
210
							return sprintf('<span class="red">%1$s</span>', $txt['ban_expired']);
211
212
						// Still need to wait a few days for this ban to expire.
213
						else
214
							return sprintf('%1$d&nbsp;%2$s', ceil(($rowData['expire_time'] - time()) / (60 * 60 * 24)), $txt['ban_days']);
215
					},
216
				),
217
				'sort' => array(
218
					'default' => 'COALESCE(bg.expire_time, 1=1) DESC, bg.expire_time DESC',
219
					'reverse' => 'COALESCE(bg.expire_time, 1=1), bg.expire_time',
220
				),
221
			),
222
			'num_triggers' => array(
223
				'header' => array(
224
					'value' => $txt['ban_triggers'],
225
				),
226
				'data' => array(
227
					'db' => 'num_triggers',
228
				),
229
				'sort' => array(
230
					'default' => 'num_triggers DESC',
231
					'reverse' => 'num_triggers',
232
				),
233
			),
234
			'actions' => array(
235
				'header' => array(
236
					'value' => $txt['ban_actions'],
237
					'class' => 'centercol',
238
				),
239
				'data' => array(
240
					'sprintf' => array(
241
						'format' => '<a href="' . $scripturl . '?action=admin;area=ban;sa=edit;bg=%1$d">' . $txt['modify'] . '</a>',
242
						'params' => array(
243
							'id_ban_group' => false,
244
						),
245
					),
246
					'class' => 'centercol',
247
				),
248
			),
249
			'check' => array(
250
				'header' => array(
251
					'value' => '<input type="checkbox" onclick="invertAll(this, this.form);">',
252
					'class' => 'centercol',
253
				),
254
				'data' => array(
255
					'sprintf' => array(
256
						'format' => '<input type="checkbox" name="remove[]" value="%1$d">',
257
						'params' => array(
258
							'id_ban_group' => false,
259
						),
260
					),
261
					'class' => 'centercol',
262
				),
263
			),
264
		),
265
		'form' => array(
266
			'href' => $scripturl . '?action=admin;area=ban;sa=list',
267
		),
268
		'additional_rows' => array(
269
			array(
270
				'position' => 'top_of_list',
271
				'value' => '<input type="submit" name="removeBans" value="' . $txt['ban_remove_selected'] . '" class="button">',
272
			),
273
			array(
274
				'position' => 'bottom_of_list',
275
				'value' => '<input type="submit" name="removeBans" value="' . $txt['ban_remove_selected'] . '" class="button">',
276
			),
277
		),
278
		'javascript' => '
279
		var removeBans = $("input[name=\'removeBans\']");
280
281
		removeBans.on( "click", function(e) {
282
			var removeItems = $("input[name=\'remove[]\']:checked").length;
283
284
			if (removeItems == 0)
285
			{
286
				e.preventDefault();
287
				return alert("' . $txt['select_item_check'] . '");
288
			}
289
290
291
			return confirm("' . $txt['ban_remove_selected_confirm'] . '");
292
		});',
293
	);
294
295
	require_once($sourcedir . '/Subs-List.php');
296
	createList($listOptions);
297
298
	$context['sub_template'] = 'show_list';
299
	$context['default_list'] = 'ban_list';
300
}
301
302
/**
303
 * Get bans, what else? For the given options.
304
 *
305
 * @param int $start Which item to start with (for pagination purposes)
306
 * @param int $items_per_page How many items to show on each page
307
 * @param string $sort A string telling ORDER BY how to sort the results
308
 * @return array An array of information about the bans for the list
309
 */
310
function list_getBans($start, $items_per_page, $sort)
311
{
312
	global $smcFunc;
313
314
	$request = $smcFunc['db_query']('', '
315
		SELECT bg.id_ban_group, bg.name, bg.ban_time, bg.expire_time, bg.reason, bg.notes, COUNT(bi.id_ban) AS num_triggers
316
		FROM {db_prefix}ban_groups AS bg
317
			LEFT JOIN {db_prefix}ban_items AS bi ON (bi.id_ban_group = bg.id_ban_group)
318
		GROUP BY bg.id_ban_group, bg.name, bg.ban_time, bg.expire_time, bg.reason, bg.notes
319
		ORDER BY {raw:sort}
320
		LIMIT {int:offset}, {int:limit}',
321
		array(
322
			'sort' => $sort,
323
			'offset' => $start,
324
			'limit' => $items_per_page,
325
		)
326
	);
327
	$bans = array();
328
	while ($row = $smcFunc['db_fetch_assoc']($request))
329
		$bans[] = $row;
330
331
	$smcFunc['db_free_result']($request);
332
333
	return $bans;
334
}
335
336
/**
337
 * Get the total number of ban from the ban group table
338
 *
339
 * @return int The total number of bans
340
 */
341
function list_getNumBans()
342
{
343
	global $smcFunc;
344
345
	$request = $smcFunc['db_query']('', '
346
		SELECT COUNT(*) AS num_bans
347
		FROM {db_prefix}ban_groups',
348
		array(
349
		)
350
	);
351
	list ($numBans) = $smcFunc['db_fetch_row']($request);
352
	$smcFunc['db_free_result']($request);
353
354
	return $numBans;
355
}
356
357
/**
358
 * This function is behind the screen for adding new bans and modifying existing ones.
359
 * Adding new bans:
360
 * 	- is accessed by ?action=admin;area=ban;sa=add.
361
 * 	- uses the ban_edit sub template of the ManageBans template.
362
 * Modifying existing bans:
363
 *  - is accessed by ?action=admin;area=ban;sa=edit;bg=x
364
 *  - uses the ban_edit sub template of the ManageBans template.
365
 *  - shows a list of ban triggers for the specified ban.
366
 */
367
function BanEdit()
368
{
369
	global $txt, $modSettings, $context, $scripturl, $smcFunc, $sourcedir;
370
371
	if ((isset($_POST['add_ban']) || isset($_POST['modify_ban']) || isset($_POST['remove_selection'])) && empty($context['ban_errors']))
372
		BanEdit2();
373
374
	$ban_group_id = isset($context['ban']['id']) ? $context['ban']['id'] : (isset($_REQUEST['bg']) ? (int) $_REQUEST['bg'] : 0);
375
376
	// Template needs this to show errors using javascript
377
	loadLanguage('Errors');
378
	createToken('admin-bet');
379
	$context['form_url'] = $scripturl . '?action=admin;area=ban;sa=edit';
380
381
	if (!empty($context['ban_errors']))
382
		foreach ($context['ban_errors'] as $error)
383
			$context['error_messages'][$error] = $txt[$error];
384
385
	else
386
	{
387
		// If we're editing an existing ban, get it from the database.
388
		if (!empty($ban_group_id))
389
		{
390
			$context['ban_group_id'] = $ban_group_id;
391
392
			// We're going to want this for making our list.
393
			require_once($sourcedir . '/Subs-List.php');
394
395
			$listOptions = array(
396
				'id' => 'ban_items',
397
				'base_href' => $scripturl . '?action=admin;area=ban;sa=edit;bg=' . $ban_group_id,
398
				'no_items_label' => $txt['ban_no_triggers'],
399
				'items_per_page' => $modSettings['defaultMaxListItems'],
400
				'get_items' => array(
401
					'function' => 'list_getBanItems',
402
					'params' => array(
403
						'ban_group_id' => $ban_group_id,
404
					),
405
				),
406
				'get_count' => array(
407
					'function' => 'list_getNumBanItems',
408
					'params' => array(
409
						'ban_group_id' => $ban_group_id,
410
					),
411
				),
412
				'columns' => array(
413
					'type' => array(
414
						'header' => array(
415
							'value' => $txt['ban_banned_entity'],
416
							'style' => 'width: 60%;text-align: left;',
417
						),
418
						'data' => array(
419
							'function' => function($ban_item) use ($txt)
420
							{
421
								if (in_array($ban_item['type'], array('ip', 'hostname', 'email')))
422
									return '<strong>' . $txt[$ban_item['type']] . ':</strong>&nbsp;' . $ban_item[$ban_item['type']];
423
								elseif ($ban_item['type'] == 'user')
424
									return '<strong>' . $txt['username'] . ':</strong>&nbsp;' . $ban_item['user']['link'];
425
								else
426
									return '<strong>' . $txt['unknown'] . ':</strong>&nbsp;' . $ban_item['no_bantype_selected'];
427
							},
428
							'style' => 'text-align: left;',
429
						),
430
					),
431
					'hits' => array(
432
						'header' => array(
433
							'value' => $txt['ban_hits'],
434
							'style' => 'width: 15%; text-align: center;',
435
						),
436
						'data' => array(
437
							'db' => 'hits',
438
							'style' => 'text-align: center;',
439
						),
440
					),
441
					'id' => array(
442
						'header' => array(
443
							'value' => $txt['ban_actions'],
444
							'style' => 'width: 15%; text-align: center;',
445
						),
446
						'data' => array(
447
							'function' => function($ban_item) use ($txt, $context, $scripturl)
448
							{
449
								return '<a href="' . $scripturl . '?action=admin;area=ban;sa=edittrigger;bg=' . $context['ban_group_id'] . ';bi=' . $ban_item['id'] . '">' . $txt['ban_edit_trigger'] . '</a>';
450
							},
451
							'style' => 'text-align: center;',
452
						),
453
					),
454
					'checkboxes' => array(
455
						'header' => array(
456
							'value' => '<input type="checkbox" onclick="invertAll(this, this.form, \'ban_items\');">',
457
							'style' => 'width: 5%; text-align: center;',
458
						),
459
						'data' => array(
460
							'sprintf' => array(
461
								'format' => '<input type="checkbox" name="ban_items[]" value="%1$d">',
462
								'params' => array(
463
									'id' => false,
464
								),
465
							),
466
							'style' => 'text-align: center;',
467
						),
468
					),
469
				),
470
				'form' => array(
471
					'href' => $scripturl . '?action=admin;area=ban;sa=edit;bg=' . $ban_group_id,
472
				),
473
				'additional_rows' => array(
474
					array(
475
						'position' => 'above_table_headers',
476
						'value' => '
477
						<input type="submit" name="remove_selection" value="' . $txt['ban_remove_selected_triggers'] . '" class="button"> <a class="button" href="' . $scripturl . '?action=admin;area=ban;sa=edittrigger;bg=' . $ban_group_id . '">' . $txt['ban_add_trigger'] . '</a>',
478
						'style' => 'text-align: right;',
479
					),
480
					array(
481
						'position' => 'above_table_headers',
482
						'value' => '
483
						<input type="hidden" name="bg" value="' . $ban_group_id . '">
484
						<input type="hidden" name="' . $context['session_var'] . '" value="' . $context['session_id'] . '">
485
						<input type="hidden" name="' . $context['admin-bet_token_var'] . '" value="' . $context['admin-bet_token'] . '">',
486
					),
487
					array(
488
						'position' => 'below_table_data',
489
						'value' => '
490
						<input type="submit" name="remove_selection" value="' . $txt['ban_remove_selected_triggers'] . '" class="button"> <a class="button" href="' . $scripturl . '?action=admin;area=ban;sa=edittrigger;bg=' . $ban_group_id . '">' . $txt['ban_add_trigger'] . '</a>',
491
						'style' => 'text-align: right;',
492
					),
493
					array(
494
						'position' => 'below_table_data',
495
						'value' => '
496
						<input type="hidden" name="bg" value="' . $ban_group_id . '">
497
						<input type="hidden" name="' . $context['session_var'] . '" value="' . $context['session_id'] . '">
498
						<input type="hidden" name="' . $context['admin-bet_token_var'] . '" value="' . $context['admin-bet_token'] . '">',
499
					),
500
				),
501
				'javascript' => '
502
		var removeBans = $("input[name=\'remove_selection\']");
503
504
		removeBans.on( "click", function(e) {
505
			var removeItems = $("input[name=\'ban_items[]\']:checked").length;
506
507
			if (removeItems == 0)
508
			{
509
				e.preventDefault();
510
				return alert("' . $txt['select_item_check'] . '");
511
			}
512
513
514
			return confirm("' . $txt['ban_remove_selected_confirm'] . '");
515
		});',
516
			);
517
			createList($listOptions);
518
		}
519
		// Not an existing one, then it's probably a new one.
520
		else
521
		{
522
			$context['ban'] = array(
523
				'id' => 0,
524
				'name' => '',
525
				'expiration' => array(
526
					'status' => 'never',
527
					'days' => 0
528
				),
529
				'reason' => '',
530
				'notes' => '',
531
				'ban_days' => 0,
532
				'cannot' => array(
533
					'access' => true,
534
					'post' => false,
535
					'register' => false,
536
					'login' => false,
537
				),
538
				'is_new' => true,
539
			);
540
			$context['ban_suggestions'] = array(
541
				'main_ip' => '',
542
				'hostname' => '',
543
				'email' => '',
544
				'member' => array(
545
					'id' => 0,
546
				),
547
			);
548
549
			// Overwrite some of the default form values if a user ID was given.
550
			if (!empty($_REQUEST['u']))
551
			{
552
				$request = $smcFunc['db_query']('', '
553
					SELECT id_member, real_name, member_ip, email_address
554
					FROM {db_prefix}members
555
					WHERE id_member = {int:current_user}
556
					LIMIT 1',
557
					array(
558
						'current_user' => (int) $_REQUEST['u'],
559
					)
560
				);
561
				if ($smcFunc['db_num_rows']($request) > 0)
562
				{
563
					list ($context['ban_suggestions']['member']['id'], $context['ban_suggestions']['member']['name'], $context['ban_suggestions']['main_ip'], $context['ban_suggestions']['email']) = $smcFunc['db_fetch_row']($request);
564
					$context['ban_suggestions']['main_ip'] = inet_dtop($context['ban_suggestions']['main_ip']);
565
				}
566
				$smcFunc['db_free_result']($request);
567
568
				if (!empty($context['ban_suggestions']['member']['id']))
569
				{
570
					$context['ban_suggestions']['href'] = $scripturl . '?action=profile;u=' . $context['ban_suggestions']['member']['id'];
571
					$context['ban_suggestions']['member']['link'] = '<a href="' . $context['ban_suggestions']['href'] . '">' . $context['ban_suggestions']['member']['name'] . '</a>';
572
573
					// Default the ban name to the name of the banned member.
574
					$context['ban']['name'] = $context['ban_suggestions']['member']['name'];
575
					// @todo: there should be a better solution...used to lock the "Ban on Username" input when banning from profile
576
					$context['ban']['from_user'] = true;
577
578
					// Would be nice if we could also ban the hostname.
579
					if ((preg_match('/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/', $context['ban_suggestions']['main_ip']) == 1 || isValidIPv6($context['ban_suggestions']['main_ip'])) && empty($modSettings['disableHostnameLookup']))
580
						$context['ban_suggestions']['hostname'] = host_from_ip($context['ban_suggestions']['main_ip']);
581
582
					$context['ban_suggestions']['other_ips'] = banLoadAdditionalIPs($context['ban_suggestions']['member']['id']);
583
				}
584
			}
585
586
			// We come from the mod center.
587
			elseif (isset($_GET['msg']) && !empty($_GET['msg']))
588
			{
589
				$request = $smcFunc['db_query']('', '
590
					SELECT poster_name, poster_ip, poster_email
591
					FROM {db_prefix}messages
592
					WHERE id_msg = {int:message}
593
					LIMIT 1',
594
					array(
595
						'message' => (int) $_REQUEST['msg'],
596
					)
597
				);
598
				if ($smcFunc['db_num_rows']($request) > 0)
599
				{
600
					list ($context['ban_suggestions']['member']['name'], $context['ban_suggestions']['main_ip'], $context['ban_suggestions']['email']) = $smcFunc['db_fetch_row']($request);
601
					$context['ban_suggestions']['main_ip'] = inet_dtop($context['ban_suggestions']['main_ip']);
602
				}
603
				$smcFunc['db_free_result']($request);
604
605
				// Can't hurt to ban base on the guest name...
606
				$context['ban']['name'] = $context['ban_suggestions']['member']['name'];
607
				$context['ban']['from_user'] = true;
608
			}
609
		}
610
	}
611
612
	loadJavaScriptFile('suggest.js', array('minimize' => true), 'smf_suggest');
613
	$context['sub_template'] = 'ban_edit';
614
615
}
616
617
/**
618
 * Retrieves all the ban items belonging to a certain ban group
619
 *
620
 * @param int $start Which item to start with (for pagination purposes)
621
 * @param int $items_per_page How many items to show on each page
622
 * @param int $sort Not used here
623
 * @param int $ban_group_id The ID of the group to get the bans for
624
 * @return array An array with information about the returned ban items
625
 */
626
function list_getBanItems($start = 0, $items_per_page = 0, $sort = 0, $ban_group_id = 0)
627
{
628
	global $context, $smcFunc, $scripturl;
629
630
	$ban_items = array();
631
	$request = $smcFunc['db_query']('', '
632
		SELECT
633
			bi.id_ban, bi.hostname, bi.email_address, bi.id_member, bi.hits,
634
			bi.ip_low, bi.ip_high,
635
			bg.id_ban_group, bg.name, bg.ban_time, COALESCE(bg.expire_time, 0) AS expire_time, bg.reason, bg.notes, bg.cannot_access, bg.cannot_register, bg.cannot_login, bg.cannot_post,
636
			COALESCE(mem.id_member, 0) AS id_member, mem.member_name, mem.real_name
637
		FROM {db_prefix}ban_groups AS bg
638
			LEFT JOIN {db_prefix}ban_items AS bi ON (bi.id_ban_group = bg.id_ban_group)
639
			LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = bi.id_member)
640
		WHERE bg.id_ban_group = {int:current_ban}
641
		LIMIT {int:start}, {int:items_per_page}',
642
		array(
643
			'current_ban' => $ban_group_id,
644
			'start' => $start,
645
			'items_per_page' => $items_per_page,
646
		)
647
	);
648
	if ($smcFunc['db_num_rows']($request) == 0)
649
		fatal_lang_error('ban_not_found', false);
650
651
	while ($row = $smcFunc['db_fetch_assoc']($request))
652
	{
653
		if (!isset($context['ban']))
654
		{
655
			$context['ban'] = array(
656
				'id' => $row['id_ban_group'],
657
				'name' => $row['name'],
658
				'expiration' => array(
659
					'status' => empty($row['expire_time']) ? 'never' : ($row['expire_time'] < time() ? 'expired' : 'one_day'),
660
					'days' => $row['expire_time'] > time() ? ($row['expire_time'] - time() < 86400 ? 1 : ceil(($row['expire_time'] - time()) / 86400)) : 0
661
				),
662
				'reason' => $row['reason'],
663
				'notes' => $row['notes'],
664
				'cannot' => array(
665
					'access' => !empty($row['cannot_access']),
666
					'post' => !empty($row['cannot_post']),
667
					'register' => !empty($row['cannot_register']),
668
					'login' => !empty($row['cannot_login']),
669
				),
670
				'is_new' => false,
671
				'hostname' => '',
672
				'email' => '',
673
			);
674
		}
675
676
		if (!empty($row['id_ban']))
677
		{
678
			$ban_items[$row['id_ban']] = array(
679
				'id' => $row['id_ban'],
680
				'hits' => $row['hits'],
681
			);
682
			if (!empty($row['ip_high']))
683
			{
684
				$ban_items[$row['id_ban']]['type'] = 'ip';
685
				$ban_items[$row['id_ban']]['ip'] = range2ip($row['ip_low'], $row['ip_high']);
686
			}
687
			elseif (!empty($row['hostname']))
688
			{
689
				$ban_items[$row['id_ban']]['type'] = 'hostname';
690
				$ban_items[$row['id_ban']]['hostname'] = str_replace('%', '*', $row['hostname']);
691
			}
692
			elseif (!empty($row['email_address']))
693
			{
694
				$ban_items[$row['id_ban']]['type'] = 'email';
695
				$ban_items[$row['id_ban']]['email'] = str_replace('%', '*', $row['email_address']);
696
			}
697
			elseif (!empty($row['id_member']))
698
			{
699
				$ban_items[$row['id_ban']]['type'] = 'user';
700
				$ban_items[$row['id_ban']]['user'] = array(
701
					'id' => $row['id_member'],
702
					'name' => $row['real_name'],
703
					'href' => $scripturl . '?action=profile;u=' . $row['id_member'],
704
					'link' => '<a href="' . $scripturl . '?action=profile;u=' . $row['id_member'] . '">' . $row['real_name'] . '</a>',
705
				);
706
			}
707
			// Invalid ban (member probably doesn't exist anymore).
708
			else
709
			{
710
				unset($ban_items[$row['id_ban']]);
711
				removeBanTriggers($row['id_ban']);
712
			}
713
		}
714
	}
715
	$smcFunc['db_free_result']($request);
716
717
	return $ban_items;
718
}
719
720
/**
721
 * Gets the number of ban items belonging to a certain ban group
722
 *
723
 * @return int The number of ban items
724
 */
725
function list_getNumBanItems()
726
{
727
	global $smcFunc, $context;
728
729
	$ban_group_id = isset($context['ban_group_id']) ? $context['ban_group_id'] : 0;
730
731
	$request = $smcFunc['db_query']('', '
732
		SELECT COUNT(bi.id_ban)
733
		FROM {db_prefix}ban_groups AS bg
734
			LEFT JOIN {db_prefix}ban_items AS bi ON (bi.id_ban_group = bg.id_ban_group)
735
			LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = bi.id_member)
736
		WHERE bg.id_ban_group = {int:current_ban}',
737
		array(
738
			'current_ban' => $ban_group_id,
739
		)
740
	);
741
	list($banNumber) = $smcFunc['db_fetch_row']($request);
742
	$smcFunc['db_free_result']($request);
743
744
	return $banNumber;
745
}
746
747
/**
748
 * Finds additional IPs related to a certain user
749
 *
750
 * @param int $member_id The ID of the member to get additional IPs for
751
 * @return array An containing two arrays - ips_in_messages (IPs used in posts) and ips_in_errors (IPs used in error messages)
752
 */
753
function banLoadAdditionalIPs($member_id)
754
{
755
	// Borrowing a few language strings from profile.
756
	loadLanguage('Profile');
757
758
	$search_list = array();
759
	call_integration_hook('integrate_load_addtional_ip_ban', array(&$search_list));
760
	$search_list += array('ips_in_messages' => 'banLoadAdditionalIPsMember', 'ips_in_errors' => 'banLoadAdditionalIPsError');
761
762
	$return = array();
763
	foreach ($search_list as $key => $callable)
764
		if (is_callable($callable))
765
			$return[$key] = call_user_func($callable, $member_id);
766
767
	return $return;
768
}
769
770
/**
771
 * @param int $member_id The ID of the member
772
 * @return array An array of IPs used in posts by this member
773
 */
774
function banLoadAdditionalIPsMember($member_id)
775
{
776
	global $smcFunc;
777
778
	// Find some additional IP's used by this member.
779
	$message_ips = array();
780
	$request = $smcFunc['db_query']('', '
781
		SELECT DISTINCT poster_ip
782
		FROM {db_prefix}messages
783
		WHERE id_member = {int:current_user}
784
			AND poster_ip IS NOT NULL
785
		ORDER BY poster_ip',
786
		array(
787
			'current_user' => $member_id,
788
		)
789
	);
790
	while ($row = $smcFunc['db_fetch_assoc']($request))
791
		$message_ips[] = inet_dtop($row['poster_ip']);
792
793
	$smcFunc['db_free_result']($request);
794
795
	return $message_ips;
796
}
797
798
/**
799
 * @param int $member_id The ID of the member
800
 * @return array An array of IPs associated with error messages generated by this user
801
 */
802
function banLoadAdditionalIPsError($member_id)
803
{
804
	global $smcFunc;
805
806
	$error_ips = array();
807
	$request = $smcFunc['db_query']('', '
808
		SELECT DISTINCT ip
809
		FROM {db_prefix}log_errors
810
		WHERE id_member = {int:current_user}
811
			AND ip IS NOT NULL
812
		ORDER BY ip',
813
		array(
814
			'current_user' => $member_id,
815
		)
816
	);
817
	while ($row = $smcFunc['db_fetch_assoc']($request))
818
		$error_ips[] = inet_dtop($row['ip']);
819
820
	$smcFunc['db_free_result']($request);
821
822
	return $error_ips;
823
}
824
825
/**
826
 * This function handles submitted forms that add, modify or remove ban triggers.
827
 */
828
function banEdit2()
829
{
830
	global $smcFunc, $context;
831
832
	checkSession();
833
	validateToken('admin-bet');
834
835
	$context['ban_errors'] = array();
836
837
	// Adding or editing a ban group
838
	if (isset($_POST['add_ban']) || isset($_POST['modify_ban']))
839
	{
840
		// Let's collect all the information we need
841
		$ban_info['id'] = isset($_REQUEST['bg']) ? (int) $_REQUEST['bg'] : 0;
0 ignored issues
show
Comprehensibility Best Practice introduced by
$ban_info was never initialized. Although not strictly required by PHP, it is generally a good practice to add $ban_info = array(); before regardless.
Loading history...
842
		$ban_info['is_new'] = empty($ban_info['id']);
843
		$ban_info['expire_date'] = !empty($_POST['expire_date']) ? (int) $_POST['expire_date'] : 0;
844
		$ban_info['expiration'] = array(
845
			'status' => isset($_POST['expiration']) && in_array($_POST['expiration'], array('never', 'one_day', 'expired')) ? $_POST['expiration'] : 'never',
846
			'days' => $ban_info['expire_date'],
847
		);
848
		$ban_info['db_expiration'] = $ban_info['expiration']['status'] == 'never' ? 'NULL' : ($ban_info['expiration']['status'] == 'one_day' ? time() + 24 * 60 * 60 * $ban_info['expire_date'] : 0);
849
		$ban_info['full_ban'] = empty($_POST['full_ban']) ? 0 : 1;
850
		$ban_info['reason'] = !empty($_POST['reason']) ? $smcFunc['htmlspecialchars']($_POST['reason'], ENT_QUOTES) : '';
851
		$ban_info['name'] = !empty($_POST['ban_name']) ? $smcFunc['htmlspecialchars']($_POST['ban_name'], ENT_QUOTES) : '';
852
		$ban_info['notes'] = isset($_POST['notes']) ? $smcFunc['htmlspecialchars']($_POST['notes'], ENT_QUOTES) : '';
853
		$ban_info['notes'] = str_replace(array("\r", "\n", '  '), array('', '<br>', '&nbsp; '), $ban_info['notes']);
854
		$ban_info['cannot']['access'] = empty($ban_info['full_ban']) ? 0 : 1;
855
		$ban_info['cannot']['post'] = !empty($ban_info['full_ban']) || empty($_POST['cannot_post']) ? 0 : 1;
856
		$ban_info['cannot']['register'] = !empty($ban_info['full_ban']) || empty($_POST['cannot_register']) ? 0 : 1;
857
		$ban_info['cannot']['login'] = !empty($ban_info['full_ban']) || empty($_POST['cannot_login']) ? 0 : 1;
858
859
		// Adding a new ban group
860
		if (empty($_REQUEST['bg']))
861
			$ban_group_id = insertBanGroup($ban_info);
862
		// Editing an existing ban group
863
		else
864
			$ban_group_id = updateBanGroup($ban_info);
865
866
		if (is_numeric($ban_group_id))
867
		{
868
			$ban_info['id'] = $ban_group_id;
869
			$ban_info['is_new'] = false;
870
		}
871
872
		$context['ban'] = $ban_info;
873
	}
874
875
	if (isset($_POST['ban_suggestions']))
876
		// @TODO: is $_REQUEST['bi'] ever set?
877
		$saved_triggers = saveTriggers($_POST['ban_suggestions'], $ban_info['id'], isset($_REQUEST['u']) ? (int) $_REQUEST['u'] : 0, isset($_REQUEST['bi']) ? (int) $_REQUEST['bi'] : 0);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $ban_info does not seem to be defined for all execution paths leading up to this point.
Loading history...
878
879
	// Something went wrong somewhere... Oh well, let's go back.
880
	if (!empty($context['ban_errors']))
881
	{
882
		$context['ban_suggestions'] = !empty($saved_triggers) ? $saved_triggers : array();
883
		$context['ban']['from_user'] = true;
884
		$context['ban_suggestions'] = array_merge($context['ban_suggestions'], getMemberData((int) $_REQUEST['u']));
885
886
		// Not strictly necessary, but it's nice
887
		if (!empty($context['ban_suggestions']['member']['id']))
888
			$context['ban_suggestions']['other_ips'] = banLoadAdditionalIPs($context['ban_suggestions']['member']['id']);
889
		return BanEdit();
890
	}
891
	$context['ban_suggestions']['saved_triggers'] = !empty($saved_triggers) ? $saved_triggers : array();
892
893
	if (isset($_POST['ban_items']))
894
	{
895
		$ban_group_id = isset($_REQUEST['bg']) ? (int) $_REQUEST['bg'] : 0;
896
		array_map('intval', $_POST['ban_items']);
897
898
		removeBanTriggers($_POST['ban_items'], $ban_group_id);
899
	}
900
901
	// Register the last modified date.
902
	updateSettings(array('banLastUpdated' => time()));
903
904
	// Update the member table to represent the new ban situation.
905
	updateBanMembers();
906
	redirectexit('action=admin;area=ban;sa=edit;bg=' . $ban_group_id);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $ban_group_id does not seem to be defined for all execution paths leading up to this point.
Loading history...
907
}
908
909
/**
910
 * Saves one or more ban triggers into a ban item: according to the suggestions
911
 * checks the $_POST variable to verify if the trigger is present
912
 *
913
 * @param array $suggestions An array of suggestedtriggers (IP, email, etc.)
914
 * @param int $ban_group The ID of the group we're saving bans for
915
 * @param int $member The ID of the member associated with this ban (if applicable)
916
 * @param int $ban_id The ID of the ban (0 if this is a new ban)
917
 *
918
 * @return array|bool An array with the triggers if there were errors or false on success
919
 */
920
function saveTriggers($suggestions = array(), $ban_group, $member = 0, $ban_id = 0)
921
{
922
	global $context;
923
924
	$triggers = array(
925
		'main_ip' => '',
926
		'hostname' => '',
927
		'email' => '',
928
		'member' => array(
929
			'id' => $member,
930
		)
931
	);
932
933
	foreach ($suggestions as $key => $value)
934
	{
935
		if (is_array($value))
936
			$triggers[$key] = $value;
937
		else
938
			$triggers[$value] = !empty($_POST[$value]) ? $_POST[$value] : '';
939
	}
940
941
	$ban_triggers = validateTriggers($triggers);
942
943
	// Time to save!
944
	if (!empty($ban_triggers['ban_triggers']) && empty($context['ban_errors']))
945
	{
946
		if (empty($ban_id))
947
			addTriggers($ban_group, $ban_triggers['ban_triggers'], $ban_triggers['log_info']);
948
		else
949
			updateTriggers($ban_id, $ban_group, array_shift($ban_triggers['ban_triggers']), $ban_triggers['log_info']);
950
	}
951
	if (!empty($context['ban_errors']))
952
		return $triggers;
953
	else
954
		return false;
955
}
956
957
/**
958
 * This function removes a bunch of triggers based on ids
959
 * Doesn't clean the inputs
960
 *
961
 * @param array $items_ids The items to remove
962
 * @param bool|int $group_id The ID of the group these triggers are associated with or false if deleting them from all groups
963
 * @return bool Always returns true
964
 */
965
function removeBanTriggers($items_ids = array(), $group_id = false)
966
{
967
	global $smcFunc, $scripturl;
968
969
	if ($group_id !== false)
970
		$group_id = (int) $group_id;
971
972
	if (empty($group_id) && empty($items_ids))
973
		return false;
974
975
	if (!is_array($items_ids))
0 ignored issues
show
The condition is_array($items_ids) is always true.
Loading history...
976
		$items_ids = array($items_ids);
977
978
	$log_info = array();
979
	$ban_items = array();
980
981
	// First order of business: Load up the info so we can log this...
982
	$request = $smcFunc['db_query']('', '
983
		SELECT
984
			bi.id_ban, bi.hostname, bi.email_address, bi.id_member, bi.hits,
985
			bi.ip_low, bi.ip_high,
986
			COALESCE(mem.id_member, 0) AS id_member, mem.member_name, mem.real_name
987
		FROM {db_prefix}ban_items AS bi
988
			LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = bi.id_member)
989
		WHERE bi.id_ban IN ({array_int:ban_list})',
990
		array(
991
			'ban_list' => $items_ids,
992
		)
993
	);
994
995
	// Get all the info for the log
996
	while ($row = $smcFunc['db_fetch_assoc']($request))
997
	{
998
		if (!empty($row['id_ban']))
999
		{
1000
			$ban_items[$row['id_ban']] = array(
1001
				'id' => $row['id_ban'],
1002
			);
1003
			if (!empty($row['ip_high']))
1004
			{
1005
				$ban_items[$row['id_ban']]['type'] = 'ip';
1006
				$ban_items[$row['id_ban']]['ip'] = range2ip($row['ip_low'], $row['ip_high']);
1007
1008
				$is_range = (strpos($ban_items[$row['id_ban']]['ip'], '-') !== false || strpos($ban_items[$row['id_ban']]['ip'], '*') !== false);
1009
1010
				$log_info[] = array(
1011
					'bantype' => ($is_range ? 'ip_range' : 'main_ip'),
1012
					'value' => $ban_items[$row['id_ban']]['ip'],
1013
				);
1014
			}
1015
			elseif (!empty($row['hostname']))
1016
			{
1017
				$ban_items[$row['id_ban']]['type'] = 'hostname';
1018
				$ban_items[$row['id_ban']]['hostname'] = str_replace('%', '*', $row['hostname']);
1019
				$log_info[] = array(
1020
					'bantype' => 'hostname',
1021
					'value' => $row['hostname'],
1022
				);
1023
			}
1024
			elseif (!empty($row['email_address']))
1025
			{
1026
				$ban_items[$row['id_ban']]['type'] = 'email';
1027
				$ban_items[$row['id_ban']]['email'] = str_replace('%', '*', $row['email_address']);
1028
				$log_info[] = array(
1029
					'bantype' => 'email',
1030
					'value' => $ban_items[$row['id_ban']]['email'],
1031
				);
1032
			}
1033
			elseif (!empty($row['id_member']))
1034
			{
1035
				$ban_items[$row['id_ban']]['type'] = 'user';
1036
				$ban_items[$row['id_ban']]['user'] = array(
1037
					'id' => $row['id_member'],
1038
					'name' => $row['real_name'],
1039
					'href' => $scripturl . '?action=profile;u=' . $row['id_member'],
1040
					'link' => '<a href="' . $scripturl . '?action=profile;u=' . $row['id_member'] . '">' . $row['real_name'] . '</a>',
1041
				);
1042
				$log_info[] = array(
1043
					'bantype' => 'user',
1044
					'value' => $row['id_member'],
1045
				);
1046
			}
1047
		}
1048
	}
1049
1050
	// Log this!
1051
	logTriggersUpdates($log_info, false, true);
1052
1053
	$smcFunc['db_free_result']($request);
1054
1055
	if ($group_id !== false)
1056
	{
1057
		$smcFunc['db_query']('', '
1058
			DELETE FROM {db_prefix}ban_items
1059
			WHERE id_ban IN ({array_int:ban_list})
1060
				AND id_ban_group = {int:ban_group}',
1061
			array(
1062
				'ban_list' => $items_ids,
1063
				'ban_group' => $group_id,
1064
			)
1065
		);
1066
	}
1067
	elseif (!empty($items_ids))
1068
	{
1069
		$smcFunc['db_query']('', '
1070
			DELETE FROM {db_prefix}ban_items
1071
			WHERE id_ban IN ({array_int:ban_list})',
1072
			array(
1073
				'ban_list' => $items_ids,
1074
			)
1075
		);
1076
	}
1077
1078
	return true;
1079
}
1080
1081
/**
1082
 * This function removes a bunch of ban groups based on ids
1083
 * Doesn't clean the inputs
1084
 *
1085
 * @param int[] $group_ids The IDs of the groups to remove
1086
 * @return bool Returns ture if successful or false if $group_ids is empty
1087
 */
1088
function removeBanGroups($group_ids)
1089
{
1090
	global $smcFunc;
1091
1092
	if (!is_array($group_ids))
0 ignored issues
show
The condition is_array($group_ids) is always true.
Loading history...
1093
		$group_ids = array($group_ids);
1094
1095
	$group_ids = array_unique($group_ids);
1096
1097
	if (empty($group_ids))
1098
		return false;
1099
1100
	$smcFunc['db_query']('', '
1101
		DELETE FROM {db_prefix}ban_groups
1102
		WHERE id_ban_group IN ({array_int:ban_list})',
1103
		array(
1104
			'ban_list' => $group_ids,
1105
		)
1106
	);
1107
1108
	return true;
1109
}
1110
1111
/**
1112
 * Removes logs - by default truncate the table
1113
 * Doesn't clean the inputs
1114
 *
1115
 * @param array $ids Empty array to clear the ban log or the IDs of the log entries to remove
1116
 * @return bool Returns true if successful or false if $ids is invalid
1117
 */
1118
function removeBanLogs($ids = array())
1119
{
1120
	global $smcFunc;
1121
1122
	if (empty($ids))
1123
		$smcFunc['db_query']('truncate_table', '
1124
			TRUNCATE {db_prefix}log_banned',
1125
			array(
1126
			)
1127
		);
1128
	else
1129
	{
1130
		if (!is_array($ids))
0 ignored issues
show
The condition is_array($ids) is always true.
Loading history...
1131
			$ids = array($ids);
1132
1133
		$ids = array_unique($ids);
1134
1135
		if (empty($ids))
1136
			return false;
1137
1138
		$smcFunc['db_query']('', '
1139
			DELETE FROM {db_prefix}log_banned
1140
			WHERE id_ban_log IN ({array_int:ban_list})',
1141
			array(
1142
				'ban_list' => $ids,
1143
			)
1144
		);
1145
	}
1146
1147
	return true;
1148
}
1149
1150
/**
1151
 * This function validates the ban triggers
1152
 *
1153
 * Errors in $context['ban_errors']
1154
 *
1155
 * @param array $triggers The triggers to validate
1156
 * @return array An array of riggers and log info ready to be used
1157
 */
1158
function validateTriggers(&$triggers)
1159
{
1160
	global $context, $smcFunc;
1161
1162
	if (empty($triggers))
1163
		$context['ban_erros'][] = 'ban_empty_triggers';
1164
1165
	$ban_triggers = array();
1166
	$log_info = array();
1167
1168
	foreach ($triggers as $key => $value)
1169
	{
1170
		if (!empty($value))
1171
		{
1172
			if ($key == 'member')
1173
				continue;
1174
1175
			if ($key == 'main_ip')
1176
			{
1177
				$value = trim($value);
1178
				$ip_parts = ip2range($value);
1179
				if (!checkExistingTriggerIP($ip_parts, $value))
1180
					$context['ban_erros'][] = 'invalid_ip';
1181
				else
1182
				{
1183
					$ban_triggers['main_ip'] = array(
1184
						'ip_low' => $ip_parts['low'],
1185
						'ip_high' => $ip_parts['high']
1186
					);
1187
				}
1188
			}
1189
			elseif ($key == 'hostname')
1190
			{
1191
				if (preg_match('/[^\w.\-*]/', $value) == 1)
1192
					$context['ban_erros'][] = 'invalid_hostname';
1193
				else
1194
				{
1195
					// Replace the * wildcard by a MySQL wildcard %.
1196
					$value = substr(str_replace('*', '%', $value), 0, 255);
1197
1198
					$ban_triggers['hostname']['hostname'] = $value;
1199
				}
1200
			}
1201
			elseif ($key == 'email')
1202
			{
1203
				if (preg_match('/[^\w.\-\+*@]/', $value) == 1)
1204
					$context['ban_erros'][] = 'invalid_email';
1205
1206
				// Check the user is not banning an admin.
1207
				$request = $smcFunc['db_query']('', '
1208
					SELECT id_member
1209
					FROM {db_prefix}members
1210
					WHERE (id_group = {int:admin_group} OR FIND_IN_SET({int:admin_group}, additional_groups) != 0)
1211
						AND email_address LIKE {string:email}
1212
					LIMIT 1',
1213
					array(
1214
						'admin_group' => 1,
1215
						'email' => $value,
1216
					)
1217
				);
1218
				if ($smcFunc['db_num_rows']($request) != 0)
1219
					$context['ban_erros'][] = 'no_ban_admin';
1220
				$smcFunc['db_free_result']($request);
1221
1222
				$value = substr(strtolower(str_replace('*', '%', $value)), 0, 255);
1223
1224
				$ban_triggers['email']['email_address'] = $value;
1225
			}
1226
			elseif ($key == 'user')
1227
			{
1228
				$user = preg_replace('~&amp;#(\d{4,5}|[2-9]\d{2,4}|1[2-9]\d);~', '&#$1;', $smcFunc['htmlspecialchars']($value, ENT_QUOTES));
1229
1230
				$request = $smcFunc['db_query']('', '
1231
					SELECT id_member, (id_group = {int:admin_group} OR FIND_IN_SET({int:admin_group}, additional_groups) != 0) AS isAdmin
1232
					FROM {db_prefix}members
1233
					WHERE member_name = {string:username} OR real_name = {string:username}
1234
					LIMIT 1',
1235
					array(
1236
						'admin_group' => 1,
1237
						'username' => $user,
1238
					)
1239
				);
1240
				if ($smcFunc['db_num_rows']($request) == 0)
1241
					$context['ban_erros'][] = 'invalid_username';
1242
				list ($value, $isAdmin) = $smcFunc['db_fetch_row']($request);
1243
				$smcFunc['db_free_result']($request);
1244
1245
				if ($isAdmin && strtolower($isAdmin) != 'f')
1246
				{
1247
					unset($value);
1248
					$context['ban_erros'][] = 'no_ban_admin';
1249
				}
1250
				else
1251
					$ban_triggers['user']['id_member'] = $value;
1252
			}
1253
			elseif (in_array($key, array('ips_in_messages', 'ips_in_errors')))
1254
			{
1255
				// Special case, those two are arrays themselves
1256
				$values = array_unique($value);
1257
				// Don't add the main IP again.
1258
				if (isset($triggers['main_ip']))
1259
					$values = array_diff($values, array($triggers['main_ip']));
1260
				unset($value);
1261
				foreach ($values as $val)
1262
				{
1263
					$val = trim($val);
1264
					$ip_parts = ip2range($val);
1265
					if (!checkExistingTriggerIP($ip_parts, $val))
1266
						$context['ban_erros'][] = 'invalid_ip';
1267
					else
1268
					{
1269
						$ban_triggers[$key][] = array(
1270
							'ip_low' => $ip_parts['low'],
1271
							'ip_high' => $ip_parts['high'],
1272
						);
1273
1274
						$log_info[] = array(
1275
							'value' => $val,
1276
							'bantype' => 'ip_range',
1277
						);
1278
					}
1279
				}
1280
			}
1281
			else
1282
				$context['ban_erros'][] = 'no_bantype_selected';
1283
1284
			if (isset($value) && !is_array($value))
1285
				$log_info[] = array(
1286
					'value' => $value,
1287
					'bantype' => $key,
1288
				);
1289
		}
1290
	}
1291
	return array('ban_triggers' => $ban_triggers, 'log_info' => $log_info);
1292
}
1293
1294
/**
1295
 * This function actually inserts the ban triggers into the database
1296
 *
1297
 * Errors in $context['ban_errors']
1298
 *
1299
 * @param int $group_id The ID of the group to add the triggers to (0 to create a new one)
1300
 * @param array $triggers The triggers to add
1301
 * @param array $logs The log data
1302
 * @return bool Whether or not the action was successful
1303
 */
1304
function addTriggers($group_id = 0, $triggers = array(), $logs = array())
1305
{
1306
	global $smcFunc, $context;
1307
1308
	if (empty($group_id))
1309
		$context['ban_errors'][] = 'ban_id_empty';
1310
1311
	// Preset all values that are required.
1312
	$values = array(
1313
		'id_ban_group' => $group_id,
1314
		'hostname' => '',
1315
		'email_address' => '',
1316
		'id_member' => 0,
1317
		'ip_low' => 'null',
1318
		'ip_high' => 'null',
1319
	);
1320
1321
	$insertKeys = array(
1322
		'id_ban_group' => 'int',
1323
		'hostname' => 'string',
1324
		'email_address' => 'string',
1325
		'id_member' => 'int',
1326
		'ip_low' => 'inet',
1327
		'ip_high' => 'inet',
1328
	);
1329
1330
	$insertTriggers = array();
1331
	foreach ($triggers as $key => $trigger)
1332
	{
1333
		// Exceptions, exceptions, exceptions...always exceptions... :P
1334
		if (in_array($key, array('ips_in_messages', 'ips_in_errors')))
1335
			foreach ($trigger as $real_trigger)
1336
				$insertTriggers[] = array_merge($values, $real_trigger);
1337
		else
1338
			$insertTriggers[] = array_merge($values, $trigger);
1339
	}
1340
1341
	if (empty($insertTriggers))
1342
		$context['ban_errors'][] = 'ban_no_triggers';
1343
1344
	if (!empty($context['ban_errors']))
1345
		return false;
1346
1347
	$smcFunc['db_insert']('',
1348
		'{db_prefix}ban_items',
1349
		$insertKeys,
1350
		$insertTriggers,
1351
		array('id_ban')
1352
	);
1353
1354
	logTriggersUpdates($logs, true);
1355
1356
	return true;
1357
}
1358
1359
/**
1360
 * This function updates an existing ban trigger into the database
1361
 *
1362
 * Errors in $context['ban_errors']
1363
 *
1364
 * @param int $ban_item The ID of the ban item
1365
 * @param int $group_id The ID of the ban group
1366
 * @param array $trigger An array of triggers
1367
 * @param array $logs An array of log info
1368
 */
1369
function updateTriggers($ban_item = 0, $group_id = 0, $trigger = array(), $logs = array())
1370
{
1371
	global $smcFunc, $context;
1372
1373
	if (empty($ban_item))
1374
		$context['ban_errors'][] = 'ban_ban_item_empty';
1375
	if (empty($group_id))
1376
		$context['ban_errors'][] = 'ban_id_empty';
1377
	if (empty($trigger))
1378
		$context['ban_errors'][] = 'ban_no_triggers';
1379
1380
	if (!empty($context['ban_errors']))
1381
		return;
1382
1383
	// Preset all values that are required.
1384
	$values = array(
1385
		'id_ban_group' => $group_id,
1386
		'hostname' => '',
1387
		'email_address' => '',
1388
		'id_member' => 0,
1389
		'ip_low' => 'null',
1390
		'ip_high' => 'null',
1391
	);
1392
1393
	$trigger = array_merge($values, $trigger);
1394
1395
	$smcFunc['db_query']('', '
1396
		UPDATE {db_prefix}ban_items
1397
		SET
1398
			hostname = {string:hostname}, email_address = {string:email_address}, id_member = {int:id_member},
1399
			ip_low = {inet:ip_low}, ip_high = {inet:ip_high}
1400
		WHERE id_ban = {int:ban_item}
1401
			AND id_ban_group = {int:id_ban_group}',
1402
		array_merge($trigger, array(
1403
			'id_ban_group' => $group_id,
1404
			'ban_item' => $ban_item,
1405
		))
1406
	);
1407
1408
	logTriggersUpdates($logs, false);
1409
}
1410
1411
/**
1412
 * A small function to unify logging of triggers (updates and new)
1413
 *
1414
 * @param array $logs an array of logs, each log contains the following keys:
1415
 *                - bantype: a known type of ban (ip_range, hostname, email, user, main_ip)
1416
 *                - value: the value of the bantype (e.g. the IP or the email address banned)
1417
 * @param bool $new Whether the trigger is new or an update of an existing one
1418
 * @param bool $removal Whether the trigger is being deleted
1419
 */
1420
function logTriggersUpdates($logs, $new = true, $removal = false)
1421
{
1422
	if (empty($logs))
1423
		return;
1424
1425
	$log_name_map = array(
1426
		'main_ip' => 'ip_range',
1427
		'hostname' => 'hostname',
1428
		'email' => 'email',
1429
		'user' => 'member',
1430
		'ip_range' => 'ip_range',
1431
	);
1432
1433
	// Log the addion of the ban entries into the moderation log.
1434
	foreach ($logs as $log)
1435
		logAction('ban' . ($removal == true ? 'remove' : ''), array(
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
1436
			$log_name_map[$log['bantype']] => $log['value'],
1437
			'new' => empty($new) ? 0 : 1,
1438
			'remove' => empty($removal) ? 0 : 1,
1439
			'type' => $log['bantype'],
1440
		));
1441
}
1442
1443
/**
1444
 * Updates an existing ban group
1445
 *
1446
 * Errors in $context['ban_errors']
1447
 *
1448
 * @param array $ban_info An array of info about the ban group. Should have name and may also have an id.
1449
 * @return int The ban group's ID
1450
 */
1451
function updateBanGroup($ban_info = array())
1452
{
1453
	global $smcFunc, $context;
1454
1455
	if (empty($ban_info['name']))
1456
		$context['ban_errors'][] = 'ban_name_empty';
1457
	if (empty($ban_info['id']))
1458
		$context['ban_errors'][] = 'ban_id_empty';
1459
	if (empty($ban_info['cannot']['access']) && empty($ban_info['cannot']['register']) && empty($ban_info['cannot']['post']) && empty($ban_info['cannot']['login']))
1460
		$context['ban_errors'][] = 'ban_unknown_restriction_type';
1461
1462
	if (!empty($ban_info['id']))
1463
	{
1464
		// Verify the ban group exists.
1465
		$request = $smcFunc['db_query']('', '
1466
			SELECT id_ban_group
1467
			FROM {db_prefix}ban_groups
1468
			WHERE id_ban_group = {int:ban_group}
1469
			LIMIT 1',
1470
			array(
1471
				'ban_group' => $ban_info['id']
1472
			)
1473
		);
1474
1475
		if ($smcFunc['db_num_rows']($request) == 0)
1476
			$context['ban_errors'][] = 'ban_not_found';
1477
		$smcFunc['db_free_result']($request);
1478
	}
1479
1480
	if (!empty($ban_info['name']))
1481
	{
1482
		// Make sure the name does not already exist (Of course, if it exists in the ban group we are editing, proceed.)
1483
		$request = $smcFunc['db_query']('', '
1484
			SELECT id_ban_group
1485
			FROM {db_prefix}ban_groups
1486
			WHERE name = {string:new_ban_name}
1487
				AND id_ban_group != {int:ban_group}
1488
			LIMIT 1',
1489
			array(
1490
				'ban_group' => empty($ban_info['id']) ? 0 : $ban_info['id'],
1491
				'new_ban_name' => $ban_info['name'],
1492
			)
1493
		);
1494
		if ($smcFunc['db_num_rows']($request) != 0)
1495
			$context['ban_errors'][] = 'ban_name_exists';
1496
		$smcFunc['db_free_result']($request);
1497
	}
1498
1499
	if (!empty($context['ban_errors']))
1500
		return $ban_info['id'];
1501
1502
	$smcFunc['db_query']('', '
1503
		UPDATE {db_prefix}ban_groups
1504
		SET
1505
			name = {string:ban_name},
1506
			reason = {string:reason},
1507
			notes = {string:notes},
1508
			expire_time = {raw:expiration},
1509
			cannot_access = {int:cannot_access},
1510
			cannot_post = {int:cannot_post},
1511
			cannot_register = {int:cannot_register},
1512
			cannot_login = {int:cannot_login}
1513
		WHERE id_ban_group = {int:id_ban_group}',
1514
		array(
1515
			'expiration' => $ban_info['db_expiration'],
1516
			'cannot_access' => $ban_info['cannot']['access'],
1517
			'cannot_post' => $ban_info['cannot']['post'],
1518
			'cannot_register' => $ban_info['cannot']['register'],
1519
			'cannot_login' => $ban_info['cannot']['login'],
1520
			'id_ban_group' => $ban_info['id'],
1521
			'ban_name' => $ban_info['name'],
1522
			'reason' => $ban_info['reason'],
1523
			'notes' => $ban_info['notes'],
1524
		)
1525
	);
1526
1527
	return $ban_info['id'];
1528
}
1529
1530
/**
1531
 * Creates a new ban group
1532
 * If the group is successfully created the ID is returned
1533
 * On error the error code is returned or false
1534
 *
1535
 * Errors in $context['ban_errors']
1536
 *
1537
 * @param array $ban_info An array containing 'name', which is the name of the ban group
1538
 * @return int The ban group's ID
1539
 */
1540
function insertBanGroup($ban_info = array())
1541
{
1542
	global $smcFunc, $context;
1543
1544
	if (empty($ban_info['name']))
1545
		$context['ban_errors'][] = 'ban_name_empty';
1546
	if (empty($ban_info['cannot']['access']) && empty($ban_info['cannot']['register']) && empty($ban_info['cannot']['post']) && empty($ban_info['cannot']['login']))
1547
		$context['ban_errors'][] = 'ban_unknown_restriction_type';
1548
1549
	if (!empty($ban_info['name']))
1550
	{
1551
		// Check whether a ban with this name already exists.
1552
		$request = $smcFunc['db_query']('', '
1553
			SELECT id_ban_group
1554
			FROM {db_prefix}ban_groups
1555
			WHERE name = {string:new_ban_name}' . '
1556
			LIMIT 1',
1557
			array(
1558
				'new_ban_name' => $ban_info['name'],
1559
			)
1560
		);
1561
1562
		if ($smcFunc['db_num_rows']($request) == 1)
1563
			$context['ban_errors'][] = 'ban_name_exists';
1564
		$smcFunc['db_free_result']($request);
1565
	}
1566
1567
	if (!empty($context['ban_errors']))
1568
		return;
1569
1570
	// Yes yes, we're ready to add now.
1571
	$ban_info['id'] = $smcFunc['db_insert']('',
1572
		'{db_prefix}ban_groups',
1573
		array(
1574
			'name' => 'string-20', 'ban_time' => 'int', 'expire_time' => 'raw', 'cannot_access' => 'int', 'cannot_register' => 'int',
1575
			'cannot_post' => 'int', 'cannot_login' => 'int', 'reason' => 'string-255', 'notes' => 'string-65534',
1576
		),
1577
		array(
1578
			$ban_info['name'], time(), $ban_info['db_expiration'], $ban_info['cannot']['access'], $ban_info['cannot']['register'],
1579
			$ban_info['cannot']['post'], $ban_info['cannot']['login'], $ban_info['reason'], $ban_info['notes'],
1580
		),
1581
		array('id_ban_group'),
1582
		1
1583
	);
1584
1585
	if (empty($ban_info['id']))
1586
		$context['ban_errors'][] = 'impossible_insert_new_bangroup';
1587
1588
	return $ban_info['id'];
1589
}
1590
1591
/**
1592
 * This function handles the ins and outs of the screen for adding new ban
1593
 * triggers or modifying existing ones.
1594
 * Adding new ban triggers:
1595
 * 	- is accessed by ?action=admin;area=ban;sa=edittrigger;bg=x
1596
 * 	- uses the ban_edit_trigger sub template of ManageBans.
1597
 * Editing existing ban triggers:
1598
 *  - is accessed by ?action=admin;area=ban;sa=edittrigger;bg=x;bi=y
1599
 *  - uses the ban_edit_trigger sub template of ManageBans.
1600
 */
1601
function BanEditTrigger()
1602
{
1603
	global $context, $smcFunc, $scripturl;
1604
1605
	$context['sub_template'] = 'ban_edit_trigger';
1606
	$context['form_url'] = $scripturl . '?action=admin;area=ban;sa=edittrigger';
1607
1608
	$ban_group = isset($_REQUEST['bg']) ? (int) $_REQUEST['bg'] : 0;
1609
	$ban_id = isset($_REQUEST['bi']) ? (int) $_REQUEST['bi'] : 0;
1610
1611
	if (empty($ban_group))
1612
		fatal_lang_error('ban_not_found', false);
1613
1614
	if (isset($_POST['add_new_trigger']) && !empty($_POST['ban_suggestions']))
1615
	{
1616
		saveTriggers($_POST['ban_suggestions'], $ban_group, 0, $ban_id);
1617
		redirectexit('action=admin;area=ban;sa=edit' . (!empty($ban_group) ? ';bg=' . $ban_group : ''));
1618
	}
1619
	elseif (isset($_POST['edit_trigger']) && !empty($_POST['ban_suggestions']))
1620
	{
1621
		// The first replaces the old one, the others are added new (simplification, otherwise it would require another query and some work...)
1622
		saveTriggers(array_shift($_POST['ban_suggestions']), $ban_group, 0, $ban_id);
1623
		if (!empty($_POST['ban_suggestions']))
1624
			saveTriggers($_POST['ban_suggestions'], $ban_group);
1625
1626
		redirectexit('action=admin;area=ban;sa=edit' . (!empty($ban_group) ? ';bg=' . $ban_group : ''));
1627
	}
1628
	elseif (isset($_POST['edit_trigger']))
1629
	{
1630
		removeBanTriggers($ban_id);
0 ignored issues
show
$ban_id of type integer is incompatible with the type array expected by parameter $items_ids of removeBanTriggers(). ( Ignorable by Annotation )

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

1630
		removeBanTriggers(/** @scrutinizer ignore-type */ $ban_id);
Loading history...
1631
		redirectexit('action=admin;area=ban;sa=edit' . (!empty($ban_group) ? ';bg=' . $ban_group : ''));
1632
	}
1633
1634
	loadJavaScriptFile('suggest.js', array('minimize' => true), 'smf_suggest');
1635
1636
	if (empty($ban_id))
1637
	{
1638
		$context['ban_trigger'] = array(
1639
			'id' => 0,
1640
			'group' => $ban_group,
1641
			'ip' => array(
1642
				'value' => '',
1643
				'selected' => true,
1644
			),
1645
			'hostname' => array(
1646
				'selected' => false,
1647
				'value' => '',
1648
			),
1649
			'email' => array(
1650
				'value' => '',
1651
				'selected' => false,
1652
			),
1653
			'banneduser' => array(
1654
				'value' => '',
1655
				'selected' => false,
1656
			),
1657
			'is_new' => true,
1658
		);
1659
	}
1660
	else
1661
	{
1662
		$request = $smcFunc['db_query']('', '
1663
			SELECT
1664
				bi.id_ban, bi.id_ban_group, bi.hostname, bi.email_address, bi.id_member,
1665
				bi.ip_low, bi.ip_high,
1666
				mem.member_name, mem.real_name
1667
			FROM {db_prefix}ban_items AS bi
1668
				LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = bi.id_member)
1669
			WHERE bi.id_ban = {int:ban_item}
1670
				AND bi.id_ban_group = {int:ban_group}
1671
			LIMIT 1',
1672
			array(
1673
				'ban_item' => $ban_id,
1674
				'ban_group' => $ban_group,
1675
			)
1676
		);
1677
		if ($smcFunc['db_num_rows']($request) == 0)
1678
			fatal_lang_error('ban_not_found', false);
1679
		$row = $smcFunc['db_fetch_assoc']($request);
1680
		$smcFunc['db_free_result']($request);
1681
1682
		$context['ban_trigger'] = array(
1683
			'id' => $row['id_ban'],
1684
			'group' => $row['id_ban_group'],
1685
			'ip' => array(
1686
				'value' => empty($row['ip_low']) ? '' : range2ip($row['ip_low'], $row['ip_high']),
1687
				'selected' => !empty($row['ip_low']),
1688
			),
1689
			'hostname' => array(
1690
				'value' => str_replace('%', '*', $row['hostname']),
1691
				'selected' => !empty($row['hostname']),
1692
			),
1693
			'email' => array(
1694
				'value' => str_replace('%', '*', $row['email_address']),
1695
				'selected' => !empty($row['email_address'])
1696
			),
1697
			'banneduser' => array(
1698
				'value' => $row['member_name'],
1699
				'selected' => !empty($row['member_name'])
1700
			),
1701
			'is_new' => false,
1702
		);
1703
	}
1704
1705
	createToken('admin-bet');
1706
}
1707
1708
/**
1709
 * This handles the screen for showing the banned entities
1710
 * It is accessed by ?action=admin;area=ban;sa=browse
1711
 * It uses sub-tabs for browsing by IP, hostname, email or username.
1712
 *
1713
 * @uses ManageBans template, browse_triggers sub template.
1714
 */
1715
function BanBrowseTriggers()
1716
{
1717
	global $modSettings, $context, $scripturl, $smcFunc, $txt;
1718
	global $sourcedir, $settings;
1719
1720
	if (!empty($_POST['remove_triggers']) && !empty($_POST['remove']) && is_array($_POST['remove']))
1721
	{
1722
		checkSession();
1723
1724
		removeBanTriggers($_POST['remove']);
1725
1726
		// Rehabilitate some members.
1727
		if ($_REQUEST['entity'] == 'member')
1728
			updateBanMembers();
1729
1730
		// Make sure the ban cache is refreshed.
1731
		updateSettings(array('banLastUpdated' => time()));
1732
	}
1733
1734
	$context['selected_entity'] = isset($_REQUEST['entity']) && in_array($_REQUEST['entity'], array('ip', 'hostname', 'email', 'member')) ? $_REQUEST['entity'] : 'ip';
1735
1736
	$listOptions = array(
1737
		'id' => 'ban_trigger_list',
1738
		'title' => $txt['ban_trigger_browse'],
1739
		'items_per_page' => $modSettings['defaultMaxListItems'],
1740
		'base_href' => $scripturl . '?action=admin;area=ban;sa=browse;entity=' . $context['selected_entity'],
1741
		'default_sort_col' => 'banned_entity',
1742
		'no_items_label' => $txt['ban_no_triggers'],
1743
		'get_items' => array(
1744
			'function' => 'list_getBanTriggers',
1745
			'params' => array(
1746
				$context['selected_entity'],
1747
			),
1748
		),
1749
		'get_count' => array(
1750
			'function' => 'list_getNumBanTriggers',
1751
			'params' => array(
1752
				$context['selected_entity'],
1753
			),
1754
		),
1755
		'columns' => array(
1756
			'banned_entity' => array(
1757
				'header' => array(
1758
					'value' => $txt['ban_banned_entity'],
1759
				),
1760
			),
1761
			'ban_name' => array(
1762
				'header' => array(
1763
					'value' => $txt['ban_name'],
1764
				),
1765
				'data' => array(
1766
					'sprintf' => array(
1767
						'format' => '<a href="' . $scripturl . '?action=admin;area=ban;sa=edit;bg=%1$d">%2$s</a>',
1768
						'params' => array(
1769
							'id_ban_group' => false,
1770
							'name' => false,
1771
						),
1772
					),
1773
				),
1774
				'sort' => array(
1775
					'default' => 'bg.name',
1776
					'reverse' => 'bg.name DESC',
1777
				),
1778
			),
1779
			'hits' => array(
1780
				'header' => array(
1781
					'value' => $txt['ban_hits'],
1782
				),
1783
				'data' => array(
1784
					'db' => 'hits',
1785
				),
1786
				'sort' => array(
1787
					'default' => 'bi.hits DESC',
1788
					'reverse' => 'bi.hits',
1789
				),
1790
			),
1791
			'check' => array(
1792
				'header' => array(
1793
					'value' => '<input type="checkbox" onclick="invertAll(this, this.form);">',
1794
					'class' => 'centercol',
1795
				),
1796
				'data' => array(
1797
					'sprintf' => array(
1798
						'format' => '<input type="checkbox" name="remove[]" value="%1$d">',
1799
						'params' => array(
1800
							'id_ban' => false,
1801
						),
1802
					),
1803
					'class' => 'centercol',
1804
				),
1805
			),
1806
		),
1807
		'form' => array(
1808
			'href' => $scripturl . '?action=admin;area=ban;sa=browse;entity=' . $context['selected_entity'],
1809
			'include_start' => true,
1810
			'include_sort' => true,
1811
		),
1812
		'additional_rows' => array(
1813
			array(
1814
				'position' => 'above_column_headers',
1815
				'value' => '<a href="' . $scripturl . '?action=admin;area=ban;sa=browse;entity=ip">' . ($context['selected_entity'] == 'ip' ? '<img src="' . $settings['images_url'] . '/selected.png" alt="&gt;"> ' : '') . $txt['ip'] . '</a>&nbsp;|&nbsp;<a href="' . $scripturl . '?action=admin;area=ban;sa=browse;entity=hostname">' . ($context['selected_entity'] == 'hostname' ? '<img src="' . $settings['images_url'] . '/selected.png" alt="&gt;"> ' : '') . $txt['hostname'] . '</a>&nbsp;|&nbsp;<a href="' . $scripturl . '?action=admin;area=ban;sa=browse;entity=email">' . ($context['selected_entity'] == 'email' ? '<img src="' . $settings['images_url'] . '/selected.png" alt="&gt;"> ' : '') . $txt['email'] . '</a>&nbsp;|&nbsp;<a href="' . $scripturl . '?action=admin;area=ban;sa=browse;entity=member">' . ($context['selected_entity'] == 'member' ? '<img src="' . $settings['images_url'] . '/selected.png" alt="&gt;"> ' : '') . $txt['username'] . '</a>',
1816
			),
1817
			array(
1818
				'position' => 'bottom_of_list',
1819
				'value' => '<input type="submit" name="remove_triggers" value="' . $txt['ban_remove_selected_triggers'] . '" data-confirm="' . $txt['ban_remove_selected_triggers_confirm'] . '" class="button you_sure">',
1820
			),
1821
		),
1822
	);
1823
1824
	// Specific data for the first column depending on the selected entity.
1825
	if ($context['selected_entity'] === 'ip')
1826
	{
1827
		$listOptions['columns']['banned_entity']['data'] = array(
1828
			'function' => function($rowData)
1829
			{
1830
				return range2ip(
1831
					$rowData['ip_low']
1832
					,
1833
					$rowData['ip_high']
1834
				);
1835
			},
1836
		);
1837
		$listOptions['columns']['banned_entity']['sort'] = array(
1838
			'default' => 'bi.ip_low, bi.ip_high, bi.ip_low',
1839
			'reverse' => 'bi.ip_low DESC, bi.ip_high DESC',
1840
		);
1841
	}
1842
	elseif ($context['selected_entity'] === 'hostname')
1843
	{
1844
		$listOptions['columns']['banned_entity']['data'] = array(
1845
			'function' => function($rowData) use ($smcFunc)
1846
			{
1847
				return strtr($smcFunc['htmlspecialchars']($rowData['hostname']), array('%' => '*'));
1848
			},
1849
		);
1850
		$listOptions['columns']['banned_entity']['sort'] = array(
1851
			'default' => 'bi.hostname',
1852
			'reverse' => 'bi.hostname DESC',
1853
		);
1854
	}
1855
	elseif ($context['selected_entity'] === 'email')
1856
	{
1857
		$listOptions['columns']['banned_entity']['data'] = array(
1858
			'function' => function($rowData) use ($smcFunc)
1859
			{
1860
				return strtr($smcFunc['htmlspecialchars']($rowData['email_address']), array('%' => '*'));
1861
			},
1862
		);
1863
		$listOptions['columns']['banned_entity']['sort'] = array(
1864
			'default' => 'bi.email_address',
1865
			'reverse' => 'bi.email_address DESC',
1866
		);
1867
	}
1868
	elseif ($context['selected_entity'] === 'member')
1869
	{
1870
		$listOptions['columns']['banned_entity']['data'] = array(
1871
			'sprintf' => array(
1872
				'format' => '<a href="' . $scripturl . '?action=profile;u=%1$d">%2$s</a>',
1873
				'params' => array(
1874
					'id_member' => false,
1875
					'real_name' => false,
1876
				),
1877
			),
1878
		);
1879
		$listOptions['columns']['banned_entity']['sort'] = array(
1880
			'default' => 'mem.real_name',
1881
			'reverse' => 'mem.real_name DESC',
1882
		);
1883
	}
1884
1885
	// Create the list.
1886
	require_once($sourcedir . '/Subs-List.php');
1887
	createList($listOptions);
1888
1889
	// The list is the only thing to show, so make it the default sub template.
1890
	$context['sub_template'] = 'show_list';
1891
	$context['default_list'] = 'ban_trigger_list';
1892
}
1893
1894
/**
1895
 * Get ban triggers for the given parameters. Callback from $listOptions['get_items'] in BanBrowseTriggers()
1896
 *
1897
 * @param int $start The item to start with (for pagination purposes)
1898
 * @param int $items_per_page How many items to show on each page
1899
 * @param string $sort A string telling ORDER BY how to sort the results
1900
 * @param string $trigger_type The trigger type - can be 'ip', 'hostname' or 'email'
1901
 * @return array An array of ban trigger info for the list
1902
 */
1903
function list_getBanTriggers($start, $items_per_page, $sort, $trigger_type)
1904
{
1905
	global $smcFunc;
1906
1907
	$where = array(
1908
		'ip' => 'bi.ip_low is not null',
1909
		'hostname' => 'bi.hostname != {string:blank_string}',
1910
		'email' => 'bi.email_address != {string:blank_string}',
1911
	);
1912
1913
	$request = $smcFunc['db_query']('', '
1914
		SELECT
1915
			bi.id_ban, bi.ip_low, bi.ip_high, bi.hostname, bi.email_address, bi.hits,
1916
			bg.id_ban_group, bg.name' . ($trigger_type === 'member' ? ',
1917
			mem.id_member, mem.real_name' : '') . '
1918
		FROM {db_prefix}ban_items AS bi
1919
			INNER JOIN {db_prefix}ban_groups AS bg ON (bg.id_ban_group = bi.id_ban_group)' . ($trigger_type === 'member' ? '
1920
			INNER JOIN {db_prefix}members AS mem ON (mem.id_member = bi.id_member)' : '
1921
		WHERE ' . $where[$trigger_type]) . '
1922
		ORDER BY {raw:sort}
1923
		LIMIT {int:start}, {int:max}',
1924
		array(
1925
			'blank_string' => '',
1926
			'sort' => $sort,
1927
			'start' => $start,
1928
			'max' => $items_per_page,
1929
		)
1930
	);
1931
	$ban_triggers = array();
1932
	while ($row = $smcFunc['db_fetch_assoc']($request))
1933
		$ban_triggers[] = $row;
1934
	$smcFunc['db_free_result']($request);
1935
1936
	return $ban_triggers;
1937
}
1938
1939
/**
1940
 * This returns the total number of ban triggers of the given type. Callback for $listOptions['get_count'] in BanBrowseTriggers().
1941
 *
1942
 * @param string $trigger_type The trigger type. Can be 'ip', 'hostname' or 'email'
1943
 * @return int The number of triggers of the specified type
1944
 */
1945
function list_getNumBanTriggers($trigger_type)
1946
{
1947
	global $smcFunc;
1948
1949
	$where = array(
1950
		'ip' => 'bi.ip_low is not null',
1951
		'hostname' => 'bi.hostname != {string:blank_string}',
1952
		'email' => 'bi.email_address != {string:blank_string}',
1953
	);
1954
1955
	$request = $smcFunc['db_query']('', '
1956
		SELECT COUNT(*)
1957
		FROM {db_prefix}ban_items AS bi' . ($trigger_type === 'member' ? '
1958
			INNER JOIN {db_prefix}members AS mem ON (mem.id_member = bi.id_member)' : '
1959
		WHERE ' . $where[$trigger_type]),
1960
		array(
1961
			'blank_string' => '',
1962
		)
1963
	);
1964
	list ($num_triggers) = $smcFunc['db_fetch_row']($request);
1965
	$smcFunc['db_free_result']($request);
1966
1967
	return $num_triggers;
1968
}
1969
1970
/**
1971
 * This handles the listing of ban log entries, and allows their deletion.
1972
 * Shows a list of logged access attempts by banned users.
1973
 * It is accessed by ?action=admin;area=ban;sa=log.
1974
 * How it works:
1975
 *  - allows sorting of several columns.
1976
 *  - also handles deletion of (a selection of) log entries.
1977
 */
1978
function BanLog()
1979
{
1980
	global $scripturl, $context, $sourcedir, $txt, $modSettings;
1981
1982
	// Delete one or more entries.
1983
	if (!empty($_POST['removeAll']) || (!empty($_POST['removeSelected']) && !empty($_POST['remove'])))
1984
	{
1985
		checkSession();
1986
		validateToken('admin-bl');
1987
1988
		// 'Delete all entries' button was pressed.
1989
		if (!empty($_POST['removeAll']))
1990
			removeBanLogs();
1991
		// 'Delete selection' button was pressed.
1992
		else
1993
		{
1994
			array_map('intval', $_POST['remove']);
1995
			removeBanLogs($_POST['remove']);
1996
		}
1997
	}
1998
1999
	$listOptions = array(
2000
		'id' => 'ban_log',
2001
		'title' => $txt['ban_log'],
2002
		'items_per_page' => $modSettings['defaultMaxListItems'],
2003
		'base_href' => $context['admin_area'] == 'ban' ? $scripturl . '?action=admin;area=ban;sa=log' : $scripturl . '?action=admin;area=logs;sa=banlog',
2004
		'default_sort_col' => 'date',
2005
		'get_items' => array(
2006
			'function' => 'list_getBanLogEntries',
2007
		),
2008
		'get_count' => array(
2009
			'function' => 'list_getNumBanLogEntries',
2010
		),
2011
		'no_items_label' => $txt['ban_log_no_entries'],
2012
		'columns' => array(
2013
			'ip' => array(
2014
				'header' => array(
2015
					'value' => $txt['ban_log_ip'],
2016
				),
2017
				'data' => array(
2018
					'sprintf' => array(
2019
						'format' => '<a href="' . $scripturl . '?action=trackip;searchip=%1$s">%1$s</a>',
2020
						'params' => array(
2021
							'ip' => false,
2022
						),
2023
					),
2024
				),
2025
				'sort' => array(
2026
					'default' => 'lb.ip',
2027
					'reverse' => 'lb.ip DESC',
2028
				),
2029
			),
2030
			'email' => array(
2031
				'header' => array(
2032
					'value' => $txt['ban_log_email'],
2033
				),
2034
				'data' => array(
2035
					'db_htmlsafe' => 'email',
2036
				),
2037
				'sort' => array(
2038
					'default' => 'lb.email = \'\', lb.email',
2039
					'reverse' => 'lb.email != \'\', lb.email DESC',
2040
				),
2041
			),
2042
			'member' => array(
2043
				'header' => array(
2044
					'value' => $txt['ban_log_member'],
2045
				),
2046
				'data' => array(
2047
					'sprintf' => array(
2048
						'format' => '<a href="' . $scripturl . '?action=profile;u=%1$d">%2$s</a>',
2049
						'params' => array(
2050
							'id_member' => false,
2051
							'real_name' => false,
2052
						),
2053
					),
2054
				),
2055
				'sort' => array(
2056
					'default' => 'COALESCE(mem.real_name, 1=1), mem.real_name',
2057
					'reverse' => 'COALESCE(mem.real_name, 1=1) DESC, mem.real_name DESC',
2058
				),
2059
			),
2060
			'date' => array(
2061
				'header' => array(
2062
					'value' => $txt['ban_log_date'],
2063
				),
2064
				'data' => array(
2065
					'function' => function($rowData)
2066
					{
2067
						return timeformat($rowData['log_time']);
2068
					},
2069
				),
2070
				'sort' => array(
2071
					'default' => 'lb.log_time DESC',
2072
					'reverse' => 'lb.log_time',
2073
				),
2074
			),
2075
			'check' => array(
2076
				'header' => array(
2077
					'value' => '<input type="checkbox" onclick="invertAll(this, this.form);">',
2078
					'class' => 'centercol',
2079
				),
2080
				'data' => array(
2081
					'sprintf' => array(
2082
						'format' => '<input type="checkbox" name="remove[]" value="%1$d">',
2083
						'params' => array(
2084
							'id_ban_log' => false,
2085
						),
2086
					),
2087
					'class' => 'centercol',
2088
				),
2089
			),
2090
		),
2091
		'form' => array(
2092
			'href' => $context['admin_area'] == 'ban' ? $scripturl . '?action=admin;area=ban;sa=log' : $scripturl . '?action=admin;area=logs;sa=banlog',
2093
			'include_start' => true,
2094
			'include_sort' => true,
2095
			'token' => 'admin-bl',
2096
		),
2097
		'additional_rows' => array(
2098
			array(
2099
				'position' => 'top_of_list',
2100
				'value' => '
2101
					<input type="submit" name="removeSelected" value="' . $txt['ban_log_remove_selected'] . '" data-confirm="' . $txt['ban_log_remove_selected_confirm'] . '" class="button you_sure">
2102
					<input type="submit" name="removeAll" value="' . $txt['ban_log_remove_all'] . '" data-confirm="' . $txt['ban_log_remove_all_confirm'] . '" class="button you_sure">',
2103
			),
2104
			array(
2105
				'position' => 'bottom_of_list',
2106
				'value' => '
2107
					<input type="submit" name="removeSelected" value="' . $txt['ban_log_remove_selected'] . '" data-confirm="' . $txt['ban_log_remove_selected_confirm'] . '" class="button you_sure">
2108
					<input type="submit" name="removeAll" value="' . $txt['ban_log_remove_all'] . '" data-confirm="' . $txt['ban_log_remove_all_confirm'] . '" class="button you_sure">',
2109
			),
2110
		),
2111
	);
2112
2113
	createToken('admin-bl');
2114
2115
	require_once($sourcedir . '/Subs-List.php');
2116
	createList($listOptions);
2117
2118
	$context['page_title'] = $txt['ban_log'];
2119
	$context['sub_template'] = 'show_list';
2120
	$context['default_list'] = 'ban_log';
2121
}
2122
2123
/**
2124
 * Load a list of ban log entries from the database.
2125
 * (no permissions check). Callback for $listOptions['get_items'] in BanLog()
2126
 *
2127
 * @param int $start The item to start with (for pagination purposes)
2128
 * @param int $items_per_page How many items to show on each page
2129
 * @param string $sort A string telling ORDER BY how to sort the results
2130
 * @return array An array of info about the ban log entries for the list.
2131
 */
2132
function list_getBanLogEntries($start, $items_per_page, $sort)
2133
{
2134
	global $smcFunc;
2135
2136
	$dash = '-';
2137
2138
	$request = $smcFunc['db_query']('', '
2139
		SELECT lb.id_ban_log, lb.id_member, lb.ip AS ip, COALESCE(lb.email, {string:dash}) AS email, lb.log_time, COALESCE(mem.real_name, {string:blank_string}) AS real_name
2140
		FROM {db_prefix}log_banned AS lb
2141
			LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = lb.id_member)
2142
		ORDER BY {raw:sort}
2143
		LIMIT {int:start}, {int:items}',
2144
		array(
2145
			'blank_string' => '',
2146
			'dash' => $dash,
2147
			'sort' => $sort,
2148
			'start' => $start,
2149
			'items' => $items_per_page,
2150
		)
2151
	);
2152
	$log_entries = array();
2153
	while ($row = $smcFunc['db_fetch_assoc']($request))
2154
	{
2155
		$row['ip'] = $row['ip'] === null ? $dash : inet_dtop($row['ip']);
2156
		$log_entries[] = $row;
2157
	}
2158
	$smcFunc['db_free_result']($request);
2159
2160
	return $log_entries;
2161
}
2162
2163
/**
2164
 * This returns the total count of ban log entries. Callback for $listOptions['get_count'] in BanLog().
2165
 *
2166
 * @return int The total number of ban log entries.
2167
 */
2168
function list_getNumBanLogEntries()
2169
{
2170
	global $smcFunc;
2171
2172
	$request = $smcFunc['db_query']('', '
2173
		SELECT COUNT(*)
2174
		FROM {db_prefix}log_banned AS lb',
2175
		array(
2176
		)
2177
	);
2178
	list ($num_entries) = $smcFunc['db_fetch_row']($request);
2179
	$smcFunc['db_free_result']($request);
2180
2181
	return $num_entries;
2182
}
2183
2184
/**
2185
 * Convert a range of given IP number into a single string.
2186
 * It's practically the reverse function of ip2range().
2187
 *
2188
 * @example
2189
 * range2ip(array(10, 10, 10, 0), array(10, 10, 20, 255)) returns '10.10.10-20.*
2190
 *
2191
 * @param array $low The low end of the range in IPv4 format
2192
 * @param array $high The high end of the range in IPv4 format
2193
 * @return string A string indicating the range
2194
 */
2195
function range2ip($low, $high)
2196
{
2197
	$low = inet_dtop($low);
0 ignored issues
show
$low of type array is incompatible with the type string expected by parameter $bin of inet_dtop(). ( Ignorable by Annotation )

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

2197
	$low = inet_dtop(/** @scrutinizer ignore-type */ $low);
Loading history...
2198
	$high = inet_dtop($high);
2199
2200
	if ($low == '255.255.255.255') return 'unknown';
2201
	if ($low == $high)
2202
		return $low;
2203
	else
2204
		return $low . '-' . $high;
2205
}
2206
2207
/**
2208
 * Checks whether a given IP range already exists in the trigger list.
2209
 * If yes, it returns an error message. Otherwise, it returns an array
2210
 *  optimized for the database.
2211
 *
2212
 * @param array $ip_array An array of IP trigger data
2213
 * @param string $fullip The full IP
2214
 * @return boolean|array False if the trigger array is invalid or the passed array if the value doesn't exist in the database
2215
 */
2216
function checkExistingTriggerIP($ip_array, $fullip = '')
2217
{
2218
	global $smcFunc, $scripturl;
2219
2220
	$values = array(
2221
		'ip_low' => $ip_array['low'],
2222
		'ip_high' => $ip_array['high']
2223
	);
2224
2225
	$request = $smcFunc['db_query']('', '
2226
		SELECT bg.id_ban_group, bg.name
2227
		FROM {db_prefix}ban_groups AS bg
2228
		INNER JOIN {db_prefix}ban_items AS bi ON
2229
			(bi.id_ban_group = bg.id_ban_group)
2230
			AND ip_low = {inet:ip_low} AND ip_high = {inet:ip_high}
2231
		LIMIT 1',
2232
		$values
2233
	);
2234
	if ($smcFunc['db_num_rows']($request) != 0)
2235
	{
2236
		list ($error_id_ban, $error_ban_name) = $smcFunc['db_fetch_row']($request);
2237
		fatal_lang_error('ban_trigger_already_exists', false, array(
2238
			$fullip,
2239
			'<a href="' . $scripturl . '?action=admin;area=ban;sa=edit;bg=' . $error_id_ban . '">' . $error_ban_name . '</a>',
2240
		));
2241
	}
2242
	$smcFunc['db_free_result']($request);
2243
2244
	return $values;
2245
}
2246
2247
/**
2248
 * As it says... this tries to review the list of banned members, to match new bans.
2249
 * Note: is_activated >= 10: a member is banned.
2250
 */
2251
function updateBanMembers()
2252
{
2253
	global $smcFunc;
2254
2255
	$updates = array();
2256
	$allMembers = array();
2257
	$newMembers = array();
2258
2259
	// Start by getting all active bans - it's quicker doing this in parts...
2260
	$request = $smcFunc['db_query']('', '
2261
		SELECT bi.id_member, bi.email_address
2262
		FROM {db_prefix}ban_items AS bi
2263
			INNER JOIN {db_prefix}ban_groups AS bg ON (bg.id_ban_group = bi.id_ban_group)
2264
		WHERE (bi.id_member > {int:no_member} OR bi.email_address != {string:blank_string})
2265
			AND bg.cannot_access = {int:cannot_access_on}
2266
			AND (bg.expire_time IS NULL OR bg.expire_time > {int:current_time})',
2267
		array(
2268
			'no_member' => 0,
2269
			'cannot_access_on' => 1,
2270
			'current_time' => time(),
2271
			'blank_string' => '',
2272
		)
2273
	);
2274
	$memberIDs = array();
2275
	$memberEmails = array();
2276
	$memberEmailWild = array();
2277
	while ($row = $smcFunc['db_fetch_assoc']($request))
2278
	{
2279
		if ($row['id_member'])
2280
			$memberIDs[$row['id_member']] = $row['id_member'];
2281
		if ($row['email_address'])
2282
		{
2283
			// Does it have a wildcard - if so we can't do a IN on it.
2284
			if (strpos($row['email_address'], '%') !== false)
2285
				$memberEmailWild[$row['email_address']] = $row['email_address'];
2286
			else
2287
				$memberEmails[$row['email_address']] = $row['email_address'];
2288
		}
2289
	}
2290
	$smcFunc['db_free_result']($request);
2291
2292
	// Build up the query.
2293
	$queryPart = array();
2294
	$queryValues = array();
2295
	if (!empty($memberIDs))
2296
	{
2297
		$queryPart[] = 'mem.id_member IN ({array_string:member_ids})';
2298
		$queryValues['member_ids'] = $memberIDs;
2299
	}
2300
	if (!empty($memberEmails))
2301
	{
2302
		$queryPart[] = 'mem.email_address IN ({array_string:member_emails})';
2303
		$queryValues['member_emails'] = $memberEmails;
2304
	}
2305
	$count = 0;
2306
	foreach ($memberEmailWild as $email)
2307
	{
2308
		$queryPart[] = 'mem.email_address LIKE {string:wild_' . $count . '}';
2309
		$queryValues['wild_' . $count++] = $email;
2310
	}
2311
2312
	// Find all banned members.
2313
	if (!empty($queryPart))
2314
	{
2315
		$request = $smcFunc['db_query']('', '
2316
			SELECT mem.id_member, mem.is_activated
2317
			FROM {db_prefix}members AS mem
2318
			WHERE ' . implode(' OR ', $queryPart),
2319
			$queryValues
2320
		);
2321
		while ($row = $smcFunc['db_fetch_assoc']($request))
2322
		{
2323
			if (!in_array($row['id_member'], $allMembers))
2324
			{
2325
				$allMembers[] = $row['id_member'];
2326
				// Do they need an update?
2327
				if ($row['is_activated'] < 10)
2328
				{
2329
					$updates[($row['is_activated'] + 10)][] = $row['id_member'];
2330
					$newMembers[] = $row['id_member'];
2331
				}
2332
			}
2333
		}
2334
		$smcFunc['db_free_result']($request);
2335
	}
2336
2337
	// We welcome our new members in the realm of the banned.
2338
	if (!empty($newMembers))
2339
		$smcFunc['db_query']('', '
2340
			DELETE FROM {db_prefix}log_online
2341
			WHERE id_member IN ({array_int:new_banned_members})',
2342
			array(
2343
				'new_banned_members' => $newMembers,
2344
			)
2345
		);
2346
2347
	// Find members that are wrongfully marked as banned.
2348
	$request = $smcFunc['db_query']('', '
2349
		SELECT mem.id_member, mem.is_activated - 10 AS new_value
2350
		FROM {db_prefix}members AS mem
2351
			LEFT JOIN {db_prefix}ban_items AS bi ON (bi.id_member = mem.id_member OR mem.email_address LIKE bi.email_address)
2352
			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}))
2353
		WHERE (bi.id_ban IS NULL OR bg.id_ban_group IS NULL)
2354
			AND mem.is_activated >= {int:ban_flag}',
2355
		array(
2356
			'cannot_access_activated' => 1,
2357
			'current_time' => time(),
2358
			'ban_flag' => 10,
2359
		)
2360
	);
2361
	while ($row = $smcFunc['db_fetch_assoc']($request))
2362
	{
2363
		// Don't do this twice!
2364
		if (!in_array($row['id_member'], $allMembers))
2365
		{
2366
			$updates[$row['new_value']][] = $row['id_member'];
2367
			$allMembers[] = $row['id_member'];
2368
		}
2369
	}
2370
	$smcFunc['db_free_result']($request);
2371
2372
	if (!empty($updates))
2373
		foreach ($updates as $newStatus => $members)
2374
			updateMemberData($members, array('is_activated' => $newStatus));
2375
2376
	// Update the latest member and our total members as banning may change them.
2377
	updateStats('member');
2378
}
2379
2380
/**
2381
 * Gets basic member data for the ban
2382
 *
2383
 * @param int $id The ID of the member to get data for
2384
 * @return array An aray containing the ID, name, main IP and email address of the specified user
2385
 */
2386
function getMemberData($id)
2387
{
2388
	global $smcFunc;
2389
2390
	$suggestions = array();
2391
	$request = $smcFunc['db_query']('', '
2392
		SELECT id_member, real_name, member_ip, email_address
2393
		FROM {db_prefix}members
2394
		WHERE id_member = {int:current_user}
2395
		LIMIT 1',
2396
		array(
2397
			'current_user' => $id,
2398
		)
2399
	);
2400
	if ($smcFunc['db_num_rows']($request) > 0)
2401
	{
2402
		list ($suggestions['member']['id'], $suggestions['member']['name'], $suggestions['main_ip'], $suggestions['email']) = $smcFunc['db_fetch_row']($request);
2403
		$suggestions['main_ip'] = inet_dtop($suggestions['main_ip']);
2404
	}
2405
	$smcFunc['db_free_result']($request);
2406
2407
	return $suggestions;
2408
}
2409
2410
?>