ManageBans::action_edit()   D
last analyzed

Complexity

Conditions 16
Paths 36

Size

Total Lines 211
Code Lines 120

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 272

Importance

Changes 0
Metric Value
cc 16
eloc 120
nc 36
nop 0
dl 0
loc 211
ccs 0
cts 172
cp 0
crap 272
rs 4.4532
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * Controls execution for admin actions in the bans area
5
 *
6
 * @package   ElkArte Forum
7
 * @copyright ElkArte Forum contributors
8
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause (see accompanying LICENSE.txt file)
9
 *
10
 * @version 2.0 Beta 1
11
 *
12
 */
13
14
namespace ElkArte\AdminController;
15
16
use ElkArte\AbstractController;
17
use ElkArte\Action;
18
use ElkArte\Errors\ErrorContext;
19
use ElkArte\Exceptions\Exception;
20
use ElkArte\Helper\Util;
21
use ElkArte\Languages\Txt;
22
23
/**
24
 * This class controls execution for admin actions in the bans area
25
 * of the admin panel.
26
 *
27
 * @package Bans
28
 */
29
class ManageBans extends AbstractController
30
{
31
	/**
32
	 * Ban center. The main entrance point for all ban center functions.
33
	 *
34
	 * What it does:
35
	 *
36
	 * - It is accessed by ?action=admin;area=ban.
37
	 * - It chooses a function based on the 'sa' parameter, like many others.
38
	 * - The default sub-action is action_list().
39
	 * - It requires the ban_members permission.
40
	 * - It initializes the admin tabs.
41
	 *
42
	 * @event integrate_manage_bans
43
	 * @uses ManageBans template.
44
	 */
45
	public function action_index()
46
	{
47
		global $context, $txt;
48
49
		theme()->getTemplates()->load('ManageBans');
50
		require_once(SUBSDIR . '/Bans.subs.php');
51
52
		$subActions = [
53
			'add' => [$this, 'action_edit', 'permission' => 'manage_bans'],
54
			'browse' => [$this, 'action_browse', 'permission' => 'manage_bans'],
55
			'edittrigger' => [$this, 'action_edittrigger', 'permission' => 'manage_bans'],
56
			'edit' => [$this, 'action_edit', 'permission' => 'manage_bans'],
57
			'list' => [$this, 'action_list', 'permission' => 'manage_bans'],
58
			'log' => [$this, 'action_log', 'permission' => 'manage_bans'],
59
		];
60
61
		// Start up the controller
62
		$action = new Action('manage_bans');
63
64
		// Default the sub-action to 'view banlist'.
65
		$subAction = $action->initialize($subActions, 'list');
66
67
		// Make the call to integrate-manage_bans
68
		call_integration_hook('integrate_manage_bans', [&$subActions]);
69
70
		// Prepare some items for the template
71
		$context['page_title'] = $txt['ban_title'];
72
		$context['sub_action'] = $subAction;
73
74
		// Tabs for browsing the different ban functions.
75
		$context[$context['admin_menu_name']]['object']->prepareTabData([
76
			'title' => 'ban_title',
77
			'help' => 'ban_members',
78
			'description' => 'ban_description',
79
			'tabs' => [
80
				'list' => [
81
					'description' => $txt['ban_description'],
82
					'href' => getUrl('admin', ['action' => 'admin', 'area' => 'ban', 'sa' => 'list']),
83
					'selected' => $subAction === 'list' || $subAction === 'edit' || $subAction === 'edittrigger',
84
				],
85
				'add' => [
86
					'description' => $txt['ban_description'],
87
					'href' => getUrl('admin', ['action' => 'admin', 'area' => 'ban', 'sa' => 'add']),
88
				],
89
				'browse' => [
90
					'description' => $txt['ban_trigger_browse_description'],
91
					'href' => getUrl('admin', ['action' => 'admin', 'area' => 'ban', 'sa' => 'browse']),
92
				],
93
				'log' => [
94
					'description' => $txt['ban_log_description'],
95
					'href' => getUrl('admin', ['action' => 'admin', 'area' => 'ban', 'sa' => 'log']),
96
					'is_last' => true,
97
				],
98
			],
99
		]);
100
101
		// Call the right function for this sub-action.
102
		$action->dispatch($subAction);
103
	}
104
105
	/**
106
	 * Shows a list of bans currently set.
107
	 *
108
	 * What it does:
109
	 *
110
	 * - It is accessed by ?action=admin;area=ban;sa=list.
111
	 * - It removes expired bans.
112
	 * - It allows sorting on different criteria.
113
	 * - It also handles removal of selected ban items.
114
	 *
115
	 * @uses the main ManageBans template.
116
	 */
117
	public function action_list(): void
118
	{
119
		global $txt, $context;
120
121
		require_once(SUBSDIR . '/Bans.subs.php');
122
123
		// User pressed the 'remove selection button'.
124
		if (!empty($this->_req->post->removeBans) && !empty($this->_req->post->remove) && is_array($this->_req->post->remove))
125
		{
126
			checkSession();
127
128
			// Make sure every entry is a proper integer.
129
			$to_remove = array_map('intval', $this->_req->post->remove);
130
131
			// Unban them all!
132
			removeBanGroups($to_remove);
133
			removeBanTriggers($to_remove);
134
135
			// No more caching this ban!
136
			updateSettings(['banLastUpdated' => time()]);
137
138
			// Some members might be unbanned now. Update the members table.
139
			updateBanMembers();
140
		}
141
142
		// Create a date string so we don't overload them with date info.
143
		if (preg_match('~%[AaBbCcDdeGghjmuYy](?:[^%]*%[AaBbCcDdeGghjmuYy])*~', $this->user->time_format, $matches) == 0 || empty($matches[0]))
0 ignored issues
show
Bug introduced by
It seems like $this->user->time_format can also be of type null; however, parameter $subject of preg_match() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

143
		if (preg_match('~%[AaBbCcDdeGghjmuYy](?:[^%]*%[AaBbCcDdeGghjmuYy])*~', /** @scrutinizer ignore-type */ $this->user->time_format, $matches) == 0 || empty($matches[0]))
Loading history...
Bug Best Practice introduced by
The property time_format does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
144
		{
145
			$context['ban_time_format'] = $this->user->time_format;
146
		}
147
		else
148
		{
149
			$context['ban_time_format'] = $matches[0];
150
		}
151
152
		// Let's build a nice "create list" to show them the bans
153
		$listOptions = [
154
			'id' => 'ban_list',
155
			'title' => $txt['ban_title'],
156
			'items_per_page' => 20,
157
			'base_href' => getUrl('admin', ['action' => 'admin', 'area' => 'ban', 'sa' => 'list']),
158
			'default_sort_col' => 'added',
159
			'default_sort_dir' => 'desc',
160
			'get_items' => [
161
				'function' => 'list_getBans',
162
			],
163
			'get_count' => [
164
				'function' => 'list_getNumBans',
165
			],
166
			'no_items_label' => $txt['ban_no_entries'],
167
			'columns' => [
168
				'name' => [
169
					'header' => [
170
						'value' => $txt['ban_name'],
171
					],
172
					'data' => [
173
						'db' => 'name',
174
					],
175
					'sort' => [
176
						'default' => 'bg.name',
177
						'reverse' => 'bg.name DESC',
178
					],
179
				],
180
				'notes' => [
181
					'header' => [
182
						'value' => $txt['ban_notes'],
183
					],
184
					'data' => [
185
						'db' => 'notes',
186
						'class' => 'smalltext',
187
					],
188
					'sort' => [
189
						'default' => 'LENGTH(bg.notes) > 0 DESC, bg.notes',
190
						'reverse' => 'LENGTH(bg.notes) > 0, bg.notes DESC',
191
					],
192
				],
193
				'reason' => [
194
					'header' => [
195
						'value' => $txt['ban_reason'],
196
					],
197
					'data' => [
198
						'db' => 'reason',
199
						'class' => 'smalltext',
200
					],
201
					'sort' => [
202
						'default' => 'LENGTH(bg.reason) > 0 DESC, bg.reason',
203
						'reverse' => 'LENGTH(bg.reason) > 0, bg.reason DESC',
204
					],
205
				],
206
				'added' => [
207
					'header' => [
208
						'value' => $txt['ban_added'],
209
					],
210
					'data' => [
211
						'function' => static function ($rowData) {
212
							global $context;
213
							return standardTime($rowData['ban_time'], empty($context['ban_time_format']) ? true : $context['ban_time_format']);
214
						},
215
					],
216
					'sort' => [
217
						'default' => 'bg.ban_time',
218
						'reverse' => 'bg.ban_time DESC',
219
					],
220
				],
221
				'expires' => [
222
					'header' => [
223
						'value' => $txt['ban_expires'],
224
					],
225
					'data' => [
226
						'function' => static function ($rowData) {
227
							global $txt;
228
							// This ban never expires...whahaha.
229
							if ($rowData['expire_time'] === null)
230
							{
231
								return $txt['never'];
232
							}
233
							// This ban has already expired.
234
							if ($rowData['expire_time'] < time())
235
							{
236
								return sprintf('<span class="error">%1$s</span>', $txt['ban_expired']);
237
							}
238
							// Still need to wait a few days for this ban to expire.
239
							else
240
							{
241
								return sprintf('%1$d&nbsp;%2$s', ceil(($rowData['expire_time'] - time()) / (60 * 60 * 24)), $txt['ban_days']);
242
							}
243
						},
244
					],
245
					'sort' => [
246
						'default' => 'COALESCE(bg.expire_time, 1=1) DESC, bg.expire_time DESC',
247
						'reverse' => 'COALESCE(bg.expire_time, 1=1), bg.expire_time',
248
					],
249
				],
250
				'num_triggers' => [
251
					'header' => [
252
						'value' => $txt['ban_triggers'],
253
						'class' => 'centertext',
254
					],
255
					'data' => [
256
						'db' => 'num_triggers',
257
						'class' => 'centertext'
258
					],
259
					'sort' => [
260
						'default' => 'num_triggers DESC',
261
						'reverse' => 'num_triggers',
262
					],
263
				],
264
				'actions' => [
265
					'header' => [
266
						'value' => $txt['ban_actions'],
267
					],
268
					'data' => [
269
						'sprintf' => [
270
							'format' => '<a href="' . getUrl('admin', ['action' => 'admin', 'area' => 'ban', 'sa' => 'edit', 'bg' => '%1$d']) . '">' . $txt['modify'] . '</a>',
271
							'params' => [
272
								'id_ban_group' => false,
273
							],
274
						],
275
					],
276
				],
277
				'check' => [
278
					'header' => [
279
						'value' => '<input type="checkbox" onclick="invertAll(this, this.form);" class="input_check" />',
280
					],
281
					'data' => [
282
						'sprintf' => [
283
							'format' => '<input type="checkbox" name="remove[]" value="%1$d" class="input_check" />',
284
							'params' => [
285
								'id_ban_group' => false,
286
							],
287
						],
288
					],
289
				],
290
			],
291
			'form' => [
292
				'href' => getUrl('admin', ['action' => 'admin', 'area' => 'ban', 'sa' => 'list']),
293
			],
294
			'additional_rows' => [
295
				[
296
					'class' => 'submitbutton flow_flex_additional_row',
297
					'position' => 'below_table_data',
298
					'value' => '<input type="submit" name="removeBans" value="' . $txt['ban_remove_selected'] . '" onclick="return confirm(\'' . $txt['ban_remove_selected_confirm'] . '\');" />',
299
				],
300
			],
301
		];
302
303
		createList($listOptions);
304
	}
305
306
	/**
307
	 * This function is behind the screen for adding new bans and modifying existing ones.
308
	 *
309
	 * Adding new bans:
310
	 *  - is accessed by ?action=admin;area=ban;sa=add.
311
	 *  - uses the ban_edit sub template of the ManageBans template.
312
	 *
313
	 * Modifying existing bans:
314
	 *  - Is accessed by ?action=admin;area=ban;sa=edit;bg=x
315
	 *  - Uses the ban_edit sub template of the ManageBans template.
316
	 *  - Shows a list of ban triggers for the specified ban.
317
	 *
318
	 * @event integrate_list_ban_items
319
	 */
320
	public function action_edit(): void
321
	{
322
		global $txt, $modSettings, $context;
323
324
		require_once(SUBSDIR . '/Bans.subs.php');
325
326
		$ban_errors = ErrorContext::context('ban', 1);
327
328
		// Saving a new or edited ban?
329
		if ((isset($this->_req->post->add_ban) || isset($this->_req->post->modify_ban) || isset($this->_req->post->remove_selection)) && !$ban_errors->hasErrors())
330
		{
331
			$this->action_edit2();
332
		}
333
334
		$ban_group_id = $context['ban']['id'] ?? $this->_req->getQuery('bg', 'intval', 0);
335
336
		// Template needs this to show errors using JavaScript
337
		Txt::load('Errors');
338
		createToken('admin-bet');
339
		$context['form_url'] = getUrl('admin', ['action' => 'admin', 'area' => 'ban', 'sa' => 'edit']);
340
341
		// Prepare any errors found in the template to show
342
		$context['ban_errors'] = [
343
			'errors' => $ban_errors->prepareErrors(),
344
			'type' => $ban_errors->getErrorType() == 0 ? 'minor' : 'serious',
345
			'title' => $txt['ban_errors_detected'],
346
		];
347
348
		if (!$ban_errors->hasErrors())
349
		{
350
			// If we're editing an existing ban, get it from the database.
351
			if (!empty($ban_group_id))
352
			{
353
				$context['ban_group_id'] = $ban_group_id;
354
355
				// Setup for a createlist
356
				$listOptions = [
357
					'id' => 'ban_items',
358
					'base_href' => getUrl('admin', ['action' => 'admin', 'area' => 'ban', 'sa' => 'edit', 'bg' => $ban_group_id]),
359
					'no_items_label' => $txt['ban_no_triggers'],
360
					'items_per_page' => $modSettings['defaultMaxMessages'],
361
					'get_items' => [
362
						'function' => 'list_getBanItems',
363
						'params' => [
364
							'ban_group_id' => $ban_group_id,
365
						],
366
					],
367
					'get_count' => [
368
						'function' => 'list_getNumBanItems',
369
						'params' => [
370
							'ban_group_id' => $ban_group_id,
371
						],
372
					],
373
					'columns' => [
374
						'type' => [
375
							'header' => [
376
								'value' => $txt['ban_banned_entity'],
377
								'style' => 'width: 60%;',
378
							],
379
							'data' => [
380
								'function' => static function ($ban_item) {
381
									global $txt;
382
383
									if (in_array($ban_item['type'], ['ip', 'hostname', 'email']))
384
									{
385
										return '<strong>' . $txt[$ban_item['type']] . ':</strong>&nbsp;' . $ban_item[$ban_item['type']];
386
									}
387
388
									if ($ban_item['type'] === 'user')
389
									{
390
										return '<strong>' . $txt['username'] . ':</strong>&nbsp;' . $ban_item['user']['link'];
391
									}
392
393
									return '<strong>' . $txt['unknown'] . ':</strong>&nbsp;' . $ban_item['no_bantype_selected'];
394
								},
395
							],
396
						],
397
						'hits' => [
398
							'header' => [
399
								'value' => $txt['ban_hits'],
400
								'style' => 'width: 15%;text-align: center',
401
							],
402
							'data' => [
403
								'db' => 'hits',
404
								'class' => 'centertext'
405
							],
406
						],
407
						'id' => [
408
							'header' => [
409
								'value' => $txt['ban_actions'],
410
								'style' => 'width: 15%;',
411
							],
412
							'data' => [
413
								'function' => static function ($ban_item) {
414
									global $txt, $context;
415
									return '<a href="' . getUrl('admin', ['action' => 'admin', 'area' => 'ban', 'sa' => 'edittrigger', 'bg' => $context['ban']['id'], 'bi' => $ban_item['id']]) . '">' . $txt['ban_edit_trigger'] . '</a>';
416
								},
417
							],
418
						],
419
						'checkboxes' => [
420
							'header' => [
421
								'value' => '<input type="checkbox" onclick="invertAll(this, this.form, \'ban_items\');" class="input_check" />',
422
								'style' => 'width: 5%;',
423
							],
424
							'data' => [
425
								'sprintf' => [
426
									'format' => '<input type="checkbox" name="ban_items[]" value="%1$d" class="input_check" />',
427
									'params' => [
428
										'id' => false,
429
									],
430
								],
431
							],
432
						],
433
					],
434
					'form' => [
435
						'href' => getUrl('admin', ['action' => 'admin', 'area' => 'ban', 'sa' => 'edit', 'bg' => $ban_group_id]),
436
					],
437
					'additional_rows' => [
438
						[
439
							'position' => 'below_table_data',
440
							'class' => 'submitbutton',
441
							'value' => '
442
								<input type="submit" name="remove_selection" value="' . $txt['ban_remove_selected_triggers'] . '" class="right_submit" />
443
								<a class="linkbutton floatright" href="' . getUrl('admin', ['action' => 'admin', 'area' => 'ban', 'sa' => 'edittrigger', 'bg' => $ban_group_id]) . '">' . $txt['ban_add_trigger'] . '</a>
444
								<input type="hidden" name="bg" value="' . $ban_group_id . '" />
445
								<input type="hidden" name="' . $context['session_var'] . '" value="' . $context['session_id'] . '" />
446
								<input type="hidden" name="' . $context['admin-bet_token_var'] . '" value="' . $context['admin-bet_token'] . '" />',
447
						],
448
					],
449
				];
450
				createList($listOptions);
451
			}
452
			// Not an existing one, then it's probably new.
453
			else
454
			{
455
				$context['ban'] = [
456
					'id' => 0,
457
					'name' => '',
458
					'expiration' => [
459
						'status' => 'never',
460
						'days' => 0
461
					],
462
					'reason' => '',
463
					'notes' => '',
464
					'ban_days' => 0,
465
					'cannot' => [
466
						'access' => true,
467
						'post' => false,
468
						'register' => false,
469
						'login' => false,
470
					],
471
					'is_new' => true,
472
				];
473
				$context['ban_suggestions'] = [
474
					'main_ip' => '',
475
					'hostname' => '',
476
					'email' => '',
477
					'member' => [
478
						'id' => 0,
479
					],
480
				];
481
482
				// Overwrite some of the default form values if a user ID was given.
483
				if ($this->_req->hasQuery('u'))
484
				{
485
					$uid = $this->_req->getQuery('u', 'intval', 0);
486
					if ($uid > 0)
487
					{
488
						$context['ban_suggestions'] = array_merge($context['ban_suggestions'], getMemberData($uid));
489
					}
490
491
					if (!empty($context['ban_suggestions']['member']['id']))
492
					{
493
						$context['ban_suggestions']['href'] = getUrl('profile', ['action' => 'profile', 'u' => $context['ban_suggestions']['member']['id'], 'name' => $context['ban_suggestions']['member']['name']]);
494
						$context['ban_suggestions']['member']['link'] = '<a href="' . $context['ban_suggestions']['href'] . '">' . $context['ban_suggestions']['member']['name'] . '</a>';
495
496
						// Default the ban name to the name of the banned member.
497
						$context['ban']['name'] = $context['ban_suggestions']['member']['name'];
498
499
						// @todo: there should be a better solution...
500
						// used to lock the "Ban on Username" input when banning from profile
501
						$context['ban']['from_user'] = true;
502
503
						// Would be nice if we could also ban the hostname.
504
						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']))
505
						{
506
							$context['ban_suggestions']['hostname'] = host_from_ip($context['ban_suggestions']['main_ip']);
507
						}
508
509
						$context['ban_suggestions']['other_ips'] = banLoadAdditionalIPs($context['ban_suggestions']['member']['id']);
510
					}
511
				}
512
				else
513
				{
514
					$context['use_autosuggest'] = true;
515
					loadJavascriptFile('suggest.js', ['defer' => true]);
516
				}
517
			}
518
		}
519
520
		// Set the right template
521
		$context['sub_template'] = 'ban_edit';
522
523
		// A couple of text strings we *may* need
524
		theme()->addJavascriptVar([
525
			'txt_ban_name_empty' => $txt['ban_name_empty'],
526
			'txt_ban_restriction_empty' => $txt['ban_restriction_empty']], true
527
		);
528
529
		// And a bit of JavaScript to enable/disable some fields
530
		theme()->addInlineJavascript('fUpdateStatus();', true);
531
	}
532
533
	/**
534
	 * This function handles submitted forms that add, modify, or remove ban triggers.
535
	 */
536
	public function action_edit2(): void
537
	{
538
		global $context;
539
540
		require_once(SUBSDIR . '/Bans.subs.php');
541
542
		// Check with security first
543
		checkSession();
544
		validateToken('admin-bet');
545
546
		$ban_errors = ErrorContext::context('ban', 1);
547
548
		// Adding or editing a ban group
549
		if (isset($this->_req->post->add_ban) || isset($this->_req->post->modify_ban))
550
		{
551
			$ban_info = [];
552
553
			// Let's collect all the information we need
554
			$ban_info['id'] = $this->_req->getQuery('bg', 'intval', 0);
555
			if (empty($ban_info['id']))
556
			{
557
				$ban_info['id'] = $this->_req->getPost('bg', 'intval', 0);
558
			}
559
560
			$ban_info['is_new'] = empty($ban_info['id']);
561
			$ban_info['expire_date'] = $this->_req->getPost('expire_date', 'intval', 0);
562
			$ban_info['expiration'] = [
563
				'status' => isset($this->_req->post->expiration) && in_array($this->_req->post->expiration, ['never', 'one_day', 'expired']) ? $this->_req->post->expiration : 'never',
564
				'days' => $ban_info['expire_date'],
565
			];
566
			$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);
567
			$ban_info['full_ban'] = empty($this->_req->post->full_ban) ? 0 : 1;
568
			$ban_info['reason'] = $this->_req->getPost('reason', 'Util::htmlspecialchars[ENT_QUOTES]', '');
569
			$ban_info['name'] = $this->_req->getPost('ban_name', 'Util::htmlspecialchars[ENT_QUOTES]', '');
570
			$ban_info['notes'] = $this->_req->getPost('notes', 'Util::htmlspecialchars[ENT_QUOTES]', '');
571
			$ban_info['notes'] = str_replace(["\r", "\n", '  '], ['', '<br />', '&nbsp; '], $ban_info['notes']);
572
			$ban_info['cannot']['access'] = empty($ban_info['full_ban']) ? 0 : 1;
573
			$ban_info['cannot']['post'] = !empty($ban_info['full_ban']) || empty($this->_req->post->cannot_post) ? 0 : 1;
574
			$ban_info['cannot']['register'] = !empty($ban_info['full_ban']) || empty($this->_req->post->cannot_register) ? 0 : 1;
575
			$ban_info['cannot']['login'] = !empty($ban_info['full_ban']) || empty($this->_req->post->cannot_login) ? 0 : 1;
576
577
			$ban_group_id = empty($ban_info['id']) ? insertBanGroup($ban_info) : updateBanGroup($ban_info);
578
579
			if ($ban_group_id !== false)
580
			{
581
				$ban_info['id'] = $ban_group_id;
582
				$ban_info['is_new'] = false;
583
			}
584
585
			$context['ban'] = $ban_info;
586
		}
587
588
		// Update the triggers associated with this ban
589
		if (isset($this->_req->post->ban_suggestions, $ban_info['id']))
590
		{
591
			$saved_triggers = saveTriggers((array) $this->_req->post, $ban_info['id'], $this->_req->getQuery('u', 'intval', 0), $this->_req->getQuery('bi', 'intval', 0));
592
			$context['ban_suggestions']['saved_triggers'] = $saved_triggers;
593
		}
594
595
		// Something went wrong somewhere, ban info or triggers, ... Oh well, let's go back.
596
		if ($ban_errors->hasErrors())
597
		{
598
			// Ensure ban_suggestions is always an array to avoid string offset errors below.
599
			if (!empty($saved_triggers) && is_array($saved_triggers))
600
			{
601
				$context['ban_suggestions'] = $saved_triggers;
602
			}
603
			else
604
			{
605
				$context['ban_suggestions'] = [
606
					'main_ip' => '',
607
					'hostname' => '',
608
					'email' => '',
609
					'member' => ['id' => 0],
610
				];
611
			}
612
			$context['ban']['from_user'] = true;
613
614
			// They may have entered a name not using the member select box
615
			if ($this->_req->hasQuery('u'))
616
			{
617
				$uid = $this->_req->getQuery('u', 'intval', 0);
618
				if ($uid > 0)
619
				{
620
					$context['ban_suggestions'] = array_merge($context['ban_suggestions'], getMemberData($uid));
621
				}
622
			}
623
			elseif ($this->_req->hasQuery('user'))
624
			{
625
				$context['ban']['from_user'] = false;
626
				$context['use_autosuggest'] = true;
627
				if (!isset($context['ban_suggestions']['member']) || !is_array($context['ban_suggestions']['member']))
628
				{
629
					$context['ban_suggestions']['member'] = ['id' => 0];
630
				}
631
				$context['ban_suggestions']['member']['name'] = $this->_req->getQuery('user', 'trim|strval', '');
632
			}
633
634
			// Not strictly necessary, but it's nice
635
			if (!empty($context['ban_suggestions']['member']['id']))
636
			{
637
				$context['ban_suggestions']['other_ips'] = banLoadAdditionalIPs($context['ban_suggestions']['member']['id']);
638
			}
639
640
			$this->action_edit();
641
			return;
642
		}
643
644
		if (isset($this->_req->post->ban_items))
645
		{
646
			$ban_group_id = $this->_req->getQuery('bg', 'intval', 0);
647
			$ban_items = array_map('intval', $this->_req->post->ban_items);
648
649
			removeBanTriggers($ban_items, $ban_group_id);
650
		}
651
652
		// Register the last modified date.
653
		updateSettings(['banLastUpdated' => time()]);
654
655
		// Update the member table to represent the new ban situation.
656
		updateBanMembers();
657
658
		// Go back to an appropriate spot
659
		redirectexit('action=admin;area=ban;sa=' . (isset($this->_req->post->add_ban) ? 'list' : 'edit;bg=' . ($ban_group_id ?? 0)));
660
	}
661
662
	/**
663
	 * This handles the listing of ban log entries and allows their deletion.
664
	 *
665
	 * What it does:
666
	 *
667
	 * - Shows a list of logged access attempts by banned users.
668
	 * - It is accessed by ?action=admin;area=ban;sa=log.
669
	 * - Allows sorting of several columns.
670
	 * - Handles deletion of (a selection of) log entries.
671
	 */
672
	public function action_log(): void
673
	{
674
		global $context, $txt;
675
676
		require_once(SUBSDIR . '/Bans.subs.php');
677
678
		// Delete one or more entries.
679
		if (!empty($this->_req->post->removeAll)
680
			|| (!empty($this->_req->post->removeSelected) && !empty($this->_req->post->remove)))
681
		{
682
			checkSession();
683
			validateToken('admin-bl');
684
685
			// 'Delete all entries' button was pressed.
686
			if (!empty($this->_req->post->removeAll))
687
			{
688
				removeBanLogs();
689
			}
690
			// 'Delete selection' button was pressed.
691
			else
692
			{
693
				$to_remove = array_map('intval', $this->_req->post->remove);
694
				removeBanLogs($to_remove);
695
			}
696
		}
697
698
		// Build a nice log list for viewing
699
		$listOptions = [
700
			'id' => 'ban_log',
701
			'title' => $txt['ban_log'],
702
			'items_per_page' => 30,
703
			'base_href' => $context['admin_area'] === 'ban' ? getUrl('admin', ['action' => 'admin', 'area' => 'ban', 'sa' => 'log']) : getUrl('admin', ['action' => 'admin', 'area' => 'logs', 'sa' => 'banlog']),
704
			'default_sort_col' => 'date',
705
			'get_items' => [
706
				'function' => 'list_getBanLogEntries',
707
			],
708
			'get_count' => [
709
				'function' => 'list_getNumBanLogEntries',
710
			],
711
			'no_items_label' => $txt['ban_log_no_entries'],
712
			'columns' => [
713
				'ip' => [
714
					'header' => [
715
						'value' => $txt['ban_log_ip'],
716
					],
717
					'data' => [
718
						'sprintf' => [
719
							'format' => '<a href="' . getUrl('admin', ['action' => 'trackip', 'searchip' => '%1$s']) . '">%1$s</a>',
720
							'params' => [
721
								'ip' => false,
722
							],
723
						],
724
					],
725
					'sort' => [
726
						'default' => 'lb.ip',
727
						'reverse' => 'lb.ip DESC',
728
					],
729
				],
730
				'email' => [
731
					'header' => [
732
						'value' => $txt['ban_log_email'],
733
					],
734
					'data' => [
735
						'db_htmlsafe' => 'email',
736
					],
737
					'sort' => [
738
						'default' => "lb.email = '', lb.email",
739
						'reverse' => "lb.email != '', lb.email DESC",
740
					],
741
				],
742
				'member' => [
743
					'header' => [
744
						'value' => $txt['ban_log_member'],
745
					],
746
					'data' => [
747
						'sprintf' => [
748
							'format' => '<a href="' . getUrl('profile', ['action' => 'profile', 'u' => '%1$d', 'name' => '%2$s']) . '">%2$s</a>',
749
							'params' => [
750
								'id_member' => false,
751
								'real_name' => false,
752
							],
753
						],
754
					],
755
					'sort' => [
756
						'default' => 'COALESCE(mem.real_name, 1=1), mem.real_name',
757
						'reverse' => 'COALESCE(mem.real_name, 1=1) DESC, mem.real_name DESC',
758
					],
759
				],
760
				'date' => [
761
					'header' => [
762
						'value' => $txt['ban_log_date'],
763
					],
764
					'data' => [
765
						'function' => static fn($rowData) => standardTime($rowData['log_time']),
766
					],
767
					'sort' => [
768
						'default' => 'lb.log_time DESC',
769
						'reverse' => 'lb.log_time',
770
					],
771
				],
772
				'check' => [
773
					'header' => [
774
						'value' => '<input type="checkbox" onclick="invertAll(this, this.form);" class="input_check" />',
775
						'class' => 'centertext',
776
					],
777
					'data' => [
778
						'sprintf' => [
779
							'format' => '<input type="checkbox" name="remove[]" value="%1$d" class="input_check" />',
780
							'params' => [
781
								'id_ban_log' => false,
782
							],
783
						],
784
						'class' => 'centertext',
785
					],
786
				],
787
			],
788
			'form' => [
789
				'href' => $context['admin_area'] === 'ban' ? getUrl('admin', ['action' => 'admin', 'area' => 'ban', 'sa' => 'log']) : getUrl('admin', ['action' => 'admin', 'area' => 'logs', 'sa' => 'banlog']),
790
				'include_start' => true,
791
				'include_sort' => true,
792
				'token' => 'admin-bl',
793
			],
794
			'additional_rows' => [
795
				[
796
					'class' => 'submitbutton',
797
					'position' => 'bottom_of_list',
798
					'value' => '
799
						<input type="submit" name="removeSelected" value="' . $txt['ban_log_remove_selected'] . '" onclick="return confirm(\'' . $txt['ban_log_remove_selected_confirm'] . '\');" />
800
						<input type="submit" name="removeAll" value="' . $txt['ban_log_remove_all'] . '" onclick="return confirm(\'' . $txt['ban_log_remove_all_confirm'] . '\');" class="right_submit" />',
801
				],
802
			],
803
		];
804
805
		createToken('admin-bl');
806
807
		// Build the list
808
		createList($listOptions);
809
810
		$context['page_title'] = $txt['ban_log'];
811
	}
812
813
	/**
814
	 * This function handles the ins and outs of the screen for adding new ban
815
	 * triggers or modifying existing ones.
816
	 *
817
	 * - Adding new ban triggers:
818
	 *   - is accessed by ?action=admin;area=ban;sa=edittrigger;bg=x
819
	 *   - uses the ban_edit_trigger sub template of ManageBans.
820
	 *
821
	 * - Editing existing ban triggers:
822
	 *   - is accessed by ?action=admin;area=ban;sa=edittrigger;bg=x;bi=y
823
	 *   - uses the ban_edit_trigger sub template of ManageBans.
824
	 *
825
	 * @uses sub template ban_edit_trigger
826
	 */
827
	public function action_edittrigger(): void
828
	{
829
		global $context;
830
831
		require_once(SUBSDIR . '/Bans.subs.php');
832
833
		$ban_group = $this->_req->get('bg', 'intval', 0);
834
		$ban_id = $this->_req->get('bi', 'intval', 0);
835
836
		if (empty($ban_group))
837
		{
838
			throw new Exception('ban_not_found', false);
839
		}
840
841
		// Adding a new trigger
842
		if (isset($this->_req->post->add_new_trigger) && !empty($this->_req->post->ban_suggestions))
843
		{
844
			saveTriggers((array) $this->_req->post, $ban_group, 0, $ban_id);
845
			redirectexit('action=admin;area=ban;sa=edit;bg=' . $ban_group);
846
		}
847
		// Edit an existing trigger with new / updated details
848
		elseif (isset($this->_req->post->edit_trigger) && !empty($this->_req->post->ban_suggestions))
849
		{
850
			// The first replaces the old one, the others are added new
851
			// (simplification, otherwise it would require another query and some work...)
852
			$dummy = (array) $this->_req->post;
853
			$dummy['ban_suggestions'] = (array) array_shift($this->_req->post->ban_suggestions);
854
			saveTriggers($dummy, $ban_group, 0, $ban_id);
855
			if ($this->_req->post->ban_suggestions !== [])
856
			{
857
				saveTriggers((array) $this->_req->post, $ban_group);
858
			}
859
860
			redirectexit('action=admin;area=ban;sa=edit;bg=' . $ban_group);
861
		}
862
		// Removing a ban trigger by clearing the checkbox
863
		elseif (isset($this->_req->post->edit_trigger))
864
		{
865
			removeBanTriggers($ban_id);
866
			redirectexit('action=admin;area=ban;sa=edit;bg=' . $ban_group);
867
		}
868
869
		// No id supplied; this must be a new trigger being added
870
		if (empty($ban_id))
871
		{
872
			$context['ban_trigger'] = [
873
				'id' => 0,
874
				'group' => $ban_group,
875
				'ip' => [
876
					'value' => '',
877
					'selected' => true,
878
				],
879
				'hostname' => [
880
					'selected' => false,
881
					'value' => '',
882
				],
883
				'email' => [
884
					'value' => '',
885
					'selected' => false,
886
				],
887
				'banneduser' => [
888
					'value' => '',
889
					'selected' => false,
890
				],
891
				'is_new' => true,
892
			];
893
		}
894
		// Otherwise it is an existing trigger they want to edit
895
		else
896
		{
897
			$ban_row = banDetails($ban_id, $ban_group);
898
			if (empty($ban_row))
899
			{
900
				throw new Exception('ban_not_found', false);
901
			}
902
903
			$row = $ban_row[$ban_id];
904
905
			// Load it up for the template
906
			$context['ban_trigger'] = [
907
				'id' => $row['id_ban'],
908
				'group' => $row['id_ban_group'],
909
				'ip' => [
910
					'value' => empty($row['ip_low1']) ? '' : range2ip([$row['ip_low1'], $row['ip_low2'], $row['ip_low3'], $row['ip_low4'], $row['ip_low5'], $row['ip_low6'], $row['ip_low7'], $row['ip_low8']], [$row['ip_high1'], $row['ip_high2'], $row['ip_high3'], $row['ip_high4'], $row['ip_high5'], $row['ip_high6'], $row['ip_high7'], $row['ip_high8']]),
911
					'selected' => !empty($row['ip_low1']),
912
				],
913
				'hostname' => [
914
					'value' => str_replace('%', '*', $row['hostname']),
915
					'selected' => !empty($row['hostname']),
916
				],
917
				'email' => [
918
					'value' => str_replace('%', '*', $row['email_address']),
919
					'selected' => !empty($row['email_address'])
920
				],
921
				'banneduser' => [
922
					'value' => $row['member_name'],
923
					'selected' => !empty($row['member_name'])
924
				],
925
				'is_new' => false,
926
			];
927
		}
928
929
		// The template uses the autosuggest functions
930
		loadJavascriptFile('suggest.js', ['defer' => true]);
931
932
		// Template we will use
933
		$context['sub_template'] = 'ban_edit_trigger';
934
		$context['form_url'] = getUrl('admin', ['action' => 'admin', 'area' => 'ban', 'sa' => 'edittrigger']);
935
936
		createToken('admin-bet');
937
	}
938
939
	/**
940
	 * This handles the screen for showing the banned entities
941
	 *
942
	 * What it does:
943
	 *
944
	 * - It is accessed by ?action=admin;area=ban;sa=browse
945
	 * - It uses sub-tabs for browsing by IP, hostname, email, or username.
946
	 *
947
	 * @uses ManageBans template, browse_triggers sub template.
948
	 */
949
	public function action_browse(): void
950
	{
951
		global $modSettings, $context, $txt;
952
953
		require_once(SUBSDIR . '/Bans.subs.php');
954
955
		if (!empty($this->_req->post->remove_triggers) && !empty($this->_req->post->remove) && is_array($this->_req->post->remove))
956
		{
957
			checkSession();
958
959
			// Make sure every entry is a proper integer.
960
			$to_remove = array_map('intval', $this->_req->post->remove);
961
962
			removeBanTriggers($to_remove);
963
964
			// Rehabilitate some members.
965
			if ($this->_req->query->entity === 'member')
966
			{
967
				updateBanMembers();
968
			}
969
970
			// Make sure the ban cache is refreshed.
971
			updateSettings(['banLastUpdated' => time()]);
972
		}
973
974
		$context['selected_entity'] = isset($this->_req->query->entity) && in_array($this->_req->query->entity, ['ip', 'hostname', 'email', 'member']) ? $this->_req->query->entity : 'ip';
975
976
		$listOptions = [
977
			'id' => 'ban_trigger_list',
978
			'title' => $txt['ban_trigger_browse'],
979
			'items_per_page' => $modSettings['defaultMaxMessages'],
980
			'base_href' => getUrl('admin', ['action' => 'admin', 'area' => 'ban', 'sa' => 'browse', 'entity' => $context['selected_entity']]),
981
			'default_sort_col' => 'banned_entity',
982
			'no_items_label' => $txt['ban_no_triggers'],
983
			'get_items' => [
984
				'function' => 'list_getBanTriggers',
985
				'params' => [
986
					$context['selected_entity'],
987
				],
988
			],
989
			'get_count' => [
990
				'function' => 'list_getNumBanTriggers',
991
				'params' => [
992
					$context['selected_entity'],
993
				],
994
			],
995
			'columns' => [
996
				'banned_entity' => [
997
					'header' => [
998
						'value' => $txt['ban_banned_entity'],
999
					],
1000
				],
1001
				'ban_name' => [
1002
					'header' => [
1003
						'value' => $txt['ban_name'],
1004
					],
1005
					'data' => [
1006
						'sprintf' => [
1007
							'format' => '<a href="' . getUrl('admin', ['action' => 'admin', 'area' => 'ban', 'sa' => 'edit', 'bg' => '%1$d']) . '">%2$s</a>',
1008
							'params' => [
1009
								'id_ban_group' => false,
1010
								'name' => false,
1011
							],
1012
						],
1013
					],
1014
					'sort' => [
1015
						'default' => 'bg.name',
1016
						'reverse' => 'bg.name DESC',
1017
					],
1018
				],
1019
				'hits' => [
1020
					'header' => [
1021
						'value' => $txt['ban_hits'],
1022
					],
1023
					'data' => [
1024
						'db' => 'hits',
1025
					],
1026
					'sort' => [
1027
						'default' => 'bi.hits DESC',
1028
						'reverse' => 'bi.hits',
1029
					],
1030
				],
1031
				'check' => [
1032
					'header' => [
1033
						'value' => '<input type="checkbox" onclick="invertAll(this, this.form);" class="input_check" />',
1034
						'class' => 'centertext',
1035
					],
1036
					'data' => [
1037
						'sprintf' => [
1038
							'format' => '<input type="checkbox" name="remove[]" value="%1$d" class="input_check" />',
1039
							'params' => [
1040
								'id_ban' => false,
1041
							],
1042
						],
1043
						'class' => 'centertext',
1044
					],
1045
				],
1046
			],
1047
			'form' => [
1048
				'href' => getUrl('admin', ['action' => 'admin', 'area' => 'ban', 'sa' => 'browse', 'entity' => $context['selected_entity']]),
1049
				'include_start' => true,
1050
				'include_sort' => true,
1051
			],
1052
			'additional_rows' => [
1053
				[
1054
					'class' => 'submitbutton flow_flex_additional_row',
1055
					'position' => 'below_table_data',
1056
					'value' => '<input type="submit" name="remove_triggers" value="' . $txt['ban_remove_selected_triggers'] . '" onclick="return confirm(\'' . $txt['ban_remove_selected_triggers_confirm'] . '\');" />',
1057
				],
1058
			],
1059
			'list_menu' => [
1060
				'show_on' => 'top',
1061
				'class' => 'flow_flex_right',
1062
				'links' => [
1063
					[
1064
						'href' => getUrl('admin', ['action' => 'admin', 'area' => 'ban', 'sa' => 'browse', 'entity' => 'ip']),
1065
						'is_selected' => $context['selected_entity'] === 'ip',
1066
						'label' => $txt['ip']
1067
					],
1068
					[
1069
						'href' => getUrl('admin', ['action' => 'admin', 'area' => 'ban', 'sa' => 'browse', 'entity' => 'hostname']),
1070
						'is_selected' => $context['selected_entity'] === 'hostname',
1071
						'label' => $txt['hostname']
1072
					],
1073
					[
1074
						'href' => getUrl('admin', ['action' => 'admin', 'area' => 'ban', 'sa' => 'browse', 'entity' => 'email']),
1075
						'is_selected' => $context['selected_entity'] === 'email',
1076
						'label' => $txt['email']
1077
					],
1078
					[
1079
						'href' => getUrl('admin', ['action' => 'admin', 'area' => 'ban', 'sa' => 'browse', 'entity' => 'member']),
1080
						'is_selected' => $context['selected_entity'] === 'member',
1081
						'label' => $txt['username']
1082
					]
1083
				],
1084
			],
1085
		];
1086
1087
		// Specific data for the first column depending on the selected entity.
1088
		if ($context['selected_entity'] === 'ip')
1089
		{
1090
			$listOptions['columns']['banned_entity']['data'] = [
1091
				'function' => static fn($rowData) => range2ip([
1092
					$rowData['ip_low1'],
1093
					$rowData['ip_low2'],
1094
					$rowData['ip_low3'],
1095
					$rowData['ip_low4'],
1096
					$rowData['ip_low5'],
1097
					$rowData['ip_low6'],
1098
					$rowData['ip_low7'],
1099
					$rowData['ip_low8']
1100
				], [
1101
					$rowData['ip_high1'],
1102
					$rowData['ip_high2'],
1103
					$rowData['ip_high3'],
1104
					$rowData['ip_high4'],
1105
					$rowData['ip_high5'],
1106
					$rowData['ip_high6'],
1107
					$rowData['ip_high7'],
1108
					$rowData['ip_high8']
1109
				]),
1110
			];
1111
			$listOptions['columns']['banned_entity']['sort'] = [
1112
				'default' => 'bi.ip_low1, bi.ip_high1, bi.ip_low2, bi.ip_high2, bi.ip_low3, bi.ip_high3, bi.ip_low4, bi.ip_high4, bi.ip_low5, bi.ip_high5, bi.ip_low6, bi.ip_high6, bi.ip_low7, bi.ip_high7, bi.ip_low8, bi.ip_high8',
1113
				'reverse' => 'bi.ip_low1 DESC, bi.ip_high1 DESC, bi.ip_low2 DESC, bi.ip_high2 DESC, bi.ip_low3 DESC, bi.ip_high3 DESC, bi.ip_low4 DESC, bi.ip_high4 DESC, bi.ip_low5 DESC, bi.ip_high5 DESC, bi.ip_low6 DESC, bi.ip_high6 DESC, bi.ip_low7 DESC, bi.ip_high7 DESC, bi.ip_low8 DESC, bi.ip_high8 DESC',
1114
			];
1115
		}
1116
		elseif ($context['selected_entity'] === 'hostname')
1117
		{
1118
			$listOptions['columns']['banned_entity']['data'] = [
1119
				'function' => static fn($rowData) => strtr(Util::htmlspecialchars($rowData['hostname']), ['%' => '*']),
1120
			];
1121
			$listOptions['columns']['banned_entity']['sort'] = [
1122
				'default' => 'bi.hostname',
1123
				'reverse' => 'bi.hostname DESC',
1124
			];
1125
		}
1126
		elseif ($context['selected_entity'] === 'email')
1127
		{
1128
			$listOptions['columns']['banned_entity']['data'] = [
1129
				'function' => static fn($rowData) => strtr(Util::htmlspecialchars($rowData['email_address']), ['%' => '*']),
1130
			];
1131
			$listOptions['columns']['banned_entity']['sort'] = [
1132
				'default' => 'bi.email_address',
1133
				'reverse' => 'bi.email_address DESC',
1134
			];
1135
		}
1136
		elseif ($context['selected_entity'] === 'member')
1137
		{
1138
			$listOptions['columns']['banned_entity']['data'] = [
1139
				'sprintf' => [
1140
					'format' => '<a href="' . getUrl('profile', ['action' => 'profile', 'u' => '%1$d']) . '">%2$s</a>',
1141
					'params' => [
1142
						'id_member' => false,
1143
						'real_name' => false,
1144
					],
1145
				],
1146
			];
1147
			$listOptions['columns']['banned_entity']['sort'] = [
1148
				'default' => 'mem.real_name',
1149
				'reverse' => 'mem.real_name DESC',
1150
			];
1151
		}
1152
1153
		// Create the list.
1154
		createList($listOptions);
1155
	}
1156
}
1157