Maintenance::list_getIntegrationHooks()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 3
dl 0
loc 3
rs 10
c 0
b 0
f 0
ccs 0
cts 3
cp 0
crap 2
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 dev
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 actions */
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 a bit (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 (empty($this->_req->query->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->query->start;
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('post');
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 optimise 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 (!isset($this->_req->query->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 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
				updateMessagesBoardID($this->_req->query->start, $this->increment);
807
				$this->start += $this->increment;
808
809
				if (microtime(true) - $time_start > 3)
810
				{
811
					$percent = round((700 + 100 * $this->start / $modSettings['maxMsgID']) / $this->total_steps);
812
					$this->_buildContinue($percent, 6);
813
814
					return;
815
				}
816
			}
817
818
			// Done with step 6, reset start for the next one
819
			$this->start = 0;
820
		}
821
822
		updateBoardsLastMessage();
823
824
		// Update all the basic statistics.
825
		require_once(SUBSDIR . '/Members.subs.php');
826
		updateMemberStats();
827
		require_once(SUBSDIR . '/Messages.subs.php');
828
		updateMessageStats();
829
		require_once(SUBSDIR . '/Topic.subs.php');
830
		updateTopicStats();
831
832
		// Finally, update the latest event times.
833
		require_once(SUBSDIR . '/ScheduledTasks.subs.php');
834
		calculateNextTrigger();
835
836
		// Ta-da
837
		redirectexit('action=admin;area=maintain;sa=routine;done=recount');
838
	}
839
840
	/**
841
	 * Helper function for teh recount process, build the continue values for
842
	 * the template
843
	 *
844
	 * @param int $percent percent done
845
	 * @param int $step step we are on
846
	 */
847
	private function _buildContinue(int $percent, int $step): void
848
	{
849
		global $context, $txt;
850
851
		createToken('admin-boardrecount');
852
853
		$context['continue_post_data'] = '
854
			<input type="hidden" name="' . $context['admin-boardrecount_token_var'] . '" value="' . $context['admin-boardrecount_token'] . '" />
855
			<input type="hidden" name="' . $context['session_var'] . '" value="' . $context['session_id'] . '" />';
856
		$context['continue_get_data'] = '?action=admin;area=maintain;sa=routine;activity=recount;step=' . $step . ';start=' . $this->start;
857
		$context['continue_percent'] = $percent;
858
		$context['not_done_title'] = $txt['not_done_title'] . ' (' . $context['continue_percent'] . '%)';
859
	}
860
861
	/**
862
	 * Re-attribute posts to the user sent from the maintenance page.
863
	 */
864
	public function action_reattribute_display(): void
865
	{
866
		global $context, $txt;
867
868
		checkSession();
869
870
		$validator = new DataValidator();
871
		$validator->sanitation_rules(['posts' => 'empty', 'type' => 'trim', 'from_email' => 'trim', 'from_name' => 'trim', 'to' => 'trim']);
872
		$validator->validation_rules(['from_email' => 'valid_email', 'from_name' => 'required', 'to' => 'required', 'type' => 'contains[name,email]']);
873
		$validator->validate($this->_req->post);
874
875
		// Fetch the Mr. Clean values
876
		$our_post = array_replace((array) $this->_req->post, $validator->validation_data());
877
878
		// Do we have a valid set of options to continue?
879
		if (($our_post['type'] === 'name' && !empty($our_post['from_name'])) || ($our_post['type'] === 'email' && !$validator->validation_errors('from_email')))
880
		{
881
			// Find the member.
882
			require_once(SUBSDIR . '/Auth.subs.php');
883
			$members = findMembers($our_post['to']);
884
885
			// No members, no further
886
			if (empty($members))
887
			{
888
				throw new Exception('reattribute_cannot_find_member');
889
			}
890
891
			$memID = array_shift($members);
892
			$memID = $memID['id'];
893
894
			$email = $our_post['type'] === 'email' ? $our_post['from_email'] : '';
895
			$memberName = $our_post['type'] === 'name' ? $our_post['from_name'] : '';
896
897
			// Now call the reattribute function.
898
			require_once(SUBSDIR . '/Members.subs.php');
899
			reattributePosts($memID, $email, $memberName, !$our_post['posts']);
900
901
			$context['maintenance_finished'] = [
902
				'errors' => [sprintf($txt['maintain_done'], $txt['maintain_reattribute_posts'])],
903
			];
904
		}
905
		else
906
		{
907
			// Show them the correct error
908
			if ($our_post['type'] === 'name' && empty($our_post['from_name']))
909
			{
910
				$error = $validator->validation_errors(['from_name', 'to']);
911
			}
912
			else
913
			{
914
				$error = $validator->validation_errors(['from_email', 'to']);
915
			}
916
917
			$context['maintenance_finished'] = [
918
				'errors' => $error,
919
				'type' => 'minor',
920
			];
921
		}
922
	}
923
924
	/**
925
	 * Handling function for the backup stuff.
926
	 *
927
	 * - It requires an administrator and the session hash by post.
928
	 * - This method simply forwards to DumpDatabase2().
929
	 */
930
	public function action_backup_display(): ?bool
931
	{
932
		validateToken('admin-maint');
933
934
		// Administrators only!
935
		if (!allowedTo('admin_forum'))
936
		{
937
			throw new Exception('no_dump_database', 'critical');
938
		}
939
940
		checkSession('post');
941
942
		// Validate access
943
		if (!defined('I_KNOW_IT_MAY_BE_UNSAFE') && $this->_validate_access() === false)
944
		{
945
			$this->action_database();
946
			return null;
947
		}
948
949
		require_once(SUBSDIR . '/Admin.subs.php');
950
		emailAdmins('admin_backup_database', [
951
			'BAK_REALNAME' => $this->user->name
952
		]);
953
954
		logAction('database_backup', ['member' => $this->user->id], 'admin');
955
		require_once(SOURCEDIR . '/DumpDatabase.php');
956
		DumpDatabase2();
957
958
		// Should not get here as DumpDatabase2 exits
959
		return true;
960
	}
961
962
	/**
963
	 * Validates the user can make an FTP connection with the supplied uid/pass
964
	 *
965
	 * - Used as an extra layer of security when performing backups
966
	 */
967
	private function _validate_access(): bool
968
	{
969
		global $context, $txt;
970
971
		$ftp = new FtpConnection($this->_req->post->ftp_server, $this->_req->post->ftp_port, $this->_req->post->ftp_username, $this->_req->post->ftp_password);
972
973
		// No errors on the connection, id/pass are good
974
		// I know, I know... but a lot of people want to type /home/xyz/... which is wrong, but logical.
975
		if ($ftp->error === false && !$ftp->chdir($this->_req->post->ftp_path))
976
		{
977
			$ftp->chdir(preg_replace('~^/home[2]?/[^/]+~', '', $this->_req->post->ftp_path));
978
		}
979
980
		// If we had an error...
981
		if ($ftp->error !== false)
982
		{
983
			Txt::load('Packages');
984
			$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

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