Issues (1686)

sources/ElkArte/AdminController/Maintenance.php (2 issues)

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

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

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