Issues (1061)

Sources/ManageBans.php (1 issue)

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 https://www.simplemachines.org
12
 * @copyright 2020 Simple Machines and individual contributors
13
 * @license https://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
518
			call_integration_hook('integrate_ban_edit_list', array(&$listOptions));
519
520
			createList($listOptions);
521
		}
522
		// Not an existing one, then it's probably a new one.
523
		else
524
		{
525
			$context['ban'] = array(
526
				'id' => 0,
527
				'name' => '',
528
				'expiration' => array(
529
					'status' => 'never',
530
					'days' => 0
531
				),
532
				'reason' => '',
533
				'notes' => '',
534
				'ban_days' => 0,
535
				'cannot' => array(
536
					'access' => true,
537
					'post' => false,
538
					'register' => false,
539
					'login' => false,
540
				),
541
				'is_new' => true,
542
			);
543
			$context['ban_suggestions'] = array(
544
				'main_ip' => '',
545
				'hostname' => '',
546
				'email' => '',
547
				'member' => array(
548
					'id' => 0,
549
				),
550
			);
551
552
			// Overwrite some of the default form values if a user ID was given.
553
			if (!empty($_REQUEST['u']))
554
			{
555
				$request = $smcFunc['db_query']('', '
556
					SELECT id_member, real_name, member_ip, email_address
557
					FROM {db_prefix}members
558
					WHERE id_member = {int:current_user}
559
					LIMIT 1',
560
					array(
561
						'current_user' => (int) $_REQUEST['u'],
562
					)
563
				);
564
				if ($smcFunc['db_num_rows']($request) > 0)
565
				{
566
					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);
567
					$context['ban_suggestions']['main_ip'] = inet_dtop($context['ban_suggestions']['main_ip']);
568
				}
569
				$smcFunc['db_free_result']($request);
570
571
				if (!empty($context['ban_suggestions']['member']['id']))
572
				{
573
					$context['ban_suggestions']['href'] = $scripturl . '?action=profile;u=' . $context['ban_suggestions']['member']['id'];
574
					$context['ban_suggestions']['member']['link'] = '<a href="' . $context['ban_suggestions']['href'] . '">' . $context['ban_suggestions']['member']['name'] . '</a>';
575
576
					// Default the ban name to the name of the banned member.
577
					$context['ban']['name'] = $context['ban_suggestions']['member']['name'];
578
					// @todo: there should be a better solution...used to lock the "Ban on Username" input when banning from profile
579
					$context['ban']['from_user'] = true;
580
581
					// Would be nice if we could also ban the hostname.
582
					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']))
583
						$context['ban_suggestions']['hostname'] = host_from_ip($context['ban_suggestions']['main_ip']);
584
585
					$context['ban_suggestions']['other_ips'] = banLoadAdditionalIPs($context['ban_suggestions']['member']['id']);
586
				}
587
			}
588
589
			// We come from the mod center.
590
			elseif (isset($_GET['msg']) && !empty($_GET['msg']))
591
			{
592
				$request = $smcFunc['db_query']('', '
593
					SELECT poster_name, poster_ip, poster_email
594
					FROM {db_prefix}messages
595
					WHERE id_msg = {int:message}
596
					LIMIT 1',
597
					array(
598
						'message' => (int) $_REQUEST['msg'],
599
					)
600
				);
601
				if ($smcFunc['db_num_rows']($request) > 0)
602
				{
603
					list ($context['ban_suggestions']['member']['name'], $context['ban_suggestions']['main_ip'], $context['ban_suggestions']['email']) = $smcFunc['db_fetch_row']($request);
604
					$context['ban_suggestions']['main_ip'] = inet_dtop($context['ban_suggestions']['main_ip']);
605
				}
606
				$smcFunc['db_free_result']($request);
607
608
				// Can't hurt to ban base on the guest name...
609
				$context['ban']['name'] = $context['ban_suggestions']['member']['name'];
610
				$context['ban']['from_user'] = true;
611
			}
612
613
			call_integration_hook('integrate_ban_edit_new', array());
614
615
		}
616
	}
617
618
	loadJavaScriptFile('suggest.js', array('minimize' => true), 'smf_suggest');
619
	$context['sub_template'] = 'ban_edit';
620
621
}
622
623
/**
624
 * Retrieves all the ban items belonging to a certain ban group
625
 *
626
 * @param int $start Which item to start with (for pagination purposes)
627
 * @param int $items_per_page How many items to show on each page
628
 * @param int $sort Not used here
629
 * @param int $ban_group_id The ID of the group to get the bans for
630
 * @return array An array with information about the returned ban items
631
 */
632
function list_getBanItems($start = 0, $items_per_page = 0, $sort = 0, $ban_group_id = 0)
633
{
634
	global $context, $smcFunc, $scripturl;
635
636
	$ban_items = array();
637
	$request = $smcFunc['db_query']('', '
638
		SELECT
639
			bi.id_ban, bi.hostname, bi.email_address, bi.id_member, bi.hits,
640
			bi.ip_low, bi.ip_high,
641
			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,
642
			COALESCE(mem.id_member, 0) AS id_member, mem.member_name, mem.real_name
643
		FROM {db_prefix}ban_groups AS bg
644
			LEFT JOIN {db_prefix}ban_items AS bi ON (bi.id_ban_group = bg.id_ban_group)
645
			LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = bi.id_member)
646
		WHERE bg.id_ban_group = {int:current_ban}
647
		LIMIT {int:start}, {int:items_per_page}',
648
		array(
649
			'current_ban' => $ban_group_id,
650
			'start' => $start,
651
			'items_per_page' => $items_per_page,
652
		)
653
	);
654
	if ($smcFunc['db_num_rows']($request) == 0)
655
		fatal_lang_error('ban_not_found', false);
656
657
	while ($row = $smcFunc['db_fetch_assoc']($request))
658
	{
659
		if (!isset($context['ban']))
660
		{
661
			$context['ban'] = array(
662
				'id' => $row['id_ban_group'],
663
				'name' => $row['name'],
664
				'expiration' => array(
665
					'status' => empty($row['expire_time']) ? 'never' : ($row['expire_time'] < time() ? 'expired' : 'one_day'),
666
					'days' => $row['expire_time'] > time() ? ($row['expire_time'] - time() < 86400 ? 1 : ceil(($row['expire_time'] - time()) / 86400)) : 0
667
				),
668
				'reason' => $row['reason'],
669
				'notes' => $row['notes'],
670
				'cannot' => array(
671
					'access' => !empty($row['cannot_access']),
672
					'post' => !empty($row['cannot_post']),
673
					'register' => !empty($row['cannot_register']),
674
					'login' => !empty($row['cannot_login']),
675
				),
676
				'is_new' => false,
677
				'hostname' => '',
678
				'email' => '',
679
			);
680
		}
681
682
		if (!empty($row['id_ban']))
683
		{
684
			$ban_items[$row['id_ban']] = array(
685
				'id' => $row['id_ban'],
686
				'hits' => $row['hits'],
687
			);
688
			if (!empty($row['ip_high']))
689
			{
690
				$ban_items[$row['id_ban']]['type'] = 'ip';
691
				$ban_items[$row['id_ban']]['ip'] = range2ip($row['ip_low'], $row['ip_high']);
692
			}
693
			elseif (!empty($row['hostname']))
694
			{
695
				$ban_items[$row['id_ban']]['type'] = 'hostname';
696
				$ban_items[$row['id_ban']]['hostname'] = str_replace('%', '*', $row['hostname']);
697
			}
698
			elseif (!empty($row['email_address']))
699
			{
700
				$ban_items[$row['id_ban']]['type'] = 'email';
701
				$ban_items[$row['id_ban']]['email'] = str_replace('%', '*', $row['email_address']);
702
			}
703
			elseif (!empty($row['id_member']))
704
			{
705
				$ban_items[$row['id_ban']]['type'] = 'user';
706
				$ban_items[$row['id_ban']]['user'] = array(
707
					'id' => $row['id_member'],
708
					'name' => $row['real_name'],
709
					'href' => $scripturl . '?action=profile;u=' . $row['id_member'],
710
					'link' => '<a href="' . $scripturl . '?action=profile;u=' . $row['id_member'] . '">' . $row['real_name'] . '</a>',
711
				);
712
			}
713
			// Invalid ban (member probably doesn't exist anymore).
714
			else
715
			{
716
				unset($ban_items[$row['id_ban']]);
717
				removeBanTriggers($row['id_ban']);
718
			}
719
		}
720
	}
721
	$smcFunc['db_free_result']($request);
722
723
	call_integration_hook('integrate_ban_list', array(&$ban_items));
724
725
	return $ban_items;
726
}
727
728
/**
729
 * Gets the number of ban items belonging to a certain ban group
730
 *
731
 * @return int The number of ban items
732
 */
733
function list_getNumBanItems()
734
{
735
	global $smcFunc, $context;
736
737
	$ban_group_id = isset($context['ban_group_id']) ? $context['ban_group_id'] : 0;
738
739
	$request = $smcFunc['db_query']('', '
740
		SELECT COUNT(bi.id_ban)
741
		FROM {db_prefix}ban_groups AS bg
742
			LEFT JOIN {db_prefix}ban_items AS bi ON (bi.id_ban_group = bg.id_ban_group)
743
			LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = bi.id_member)
744
		WHERE bg.id_ban_group = {int:current_ban}',
745
		array(
746
			'current_ban' => $ban_group_id,
747
		)
748
	);
749
	list($banNumber) = $smcFunc['db_fetch_row']($request);
750
	$smcFunc['db_free_result']($request);
751
752
	return $banNumber;
753
}
754
755
/**
756
 * Finds additional IPs related to a certain user
757
 *
758
 * @param int $member_id The ID of the member to get additional IPs for
759
 * @return array An containing two arrays - ips_in_messages (IPs used in posts) and ips_in_errors (IPs used in error messages)
760
 */
761
function banLoadAdditionalIPs($member_id)
762
{
763
	// Borrowing a few language strings from profile.
764
	loadLanguage('Profile');
765
766
	$search_list = array();
767
	call_integration_hook('integrate_load_addtional_ip_ban', array(&$search_list));
768
	$search_list += array('ips_in_messages' => 'banLoadAdditionalIPsMember', 'ips_in_errors' => 'banLoadAdditionalIPsError');
769
770
	$return = array();
771
	foreach ($search_list as $key => $callable)
772
		if (is_callable($callable))
773
			$return[$key] = call_user_func($callable, $member_id);
774
775
	return $return;
776
}
777
778
/**
779
 * Loads additional IPs used by a specific member
780
 *
781
 * @param int $member_id The ID of the member
782
 * @return array An array of IPs used in posts by this member
783
 */
784
function banLoadAdditionalIPsMember($member_id)
785
{
786
	global $smcFunc;
787
788
	// Find some additional IP's used by this member.
789
	$message_ips = array();
790
	$request = $smcFunc['db_query']('', '
791
		SELECT DISTINCT poster_ip
792
		FROM {db_prefix}messages
793
		WHERE id_member = {int:current_user}
794
			AND poster_ip IS NOT NULL
795
		ORDER BY poster_ip',
796
		array(
797
			'current_user' => $member_id,
798
		)
799
	);
800
	while ($row = $smcFunc['db_fetch_assoc']($request))
801
		$message_ips[] = inet_dtop($row['poster_ip']);
802
803
	$smcFunc['db_free_result']($request);
804
805
	return $message_ips;
806
}
807
808
/**
809
 * Loads additional IPs used by a member from the error log
810
 *
811
 * @param int $member_id The ID of the member
812
 * @return array An array of IPs associated with error messages generated by this user
813
 */
814
function banLoadAdditionalIPsError($member_id)
815
{
816
	global $smcFunc;
817
818
	$error_ips = array();
819
	$request = $smcFunc['db_query']('', '
820
		SELECT DISTINCT ip
821
		FROM {db_prefix}log_errors
822
		WHERE id_member = {int:current_user}
823
			AND ip IS NOT NULL
824
		ORDER BY ip',
825
		array(
826
			'current_user' => $member_id,
827
		)
828
	);
829
	while ($row = $smcFunc['db_fetch_assoc']($request))
830
		$error_ips[] = inet_dtop($row['ip']);
831
832
	$smcFunc['db_free_result']($request);
833
834
	return $error_ips;
835
}
836
837
/**
838
 * This function handles submitted forms that add, modify or remove ban triggers.
839
 */
840
function banEdit2()
841
{
842
	global $smcFunc, $context;
843
844
	checkSession();
845
	validateToken('admin-bet');
846
847
	$context['ban_errors'] = array();
848
849
	// Adding or editing a ban group
850
	if (isset($_POST['add_ban']) || isset($_POST['modify_ban']))
851
	{
852
		// Let's collect all the information we need
853
		$ban_info['id'] = isset($_REQUEST['bg']) ? (int) $_REQUEST['bg'] : 0;
854
		$ban_info['is_new'] = empty($ban_info['id']);
855
		$ban_info['expire_date'] = !empty($_POST['expire_date']) ? (int) $_POST['expire_date'] : 0;
856
		$ban_info['expiration'] = array(
857
			'status' => isset($_POST['expiration']) && in_array($_POST['expiration'], array('never', 'one_day', 'expired')) ? $_POST['expiration'] : 'never',
858
			'days' => $ban_info['expire_date'],
859
		);
860
		$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);
861
		$ban_info['full_ban'] = empty($_POST['full_ban']) ? 0 : 1;
862
		$ban_info['reason'] = !empty($_POST['reason']) ? $smcFunc['htmlspecialchars']($_POST['reason'], ENT_QUOTES) : '';
863
		$ban_info['name'] = !empty($_POST['ban_name']) ? $smcFunc['htmlspecialchars']($_POST['ban_name'], ENT_QUOTES) : '';
864
		$ban_info['notes'] = isset($_POST['notes']) ? $smcFunc['htmlspecialchars']($_POST['notes'], ENT_QUOTES) : '';
865
		$ban_info['notes'] = str_replace(array("\r", "\n", '  '), array('', '<br>', '&nbsp; '), $ban_info['notes']);
866
		$ban_info['cannot']['access'] = empty($ban_info['full_ban']) ? 0 : 1;
867
		$ban_info['cannot']['post'] = !empty($ban_info['full_ban']) || empty($_POST['cannot_post']) ? 0 : 1;
868
		$ban_info['cannot']['register'] = !empty($ban_info['full_ban']) || empty($_POST['cannot_register']) ? 0 : 1;
869
		$ban_info['cannot']['login'] = !empty($ban_info['full_ban']) || empty($_POST['cannot_login']) ? 0 : 1;
870
871
		call_integration_hook('integrate_edit_bans', array(&$ban_info, empty($_REQUEST['bg'])));
872
873
		// Adding a new ban group
874
		if (empty($_REQUEST['bg']))
875
			$ban_group_id = insertBanGroup($ban_info);
876
		// Editing an existing ban group
877
		else
878
			$ban_group_id = updateBanGroup($ban_info);
879
880
		if (is_numeric($ban_group_id))
881
		{
882
			$ban_info['id'] = $ban_group_id;
883
			$ban_info['is_new'] = false;
884
		}
885
886
		$context['ban'] = $ban_info;
887
	}
888
889
	if (isset($_POST['ban_suggestions']))
890
		// @TODO: is $_REQUEST['bi'] ever set?
891
		$saved_triggers = saveTriggers($_POST['ban_suggestions'], $ban_info['id'], isset($_REQUEST['u']) ? (int) $_REQUEST['u'] : 0, isset($_REQUEST['bi']) ? (int) $_REQUEST['bi'] : 0);
892
893
	// Something went wrong somewhere... Oh well, let's go back.
894
	if (!empty($context['ban_errors']))
895
	{
896
		$context['ban_suggestions'] = !empty($saved_triggers) ? $saved_triggers : array();
897
		$context['ban']['from_user'] = true;
898
		$context['ban_suggestions'] = array_merge($context['ban_suggestions'], getMemberData((int) $_REQUEST['u']));
899
900
		// Not strictly necessary, but it's nice
901
		if (!empty($context['ban_suggestions']['member']['id']))
902
			$context['ban_suggestions']['other_ips'] = banLoadAdditionalIPs($context['ban_suggestions']['member']['id']);
903
		return BanEdit();
904
	}
905
	$context['ban_suggestions']['saved_triggers'] = !empty($saved_triggers) ? $saved_triggers : array();
906
907
	if (isset($_POST['ban_items']))
908
	{
909
		$ban_group_id = isset($_REQUEST['bg']) ? (int) $_REQUEST['bg'] : 0;
910
		array_map('intval', $_POST['ban_items']);
911
912
		removeBanTriggers($_POST['ban_items'], $ban_group_id);
913
	}
914
915
	call_integration_hook('integrate_edit_bans_post', array());
916
917
	// Register the last modified date.
918
	updateSettings(array('banLastUpdated' => time()));
919
920
	// Update the member table to represent the new ban situation.
921
	updateBanMembers();
922
	redirectexit('action=admin;area=ban;sa=edit;bg=' . $ban_group_id);
923
}
924
925
/**
926
 * Saves one or more ban triggers into a ban item: according to the suggestions
927
 * checks the $_POST variable to verify if the trigger is present
928
 *
929
 * @param array $suggestions An array of suggestedtriggers (IP, email, etc.)
930
 * @param int $ban_group The ID of the group we're saving bans for
931
 * @param int $member The ID of the member associated with this ban (if applicable)
932
 * @param int $ban_id The ID of the ban (0 if this is a new ban)
933
 *
934
 * @return array|bool An array with the triggers if there were errors or false on success
935
 */
936
function saveTriggers($suggestions = array(), $ban_group, $member = 0, $ban_id = 0)
937
{
938
	global $context;
939
940
	$triggers = array(
941
		'main_ip' => '',
942
		'hostname' => '',
943
		'email' => '',
944
		'member' => array(
945
			'id' => $member,
946
		)
947
	);
948
949
	foreach ($suggestions as $key => $value)
950
	{
951
		if (is_array($value))
952
			$triggers[$key] = $value;
953
		else
954
			$triggers[$value] = !empty($_POST[$value]) ? $_POST[$value] : '';
955
	}
956
957
	$ban_triggers = validateTriggers($triggers);
958
959
	call_integration_hook('integrate_save_triggers', array(&$ban_triggers, &$ban_group));
960
961
	// Time to save!
962
	if (!empty($ban_triggers['ban_triggers']) && empty($context['ban_errors']))
963
	{
964
		if (empty($ban_id))
965
			addTriggers($ban_group, $ban_triggers['ban_triggers'], $ban_triggers['log_info']);
966
		else
967
			updateTriggers($ban_id, $ban_group, array_shift($ban_triggers['ban_triggers']), $ban_triggers['log_info']);
968
	}
969
	if (!empty($context['ban_errors']))
970
		return $triggers;
971
	else
972
		return false;
973
}
974
975
/**
976
 * This function removes a bunch of triggers based on ids
977
 * Doesn't clean the inputs
978
 *
979
 * @param array $items_ids The items to remove
980
 * @param bool|int $group_id The ID of the group these triggers are associated with or false if deleting them from all groups
981
 * @return bool Always returns true
982
 */
983
function removeBanTriggers($items_ids = array(), $group_id = false)
984
{
985
	global $smcFunc, $scripturl;
986
987
	if ($group_id !== false)
988
		$group_id = (int) $group_id;
989
990
	if (empty($group_id) && empty($items_ids))
991
		return false;
992
993
	if (!is_array($items_ids))
994
		$items_ids = array($items_ids);
995
996
	$log_info = array();
997
	$ban_items = array();
998
999
	call_integration_hook('integrate_remove_triggers', array(&$items_ids, $group_id));
1000
1001
	// First order of business: Load up the info so we can log this...
1002
	$request = $smcFunc['db_query']('', '
1003
		SELECT
1004
			bi.id_ban, bi.hostname, bi.email_address, bi.id_member, bi.hits,
1005
			bi.ip_low, bi.ip_high,
1006
			COALESCE(mem.id_member, 0) AS id_member, mem.member_name, mem.real_name
1007
		FROM {db_prefix}ban_items AS bi
1008
			LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = bi.id_member)
1009
		WHERE bi.id_ban IN ({array_int:ban_list})',
1010
		array(
1011
			'ban_list' => $items_ids,
1012
		)
1013
	);
1014
1015
	// Get all the info for the log
1016
	while ($row = $smcFunc['db_fetch_assoc']($request))
1017
	{
1018
		if (!empty($row['id_ban']))
1019
		{
1020
			$ban_items[$row['id_ban']] = array(
1021
				'id' => $row['id_ban'],
1022
			);
1023
			if (!empty($row['ip_high']))
1024
			{
1025
				$ban_items[$row['id_ban']]['type'] = 'ip';
1026
				$ban_items[$row['id_ban']]['ip'] = range2ip($row['ip_low'], $row['ip_high']);
1027
1028
				$is_range = (strpos($ban_items[$row['id_ban']]['ip'], '-') !== false || strpos($ban_items[$row['id_ban']]['ip'], '*') !== false);
1029
1030
				$log_info[] = array(
1031
					'bantype' => ($is_range ? 'ip_range' : 'main_ip'),
1032
					'value' => $ban_items[$row['id_ban']]['ip'],
1033
				);
1034
			}
1035
			elseif (!empty($row['hostname']))
1036
			{
1037
				$ban_items[$row['id_ban']]['type'] = 'hostname';
1038
				$ban_items[$row['id_ban']]['hostname'] = str_replace('%', '*', $row['hostname']);
1039
				$log_info[] = array(
1040
					'bantype' => 'hostname',
1041
					'value' => $row['hostname'],
1042
				);
1043
			}
1044
			elseif (!empty($row['email_address']))
1045
			{
1046
				$ban_items[$row['id_ban']]['type'] = 'email';
1047
				$ban_items[$row['id_ban']]['email'] = str_replace('%', '*', $row['email_address']);
1048
				$log_info[] = array(
1049
					'bantype' => 'email',
1050
					'value' => $ban_items[$row['id_ban']]['email'],
1051
				);
1052
			}
1053
			elseif (!empty($row['id_member']))
1054
			{
1055
				$ban_items[$row['id_ban']]['type'] = 'user';
1056
				$ban_items[$row['id_ban']]['user'] = array(
1057
					'id' => $row['id_member'],
1058
					'name' => $row['real_name'],
1059
					'href' => $scripturl . '?action=profile;u=' . $row['id_member'],
1060
					'link' => '<a href="' . $scripturl . '?action=profile;u=' . $row['id_member'] . '">' . $row['real_name'] . '</a>',
1061
				);
1062
				$log_info[] = array(
1063
					'bantype' => 'user',
1064
					'value' => $row['id_member'],
1065
				);
1066
			}
1067
		}
1068
	}
1069
1070
	// Log this!
1071
	logTriggersUpdates($log_info, false, true);
1072
1073
	$smcFunc['db_free_result']($request);
1074
1075
	if ($group_id !== false)
1076
	{
1077
		$smcFunc['db_query']('', '
1078
			DELETE FROM {db_prefix}ban_items
1079
			WHERE id_ban IN ({array_int:ban_list})
1080
				AND id_ban_group = {int:ban_group}',
1081
			array(
1082
				'ban_list' => $items_ids,
1083
				'ban_group' => $group_id,
1084
			)
1085
		);
1086
	}
1087
	elseif (!empty($items_ids))
1088
	{
1089
		$smcFunc['db_query']('', '
1090
			DELETE FROM {db_prefix}ban_items
1091
			WHERE id_ban IN ({array_int:ban_list})',
1092
			array(
1093
				'ban_list' => $items_ids,
1094
			)
1095
		);
1096
	}
1097
1098
	return true;
1099
}
1100
1101
/**
1102
 * This function removes a bunch of ban groups based on ids
1103
 * Doesn't clean the inputs
1104
 *
1105
 * @param int[] $group_ids The IDs of the groups to remove
1106
 * @return bool Returns ture if successful or false if $group_ids is empty
1107
 */
1108
function removeBanGroups($group_ids)
1109
{
1110
	global $smcFunc;
1111
1112
	if (!is_array($group_ids))
1113
		$group_ids = array($group_ids);
1114
1115
	$group_ids = array_unique($group_ids);
1116
1117
	if (empty($group_ids))
1118
		return false;
1119
1120
	$smcFunc['db_query']('', '
1121
		DELETE FROM {db_prefix}ban_groups
1122
		WHERE id_ban_group IN ({array_int:ban_list})',
1123
		array(
1124
			'ban_list' => $group_ids,
1125
		)
1126
	);
1127
1128
	return true;
1129
}
1130
1131
/**
1132
 * Removes logs - by default truncate the table
1133
 * Doesn't clean the inputs
1134
 *
1135
 * @param array $ids Empty array to clear the ban log or the IDs of the log entries to remove
1136
 * @return bool Returns true if successful or false if $ids is invalid
1137
 */
1138
function removeBanLogs($ids = array())
1139
{
1140
	global $smcFunc;
1141
1142
	if (empty($ids))
1143
		$smcFunc['db_query']('truncate_table', '
1144
			TRUNCATE {db_prefix}log_banned',
1145
			array(
1146
			)
1147
		);
1148
	else
1149
	{
1150
		if (!is_array($ids))
1151
			$ids = array($ids);
1152
1153
		$ids = array_unique($ids);
1154
1155
		if (empty($ids))
1156
			return false;
1157
1158
		$smcFunc['db_query']('', '
1159
			DELETE FROM {db_prefix}log_banned
1160
			WHERE id_ban_log IN ({array_int:ban_list})',
1161
			array(
1162
				'ban_list' => $ids,
1163
			)
1164
		);
1165
	}
1166
1167
	return true;
1168
}
1169
1170
/**
1171
 * This function validates the ban triggers
1172
 *
1173
 * Errors in $context['ban_errors']
1174
 *
1175
 * @param array $triggers The triggers to validate
1176
 * @return array An array of riggers and log info ready to be used
1177
 */
1178
function validateTriggers(&$triggers)
1179
{
1180
	global $context, $smcFunc;
1181
1182
	if (empty($triggers))
1183
		$context['ban_erros'][] = 'ban_empty_triggers';
1184
1185
	$ban_triggers = array();
1186
	$log_info = array();
1187
1188
	foreach ($triggers as $key => $value)
1189
	{
1190
		if (!empty($value))
1191
		{
1192
			if ($key == 'member')
1193
				continue;
1194
1195
			if ($key == 'main_ip')
1196
			{
1197
				$value = trim($value);
1198
				$ip_parts = ip2range($value);
1199
				if (!checkExistingTriggerIP($ip_parts, $value))
1200
					$context['ban_erros'][] = 'invalid_ip';
1201
				else
1202
				{
1203
					$ban_triggers['main_ip'] = array(
1204
						'ip_low' => $ip_parts['low'],
1205
						'ip_high' => $ip_parts['high']
1206
					);
1207
				}
1208
			}
1209
			elseif ($key == 'hostname')
1210
			{
1211
				if (preg_match('/[^\w.\-*]/', $value) == 1)
1212
					$context['ban_erros'][] = 'invalid_hostname';
1213
				else
1214
				{
1215
					// Replace the * wildcard by a MySQL wildcard %.
1216
					$value = substr(str_replace('*', '%', $value), 0, 255);
1217
1218
					$ban_triggers['hostname']['hostname'] = $value;
1219
				}
1220
			}
1221
			elseif ($key == 'email')
1222
			{
1223
				if (preg_match('/[^\w.\-\+*@]/', $value) == 1)
1224
					$context['ban_erros'][] = 'invalid_email';
1225
1226
				// Check the user is not banning an admin.
1227
				$request = $smcFunc['db_query']('', '
1228
					SELECT id_member
1229
					FROM {db_prefix}members
1230
					WHERE (id_group = {int:admin_group} OR FIND_IN_SET({int:admin_group}, additional_groups) != 0)
1231
						AND email_address LIKE {string:email}
1232
					LIMIT 1',
1233
					array(
1234
						'admin_group' => 1,
1235
						'email' => $value,
1236
					)
1237
				);
1238
				if ($smcFunc['db_num_rows']($request) != 0)
1239
					$context['ban_erros'][] = 'no_ban_admin';
1240
				$smcFunc['db_free_result']($request);
1241
1242
				$value = substr(strtolower(str_replace('*', '%', $value)), 0, 255);
1243
1244
				$ban_triggers['email']['email_address'] = $value;
1245
			}
1246
			elseif ($key == 'user')
1247
			{
1248
				$user = preg_replace('~&amp;#(\d{4,5}|[2-9]\d{2,4}|1[2-9]\d);~', '&#$1;', $smcFunc['htmlspecialchars']($value, ENT_QUOTES));
1249
1250
				$request = $smcFunc['db_query']('', '
1251
					SELECT id_member, (id_group = {int:admin_group} OR FIND_IN_SET({int:admin_group}, additional_groups) != 0) AS isAdmin
1252
					FROM {db_prefix}members
1253
					WHERE member_name = {string:username} OR real_name = {string:username}
1254
					LIMIT 1',
1255
					array(
1256
						'admin_group' => 1,
1257
						'username' => $user,
1258
					)
1259
				);
1260
				if ($smcFunc['db_num_rows']($request) == 0)
1261
					$context['ban_erros'][] = 'invalid_username';
1262
				list ($value, $isAdmin) = $smcFunc['db_fetch_row']($request);
1263
				$smcFunc['db_free_result']($request);
1264
1265
				if ($isAdmin && strtolower($isAdmin) != 'f')
1266
				{
1267
					unset($value);
1268
					$context['ban_erros'][] = 'no_ban_admin';
1269
				}
1270
				else
1271
					$ban_triggers['user']['id_member'] = $value;
1272
			}
1273
			elseif (in_array($key, array('ips_in_messages', 'ips_in_errors')))
1274
			{
1275
				// Special case, those two are arrays themselves
1276
				$values = array_unique($value);
1277
				// Don't add the main IP again.
1278
				if (isset($triggers['main_ip']))
1279
					$values = array_diff($values, array($triggers['main_ip']));
1280
				unset($value);
1281
				foreach ($values as $val)
1282
				{
1283
					$val = trim($val);
1284
					$ip_parts = ip2range($val);
1285
					if (!checkExistingTriggerIP($ip_parts, $val))
1286
						$context['ban_erros'][] = 'invalid_ip';
1287
					else
1288
					{
1289
						$ban_triggers[$key][] = array(
1290
							'ip_low' => $ip_parts['low'],
1291
							'ip_high' => $ip_parts['high'],
1292
						);
1293
1294
						$log_info[] = array(
1295
							'value' => $val,
1296
							'bantype' => 'ip_range',
1297
						);
1298
					}
1299
				}
1300
			}
1301
			else
1302
				$context['ban_erros'][] = 'no_bantype_selected';
1303
1304
			if (isset($value) && !is_array($value))
1305
				$log_info[] = array(
1306
					'value' => $value,
1307
					'bantype' => $key,
1308
				);
1309
		}
1310
	}
1311
	return array('ban_triggers' => $ban_triggers, 'log_info' => $log_info);
1312
}
1313
1314
/**
1315
 * This function actually inserts the ban triggers into the database
1316
 *
1317
 * Errors in $context['ban_errors']
1318
 *
1319
 * @param int $group_id The ID of the group to add the triggers to (0 to create a new one)
1320
 * @param array $triggers The triggers to add
1321
 * @param array $logs The log data
1322
 * @return bool Whether or not the action was successful
1323
 */
1324
function addTriggers($group_id = 0, $triggers = array(), $logs = array())
1325
{
1326
	global $smcFunc, $context;
1327
1328
	if (empty($group_id))
1329
		$context['ban_errors'][] = 'ban_id_empty';
1330
1331
	// Preset all values that are required.
1332
	$values = array(
1333
		'id_ban_group' => $group_id,
1334
		'hostname' => '',
1335
		'email_address' => '',
1336
		'id_member' => 0,
1337
		'ip_low' => 'null',
1338
		'ip_high' => 'null',
1339
	);
1340
1341
	$insertKeys = array(
1342
		'id_ban_group' => 'int',
1343
		'hostname' => 'string',
1344
		'email_address' => 'string',
1345
		'id_member' => 'int',
1346
		'ip_low' => 'inet',
1347
		'ip_high' => 'inet',
1348
	);
1349
1350
	$insertTriggers = array();
1351
	foreach ($triggers as $key => $trigger)
1352
	{
1353
		// Exceptions, exceptions, exceptions...always exceptions... :P
1354
		if (in_array($key, array('ips_in_messages', 'ips_in_errors')))
1355
			foreach ($trigger as $real_trigger)
1356
				$insertTriggers[] = array_merge($values, $real_trigger);
1357
		else
1358
			$insertTriggers[] = array_merge($values, $trigger);
1359
	}
1360
1361
	if (empty($insertTriggers))
1362
		$context['ban_errors'][] = 'ban_no_triggers';
1363
1364
	if (!empty($context['ban_errors']))
1365
		return false;
1366
1367
	$smcFunc['db_insert']('',
1368
		'{db_prefix}ban_items',
1369
		$insertKeys,
1370
		$insertTriggers,
1371
		array('id_ban')
1372
	);
1373
1374
	logTriggersUpdates($logs, true);
1375
1376
	return true;
1377
}
1378
1379
/**
1380
 * This function updates an existing ban trigger into the database
1381
 *
1382
 * Errors in $context['ban_errors']
1383
 *
1384
 * @param int $ban_item The ID of the ban item
1385
 * @param int $group_id The ID of the ban group
1386
 * @param array $trigger An array of triggers
1387
 * @param array $logs An array of log info
1388
 */
1389
function updateTriggers($ban_item = 0, $group_id = 0, $trigger = array(), $logs = array())
1390
{
1391
	global $smcFunc, $context;
1392
1393
	if (empty($ban_item))
1394
		$context['ban_errors'][] = 'ban_ban_item_empty';
1395
	if (empty($group_id))
1396
		$context['ban_errors'][] = 'ban_id_empty';
1397
	if (empty($trigger))
1398
		$context['ban_errors'][] = 'ban_no_triggers';
1399
1400
	if (!empty($context['ban_errors']))
1401
		return;
1402
1403
	// Preset all values that are required.
1404
	$values = array(
1405
		'id_ban_group' => $group_id,
1406
		'hostname' => '',
1407
		'email_address' => '',
1408
		'id_member' => 0,
1409
		'ip_low' => 'null',
1410
		'ip_high' => 'null',
1411
	);
1412
1413
	$trigger = array_merge($values, $trigger);
1414
1415
	$smcFunc['db_query']('', '
1416
		UPDATE {db_prefix}ban_items
1417
		SET
1418
			hostname = {string:hostname}, email_address = {string:email_address}, id_member = {int:id_member},
1419
			ip_low = {inet:ip_low}, ip_high = {inet:ip_high}
1420
		WHERE id_ban = {int:ban_item}
1421
			AND id_ban_group = {int:id_ban_group}',
1422
		array_merge($trigger, array(
1423
			'id_ban_group' => $group_id,
1424
			'ban_item' => $ban_item,
1425
		))
1426
	);
1427
1428
	logTriggersUpdates($logs, false);
1429
}
1430
1431
/**
1432
 * A small function to unify logging of triggers (updates and new)
1433
 *
1434
 * @param array $logs an array of logs, each log contains the following keys:
1435
 *                - bantype: a known type of ban (ip_range, hostname, email, user, main_ip)
1436
 *                - value: the value of the bantype (e.g. the IP or the email address banned)
1437
 * @param bool $new Whether the trigger is new or an update of an existing one
1438
 * @param bool $removal Whether the trigger is being deleted
1439
 */
1440
function logTriggersUpdates($logs, $new = true, $removal = false)
1441
{
1442
	if (empty($logs))
1443
		return;
1444
1445
	$log_name_map = array(
1446
		'main_ip' => 'ip_range',
1447
		'hostname' => 'hostname',
1448
		'email' => 'email',
1449
		'user' => 'member',
1450
		'ip_range' => 'ip_range',
1451
	);
1452
1453
	// Log the addion of the ban entries into the moderation log.
1454
	foreach ($logs as $log)
1455
		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...
1456
			$log_name_map[$log['bantype']] => $log['value'],
1457
			'new' => empty($new) ? 0 : 1,
1458
			'remove' => empty($removal) ? 0 : 1,
1459
			'type' => $log['bantype'],
1460
		));
1461
}
1462
1463
/**
1464
 * Updates an existing ban group
1465
 *
1466
 * Errors in $context['ban_errors']
1467
 *
1468
 * @param array $ban_info An array of info about the ban group. Should have name and may also have an id.
1469
 * @return int The ban group's ID
1470
 */
1471
function updateBanGroup($ban_info = array())
1472
{
1473
	global $smcFunc, $context;
1474
1475
	if (empty($ban_info['name']))
1476
		$context['ban_errors'][] = 'ban_name_empty';
1477
	if (empty($ban_info['id']))
1478
		$context['ban_errors'][] = 'ban_id_empty';
1479
	if (empty($ban_info['cannot']['access']) && empty($ban_info['cannot']['register']) && empty($ban_info['cannot']['post']) && empty($ban_info['cannot']['login']))
1480
		$context['ban_errors'][] = 'ban_unknown_restriction_type';
1481
1482
	if (!empty($ban_info['id']))
1483
	{
1484
		// Verify the ban group exists.
1485
		$request = $smcFunc['db_query']('', '
1486
			SELECT id_ban_group
1487
			FROM {db_prefix}ban_groups
1488
			WHERE id_ban_group = {int:ban_group}
1489
			LIMIT 1',
1490
			array(
1491
				'ban_group' => $ban_info['id']
1492
			)
1493
		);
1494
1495
		if ($smcFunc['db_num_rows']($request) == 0)
1496
			$context['ban_errors'][] = 'ban_not_found';
1497
		$smcFunc['db_free_result']($request);
1498
	}
1499
1500
	if (!empty($ban_info['name']))
1501
	{
1502
		// Make sure the name does not already exist (Of course, if it exists in the ban group we are editing, proceed.)
1503
		$request = $smcFunc['db_query']('', '
1504
			SELECT id_ban_group
1505
			FROM {db_prefix}ban_groups
1506
			WHERE name = {string:new_ban_name}
1507
				AND id_ban_group != {int:ban_group}
1508
			LIMIT 1',
1509
			array(
1510
				'ban_group' => empty($ban_info['id']) ? 0 : $ban_info['id'],
1511
				'new_ban_name' => $ban_info['name'],
1512
			)
1513
		);
1514
		if ($smcFunc['db_num_rows']($request) != 0)
1515
			$context['ban_errors'][] = 'ban_name_exists';
1516
		$smcFunc['db_free_result']($request);
1517
	}
1518
1519
	if (!empty($context['ban_errors']))
1520
		return $ban_info['id'];
1521
1522
	$smcFunc['db_query']('', '
1523
		UPDATE {db_prefix}ban_groups
1524
		SET
1525
			name = {string:ban_name},
1526
			reason = {string:reason},
1527
			notes = {string:notes},
1528
			expire_time = {raw:expiration},
1529
			cannot_access = {int:cannot_access},
1530
			cannot_post = {int:cannot_post},
1531
			cannot_register = {int:cannot_register},
1532
			cannot_login = {int:cannot_login}
1533
		WHERE id_ban_group = {int:id_ban_group}',
1534
		array(
1535
			'expiration' => $ban_info['db_expiration'],
1536
			'cannot_access' => $ban_info['cannot']['access'],
1537
			'cannot_post' => $ban_info['cannot']['post'],
1538
			'cannot_register' => $ban_info['cannot']['register'],
1539
			'cannot_login' => $ban_info['cannot']['login'],
1540
			'id_ban_group' => $ban_info['id'],
1541
			'ban_name' => $ban_info['name'],
1542
			'reason' => $ban_info['reason'],
1543
			'notes' => $ban_info['notes'],
1544
		)
1545
	);
1546
1547
	return $ban_info['id'];
1548
}
1549
1550
/**
1551
 * Creates a new ban group
1552
 * If the group is successfully created the ID is returned
1553
 * On error the error code is returned or false
1554
 *
1555
 * Errors in $context['ban_errors']
1556
 *
1557
 * @param array $ban_info An array containing 'name', which is the name of the ban group
1558
 * @return int The ban group's ID
1559
 */
1560
function insertBanGroup($ban_info = array())
1561
{
1562
	global $smcFunc, $context;
1563
1564
	if (empty($ban_info['name']))
1565
		$context['ban_errors'][] = 'ban_name_empty';
1566
	if (empty($ban_info['cannot']['access']) && empty($ban_info['cannot']['register']) && empty($ban_info['cannot']['post']) && empty($ban_info['cannot']['login']))
1567
		$context['ban_errors'][] = 'ban_unknown_restriction_type';
1568
1569
	if (!empty($ban_info['name']))
1570
	{
1571
		// Check whether a ban with this name already exists.
1572
		$request = $smcFunc['db_query']('', '
1573
			SELECT id_ban_group
1574
			FROM {db_prefix}ban_groups
1575
			WHERE name = {string:new_ban_name}' . '
1576
			LIMIT 1',
1577
			array(
1578
				'new_ban_name' => $ban_info['name'],
1579
			)
1580
		);
1581
1582
		if ($smcFunc['db_num_rows']($request) == 1)
1583
			$context['ban_errors'][] = 'ban_name_exists';
1584
		$smcFunc['db_free_result']($request);
1585
	}
1586
1587
	if (!empty($context['ban_errors']))
1588
		return;
1589
1590
	// Yes yes, we're ready to add now.
1591
	$ban_info['id'] = $smcFunc['db_insert']('',
1592
		'{db_prefix}ban_groups',
1593
		array(
1594
			'name' => 'string-20', 'ban_time' => 'int', 'expire_time' => 'raw', 'cannot_access' => 'int', 'cannot_register' => 'int',
1595
			'cannot_post' => 'int', 'cannot_login' => 'int', 'reason' => 'string-255', 'notes' => 'string-65534',
1596
		),
1597
		array(
1598
			$ban_info['name'], time(), $ban_info['db_expiration'], $ban_info['cannot']['access'], $ban_info['cannot']['register'],
1599
			$ban_info['cannot']['post'], $ban_info['cannot']['login'], $ban_info['reason'], $ban_info['notes'],
1600
		),
1601
		array('id_ban_group'),
1602
		1
1603
	);
1604
1605
	if (empty($ban_info['id']))
1606
		$context['ban_errors'][] = 'impossible_insert_new_bangroup';
1607
1608
	return $ban_info['id'];
1609
}
1610
1611
/**
1612
 * This function handles the ins and outs of the screen for adding new ban
1613
 * triggers or modifying existing ones.
1614
 * Adding new ban triggers:
1615
 * 	- is accessed by ?action=admin;area=ban;sa=edittrigger;bg=x
1616
 * 	- uses the ban_edit_trigger sub template of ManageBans.
1617
 * Editing existing ban triggers:
1618
 *  - is accessed by ?action=admin;area=ban;sa=edittrigger;bg=x;bi=y
1619
 *  - uses the ban_edit_trigger sub template of ManageBans.
1620
 */
1621
function BanEditTrigger()
1622
{
1623
	global $context, $smcFunc, $scripturl;
1624
1625
	$context['sub_template'] = 'ban_edit_trigger';
1626
	$context['form_url'] = $scripturl . '?action=admin;area=ban;sa=edittrigger';
1627
1628
	$ban_group = isset($_REQUEST['bg']) ? (int) $_REQUEST['bg'] : 0;
1629
	$ban_id = isset($_REQUEST['bi']) ? (int) $_REQUEST['bi'] : 0;
1630
1631
	if (empty($ban_group))
1632
		fatal_lang_error('ban_not_found', false);
1633
1634
	if (isset($_POST['add_new_trigger']) && !empty($_POST['ban_suggestions']))
1635
	{
1636
		saveTriggers($_POST['ban_suggestions'], $ban_group, 0, $ban_id);
1637
		redirectexit('action=admin;area=ban;sa=edit' . (!empty($ban_group) ? ';bg=' . $ban_group : ''));
1638
	}
1639
	elseif (isset($_POST['edit_trigger']) && !empty($_POST['ban_suggestions']))
1640
	{
1641
		// The first replaces the old one, the others are added new (simplification, otherwise it would require another query and some work...)
1642
		saveTriggers(array_shift($_POST['ban_suggestions']), $ban_group, 0, $ban_id);
1643
		if (!empty($_POST['ban_suggestions']))
1644
			saveTriggers($_POST['ban_suggestions'], $ban_group);
1645
1646
		redirectexit('action=admin;area=ban;sa=edit' . (!empty($ban_group) ? ';bg=' . $ban_group : ''));
1647
	}
1648
	elseif (isset($_POST['edit_trigger']))
1649
	{
1650
		removeBanTriggers($ban_id);
1651
		redirectexit('action=admin;area=ban;sa=edit' . (!empty($ban_group) ? ';bg=' . $ban_group : ''));
1652
	}
1653
1654
	loadJavaScriptFile('suggest.js', array('minimize' => true), 'smf_suggest');
1655
1656
	if (empty($ban_id))
1657
	{
1658
		$context['ban_trigger'] = array(
1659
			'id' => 0,
1660
			'group' => $ban_group,
1661
			'ip' => array(
1662
				'value' => '',
1663
				'selected' => true,
1664
			),
1665
			'hostname' => array(
1666
				'selected' => false,
1667
				'value' => '',
1668
			),
1669
			'email' => array(
1670
				'value' => '',
1671
				'selected' => false,
1672
			),
1673
			'banneduser' => array(
1674
				'value' => '',
1675
				'selected' => false,
1676
			),
1677
			'is_new' => true,
1678
		);
1679
	}
1680
	else
1681
	{
1682
		$request = $smcFunc['db_query']('', '
1683
			SELECT
1684
				bi.id_ban, bi.id_ban_group, bi.hostname, bi.email_address, bi.id_member,
1685
				bi.ip_low, bi.ip_high,
1686
				mem.member_name, mem.real_name
1687
			FROM {db_prefix}ban_items AS bi
1688
				LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = bi.id_member)
1689
			WHERE bi.id_ban = {int:ban_item}
1690
				AND bi.id_ban_group = {int:ban_group}
1691
			LIMIT 1',
1692
			array(
1693
				'ban_item' => $ban_id,
1694
				'ban_group' => $ban_group,
1695
			)
1696
		);
1697
		if ($smcFunc['db_num_rows']($request) == 0)
1698
			fatal_lang_error('ban_not_found', false);
1699
		$row = $smcFunc['db_fetch_assoc']($request);
1700
		$smcFunc['db_free_result']($request);
1701
1702
		$context['ban_trigger'] = array(
1703
			'id' => $row['id_ban'],
1704
			'group' => $row['id_ban_group'],
1705
			'ip' => array(
1706
				'value' => empty($row['ip_low']) ? '' : range2ip($row['ip_low'], $row['ip_high']),
1707
				'selected' => !empty($row['ip_low']),
1708
			),
1709
			'hostname' => array(
1710
				'value' => str_replace('%', '*', $row['hostname']),
1711
				'selected' => !empty($row['hostname']),
1712
			),
1713
			'email' => array(
1714
				'value' => str_replace('%', '*', $row['email_address']),
1715
				'selected' => !empty($row['email_address'])
1716
			),
1717
			'banneduser' => array(
1718
				'value' => $row['member_name'],
1719
				'selected' => !empty($row['member_name'])
1720
			),
1721
			'is_new' => false,
1722
		);
1723
	}
1724
1725
	createToken('admin-bet');
1726
}
1727
1728
/**
1729
 * This handles the screen for showing the banned entities
1730
 * It is accessed by ?action=admin;area=ban;sa=browse
1731
 * It uses sub-tabs for browsing by IP, hostname, email or username.
1732
 *
1733
 * Uses a standard list (@see createList())
1734
 */
1735
function BanBrowseTriggers()
1736
{
1737
	global $modSettings, $context, $scripturl, $smcFunc, $txt;
1738
	global $sourcedir, $settings;
1739
1740
	if (!empty($_POST['remove_triggers']) && !empty($_POST['remove']) && is_array($_POST['remove']))
1741
	{
1742
		checkSession();
1743
1744
		removeBanTriggers($_POST['remove']);
1745
1746
		// Rehabilitate some members.
1747
		if ($_REQUEST['entity'] == 'member')
1748
			updateBanMembers();
1749
1750
		// Make sure the ban cache is refreshed.
1751
		updateSettings(array('banLastUpdated' => time()));
1752
	}
1753
1754
	$context['selected_entity'] = isset($_REQUEST['entity']) && in_array($_REQUEST['entity'], array('ip', 'hostname', 'email', 'member')) ? $_REQUEST['entity'] : 'ip';
1755
1756
	$listOptions = array(
1757
		'id' => 'ban_trigger_list',
1758
		'title' => $txt['ban_trigger_browse'],
1759
		'items_per_page' => $modSettings['defaultMaxListItems'],
1760
		'base_href' => $scripturl . '?action=admin;area=ban;sa=browse;entity=' . $context['selected_entity'],
1761
		'default_sort_col' => 'banned_entity',
1762
		'no_items_label' => $txt['ban_no_triggers'],
1763
		'get_items' => array(
1764
			'function' => 'list_getBanTriggers',
1765
			'params' => array(
1766
				$context['selected_entity'],
1767
			),
1768
		),
1769
		'get_count' => array(
1770
			'function' => 'list_getNumBanTriggers',
1771
			'params' => array(
1772
				$context['selected_entity'],
1773
			),
1774
		),
1775
		'columns' => array(
1776
			'banned_entity' => array(
1777
				'header' => array(
1778
					'value' => $txt['ban_banned_entity'],
1779
				),
1780
			),
1781
			'ban_name' => array(
1782
				'header' => array(
1783
					'value' => $txt['ban_name'],
1784
				),
1785
				'data' => array(
1786
					'sprintf' => array(
1787
						'format' => '<a href="' . $scripturl . '?action=admin;area=ban;sa=edit;bg=%1$d">%2$s</a>',
1788
						'params' => array(
1789
							'id_ban_group' => false,
1790
							'name' => false,
1791
						),
1792
					),
1793
				),
1794
				'sort' => array(
1795
					'default' => 'bg.name',
1796
					'reverse' => 'bg.name DESC',
1797
				),
1798
			),
1799
			'hits' => array(
1800
				'header' => array(
1801
					'value' => $txt['ban_hits'],
1802
				),
1803
				'data' => array(
1804
					'db' => 'hits',
1805
				),
1806
				'sort' => array(
1807
					'default' => 'bi.hits DESC',
1808
					'reverse' => 'bi.hits',
1809
				),
1810
			),
1811
			'check' => array(
1812
				'header' => array(
1813
					'value' => '<input type="checkbox" onclick="invertAll(this, this.form);">',
1814
					'class' => 'centercol',
1815
				),
1816
				'data' => array(
1817
					'sprintf' => array(
1818
						'format' => '<input type="checkbox" name="remove[]" value="%1$d">',
1819
						'params' => array(
1820
							'id_ban' => false,
1821
						),
1822
					),
1823
					'class' => 'centercol',
1824
				),
1825
			),
1826
		),
1827
		'form' => array(
1828
			'href' => $scripturl . '?action=admin;area=ban;sa=browse;entity=' . $context['selected_entity'],
1829
			'include_start' => true,
1830
			'include_sort' => true,
1831
		),
1832
		'additional_rows' => array(
1833
			array(
1834
				'position' => 'above_column_headers',
1835
				'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>',
1836
			),
1837
			array(
1838
				'position' => 'bottom_of_list',
1839
				'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">',
1840
			),
1841
		),
1842
	);
1843
1844
	// Specific data for the first column depending on the selected entity.
1845
	if ($context['selected_entity'] === 'ip')
1846
	{
1847
		$listOptions['columns']['banned_entity']['data'] = array(
1848
			'function' => function($rowData)
1849
			{
1850
				return range2ip(
1851
					$rowData['ip_low']
1852
					,
1853
					$rowData['ip_high']
1854
				);
1855
			},
1856
		);
1857
		$listOptions['columns']['banned_entity']['sort'] = array(
1858
			'default' => 'bi.ip_low, bi.ip_high, bi.ip_low',
1859
			'reverse' => 'bi.ip_low DESC, bi.ip_high DESC',
1860
		);
1861
	}
1862
	elseif ($context['selected_entity'] === 'hostname')
1863
	{
1864
		$listOptions['columns']['banned_entity']['data'] = array(
1865
			'function' => function($rowData) use ($smcFunc)
1866
			{
1867
				return strtr($smcFunc['htmlspecialchars']($rowData['hostname']), array('%' => '*'));
1868
			},
1869
		);
1870
		$listOptions['columns']['banned_entity']['sort'] = array(
1871
			'default' => 'bi.hostname',
1872
			'reverse' => 'bi.hostname DESC',
1873
		);
1874
	}
1875
	elseif ($context['selected_entity'] === 'email')
1876
	{
1877
		$listOptions['columns']['banned_entity']['data'] = array(
1878
			'function' => function($rowData) use ($smcFunc)
1879
			{
1880
				return strtr($smcFunc['htmlspecialchars']($rowData['email_address']), array('%' => '*'));
1881
			},
1882
		);
1883
		$listOptions['columns']['banned_entity']['sort'] = array(
1884
			'default' => 'bi.email_address',
1885
			'reverse' => 'bi.email_address DESC',
1886
		);
1887
	}
1888
	elseif ($context['selected_entity'] === 'member')
1889
	{
1890
		$listOptions['columns']['banned_entity']['data'] = array(
1891
			'sprintf' => array(
1892
				'format' => '<a href="' . $scripturl . '?action=profile;u=%1$d">%2$s</a>',
1893
				'params' => array(
1894
					'id_member' => false,
1895
					'real_name' => false,
1896
				),
1897
			),
1898
		);
1899
		$listOptions['columns']['banned_entity']['sort'] = array(
1900
			'default' => 'mem.real_name',
1901
			'reverse' => 'mem.real_name DESC',
1902
		);
1903
	}
1904
1905
	// Create the list.
1906
	require_once($sourcedir . '/Subs-List.php');
1907
	createList($listOptions);
1908
1909
	// The list is the only thing to show, so make it the default sub template.
1910
	$context['sub_template'] = 'show_list';
1911
	$context['default_list'] = 'ban_trigger_list';
1912
}
1913
1914
/**
1915
 * Get ban triggers for the given parameters. Callback from $listOptions['get_items'] in BanBrowseTriggers()
1916
 *
1917
 * @param int $start The item to start with (for pagination purposes)
1918
 * @param int $items_per_page How many items to show on each page
1919
 * @param string $sort A string telling ORDER BY how to sort the results
1920
 * @param string $trigger_type The trigger type - can be 'ip', 'hostname' or 'email'
1921
 * @return array An array of ban trigger info for the list
1922
 */
1923
function list_getBanTriggers($start, $items_per_page, $sort, $trigger_type)
1924
{
1925
	global $smcFunc;
1926
1927
	$where = array(
1928
		'ip' => 'bi.ip_low is not null',
1929
		'hostname' => 'bi.hostname != {string:blank_string}',
1930
		'email' => 'bi.email_address != {string:blank_string}',
1931
	);
1932
1933
	$request = $smcFunc['db_query']('', '
1934
		SELECT
1935
			bi.id_ban, bi.ip_low, bi.ip_high, bi.hostname, bi.email_address, bi.hits,
1936
			bg.id_ban_group, bg.name' . ($trigger_type === 'member' ? ',
1937
			mem.id_member, mem.real_name' : '') . '
1938
		FROM {db_prefix}ban_items AS bi
1939
			INNER JOIN {db_prefix}ban_groups AS bg ON (bg.id_ban_group = bi.id_ban_group)' . ($trigger_type === 'member' ? '
1940
			INNER JOIN {db_prefix}members AS mem ON (mem.id_member = bi.id_member)' : '
1941
		WHERE ' . $where[$trigger_type]) . '
1942
		ORDER BY {raw:sort}
1943
		LIMIT {int:start}, {int:max}',
1944
		array(
1945
			'blank_string' => '',
1946
			'sort' => $sort,
1947
			'start' => $start,
1948
			'max' => $items_per_page,
1949
		)
1950
	);
1951
	$ban_triggers = array();
1952
	while ($row = $smcFunc['db_fetch_assoc']($request))
1953
		$ban_triggers[] = $row;
1954
	$smcFunc['db_free_result']($request);
1955
1956
	return $ban_triggers;
1957
}
1958
1959
/**
1960
 * This returns the total number of ban triggers of the given type. Callback for $listOptions['get_count'] in BanBrowseTriggers().
1961
 *
1962
 * @param string $trigger_type The trigger type. Can be 'ip', 'hostname' or 'email'
1963
 * @return int The number of triggers of the specified type
1964
 */
1965
function list_getNumBanTriggers($trigger_type)
1966
{
1967
	global $smcFunc;
1968
1969
	$where = array(
1970
		'ip' => 'bi.ip_low is not null',
1971
		'hostname' => 'bi.hostname != {string:blank_string}',
1972
		'email' => 'bi.email_address != {string:blank_string}',
1973
	);
1974
1975
	$request = $smcFunc['db_query']('', '
1976
		SELECT COUNT(*)
1977
		FROM {db_prefix}ban_items AS bi' . ($trigger_type === 'member' ? '
1978
			INNER JOIN {db_prefix}members AS mem ON (mem.id_member = bi.id_member)' : '
1979
		WHERE ' . $where[$trigger_type]),
1980
		array(
1981
			'blank_string' => '',
1982
		)
1983
	);
1984
	list ($num_triggers) = $smcFunc['db_fetch_row']($request);
1985
	$smcFunc['db_free_result']($request);
1986
1987
	return $num_triggers;
1988
}
1989
1990
/**
1991
 * This handles the listing of ban log entries, and allows their deletion.
1992
 * Shows a list of logged access attempts by banned users.
1993
 * It is accessed by ?action=admin;area=ban;sa=log.
1994
 * How it works:
1995
 *  - allows sorting of several columns.
1996
 *  - also handles deletion of (a selection of) log entries.
1997
 */
1998
function BanLog()
1999
{
2000
	global $scripturl, $context, $sourcedir, $txt, $modSettings;
2001
2002
	// Delete one or more entries.
2003
	if (!empty($_POST['removeAll']) || (!empty($_POST['removeSelected']) && !empty($_POST['remove'])))
2004
	{
2005
		checkSession();
2006
		validateToken('admin-bl');
2007
2008
		// 'Delete all entries' button was pressed.
2009
		if (!empty($_POST['removeAll']))
2010
			removeBanLogs();
2011
		// 'Delete selection' button was pressed.
2012
		else
2013
		{
2014
			array_map('intval', $_POST['remove']);
2015
			removeBanLogs($_POST['remove']);
2016
		}
2017
	}
2018
2019
	$listOptions = array(
2020
		'id' => 'ban_log',
2021
		'title' => $txt['ban_log'],
2022
		'items_per_page' => $modSettings['defaultMaxListItems'],
2023
		'base_href' => $context['admin_area'] == 'ban' ? $scripturl . '?action=admin;area=ban;sa=log' : $scripturl . '?action=admin;area=logs;sa=banlog',
2024
		'default_sort_col' => 'date',
2025
		'get_items' => array(
2026
			'function' => 'list_getBanLogEntries',
2027
		),
2028
		'get_count' => array(
2029
			'function' => 'list_getNumBanLogEntries',
2030
		),
2031
		'no_items_label' => $txt['ban_log_no_entries'],
2032
		'columns' => array(
2033
			'ip' => array(
2034
				'header' => array(
2035
					'value' => $txt['ban_log_ip'],
2036
				),
2037
				'data' => array(
2038
					'sprintf' => array(
2039
						'format' => '<a href="' . $scripturl . '?action=trackip;searchip=%1$s">%1$s</a>',
2040
						'params' => array(
2041
							'ip' => false,
2042
						),
2043
					),
2044
				),
2045
				'sort' => array(
2046
					'default' => 'lb.ip',
2047
					'reverse' => 'lb.ip DESC',
2048
				),
2049
			),
2050
			'email' => array(
2051
				'header' => array(
2052
					'value' => $txt['ban_log_email'],
2053
				),
2054
				'data' => array(
2055
					'db_htmlsafe' => 'email',
2056
				),
2057
				'sort' => array(
2058
					'default' => 'lb.email = \'\', lb.email',
2059
					'reverse' => 'lb.email != \'\', lb.email DESC',
2060
				),
2061
			),
2062
			'member' => array(
2063
				'header' => array(
2064
					'value' => $txt['ban_log_member'],
2065
				),
2066
				'data' => array(
2067
					'sprintf' => array(
2068
						'format' => '<a href="' . $scripturl . '?action=profile;u=%1$d">%2$s</a>',
2069
						'params' => array(
2070
							'id_member' => false,
2071
							'real_name' => false,
2072
						),
2073
					),
2074
				),
2075
				'sort' => array(
2076
					'default' => 'COALESCE(mem.real_name, 1=1), mem.real_name',
2077
					'reverse' => 'COALESCE(mem.real_name, 1=1) DESC, mem.real_name DESC',
2078
				),
2079
			),
2080
			'date' => array(
2081
				'header' => array(
2082
					'value' => $txt['ban_log_date'],
2083
				),
2084
				'data' => array(
2085
					'function' => function($rowData)
2086
					{
2087
						return timeformat($rowData['log_time']);
2088
					},
2089
				),
2090
				'sort' => array(
2091
					'default' => 'lb.log_time DESC',
2092
					'reverse' => 'lb.log_time',
2093
				),
2094
			),
2095
			'check' => array(
2096
				'header' => array(
2097
					'value' => '<input type="checkbox" onclick="invertAll(this, this.form);">',
2098
					'class' => 'centercol',
2099
				),
2100
				'data' => array(
2101
					'sprintf' => array(
2102
						'format' => '<input type="checkbox" name="remove[]" value="%1$d">',
2103
						'params' => array(
2104
							'id_ban_log' => false,
2105
						),
2106
					),
2107
					'class' => 'centercol',
2108
				),
2109
			),
2110
		),
2111
		'form' => array(
2112
			'href' => $context['admin_area'] == 'ban' ? $scripturl . '?action=admin;area=ban;sa=log' : $scripturl . '?action=admin;area=logs;sa=banlog',
2113
			'include_start' => true,
2114
			'include_sort' => true,
2115
			'token' => 'admin-bl',
2116
		),
2117
		'additional_rows' => array(
2118
			array(
2119
				'position' => 'top_of_list',
2120
				'value' => '
2121
					<input type="submit" name="removeSelected" value="' . $txt['ban_log_remove_selected'] . '" data-confirm="' . $txt['ban_log_remove_selected_confirm'] . '" class="button you_sure">
2122
					<input type="submit" name="removeAll" value="' . $txt['ban_log_remove_all'] . '" data-confirm="' . $txt['ban_log_remove_all_confirm'] . '" class="button you_sure">',
2123
			),
2124
			array(
2125
				'position' => 'bottom_of_list',
2126
				'value' => '
2127
					<input type="submit" name="removeSelected" value="' . $txt['ban_log_remove_selected'] . '" data-confirm="' . $txt['ban_log_remove_selected_confirm'] . '" class="button you_sure">
2128
					<input type="submit" name="removeAll" value="' . $txt['ban_log_remove_all'] . '" data-confirm="' . $txt['ban_log_remove_all_confirm'] . '" class="button you_sure">',
2129
			),
2130
		),
2131
	);
2132
2133
	createToken('admin-bl');
2134
2135
	require_once($sourcedir . '/Subs-List.php');
2136
	createList($listOptions);
2137
2138
	$context['page_title'] = $txt['ban_log'];
2139
	$context['sub_template'] = 'show_list';
2140
	$context['default_list'] = 'ban_log';
2141
}
2142
2143
/**
2144
 * Load a list of ban log entries from the database.
2145
 * (no permissions check). Callback for $listOptions['get_items'] in BanLog()
2146
 *
2147
 * @param int $start The item to start with (for pagination purposes)
2148
 * @param int $items_per_page How many items to show on each page
2149
 * @param string $sort A string telling ORDER BY how to sort the results
2150
 * @return array An array of info about the ban log entries for the list.
2151
 */
2152
function list_getBanLogEntries($start, $items_per_page, $sort)
2153
{
2154
	global $smcFunc;
2155
2156
	$dash = '-';
2157
2158
	$request = $smcFunc['db_query']('', '
2159
		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
2160
		FROM {db_prefix}log_banned AS lb
2161
			LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = lb.id_member)
2162
		ORDER BY {raw:sort}
2163
		LIMIT {int:start}, {int:items}',
2164
		array(
2165
			'blank_string' => '',
2166
			'dash' => $dash,
2167
			'sort' => $sort,
2168
			'start' => $start,
2169
			'items' => $items_per_page,
2170
		)
2171
	);
2172
	$log_entries = array();
2173
	while ($row = $smcFunc['db_fetch_assoc']($request))
2174
	{
2175
		$row['ip'] = $row['ip'] === null ? $dash : inet_dtop($row['ip']);
2176
		$log_entries[] = $row;
2177
	}
2178
	$smcFunc['db_free_result']($request);
2179
2180
	return $log_entries;
2181
}
2182
2183
/**
2184
 * This returns the total count of ban log entries. Callback for $listOptions['get_count'] in BanLog().
2185
 *
2186
 * @return int The total number of ban log entries.
2187
 */
2188
function list_getNumBanLogEntries()
2189
{
2190
	global $smcFunc;
2191
2192
	$request = $smcFunc['db_query']('', '
2193
		SELECT COUNT(*)
2194
		FROM {db_prefix}log_banned AS lb',
2195
		array(
2196
		)
2197
	);
2198
	list ($num_entries) = $smcFunc['db_fetch_row']($request);
2199
	$smcFunc['db_free_result']($request);
2200
2201
	return $num_entries;
2202
}
2203
2204
/**
2205
 * Convert a range of given IP number into a single string.
2206
 * It's practically the reverse function of ip2range().
2207
 *
2208
 * @example
2209
 * range2ip(array(10, 10, 10, 0), array(10, 10, 20, 255)) returns '10.10.10-20.*
2210
 *
2211
 * @param array $low The low end of the range in IPv4 format
2212
 * @param array $high The high end of the range in IPv4 format
2213
 * @return string A string indicating the range
2214
 */
2215
function range2ip($low, $high)
2216
{
2217
	$low = inet_dtop($low);
2218
	$high = inet_dtop($high);
2219
2220
	if ($low == '255.255.255.255') return 'unknown';
2221
	if ($low == $high)
2222
		return $low;
2223
	else
2224
		return $low . '-' . $high;
2225
}
2226
2227
/**
2228
 * Checks whether a given IP range already exists in the trigger list.
2229
 * If yes, it returns an error message. Otherwise, it returns an array
2230
 *  optimized for the database.
2231
 *
2232
 * @param array $ip_array An array of IP trigger data
2233
 * @param string $fullip The full IP
2234
 * @return boolean|array False if the trigger array is invalid or the passed array if the value doesn't exist in the database
2235
 */
2236
function checkExistingTriggerIP($ip_array, $fullip = '')
2237
{
2238
	global $smcFunc, $scripturl;
2239
2240
	$values = array(
2241
		'ip_low' => $ip_array['low'],
2242
		'ip_high' => $ip_array['high']
2243
	);
2244
2245
	$request = $smcFunc['db_query']('', '
2246
		SELECT bg.id_ban_group, bg.name
2247
		FROM {db_prefix}ban_groups AS bg
2248
		INNER JOIN {db_prefix}ban_items AS bi ON
2249
			(bi.id_ban_group = bg.id_ban_group)
2250
			AND ip_low = {inet:ip_low} AND ip_high = {inet:ip_high}
2251
		LIMIT 1',
2252
		$values
2253
	);
2254
	if ($smcFunc['db_num_rows']($request) != 0)
2255
	{
2256
		list ($error_id_ban, $error_ban_name) = $smcFunc['db_fetch_row']($request);
2257
		fatal_lang_error('ban_trigger_already_exists', false, array(
2258
			$fullip,
2259
			'<a href="' . $scripturl . '?action=admin;area=ban;sa=edit;bg=' . $error_id_ban . '">' . $error_ban_name . '</a>',
2260
		));
2261
	}
2262
	$smcFunc['db_free_result']($request);
2263
2264
	return $values;
2265
}
2266
2267
/**
2268
 * As it says... this tries to review the list of banned members, to match new bans.
2269
 * Note: is_activated >= 10: a member is banned.
2270
 */
2271
function updateBanMembers()
2272
{
2273
	global $smcFunc;
2274
2275
	$updates = array();
2276
	$allMembers = array();
2277
	$newMembers = array();
2278
2279
	// Start by getting all active bans - it's quicker doing this in parts...
2280
	$request = $smcFunc['db_query']('', '
2281
		SELECT bi.id_member, bi.email_address
2282
		FROM {db_prefix}ban_items AS bi
2283
			INNER JOIN {db_prefix}ban_groups AS bg ON (bg.id_ban_group = bi.id_ban_group)
2284
		WHERE (bi.id_member > {int:no_member} OR bi.email_address != {string:blank_string})
2285
			AND bg.cannot_access = {int:cannot_access_on}
2286
			AND (bg.expire_time IS NULL OR bg.expire_time > {int:current_time})',
2287
		array(
2288
			'no_member' => 0,
2289
			'cannot_access_on' => 1,
2290
			'current_time' => time(),
2291
			'blank_string' => '',
2292
		)
2293
	);
2294
	$memberIDs = array();
2295
	$memberEmails = array();
2296
	$memberEmailWild = array();
2297
	while ($row = $smcFunc['db_fetch_assoc']($request))
2298
	{
2299
		if ($row['id_member'])
2300
			$memberIDs[$row['id_member']] = $row['id_member'];
2301
		if ($row['email_address'])
2302
		{
2303
			// Does it have a wildcard - if so we can't do a IN on it.
2304
			if (strpos($row['email_address'], '%') !== false)
2305
				$memberEmailWild[$row['email_address']] = $row['email_address'];
2306
			else
2307
				$memberEmails[$row['email_address']] = $row['email_address'];
2308
		}
2309
	}
2310
	$smcFunc['db_free_result']($request);
2311
2312
	// Build up the query.
2313
	$queryPart = array();
2314
	$queryValues = array();
2315
	if (!empty($memberIDs))
2316
	{
2317
		$queryPart[] = 'mem.id_member IN ({array_string:member_ids})';
2318
		$queryValues['member_ids'] = $memberIDs;
2319
	}
2320
	if (!empty($memberEmails))
2321
	{
2322
		$queryPart[] = 'mem.email_address IN ({array_string:member_emails})';
2323
		$queryValues['member_emails'] = $memberEmails;
2324
	}
2325
	$count = 0;
2326
	foreach ($memberEmailWild as $email)
2327
	{
2328
		$queryPart[] = 'mem.email_address LIKE {string:wild_' . $count . '}';
2329
		$queryValues['wild_' . $count++] = $email;
2330
	}
2331
2332
	// Find all banned members.
2333
	if (!empty($queryPart))
2334
	{
2335
		$request = $smcFunc['db_query']('', '
2336
			SELECT mem.id_member, mem.is_activated
2337
			FROM {db_prefix}members AS mem
2338
			WHERE ' . implode(' OR ', $queryPart),
2339
			$queryValues
2340
		);
2341
		while ($row = $smcFunc['db_fetch_assoc']($request))
2342
		{
2343
			if (!in_array($row['id_member'], $allMembers))
2344
			{
2345
				$allMembers[] = $row['id_member'];
2346
				// Do they need an update?
2347
				if ($row['is_activated'] < 10)
2348
				{
2349
					$updates[($row['is_activated'] + 10)][] = $row['id_member'];
2350
					$newMembers[] = $row['id_member'];
2351
				}
2352
			}
2353
		}
2354
		$smcFunc['db_free_result']($request);
2355
	}
2356
2357
	// We welcome our new members in the realm of the banned.
2358
	if (!empty($newMembers))
2359
		$smcFunc['db_query']('', '
2360
			DELETE FROM {db_prefix}log_online
2361
			WHERE id_member IN ({array_int:new_banned_members})',
2362
			array(
2363
				'new_banned_members' => $newMembers,
2364
			)
2365
		);
2366
2367
	// Find members that are wrongfully marked as banned.
2368
	$request = $smcFunc['db_query']('', '
2369
		SELECT mem.id_member, mem.is_activated - 10 AS new_value
2370
		FROM {db_prefix}members AS mem
2371
			LEFT JOIN {db_prefix}ban_items AS bi ON (bi.id_member = mem.id_member OR mem.email_address LIKE bi.email_address)
2372
			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}))
2373
		WHERE (bi.id_ban IS NULL OR bg.id_ban_group IS NULL)
2374
			AND mem.is_activated >= {int:ban_flag}',
2375
		array(
2376
			'cannot_access_activated' => 1,
2377
			'current_time' => time(),
2378
			'ban_flag' => 10,
2379
		)
2380
	);
2381
	while ($row = $smcFunc['db_fetch_assoc']($request))
2382
	{
2383
		// Don't do this twice!
2384
		if (!in_array($row['id_member'], $allMembers))
2385
		{
2386
			$updates[$row['new_value']][] = $row['id_member'];
2387
			$allMembers[] = $row['id_member'];
2388
		}
2389
	}
2390
	$smcFunc['db_free_result']($request);
2391
2392
	if (!empty($updates))
2393
		foreach ($updates as $newStatus => $members)
2394
			updateMemberData($members, array('is_activated' => $newStatus));
2395
2396
	// Update the latest member and our total members as banning may change them.
2397
	updateStats('member');
2398
}
2399
2400
/**
2401
 * Gets basic member data for the ban
2402
 *
2403
 * @param int $id The ID of the member to get data for
2404
 * @return array An aray containing the ID, name, main IP and email address of the specified user
2405
 */
2406
function getMemberData($id)
2407
{
2408
	global $smcFunc;
2409
2410
	$suggestions = array();
2411
	$request = $smcFunc['db_query']('', '
2412
		SELECT id_member, real_name, member_ip, email_address
2413
		FROM {db_prefix}members
2414
		WHERE id_member = {int:current_user}
2415
		LIMIT 1',
2416
		array(
2417
			'current_user' => $id,
2418
		)
2419
	);
2420
	if ($smcFunc['db_num_rows']($request) > 0)
2421
	{
2422
		list ($suggestions['member']['id'], $suggestions['member']['name'], $suggestions['main_ip'], $suggestions['email']) = $smcFunc['db_fetch_row']($request);
2423
		$suggestions['main_ip'] = inet_dtop($suggestions['main_ip']);
2424
	}
2425
	$smcFunc['db_free_result']($request);
2426
2427
	return $suggestions;
2428
}
2429
2430
?>