Maintenance::action_convertmsgbody_display()   F
last analyzed

Complexity

Conditions 21
Paths 391

Size

Total Lines 121
Code Lines 65

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 462

Importance

Changes 0
Metric Value
cc 21
eloc 65
c 0
b 0
f 0
nc 391
nop 0
dl 0
loc 121
ccs 0
cts 77
cp 0
crap 462
rs 0.9458

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
 * Forum maintenance. Important stuff.
5
 *
6
 * @package   ElkArte Forum
7
 * @copyright ElkArte Forum contributors
8
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause (see accompanying LICENSE.txt file)
9
 *
10
 * This file contains code covered by:
11
 * copyright: 2011 Simple Machines (http://www.simplemachines.org)
12
 *
13
 * @version 2.0 Beta 1
14
 *
15
 */
16
17
namespace ElkArte\AdminController;
18
19
use ElkArte\AbstractController;
20
use ElkArte\Action;
21
use ElkArte\Cache\Cache;
22
use ElkArte\Debug;
23
use ElkArte\EventManager;
24
use ElkArte\Exceptions\Exception;
25
use ElkArte\Helper\DataValidator;
26
use ElkArte\Http\FtpConnection;
27
use ElkArte\Languages\Txt;
28
use ElkArte\User;
29
30
/**
31
 * Entry point class for all maintenance, routine, members, database,
32
 * attachments, topics, and hooks
33
 *
34
 * @package Maintenance
35
 */
36
class Maintenance extends AbstractController
37
{
38
	/** @var int Maximum topic counter */
39
	public int $max_topics;
40
41
	/** @var int How many actions to take for a maintenance action */
42
	public int $increment;
43
44
	/** @var int Total steps for a given maintenance action */
45
	public int $total_steps;
46
47
	/** @var int reStart pointer for paused maintenance actions */
48
	public int $start;
49
50
	/** @var int Loop counter for paused maintenance actions */
51
	public int $step;
52
53
	/**
54
	 * Main dispatcher, the maintenance access point.
55
	 *
56
	 * What it does:
57
	 *
58
	 * - This, as usual, checks permissions, loads language files,
59
	 * and forwards to the actual workers.
60
	 *
61
	 * @see AbstractController::action_index
62
	 */
63
	public function action_index()
64
	{
65
		global $txt, $context;
66
67
		// You absolutely must be an admin by here!
68
		isAllowedTo('admin_forum');
69
70
		// Need something to talk about?
71
		Txt::load('Maintenance');
72
		theme()->getTemplates()->load('Maintenance');
73
74
		// This uses admin tabs - as it should!
75
		// Create the tabs
76
		$context[$context['admin_menu_name']]['object']->prepareTabData([
77
				'title' => 'maintain_title',
78
				'description' => 'maintain_info',
79
				'class' => 'i-cog']
80
		);
81
82
		// So many things you can do - but frankly, I won't let you - just these!
83
		$subActions = [
84
			'routine' => [
85
				'controller' => $this,
86
				'function' => 'action_routine',
87
				'activities' => [
88
					'repair' => 'action_repair_display',
89
					'recount' => 'action_recount_display',
90
					'logs' => 'action_logs_display',
91
					'cleancache' => 'action_cleancache_display',
92
				],
93
			],
94
			'database' => [
95
				'controller' => $this,
96
				'function' => 'action_database',
97
				'activities' => [
98
					'optimize' => 'action_optimize_display',
99
					'backup' => 'action_backup_display',
100
					'convertmsgbody' => 'action_convertmsgbody_display',
101
				],
102
			],
103
			'members' => [
104
				'controller' => $this,
105
				'function' => 'action_members',
106
				'activities' => [
107
					'reattribute' => 'action_reattribute_display',
108
					'purgeinactive' => 'action_purgeinactive_display',
109
					'recountposts' => 'action_recountposts_display',
110
				],
111
			],
112
			'topics' => [
113
				'controller' => $this,
114
				'function' => 'action_topics',
115
				'activities' => [
116
					'massmove' => 'action_massmove_display',
117
					'pruneold' => 'action_pruneold_display',
118
				],
119
			],
120
			'hooks' => [
121
				'controller' => $this,
122
				'function' => 'action_hooks',
123
			],
124
			'attachments' => [
125
				'controller' => ManageAttachments::class,
126
				'function' => 'action_maintenance',
127
			],
128
		];
129
130
		// Set up the action handler
131
		$action = new Action('manage_maintenance');
132
133
		// Yep, sub-action time and call integrate_sa_manage_maintenance as well
134
		$subAction = $action->initialize($subActions, 'routine');
135
136
		// Doing something special, does it exist?
137
		$activity = $this->_req->getQuery('activity', 'trim|strval', '');
138
139
		// Set a few things.
140
		$context[$context['admin_menu_name']]['current_subsection'] = $subAction;
141
		$context['page_title'] = $txt['maintain_title'];
142
		$context['sub_action'] = $subAction;
143
144
		// Finally, fall through to what we are doing.
145
		$action->dispatch($subAction);
146
147
		// Any special activity defined, then go to it.
148
		if (isset($subActions[$subAction]['activities'][$activity]))
149
		{
150
			if (is_string($subActions[$subAction]['activities'][$activity]) && method_exists($this, $subActions[$subAction]['activities'][$activity]))
151
			{
152
				$this->{$subActions[$subAction]['activities'][$activity]}();
153
			}
154
			elseif (is_string($subActions[$subAction]['activities'][$activity]))
155
			{
156
				$subActions[$subAction]['activities'][$activity]();
157
			}
158
			elseif (is_array($subActions[$subAction]['activities'][$activity]))
159
			{
160
				$activity_obj = new $subActions[$subAction]['activities'][$activity]['class']();
161
				$activity_obj->{$subActions[$subAction]['activities'][$activity]['method']}();
162
			}
163
			else
164
			{
165
				$subActions[$subAction]['activities'][$activity]();
166
			}
167
		}
168
169
		// Create a maintenance token.  Kinda hard to do it any other way.
170
		createToken('admin-maint');
171
	}
172
173
	/**
174
	 * Supporting function for the routine maintenance area.
175
	 *
176
	 * @event integrate_routine_maintenance, passed $context['routine_actions'] array to allow
177
	 * addons to add more options
178
	 * @uses Template Maintenance, sub template maintain_routine
179
	 */
180
	public function action_routine(): void
181
	{
182
		global $context, $txt;
183
184
		if ($this->_req->compareQuery('done', 'recount', 'trim|strval'))
185
		{
186
			$context['maintenance_finished'] = $txt['maintain_recount'];
187
		}
188
189
		// set up the sub-template
190
		$context['sub_template'] = 'maintain_routine';
191
		$context['routine_actions'] = [
192
			'repair' => [
193
				'url' => getUrl('admin', ['action' => 'admin', 'area' => 'repairboards']),
194
				'title' => $txt['maintain_errors'],
195
				'description' => $txt['maintain_errors_info'],
196
				'submit' => $txt['maintain_run_now'],
197
				'hidden' => [
198
					'session_var' => 'session_id',
199
					'admin-maint_token_var' => 'admin-maint_token',
200
				]
201
			],
202
			'recount' => [
203
				'url' => getUrl('admin', ['action' => 'admin', 'area' => 'maintain', 'sa' => 'routine', 'activity' => 'recount']),
204
				'title' => $txt['maintain_recount'],
205
				'description' => $txt['maintain_recount_info'],
206
				'submit' => $txt['maintain_run_now'],
207
				'hidden' => [
208
					'session_var' => 'session_id',
209
					'admin-maint_token_var' => 'admin-maint_token',
210
				]
211
			],
212
			'logs' => [
213
				'url' => getUrl('admin', ['action' => 'admin', 'area' => 'maintain', 'sa' => 'routine', 'activity' => 'logs']),
214
				'title' => $txt['maintain_logs'],
215
				'description' => $txt['maintain_logs_info'],
216
				'submit' => $txt['maintain_run_now'],
217
				'hidden' => [
218
					'session_var' => 'session_id',
219
					'admin-maint_token_var' => 'admin-maint_token',
220
				]
221
			],
222
			'cleancache' => [
223
				'url' => getUrl('admin', ['action' => 'admin', 'area' => 'maintain', 'sa' => 'routine', 'activity' => 'cleancache']),
224
				'title' => $txt['maintain_cache'],
225
				'description' => $txt['maintain_cache_info'],
226
				'submit' => $txt['maintain_run_now'],
227
				'hidden' => [
228
					'session_var' => 'session_id',
229
					'admin-maint_token_var' => 'admin-maint_token',
230
				]
231
			],
232
		];
233
234
		call_integration_hook('integrate_routine_maintenance', [&$context['routine_actions']]);
235
	}
236
237
	/**
238
	 * Supporting function for the members maintenance area.
239
	 */
240
	public function action_members(): void
241
	{
242
		global $context, $txt;
243
244
		require_once(SUBSDIR . '/Membergroups.subs.php');
245
246
		// Get all membergroups - for deleting members and the like.
247
		$context['membergroups'] = getBasicMembergroupData(['all']);
248
249
		// Show that we completed this action
250
		if ($this->_req->compareQuery('done', 'recountposts', 'trim|strval'))
251
		{
252
			$context['maintenance_finished'] = [
253
				'errors' => [sprintf($txt['maintain_done'], $txt['maintain_recountposts'])],
254
			];
255
		}
256
257
		loadJavascriptFile('suggest.js', ['defer' => true]);
258
259
		// Set up the sub-template
260
		$context['sub_template'] = 'maintain_members';
261
	}
262
263
	/**
264
	 * Supporting function for the topics maintenance area.
265
	 *
266
	 * @event integrate_topics_maintenance, passed $context['topics_actions'] to allow addons
267
	 * to add additonal topic maintance functions
268
	 * @uses GenericBoards template, sub template maintain_topics
269
	 */
270
	public function action_topics(): void
271
	{
272
		global $context, $txt;
273
274
		require_once(SUBSDIR . '/Boards.subs.php');
275
276
		// Let's load up the boards in case they are useful.
277
		$context += getBoardList(['not_redirection' => true]);
278
279
		// Include a list of boards per category for easy toggling.
280
		foreach ($context['categories'] as $cat => &$category)
281
		{
282
			$context['boards_in_category'][$cat] = count($category['boards']);
283
			$category['child_ids'] = array_keys($category['boards']);
284
		}
285
286
		// @todo Hacky!
287
		$txt['choose_board'] = $txt['maintain_old_all'];
288
		$context['boards_check_all'] = true;
289
		theme()->getTemplates()->load('GenericBoards');
290
291
		$context['topics_actions'] = [
292
			'pruneold' => [
293
				'url' => getUrl('admin', ['action' => 'admin', 'area' => 'maintain', 'sa' => 'topics', 'activity' => 'pruneold']),
294
				'title' => $txt['maintain_old'],
295
				'submit' => $txt['maintain_old_remove'],
296
				'confirm' => $txt['maintain_old_confirm'],
297
				'hidden' => [
298
					'session_var' => 'session_id',
299
					'admin-maint_token_var' => 'admin-maint_token',
300
				]
301
			],
302
			'massmove' => [
303
				'url' => getUrl('admin', ['action' => 'admin', 'area' => 'maintain', 'sa' => 'topics', 'activity' => 'massmove']),
304
				'title' => $txt['move_topics_maintenance'],
305
				'submit' => $txt['move_topics_now'],
306
				'confirm' => $txt['move_topics_confirm'],
307
				'hidden' => [
308
					'session_var' => 'session_id',
309
					'admin-maint_token_var' => 'admin-maint_token',
310
				]
311
			],
312
		];
313
314
		call_integration_hook('integrate_topics_maintenance', [&$context['topics_actions']]);
315
316
		if ($this->_req->compareQuery('done', 'purgeold', 'trim|strval'))
317
		{
318
			$context['maintenance_finished'] = [
319
				'errors' => [sprintf($txt['maintain_done'], $txt['maintain_old'])],
320
			];
321
		}
322
		elseif ($this->_req->compareQuery('done', 'massmove', 'trim|strval'))
323
		{
324
			$context['maintenance_finished'] = [
325
				'errors' => [sprintf($txt['maintain_done'], $txt['move_topics_maintenance'])],
326
			];
327
		}
328
329
		// Set up the sub-template
330
		$context['sub_template'] = 'maintain_topics';
331
	}
332
333
	/**
334
	 * Find and try to fix all errors on the forum.
335
	 *
336
	 * - Forwards to repair boards controller.
337
	 */
338
	public function action_repair_display(): void
339
	{
340
		// Honestly, this should be done in the sub function.
341
		validateToken('admin-maint');
342
343
		$controller = new RepairBoards(new EventManager());
344
		$controller->setUser(User::$info);
345
		$controller->pre_dispatch();
346
		$controller->action_repairboards();
347
	}
348
349
	/**
350
	 * Wipes the current cache entries as best it can.
351
	 *
352
	 * - This only applies to our own cache entries, opcache, and data.
353
	 * - This action, like other maintenance tasks, may be called automatically
354
	 * by the task scheduler or manually by the admin in Maintenance area.
355
	 */
356
	public function action_cleancache_display(): void
357
	{
358
		global $context, $txt;
359
360
		checkSession();
361
		validateToken('admin-maint');
362
363
		// Just wipe the whole cache directory!
364
		Cache::instance()->clean();
365
366
		// Change the PWA stale so it will refresh (if enabled)
367
		setPWACacheStale(true);
368
369
		$context['maintenance_finished'] = $txt['maintain_cache'];
370
	}
371
372
	/**
373
	 * Empties all unimportant logs.
374
	 *
375
	 * - This action may be called periodically, by the tasks scheduler,
376
	 * or manually by the admin in Maintenance area.
377
	 */
378
	public function action_logs_display(): void
379
	{
380
		global $context, $txt;
381
382
		require_once(SUBSDIR . '/Maintenance.subs.php');
383
384
		checkSession();
385
		validateToken('admin-maint');
386
387
		// Maintenance time was scheduled!
388
		// When there is no intelligent life on this planet.
389
		// Apart from me, I mean.
390
		flushLogTables();
391
392
		updateSettings(['search_pointer' => 0]);
393
394
		$context['maintenance_finished'] = $txt['maintain_logs'];
395
	}
396
397
	/**
398
	 * Convert the column "body" of the table {db_prefix}messages from TEXT to
399
	 * MEDIUMTEXT and vice versa.
400
	 *
401
	 * What it does:
402
	 *
403
	 * - It requires the admin_forum permission.
404
	 * - This is needed only for MySQL.
405
	 * - During the conversion from MEDIUMTEXT to TEXT it check if any of the
406
	 * posts exceed the TEXT length and if so it aborts.
407
	 * - This action is linked from the maintenance screen (if it's applicable).
408
	 * - Accessed by ?action=admin;area=maintain;sa=database;activity=convertmsgbody.
409
	 *
410
	 * @uses the convert_msgbody sub template of the Admin template.
411
	 */
412
	public function action_convertmsgbody_display(): void
413
	{
414
		global $context, $txt, $modSettings, $time_start;
415
416
		// Show me your badge!
417
		isAllowedTo('admin_forum');
418
		$db = database();
419
420
		if ($db->supportMediumtext() === false)
0 ignored issues
show
Bug introduced by
The method supportMediumtext() does not exist on ElkArte\Database\QueryInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to ElkArte\Database\QueryInterface. ( Ignorable by Annotation )

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

420
		if ($db->/** @scrutinizer ignore-call */ supportMediumtext() === false)
Loading history...
421
		{
422
			return;
423
		}
424
425
		$body_type = '';
426
427
		// Find the body column "type" from the message table
428
		$colData = getMessageTableColumns();
429
		foreach ($colData as $column)
430
		{
431
			if ($column['name'] === 'body')
432
			{
433
				$body_type = $column['type'];
434
				break;
435
			}
436
		}
437
438
		$context['convert_to'] = $body_type === 'text' ? 'mediumtext' : 'text';
439
		if ($body_type === 'text' || isset($this->_req->post->do_conversion))
440
		{
441
			checkSession();
442
			validateToken('admin-maint');
443
444
			// Make it longer so we can do their limit.
445
			if ($body_type === 'text')
446
			{
447
				resizeMessageTableBody('mediumtext');
448
			}
449
			// Shorten the column so we can have (literally per record) less space occupied.
450
			else
451
			{
452
				resizeMessageTableBody('text');
453
			}
454
455
			$colData = getMessageTableColumns();
456
			foreach ($colData as $column)
457
			{
458
				if ($column['name'] === 'body')
459
				{
460
					$body_type = $column['type'];
461
				}
462
			}
463
464
			$context['maintenance_finished'] = $txt[$context['convert_to'] . '_title'];
465
			$context['convert_to'] = $body_type === 'text' ? 'mediumtext' : 'text';
466
			$context['convert_to_suggest'] = ($body_type !== 'text' && !empty($modSettings['max_messageLength']) && $modSettings['max_messageLength'] < 65536);
467
468
			return;
469
		}
470
471
		if (!isset($this->_req->post->do_conversion) || isset($this->_req->post->cont))
472
		{
473
			checkSession();
474
			if (!$this->_req->hasQuery('start'))
475
			{
476
				validateToken('admin-maint');
477
			}
478
			else
479
			{
480
				validateToken('admin-convertMsg');
481
			}
482
483
			$context['page_title'] = $txt['not_done_title'];
484
			$context['continue_post_data'] = '';
485
			$context['continue_countdown'] = 3;
486
			$context['sub_template'] = 'not_done';
487
488
			$increment = 500;
489
			$id_msg_exceeding = isset($this->_req->post->id_msg_exceeding) ? explode(',', $this->_req->post->id_msg_exceeding) : [];
490
			$max_msgs = countMessages();
491
			$start = $this->_req->getQuery('start', 'intval', 0);
492
493
			// Try for as much time as possible.
494
			detectServer()->setTimeLimit(600);
495
			while ($start < $max_msgs)
496
			{
497
				$id_msg_exceeding = detectExceedingMessages($start, $increment);
498
499
				$start += $increment;
500
501
				if (microtime(true) - $time_start > 3)
502
				{
503
					createToken('admin-convertMsg');
504
					$context['continue_post_data'] = '
505
						<input type="hidden" name="' . $context['admin-convertMsg_token_var'] . '" value="' . $context['admin-convertMsg_token'] . '" />
506
						<input type="hidden" name="' . $context['session_var'] . '" value="' . $context['session_id'] . '" />
507
						<input type="hidden" name="id_msg_exceeding" value="' . implode(',', $id_msg_exceeding) . '" />';
508
					$context['continue_get_data'] = '?action=admin;area=maintain;sa=database;activity=convertmsgbody;start=' . $start;
509
					$context['continue_percent'] = round(100 * $start / $max_msgs);
510
					$context['not_done_title'] = $txt['not_done_title'] . ' (' . $context['continue_percent'] . '%)';
511
512
					return;
513
				}
514
			}
515
516
			createToken('admin-maint');
517
			$context['page_title'] = $txt[$context['convert_to'] . '_title'];
518
			$context['sub_template'] = 'convert_msgbody';
519
520
			if (!empty($id_msg_exceeding))
521
			{
522
				if (count($id_msg_exceeding) > 100)
523
				{
524
					$query_msg = array_slice($id_msg_exceeding, 0, 100);
525
					$context['exceeding_messages_morethan'] = sprintf($txt['exceeding_messages_morethan'], count($id_msg_exceeding));
526
				}
527
				else
528
				{
529
					$query_msg = $id_msg_exceeding;
530
				}
531
532
				$context['exceeding_messages'] = getExceedingMessages($query_msg);
533
			}
534
		}
535
	}
536
537
	/**
538
	 * Optimizes all tables in the database and lists how much was saved.
539
	 *
540
	 * What it does:
541
	 *
542
	 * - It requires the admin_forum permission.
543
	 * - It shows as the maintain_forum admin area.
544
	 * - It is accessed from ?action=admin;area=maintain;sa=database;activity=optimize.
545
	 * - It also updates the optimize scheduled task such that the tables are not automatically optimized again too soon.
546
	 */
547
	public function action_optimize_display(): void
548
	{
549
		global $txt, $context;
550
551
		isAllowedTo('admin_forum');
552
553
		// Some validation
554
		checkSession();
555
		validateToken('admin-maint');
556
557
		ignore_user_abort(true);
558
559
		require_once(SUBSDIR . '/Maintenance.subs.php');
560
561
		$context['page_title'] = $txt['database_optimize'];
562
		$context['sub_template'] = 'optimize';
563
564
		$tables = getElkTables();
565
566
		// If there aren't any tables, then I believe that would mean the world has exploded...
567
		$context['num_tables'] = count($tables);
568
		if ($context['num_tables'] === 0)
569
		{
570
			throw new Exception('You appear to be running ElkArte in a flat file mode... fantastic!', false);
571
		}
572
573
		// For each table....
574
		$context['optimized_tables'] = [];
575
		$db_table = db_table();
576
577
		foreach ($tables as $table)
578
		{
579
			// Optimize the table!  We use backticks here because it might be a custom table.
580
			$data_freed = $db_table->optimize($table['table_name']);
581
582
			if ($data_freed > 0)
583
			{
584
				$context['optimized_tables'][] = [
585
					'name' => $table['table_name'],
586
					'data_freed' => $data_freed,
587
				];
588
			}
589
		}
590
591
		// Number of tables, etc....
592
		$txt['database_numb_tables'] = sprintf($txt['database_numb_tables'], $context['num_tables']);
593
		$context['num_tables_optimized'] = count($context['optimized_tables']);
594
595
		// Check that we don't auto-optimize again too soon!
596
		require_once(SUBSDIR . '/ScheduledTasks.subs.php');
597
		calculateNextTrigger('auto_optimize', true);
598
	}
599
600
	/**
601
	 * Recount many forum totals that can be recounted automatically without harm.
602
	 *
603
	 * What it does:
604
	 *
605
	 * - It requires the admin_forum permission.
606
	 * - It shows the maintain_forum admin area.
607
	 * - The function redirects back to ?action=admin;area=maintain when complete.
608
	 * - It is accessed via ?action=admin;area=maintain;sa=database;activity=recount.
609
	 *
610
	 * Totals recounted:
611
	 * - fixes for topics with wrong num_replies.
612
	 * - updates for num_posts and num_topics of all boards.
613
	 * - recounts personal_messages but not unread_messages.
614
	 * - repairs messages pointing to boards with topics pointing to other boards.
615
	 * - updates the last message posted in boards and children.
616
	 * - updates member count, latest member, topic count, and message count.
617
	 */
618
	public function action_recount_display(): void
619
	{
620
		global $txt, $context, $modSettings, $time_start;
621
622
		isAllowedTo('admin_forum');
623
		checkSession();
624
625
		// Functions
626
		require_once(SUBSDIR . '/Maintenance.subs.php');
627
		require_once(SUBSDIR . '/Topic.subs.php');
628
629
		// Validate the request or the loop
630
		if (!$this->_req->hasQuery('step'))
631
		{
632
			validateToken('admin-maint');
633
		}
634
		else
635
		{
636
			validateToken('admin-boardrecount');
637
		}
638
639
		// For the loop template
640
		$context['page_title'] = $txt['not_done_title'];
641
		$context['continue_post_data'] = '';
642
		$context['continue_countdown'] = 3;
643
		$context['sub_template'] = 'not_done';
644
645
		// Try for as much time as possible.
646
		detectServer()->setTimeLimit(600);
647
648
		// Step the number of topics at a time so things don't time out...
649
		$this->max_topics = getMaxTopicID();
650
		$this->increment = (int) min(max(50, ceil($this->max_topics / 4)), 2000);
651
652
		// An 8-step process should be 12 for the admin
653
		$this->total_steps = 8;
654
		$this->start = $this->_req->getQuery('start', 'inval', 0);
655
		$this->step = $this->_req->getQuery('step', 'intval', 0);
656
657
		// Get each topic with a wrong reply count and fix it
658
		if (empty($this->step))
659
		{
660
			// let's just do some at a time, though.
661
			while ($this->start < $this->max_topics)
662
			{
663
				recountApprovedMessages($this->start, $this->increment);
664
				recountUnapprovedMessages($this->start, $this->increment);
665
				$this->start += $this->increment;
666
667
				if (microtime(true) - $time_start > 3)
668
				{
669
					$percent = round((100 * $this->start / $this->max_topics) / $this->total_steps);
670
					$this->_buildContinue($percent, 0);
671
672
					return;
673
				}
674
			}
675
676
			// Done with step 0, reset start for the next one
677
			$this->start = 0;
678
		}
679
680
		// Update the post-count of each board.
681
		if ($this->step <= 1)
682
		{
683
			if (empty($this->start))
684
			{
685
				resetBoardsCounter('num_posts');
686
			}
687
688
			while ($this->start < $this->max_topics)
689
			{
690
				// Recount the posts
691
				updateBoardsCounter('posts', $this->start, $this->increment);
692
				$this->start += $this->increment;
693
694
				if (microtime(true) - $time_start > 3)
695
				{
696
					$percent = round((200 + 100 * $this->start / $this->max_topics) / $this->total_steps);
697
					$this->_buildContinue($percent, 1);
698
699
					return;
700
				}
701
			}
702
703
			// Done with step 1, reset start for the next one
704
			$this->start = 0;
705
		}
706
707
		// Update the topic count of each board.
708
		if ($this->step <= 2)
709
		{
710
			if (empty($this->start))
711
			{
712
				resetBoardsCounter('num_topics');
713
			}
714
715
			while ($this->start < $this->max_topics)
716
			{
717
				updateBoardsCounter('topics', $this->start, $this->increment);
718
				$this->start += $this->increment;
719
720
				if (microtime(true) - $time_start > 3)
721
				{
722
					$percent = round((300 + 100 * $this->start / $this->max_topics) / $this->total_steps);
723
					$this->_buildContinue($percent, 2);
724
725
					return;
726
				}
727
			}
728
729
			// Done with step 2, reset start for the next one
730
			$this->start = 0;
731
		}
732
733
		// Update the unapproved post-count of each board.
734
		if ($this->step <= 3)
735
		{
736
			if (empty($this->start))
737
			{
738
				resetBoardsCounter('unapproved_posts');
739
			}
740
741
			while ($this->start < $this->max_topics)
742
			{
743
				updateBoardsCounter('unapproved_posts', $this->start, $this->increment);
744
				$this->start += $this->increment;
745
746
				if (microtime(true) - $time_start > 3)
747
				{
748
					$percent = round((400 + 100 * $this->start / $this->max_topics) / $this->total_steps);
749
					$this->_buildContinue($percent, 3);
750
751
					return;
752
				}
753
			}
754
755
			// Done with step 3, reset start for the next one
756
			$this->start = 0;
757
		}
758
759
		// Update the unapproved topic count of each board.
760
		if ($this->step <= 4)
761
		{
762
			if (empty($this->start))
763
			{
764
				resetBoardsCounter('unapproved_topics');
765
			}
766
767
			while ($this->start < $this->max_topics)
768
			{
769
				updateBoardsCounter('unapproved_topics', $this->start, $this->increment);
770
				$this->start += $this->increment;
771
772
				if (microtime(true) - $time_start > 3)
773
				{
774
					$percent = round((500 + 100 * $this->start / $this->max_topics) / $this->total_steps);
775
					$this->_buildContinue($percent, 4);
776
777
					return;
778
				}
779
			}
780
781
			// Done with step 4, reset start for the next one
782
			$this->start = 0;
783
		}
784
785
		// Get all members with the wrong number of personal messages.
786
		if ($this->step <= 5)
787
		{
788
			updatePersonalMessagesCounter();
789
790
			// Done with step 5, reset start for the next one
791
			$this->start = 0;
792
			if (microtime(true) - $time_start > 3)
793
			{
794
				$percent = round(700 / $this->total_steps);
795
				$this->_buildContinue($percent, 6);
796
797
				return;
798
			}
799
		}
800
801
		// Any messages pointing to the wrong board?
802
		if ($this->step <= 6)
803
		{
804
			while ($this->start < $modSettings['maxMsgID'])
805
			{
806
				// Use the controller's start pointer, not the raw request
807
				updateMessagesBoardID($this->start, $this->increment);
808
				$this->start += $this->increment;
809
810
				if (microtime(true) - $time_start > 3)
811
				{
812
					$percent = round((700 + 100 * $this->start / $modSettings['maxMsgID']) / $this->total_steps);
813
					$this->_buildContinue($percent, 6);
814
815
					return;
816
				}
817
			}
818
819
			// Done with step 6, reset start for the next one
820
			$this->start = 0;
821
		}
822
823
		updateBoardsLastMessage();
824
825
		// Update all the basic statistics.
826
		require_once(SUBSDIR . '/Members.subs.php');
827
		updateMemberStats();
828
		require_once(SUBSDIR . '/Messages.subs.php');
829
		updateMessageStats();
830
		require_once(SUBSDIR . '/Topic.subs.php');
831
		updateTopicStats();
832
833
		// Finally, update the latest event times.
834
		require_once(SUBSDIR . '/ScheduledTasks.subs.php');
835
		calculateNextTrigger();
836
837
		// Ta-da
838
		redirectexit('action=admin;area=maintain;sa=routine;done=recount');
839
	}
840
841
	/**
842
	 * Helper function for the recount process, build the continued values for
843
	 * the template
844
	 *
845
	 * @param int $percent percent done
846
	 * @param int $step step we are on
847
	 */
848
	private function _buildContinue(int $percent, int $step): void
849
	{
850
		global $context, $txt;
851
852
		createToken('admin-boardrecount');
853
854
		$context['continue_post_data'] = '
855
			<input type="hidden" name="' . $context['admin-boardrecount_token_var'] . '" value="' . $context['admin-boardrecount_token'] . '" />
856
			<input type="hidden" name="' . $context['session_var'] . '" value="' . $context['session_id'] . '" />';
857
		$context['continue_get_data'] = '?action=admin;area=maintain;sa=routine;activity=recount;step=' . $step . ';start=' . $this->start;
858
		$context['continue_percent'] = $percent;
859
		$context['not_done_title'] = $txt['not_done_title'] . ' (' . $context['continue_percent'] . '%)';
860
	}
861
862
	/**
863
	 * Re-attribute posts to the user sent from the maintenance page.
864
	 */
865
	public function action_reattribute_display(): void
866
	{
867
		global $context, $txt;
868
869
		checkSession();
870
871
		$validator = new DataValidator();
872
		$validator->sanitation_rules(['posts' => 'empty', 'type' => 'trim', 'from_email' => 'trim', 'from_name' => 'trim', 'to' => 'trim']);
873
		$validator->validation_rules(['from_email' => 'valid_email', 'from_name' => 'required', 'to' => 'required', 'type' => 'contains[name,email]']);
874
		$validator->validate($this->_req->post);
875
876
		// Fetch the Mr. Clean values
877
		$our_post = array_replace((array) $this->_req->post, $validator->validation_data());
878
879
		// Do we have a valid set of options to continue?
880
		if (($our_post['type'] === 'name' && !empty($our_post['from_name'])) || ($our_post['type'] === 'email' && !$validator->validation_errors('from_email')))
881
		{
882
			// Find the member.
883
			require_once(SUBSDIR . '/Auth.subs.php');
884
			$members = findMembers($our_post['to']);
885
886
			// No members, no further
887
			if (empty($members))
888
			{
889
				throw new Exception('reattribute_cannot_find_member');
890
			}
891
892
			$memID = array_shift($members);
893
			$memID = $memID['id'];
894
895
			$email = $our_post['type'] === 'email' ? $our_post['from_email'] : '';
896
			$memberName = $our_post['type'] === 'name' ? $our_post['from_name'] : '';
897
898
			// Now call the reattribute function.
899
			require_once(SUBSDIR . '/Members.subs.php');
900
			reattributePosts($memID, $email, $memberName, !$our_post['posts']);
901
902
			$context['maintenance_finished'] = [
903
				'errors' => [sprintf($txt['maintain_done'], $txt['maintain_reattribute_posts'])],
904
			];
905
		}
906
		else
907
		{
908
			// Show them the correct error
909
			if ($our_post['type'] === 'name' && empty($our_post['from_name']))
910
			{
911
				$error = $validator->validation_errors(['from_name', 'to']);
912
			}
913
			else
914
			{
915
				$error = $validator->validation_errors(['from_email', 'to']);
916
			}
917
918
			$context['maintenance_finished'] = [
919
				'errors' => $error,
920
				'type' => 'minor',
921
			];
922
		}
923
	}
924
925
	/**
926
	 * Handling function for the backup stuff.
927
	 *
928
	 * - It requires an administrator and the session hash by post.
929
	 * - This method simply forwards to DumpDatabase2().
930
	 */
931
	public function action_backup_display(): ?bool
932
	{
933
		validateToken('admin-maint');
934
935
		// Administrators only!
936
		if (!allowedTo('admin_forum'))
937
		{
938
			throw new Exception('no_dump_database', 'critical');
939
		}
940
941
		checkSession();
942
943
		// Validate access
944
		if (!defined('I_KNOW_IT_MAY_BE_UNSAFE') && $this->_validate_access() === false)
945
		{
946
			$this->action_database();
947
			return null;
948
		}
949
950
		require_once(SUBSDIR . '/Admin.subs.php');
951
		emailAdmins('admin_backup_database', [
952
			'BAK_REALNAME' => $this->user->name
953
		]);
954
955
		logAction('database_backup', ['member' => $this->user->id], 'admin');
956
		require_once(SOURCEDIR . '/DumpDatabase.php');
957
		DumpDatabase2();
958
959
		// Should not get here as DumpDatabase2 exits
960
		return true;
961
	}
962
963
	/**
964
	 * Validates the user can make an FTP connection with the supplied uid/pass
965
	 *
966
	 * - Used as an extra layer of security when performing backups
967
	 */
968
	private function _validate_access(): bool
969
	{
970
		global $context, $txt;
971
972
		$ftp = new FtpConnection($this->_req->post->ftp_server, $this->_req->post->ftp_port, $this->_req->post->ftp_username, $this->_req->post->ftp_password);
973
974
		// No errors on the connection, id/pass are good
975
		// I know, I know... but a lot of people want to type /home/xyz/... which is wrong but logical.
976
		if ($ftp->error === false && !$ftp->chdir($this->_req->post->ftp_path))
977
		{
978
			$ftp->chdir(preg_replace('~^/home[2]?/[^/]+~', '', $this->_req->post->ftp_path));
979
		}
980
981
		// If we had an error...
982
		if ($ftp->error !== false)
983
		{
984
			Txt::load('Packages');
985
			$ftp_error = $ftp->last_message ?? $txt['package_ftp_' . $ftp->error] ?? '';
0 ignored issues
show
Bug introduced by
Are you sure $ftp->error of type string|true can be used in concatenation? ( Ignorable by Annotation )

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

985
			$ftp_error = $ftp->last_message ?? $txt['package_ftp_' . /** @scrutinizer ignore-type */ $ftp->error] ?? '';
Loading history...
986
987
			// Fill the boxes for an FTP connection with data from the previous attempt
988
			$context['package_ftp'] = [
989
				'form_elements_only' => 1,
990
				'server' => $this->_req->post->ftp_server,
991
				'port' => $this->_req->post->ftp_port,
992
				'username' => $this->_req->post->ftp_username,
993
				'path' => $this->_req->post->ftp_path,
994
				'error' => empty($ftp_error) ? null : $ftp_error,
995
			];
996
997
			return false;
998
		}
999
1000
		return true;
1001
	}
1002
1003
	/**
1004
	 * Supporting function for the database maintenance area.
1005
	 */
1006
	public function action_database(): void
1007
	{
1008
		global $context, $modSettings, $maintenance;
1009
1010
		// We need this, really...
1011
		require_once(SUBSDIR . '/Maintenance.subs.php');
1012
1013
		// Set up the sub-template
1014
		$context['sub_template'] = 'maintain_database';
1015
		$db = database();
1016
1017
		if ($db->supportMediumtext())
1018
		{
1019
			$body_type = fetchBodyType();
1020
1021
			$context['convert_to'] = $body_type === 'text' ? 'mediumtext' : 'text';
1022
			$context['convert_to_suggest'] = ($body_type !== 'text' && !empty($modSettings['max_messageLength']) && $modSettings['max_messageLength'] < 65536);
1023
		}
1024
1025
		// Check a few things to give advices before make a backup
1026
		// If safe mod is enabled, the external tool is *always* the best (and probably the only) solution
1027
		$context['safe_mode_enable'] = false;
1028
1029
		// This is just a...guess
1030
		$messages = countMessages();
1031
1032
		// 256 is what we use in the backup script
1033
		detectServer()->setMemoryLimit('256M');
1034
		$memory_limit = memoryReturnBytes(ini_get('memory_limit')) / (1024 * 1024);
1035
1036
		// Zip limit is set to more or less 1/4th the size of the available memory * 1500
1037
		// 1500 is an estimate of the number of messages that generates a database of 1 MB (yeah, I know IT'S AN ESTIMATION!!!)
1038
		// Why that? Because the only reliable zip package is the one sent out the first time,
1039
		// so when the backup takes 1/5th (just to stay on the safe side) of the memory available
1040
		$zip_limit = $memory_limit * 1500 / 5;
1041
1042
		// Here is more tricky: it depends on many factors, but the main idea is that
1043
		// if it takes "too long" the backup is not reliable. So, I know that on my computer it takes
1044
		// 20 minutes to back up 2.5 GB; of course, my computer is not representative, so I'll multiply by 4 the time.
1045
		// I would consider "too long" 5 minutes (I know it can be a long time, but let's start with that):
1046
		// 80 minutes for a 2.5 GB and a 5-minute limit means 160 MB approx
1047
		$plain_limit = 240000;
1048
1049
		// Last thing: are we able to gain time?
1050
		$current_time_limit = (int) ini_get('max_execution_time');
1051
		$new_time_limit = detectServer()->setTimeLimit(159); //something strange just to be sure
1052
		detectServer()->setTimeLimit($current_time_limit);
1053
1054
		$context['use_maintenance'] = 0;
1055
1056
		// External tool if:
1057
		//  * cannot change the execution time OR
1058
		//  * cannot reset timeout
1059
		if (empty($new_time_limit) || ($current_time_limit === $new_time_limit && !function_exists('apache_reset_timeout')))
1060
		{
1061
			$context['suggested_method'] = 'use_external_tool';
1062
		}
1063
		elseif ($zip_limit < $plain_limit && $messages < $zip_limit)
1064
		{
1065
			$context['suggested_method'] = 'zipped_file';
1066
		}
1067
		elseif ($zip_limit > $plain_limit || ($zip_limit < $plain_limit && $plain_limit < $messages))
1068
		{
1069
			$context['suggested_method'] = 'use_external_tool';
1070
			$context['use_maintenance'] = empty($maintenance) ? 2 : 0;
1071
		}
1072
		else
1073
		{
1074
			$context['use_maintenance'] = 1;
1075
			$context['suggested_method'] = 'plain_text';
1076
		}
1077
1078
		theme()->getTemplates()->load('Packages');
1079
		Txt::load('Packages');
1080
1081
		// $context['package_ftp'] may be set action_backup_display when an error occurs
1082
		if (!isset($context['package_ftp']))
1083
		{
1084
			$context['package_ftp'] = [
1085
				'form_elements_only' => true,
1086
				'server' => '',
1087
				'port' => '',
1088
				'username' => $modSettings['package_username'] ?? '',
1089
				'path' => '',
1090
				'error' => '',
1091
			];
1092
		}
1093
1094
		$context['skip_security'] = defined('I_KNOW_IT_MAY_BE_UNSAFE');
1095
	}
1096
1097
	/**
1098
	 * Removing old and inactive members.
1099
	 */
1100
	public function action_purgeinactive_display(): void
1101
	{
1102
		global $context, $txt;
1103
1104
		checkSession();
1105
		validateToken('admin-maint');
1106
1107
		// Start with checking and cleaning what was sent
1108
		$validator = new DataValidator();
1109
		$validator->sanitation_rules(['maxdays' => 'intval']);
1110
		$validator->validation_rules(['maxdays' => 'required', 'groups' => 'isarray', 'del_type' => 'required']);
1111
1112
		// Validator says, you can pass or not
1113
		if ($validator->validate($this->_req->post))
1114
		{
1115
			// Get the clean data
1116
			$our_post = array_replace((array) $this->_req->post, $validator->validation_data());
1117
1118
			require_once(SUBSDIR . '/Maintenance.subs.php');
1119
			require_once(SUBSDIR . '/Members.subs.php');
1120
1121
			$groups = [];
1122
			foreach ($our_post['groups'] as $id => $dummy)
1123
			{
1124
				$groups[] = (int) $id;
1125
			}
1126
1127
			$time_limit = (time() - ($our_post['maxdays'] * 24 * 3600));
1128
			$members = purgeMembers($our_post['del_type'], $groups, $time_limit);
1129
			deleteMembers($members);
1130
1131
			$context['maintenance_finished'] = [
1132
				'errors' => [sprintf($txt['maintain_done'], $txt['maintain_members'])],
1133
			];
1134
		}
1135
		else
1136
		{
1137
			$context['maintenance_finished'] = [
1138
				'errors' => $validator->validation_errors(),
1139
				'type' => 'minor',
1140
			];
1141
		}
1142
	}
1143
1144
	/**
1145
	 * This method takes care of removal of old posts.
1146
	 * They're very, very old, perhaps even older.
1147
	 */
1148
	public function action_pruneold_display(): void
1149
	{
1150
		validateToken('admin-maint');
1151
1152
		isAllowedTo('admin_forum');
1153
		checkSession('post', 'admin');
1154
1155
		// No boards at all?  Forget it then :/.
1156
		if (empty($this->_req->post->boards))
1157
		{
1158
			redirectexit('action=admin;area=maintain;sa=topics');
1159
		}
1160
1161
		$boards = array_keys($this->_req->post->boards);
1162
1163
		if (!isset($this->_req->post->delete_type) || !in_array($this->_req->post->delete_type, ['moved', 'nothing', 'locked']))
1164
		{
1165
			$delete_type = 'nothing';
1166
		}
1167
		else
1168
		{
1169
			$delete_type = $this->_req->post->delete_type;
1170
		}
1171
1172
		$exclude_stickies = isset($this->_req->post->delete_old_not_sticky);
1173
1174
		// @todo what is the minimum for maxdays? Maybe throw an error?
1175
		$older_than = time() - 3600 * 24 * max($this->_req->getPost('maxdays', 'intval', 0), 1);
1176
1177
		require_once(SUBSDIR . '/Topic.subs.php');
1178
		removeOldTopics($boards, $delete_type, $exclude_stickies, $older_than);
1179
1180
		// Log an action into the moderation log.
1181
		logAction('pruned', ['days' => max($this->_req->getPost('maxdays', 'intval', 0), 1)]);
1182
1183
		redirectexit('action=admin;area=maintain;sa=topics;done=purgeold');
1184
	}
1185
1186
	/**
1187
	 * Moves topics from one board to another.
1188
	 *
1189
	 * @uses not_done template to pause the process.
1190
	 */
1191
	public function action_massmove_display(): void
1192
	{
1193
		global $context, $txt, $time_start;
1194
1195
		// Only admins.
1196
		isAllowedTo('admin_forum');
1197
1198
		// And valid requests
1199
		checkSession();
1200
1201
		// Set up to the context.
1202
		$context['page_title'] = $txt['not_done_title'];
1203
		$context['continue_countdown'] = 3;
1204
		$context['continue_post_data'] = '';
1205
		$context['continue_get_data'] = '';
1206
		$context['sub_template'] = 'not_done';
1207
		$context['start'] = $this->_req->getQuery('start', 'intval', 0);
1208
1209
		// First time we do this?
1210
		$id_board_from = $this->_req->getPost('id_board_from', 'intval', $this->_req->getQuery('id_board_from', 'intval', 0));
1211
		$id_board_to = $this->_req->getPost('id_board_to', 'intval', $this->_req->getQuery('id_board_to', 'intval', 0));
1212
1213
		// No boards, then this is your stop.
1214
		if (empty($id_board_from) || empty($id_board_to))
1215
		{
1216
			return;
1217
		}
1218
1219
		// These will be needed
1220
		require_once(SUBSDIR . '/Maintenance.subs.php');
1221
		require_once(SUBSDIR . '/Topic.subs.php');
1222
1223
		// How many topics are we moving?
1224
		$total_topics = $this->_req->getQuery('totaltopics', 'intval', 0);
1225
		if (empty($total_topics) || $context['start'] === 0)
1226
		{
1227
			validateToken('admin-maint');
1228
			$total_topics = countTopicsFromBoard($id_board_from);
1229
		}
1230
		else
1231
		{
1232
			$total_topics = $this->_req->getQuery('totaltopics', 'intval');
1233
			validateToken('admin_movetopics');
1234
		}
1235
1236
		// We have topics to move so start the process.
1237
		if (!empty($total_topics))
1238
		{
1239
			while ($context['start'] <= $total_topics)
1240
			{
1241
				// Let's get the next 10 topics.
1242
				$topics = getTopicsToMove($id_board_from);
1243
1244
				// Just return if we don't have any topics left to move.
1245
				if (empty($topics))
1246
				{
1247
					break;
1248
				}
1249
1250
				// Let's move them.
1251
				moveTopics($topics, $id_board_to);
1252
1253
				// Increase the counter
1254
				$context['start'] += 10;
1255
1256
				// If this is really taking some time, show the pause screen
1257
				if (microtime(true) - $time_start > 3)
1258
				{
1259
					// What's the percent?
1260
					$context['continue_percent'] = round(100 * ($context['start'] / $total_topics), 1);
1261
1262
					// Set up for the form
1263
					$context['continue_get_data'] = '?action=admin;area=maintain;sa=topics;activity=massmove;id_board_from=' . $id_board_from . ';id_board_to=' . $id_board_to . ';totaltopics=' . $total_topics . ';start=' . $context['start'];
1264
					$context['continue_post_data'] = '
1265
						<input type="hidden" name="' . $context['session_var'] . '" value="' . $context['session_id'] . '" />';
1266
1267
					// Let the template system do it's thang.
1268
					return;
1269
				}
1270
			}
1271
		}
1272
1273
		// Don't confuse admins by having an out-of-date cache.
1274
		Cache::instance()->remove('board-' . $id_board_from);
1275
		Cache::instance()->remove('board-' . $id_board_to);
1276
1277
		redirectexit('action=admin;area=maintain;sa=topics;done=massmove');
1278
	}
1279
1280
	/**
1281
	 * Generates a list of integration hooks for display
1282
	 *
1283
	 * - Accessed through ?action=admin;area=maintain;sa=hooks;
1284
	 */
1285
	public function action_hooks(): void
1286
	{
1287
		global $context, $txt;
1288
1289
		require_once(SUBSDIR . '/AddonSettings.subs.php');
1290
1291
		$context['filter_url'] = '';
1292
		$context['current_filter'] = '';
1293
1294
		// Get the list of the current system hooks, filter them if needed
1295
		$currentHooks = get_integration_hooks();
1296
		$filter = $this->_req->getQuery('filter', 'trim', '');
1297
		if (array_key_exists($filter, $currentHooks))
1298
		{
1299
			$context['filter_url'] = ';filter=' . $filter;
1300
			$context['current_filter'] = $filter;
1301
		}
1302
1303
		$list_options = [
1304
			'id' => 'list_integration_hooks',
1305
			'title' => $txt['maintain_sub_hooks_list'],
1306
			'items_per_page' => 20,
1307
			'base_href' => getUrl('admin', ['action' => 'admin', 'area' => 'maintain', 'sa' => 'hooks', $context['filter_url'], '{session_data}']),
1308
			'default_sort_col' => 'hook_name',
1309
			'get_items' => [
1310
				'function' => fn($start, $items_per_page, $sort) => $this->list_getIntegrationHooks($start, $items_per_page, $sort),
1311
			],
1312
			'get_count' => [
1313
				'function' => fn() => $this->list_getIntegrationHooksCount(),
1314
			],
1315
			'no_items_label' => $txt['hooks_no_hooks'],
1316
			'columns' => [
1317
				'hook_name' => [
1318
					'header' => [
1319
						'value' => $txt['hooks_field_hook_name'],
1320
					],
1321
					'data' => [
1322
						'db' => 'hook_name',
1323
					],
1324
					'sort' => [
1325
						'default' => 'hook_name',
1326
						'reverse' => 'hook_name DESC',
1327
					],
1328
				],
1329
				'function_name' => [
1330
					'header' => [
1331
						'value' => $txt['hooks_field_function_name'],
1332
					],
1333
					'data' => [
1334
						'function' => static function ($data) {
1335
							global $txt;
1336
1337
							if (!empty($data['included_file']))
1338
							{
1339
								return $txt['hooks_field_function'] . ': ' . $data['real_function'] . '<br />' . $txt['hooks_field_included_file'] . ': ' . $data['included_file'];
1340
							}
1341
1342
							return $data['real_function'];
1343
						},
1344
					],
1345
					'sort' => [
1346
						'default' => 'function_name',
1347
						'reverse' => 'function_name DESC',
1348
					],
1349
				],
1350
				'file_name' => [
1351
					'header' => [
1352
						'value' => $txt['hooks_field_file_name'],
1353
					],
1354
					'data' => [
1355
						'db' => 'file_name',
1356
					],
1357
					'sort' => [
1358
						'default' => 'file_name',
1359
						'reverse' => 'file_name DESC',
1360
					],
1361
				],
1362
				'status' => [
1363
					'header' => [
1364
						'value' => $txt['hooks_field_hook_exists'],
1365
						'class' => 'nowrap',
1366
					],
1367
					'data' => [
1368
						'function' => static fn($data) => '<i class="icon i-post_moderation_' . $data['status'] . '" title="' . $data['img_text'] . '"></i>',
1369
						'class' => 'centertext',
1370
					],
1371
					'sort' => [
1372
						'default' => 'status',
1373
						'reverse' => 'status DESC',
1374
					],
1375
				],
1376
			],
1377
			'additional_rows' => [
1378
				[
1379
					'position' => 'after_title',
1380
					'value' => $txt['hooks_disable_legend'] . ':
1381
					<ul>
1382
						<li>
1383
							<i class="icon i-post_moderation_allow" title="' . $txt['hooks_active'] . '"></i>' . $txt['hooks_disable_legend_exists'] . '
1384
						</li>
1385
						<li>
1386
							<i class="icon i-post_moderation_deny col" title="' . $txt['hooks_missing'] . '"></i>' . $txt['hooks_disable_legend_missing'] . '
1387
						</li>
1388
					</ul>'
1389
				],
1390
			],
1391
		];
1392
1393
		createList($list_options);
1394
1395
		$context['page_title'] = $txt['maintain_sub_hooks_list'];
1396
		$context['sub_template'] = 'show_list';
1397
		$context['default_list'] = 'list_integration_hooks';
1398
	}
1399
1400
	/**
1401
	 * Callback for createList(). Called by action_hooks
1402
	 *
1403
	 * @param int $start The item to start with (for pagination purposes)
1404
	 * @param int $items_per_page The number of items to show per page
1405
	 * @param string $sort A string indicating how to sort the results
1406
	 *
1407
	 * @return array
1408
	 */
1409
	public function list_getIntegrationHooks(int $start, int $items_per_page, string $sort): array
1410
	{
1411
		return list_integration_hooks_data($start, $items_per_page, $sort);
1412
	}
1413
1414
	/**
1415
	 * Simply returns the total count of integration hooks
1416
	 * Callback for createList().
1417
	 *
1418
	 * @return int
1419
	 */
1420
	public function list_getIntegrationHooksCount(): int
1421
	{
1422
		global $context;
1423
1424
		$context['filter'] = $this->_req->getQuery('filter', 'trim|strval', false);
1425
1426
		return integration_hooks_count($context['filter']);
1427
	}
1428
1429
	/**
1430
	 * Recalculate all members post-counts
1431
	 *
1432
	 * What it does:
1433
	 *
1434
	 * - It requires the admin_forum permission.
1435
	 * - Recounts all posts for members found in the message table
1436
	 * - Updates the members post-count record in the members table
1437
	 * - Honors the boards post-count flag
1438
	 * - Does not count posts in the recycle bin
1439
	 * - Zeros post-counts for all members with no posts in the message table
1440
	 * - Runs as a delayed loop to avoid server overload
1441
	 * - Uses the not_done template in Admin.template
1442
	 * - Redirects back to action=admin;area=maintain;sa=members when complete.
1443
	 * - Accessed via ?action=admin;area=maintain;sa=members;activity=recountposts
1444
	 */
1445
	public function action_recountposts_display(): void
1446
	{
1447
		global $txt, $context;
1448
1449
		// Check the session
1450
		checkSession();
1451
1452
		// Set up to the context for the pause screen
1453
		$context['page_title'] = $txt['not_done_title'];
1454
		$context['continue_countdown'] = 3;
1455
		$context['continue_get_data'] = '';
1456
		$context['sub_template'] = 'not_done';
1457
1458
		// Init, do 200 members in a bunch
1459
		$increment = 200;
1460
		$start = $this->_req->getQuery('start', 'intval', 0);
1461
1462
		// Ask for some extra time, on big boards this may take a bit
1463
		detectServer()->setTimeLimit(600);
1464
		Debug::instance()->off();
1465
1466
		// The functions here will come in handy
1467
		require_once(SUBSDIR . '/Maintenance.subs.php');
1468
1469
		// Only run this query if we don't have the total number of members that have posted
1470
		if (!isset($_SESSION['total_members']) || $start === 0)
1471
		{
1472
			validateToken('admin-maint');
1473
			$total_members = countContributors();
1474
			$_SESSION['total_member'] = $total_members;
1475
		}
1476
		else
1477
		{
1478
			validateToken('admin-recountposts');
1479
			$total_members = $this->_req->session->total_members;
1480
		}
1481
1482
		// Let's get the next group of members and determine their post-count
1483
		// (from the boards that have post-count enabled, of course).
1484
		$total_rows = updateMembersPostCount($start, $increment);
1485
1486
		// Continue?
1487
		if ($total_rows === $increment)
1488
		{
1489
			createToken('admin-recountposts');
1490
1491
			$start += $increment;
1492
			$context['continue_get_data'] = '?action=admin;area=maintain;sa=members;activity=recountposts;start=' . $start;
1493
			$context['continue_percent'] = round(100 * $start / $total_members);
1494
			$context['not_done_title'] = $txt['not_done_title'] . ' (' . $context['continue_percent'] . '%)';
1495
			$context['continue_post_data'] = '<input type="hidden" name="' . $context['admin-recountposts_token_var'] . '" value="' . $context['admin-recountposts_token'] . '" />
1496
				<input type="hidden" name="' . $context['session_var'] . '" value="' . $context['session_id'] . '" />';
1497
1498
			Debug::instance()->on();
1499
1500
			return;
1501
		}
1502
1503
		// No countable posts? set posts counter to 0
1504
		updateZeroPostMembers();
1505
1506
		Debug::instance()->on();
1507
1508
		// All done, clean up and go back to maintenance
1509
		unset($_SESSION['total_members']);
1510
		redirectexit('action=admin;area=maintain;sa=members;done=recountposts');
1511
	}
1512
}
1513