Passed
Push — release-2.1 ( 12bf43...3d148c )
by Jon
06:14 queued 11s
created

RebuildSettingsFile()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 5
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 10
rs 10
1
<?php
2
3
/**
4
 * Forum maintenance. Important stuff.
5
 *
6
 * Simple Machines Forum (SMF)
7
 *
8
 * @package SMF
9
 * @author Simple Machines https://www.simplemachines.org
10
 * @copyright 2021 Simple Machines and individual contributors
11
 * @license https://www.simplemachines.org/about/smf/license.php BSD
12
 *
13
 * @version 2.1 RC4
14
 */
15
16
if (!defined('SMF'))
17
	die('No direct access...');
18
19
/**
20
 * Main dispatcher, the maintenance access point.
21
 * This, as usual, checks permissions, loads language files, and forwards to the actual workers.
22
 */
23
function ManageMaintenance()
24
{
25
	global $txt, $context;
26
27
	// You absolutely must be an admin by here!
28
	isAllowedTo('admin_forum');
29
30
	// Need something to talk about?
31
	loadLanguage('ManageMaintenance');
32
	loadTemplate('ManageMaintenance');
33
34
	// This uses admin tabs - as it should!
35
	$context[$context['admin_menu_name']]['tab_data'] = array(
36
		'title' => $txt['maintain_title'],
37
		'description' => $txt['maintain_info'],
38
		'tabs' => array(
39
			'routine' => array(),
40
			'database' => array(),
41
			'members' => array(),
42
			'topics' => array(),
43
		),
44
	);
45
46
	// So many things you can do - but frankly I won't let you - just these!
47
	$subActions = array(
48
		'routine' => array(
49
			'function' => 'MaintainRoutine',
50
			'template' => 'maintain_routine',
51
			'activities' => array(
52
				'version' => 'VersionDetail',
53
				'repair' => 'MaintainFindFixErrors',
54
				'recount' => 'AdminBoardRecount',
55
				'rebuild_settings' => 'RebuildSettingsFile',
56
				'logs' => 'MaintainEmptyUnimportantLogs',
57
				'cleancache' => 'MaintainCleanCache',
58
			),
59
		),
60
		'database' => array(
61
			'function' => 'MaintainDatabase',
62
			'template' => 'maintain_database',
63
			'activities' => array(
64
				'optimize' => 'OptimizeTables',
65
				'convertentities' => 'ConvertEntities',
66
				'convertmsgbody' => 'ConvertMsgBody',
67
			),
68
		),
69
		'members' => array(
70
			'function' => 'MaintainMembers',
71
			'template' => 'maintain_members',
72
			'activities' => array(
73
				'reattribute' => 'MaintainReattributePosts',
74
				'purgeinactive' => 'MaintainPurgeInactiveMembers',
75
				'recountposts' => 'MaintainRecountPosts',
76
			),
77
		),
78
		'topics' => array(
79
			'function' => 'MaintainTopics',
80
			'template' => 'maintain_topics',
81
			'activities' => array(
82
				'massmove' => 'MaintainMassMoveTopics',
83
				'pruneold' => 'MaintainRemoveOldPosts',
84
				'olddrafts' => 'MaintainRemoveOldDrafts',
85
			),
86
		),
87
		'hooks' => array(
88
			'function' => 'list_integration_hooks',
89
		),
90
		'destroy' => array(
91
			'function' => 'Destroy',
92
			'activities' => array(),
93
		),
94
	);
95
96
	call_integration_hook('integrate_manage_maintenance', array(&$subActions));
97
98
	// Yep, sub-action time!
99
	if (isset($_REQUEST['sa']) && isset($subActions[$_REQUEST['sa']]))
100
		$subAction = $_REQUEST['sa'];
101
	else
102
		$subAction = 'routine';
103
104
	// Doing something special?
105
	if (isset($_REQUEST['activity']) && isset($subActions[$subAction]['activities'][$_REQUEST['activity']]))
106
		$activity = $_REQUEST['activity'];
107
108
	// Set a few things.
109
	$context['page_title'] = $txt['maintain_title'];
110
	$context['sub_action'] = $subAction;
111
	$context['sub_template'] = !empty($subActions[$subAction]['template']) ? $subActions[$subAction]['template'] : '';
112
113
	// Finally fall through to what we are doing.
114
	call_helper($subActions[$subAction]['function']);
115
116
	// Any special activity?
117
	if (isset($activity))
118
		call_helper($subActions[$subAction]['activities'][$activity]);
119
120
	// Create a maintenance token.  Kinda hard to do it any other way.
121
	createToken('admin-maint');
122
}
123
124
/**
125
 * Supporting function for the database maintenance area.
126
 */
127
function MaintainDatabase()
128
{
129
	global $context, $db_type, $db_character_set, $modSettings, $smcFunc, $txt;
130
131
	// Show some conversion options?
132
	$context['convert_entities'] = isset($modSettings['global_character_set']) && $modSettings['global_character_set'] === 'UTF-8';
133
134
	if ($db_type == 'mysql')
135
	{
136
		db_extend('packages');
137
138
		$colData = $smcFunc['db_list_columns']('{db_prefix}messages', true);
139
		foreach ($colData as $column)
140
			if ($column['name'] == 'body')
141
				$body_type = $column['type'];
142
143
		$context['convert_to'] = $body_type == 'text' ? 'mediumtext' : 'text';
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $body_type does not seem to be defined for all execution paths leading up to this point.
Loading history...
144
		$context['convert_to_suggest'] = ($body_type != 'text' && !empty($modSettings['max_messageLength']) && $modSettings['max_messageLength'] < 65536);
145
	}
146
147
	if (isset($_GET['done']) && $_GET['done'] == 'convertentities')
148
		$context['maintenance_finished'] = $txt['entity_convert_title'];
149
}
150
151
/**
152
 * Supporting function for the routine maintenance area.
153
 */
154
function MaintainRoutine()
155
{
156
	global $context, $txt;
157
158
	if (isset($_GET['done']) && in_array($_GET['done'], array('recount', 'rebuild_settings')))
159
		$context['maintenance_finished'] = $txt['maintain_' . $_GET['done']];
160
}
161
162
/**
163
 * Supporting function for the members maintenance area.
164
 */
165
function MaintainMembers()
166
{
167
	global $context, $smcFunc, $txt;
168
169
	// Get membergroups - for deleting members and the like.
170
	$result = $smcFunc['db_query']('', '
171
		SELECT id_group, group_name
172
		FROM {db_prefix}membergroups',
173
		array(
174
		)
175
	);
176
	$context['membergroups'] = array(
177
		array(
178
			'id' => 0,
179
			'name' => $txt['maintain_members_ungrouped']
180
		),
181
	);
182
	while ($row = $smcFunc['db_fetch_assoc']($result))
183
	{
184
		$context['membergroups'][] = array(
185
			'id' => $row['id_group'],
186
			'name' => $row['group_name']
187
		);
188
	}
189
	$smcFunc['db_free_result']($result);
190
191
	if (isset($_GET['done']) && $_GET['done'] == 'recountposts')
192
		$context['maintenance_finished'] = $txt['maintain_recountposts'];
193
194
	loadJavaScriptFile('suggest.js', array('defer' => false, 'minimize' => true), 'smf_suggest');
195
}
196
197
/**
198
 * Supporting function for the topics maintenance area.
199
 */
200
function MaintainTopics()
201
{
202
	global $context, $smcFunc, $txt, $sourcedir;
203
204
	// Let's load up the boards in case they are useful.
205
	$result = $smcFunc['db_query']('order_by_board_order', '
206
		SELECT b.id_board, b.name, b.child_level, c.name AS cat_name, c.id_cat
207
		FROM {db_prefix}boards AS b
208
			LEFT JOIN {db_prefix}categories AS c ON (c.id_cat = b.id_cat)
209
		WHERE {query_see_board}
210
			AND redirect = {string:blank_redirect}',
211
		array(
212
			'blank_redirect' => '',
213
		)
214
	);
215
	$context['categories'] = array();
216
	while ($row = $smcFunc['db_fetch_assoc']($result))
217
	{
218
		if (!isset($context['categories'][$row['id_cat']]))
219
			$context['categories'][$row['id_cat']] = array(
220
				'name' => $row['cat_name'],
221
				'boards' => array()
222
			);
223
224
		$context['categories'][$row['id_cat']]['boards'][$row['id_board']] = array(
225
			'id' => $row['id_board'],
226
			'name' => $row['name'],
227
			'child_level' => $row['child_level']
228
		);
229
	}
230
	$smcFunc['db_free_result']($result);
231
232
	require_once($sourcedir . '/Subs-Boards.php');
233
	sortCategories($context['categories']);
234
235
	if (isset($_GET['done']) && $_GET['done'] == 'purgeold')
236
		$context['maintenance_finished'] = $txt['maintain_old'];
237
	elseif (isset($_GET['done']) && $_GET['done'] == 'massmove')
238
		$context['maintenance_finished'] = $txt['move_topics_maintenance'];
239
}
240
241
/**
242
 * Find and fix all errors on the forum.
243
 */
244
function MaintainFindFixErrors()
245
{
246
	global $sourcedir;
247
248
	// Honestly, this should be done in the sub function.
249
	validateToken('admin-maint');
250
251
	require_once($sourcedir . '/RepairBoards.php');
252
	RepairBoards();
253
}
254
255
/**
256
 * Wipes the whole cache directory.
257
 * This only applies to SMF's own cache directory, though.
258
 */
259
function MaintainCleanCache()
260
{
261
	global $context, $txt;
262
263
	checkSession();
264
	validateToken('admin-maint');
265
266
	// Just wipe the whole cache directory!
267
	clean_cache();
268
269
	$context['maintenance_finished'] = $txt['maintain_cache'];
270
}
271
272
/**
273
 * Empties all uninmportant logs
274
 */
275
function MaintainEmptyUnimportantLogs()
276
{
277
	global $context, $smcFunc, $txt;
278
279
	checkSession();
280
	validateToken('admin-maint');
281
282
	// No one's online now.... MUHAHAHAHA :P.
283
	$smcFunc['db_query']('', '
284
		DELETE FROM {db_prefix}log_online');
285
286
	// Dump the banning logs.
287
	$smcFunc['db_query']('', '
288
		DELETE FROM {db_prefix}log_banned');
289
290
	// Start id_error back at 0 and dump the error log.
291
	$smcFunc['db_query']('truncate_table', '
292
		TRUNCATE {db_prefix}log_errors');
293
294
	// Clear out the spam log.
295
	$smcFunc['db_query']('', '
296
		DELETE FROM {db_prefix}log_floodcontrol');
297
298
	// Last but not least, the search logs!
299
	$smcFunc['db_query']('truncate_table', '
300
		TRUNCATE {db_prefix}log_search_topics');
301
302
	$smcFunc['db_query']('truncate_table', '
303
		TRUNCATE {db_prefix}log_search_messages');
304
305
	$smcFunc['db_query']('truncate_table', '
306
		TRUNCATE {db_prefix}log_search_results');
307
308
	updateSettings(array('search_pointer' => 0));
309
310
	$context['maintenance_finished'] = $txt['maintain_logs'];
311
}
312
313
/**
314
 * Oh noes! I'd document this but that would give it away
315
 */
316
function Destroy()
317
{
318
	global $context;
319
320
	echo '<!DOCTYPE html>
321
		<html', $context['right_to_left'] ? ' dir="rtl"' : '', '><head><title>', $context['forum_name_html_safe'], ' deleted!</title></head>
322
		<body style="background-color: orange; font-family: arial, sans-serif; text-align: center;">
323
		<div style="margin-top: 8%; font-size: 400%; color: black;">Oh my, you killed ', $context['forum_name_html_safe'], '!</div>
324
		<div style="margin-top: 7%; font-size: 500%; color: red;"><strong>You lazy bum!</strong></div>
325
		</body></html>';
326
	obExit(false);
327
}
328
329
/**
330
 * Convert the column "body" of the table {db_prefix}messages from TEXT to MEDIUMTEXT and vice versa.
331
 * It requires the admin_forum permission.
332
 * This is needed only for MySQL.
333
 * During the conversion from MEDIUMTEXT to TEXT it check if any of the posts exceed the TEXT length and if so it aborts.
334
 * This action is linked from the maintenance screen (if it's applicable).
335
 * Accessed by ?action=admin;area=maintain;sa=database;activity=convertmsgbody.
336
 *
337
 * @uses template_convert_msgbody()
338
 */
339
function ConvertMsgBody()
340
{
341
	global $scripturl, $context, $txt, $db_type;
342
	global $modSettings, $smcFunc;
343
344
	// Show me your badge!
345
	isAllowedTo('admin_forum');
346
347
	if ($db_type != 'mysql')
348
		return;
349
350
	db_extend('packages');
351
352
	$colData = $smcFunc['db_list_columns']('{db_prefix}messages', true);
353
	foreach ($colData as $column)
354
		if ($column['name'] == 'body')
355
			$body_type = $column['type'];
356
357
	$context['convert_to'] = $body_type == 'text' ? 'mediumtext' : 'text';
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $body_type does not seem to be defined for all execution paths leading up to this point.
Loading history...
358
359
	if ($body_type == 'text' || ($body_type != 'text' && isset($_POST['do_conversion'])))
360
	{
361
		checkSession();
362
		validateToken('admin-maint');
363
364
		// Make it longer so we can do their limit.
365
		if ($body_type == 'text')
366
			$smcFunc['db_change_column']('{db_prefix}messages', 'body', array('type' => 'mediumtext'));
367
		// Shorten the column so we can have a bit (literally per record) less space occupied
368
		else
369
			$smcFunc['db_change_column']('{db_prefix}messages', 'body', array('type' => 'text'));
370
371
		// 3rd party integrations may be interested in knowning about this.
372
		call_integration_hook('integrate_convert_msgbody', array($body_type));
373
374
		$colData = $smcFunc['db_list_columns']('{db_prefix}messages', true);
375
		foreach ($colData as $column)
376
			if ($column['name'] == 'body')
377
				$body_type = $column['type'];
378
379
		$context['maintenance_finished'] = $txt[$context['convert_to'] . '_title'];
380
		$context['convert_to'] = $body_type == 'text' ? 'mediumtext' : 'text';
381
		$context['convert_to_suggest'] = ($body_type != 'text' && !empty($modSettings['max_messageLength']) && $modSettings['max_messageLength'] < 65536);
382
383
		return;
384
	}
385
	elseif ($body_type != 'text' && (!isset($_POST['do_conversion']) || isset($_POST['cont'])))
386
	{
387
		checkSession();
388
		if (empty($_REQUEST['start']))
389
			validateToken('admin-maint');
390
		else
391
			validateToken('admin-convertMsg');
392
393
		$context['page_title'] = $txt['not_done_title'];
394
		$context['continue_post_data'] = '';
395
		$context['continue_countdown'] = 3;
396
		$context['sub_template'] = 'not_done';
397
		$increment = 500;
398
		$id_msg_exceeding = isset($_POST['id_msg_exceeding']) ? explode(',', $_POST['id_msg_exceeding']) : array();
399
400
		$request = $smcFunc['db_query']('', '
401
			SELECT COUNT(*) as count
402
			FROM {db_prefix}messages',
403
			array()
404
		);
405
		list($max_msgs) = $smcFunc['db_fetch_row']($request);
406
		$smcFunc['db_free_result']($request);
407
408
		// Try for as much time as possible.
409
		@set_time_limit(600);
410
411
		while ($_REQUEST['start'] < $max_msgs)
412
		{
413
			$request = $smcFunc['db_query']('', '
414
				SELECT id_msg
415
				FROM {db_prefix}messages
416
				WHERE id_msg BETWEEN {int:start} AND {int:start} + {int:increment}
417
					AND LENGTH(body) > 65535',
418
				array(
419
					'start' => $_REQUEST['start'],
420
					'increment' => $increment - 1,
421
				)
422
			);
423
			while ($row = $smcFunc['db_fetch_assoc']($request))
424
				$id_msg_exceeding[] = $row['id_msg'];
425
			$smcFunc['db_free_result']($request);
426
427
			$_REQUEST['start'] += $increment;
428
429
			if (microtime(true) - TIME_START > 3)
430
			{
431
				createToken('admin-convertMsg');
432
				$context['continue_post_data'] = '
433
					<input type="hidden" name="' . $context['admin-convertMsg_token_var'] . '" value="' . $context['admin-convertMsg_token'] . '">
434
					<input type="hidden" name="' . $context['session_var'] . '" value="' . $context['session_id'] . '">
435
					<input type="hidden" name="id_msg_exceeding" value="' . implode(',', $id_msg_exceeding) . '">';
436
437
				$context['continue_get_data'] = '?action=admin;area=maintain;sa=database;activity=convertmsgbody;start=' . $_REQUEST['start'];
438
				$context['continue_percent'] = round(100 * $_REQUEST['start'] / $max_msgs);
439
440
				return;
441
			}
442
		}
443
		createToken('admin-maint');
444
		$context['page_title'] = $txt[$context['convert_to'] . '_title'];
445
		$context['sub_template'] = 'convert_msgbody';
446
447
		if (!empty($id_msg_exceeding))
448
		{
449
			if (count($id_msg_exceeding) > 100)
450
			{
451
				$query_msg = array_slice($id_msg_exceeding, 0, 100);
452
				$context['exceeding_messages_morethan'] = sprintf($txt['exceeding_messages_morethan'], count($id_msg_exceeding));
453
			}
454
			else
455
				$query_msg = $id_msg_exceeding;
456
457
			$context['exceeding_messages'] = array();
458
			$request = $smcFunc['db_query']('', '
459
				SELECT id_msg, id_topic, subject
460
				FROM {db_prefix}messages
461
				WHERE id_msg IN ({array_int:messages})',
462
				array(
463
					'messages' => $query_msg,
464
				)
465
			);
466
			while ($row = $smcFunc['db_fetch_assoc']($request))
467
				$context['exceeding_messages'][] = '<a href="' . $scripturl . '?topic=' . $row['id_topic'] . '.msg' . $row['id_msg'] . '#msg' . $row['id_msg'] . '">' . $row['subject'] . '</a>';
468
			$smcFunc['db_free_result']($request);
469
		}
470
	}
471
}
472
473
/**
474
 * Converts HTML-entities to their UTF-8 character equivalents.
475
 * This requires the admin_forum permission.
476
 * Pre-condition: UTF-8 has been set as database and global character set.
477
 *
478
 * It is divided in steps of 10 seconds.
479
 * This action is linked from the maintenance screen (if applicable).
480
 * It is accessed by ?action=admin;area=maintain;sa=database;activity=convertentities.
481
 *
482
 * @uses template_convert_entities()
483
 */
484
function ConvertEntities()
485
{
486
	global $db_character_set, $modSettings, $context, $smcFunc, $db_type, $db_prefix;
487
488
	isAllowedTo('admin_forum');
489
490
	// Check to see if UTF-8 is currently the default character set.
491
	if ($modSettings['global_character_set'] !== 'UTF-8')
492
		fatal_lang_error('entity_convert_only_utf8');
493
494
	// Some starting values.
495
	$context['table'] = empty($_REQUEST['table']) ? 0 : (int) $_REQUEST['table'];
496
	$context['start'] = empty($_REQUEST['start']) ? 0 : (int) $_REQUEST['start'];
497
498
	$context['start_time'] = time();
499
500
	$context['first_step'] = !isset($_REQUEST[$context['session_var']]);
501
	$context['last_step'] = false;
502
503
	// The first step is just a text screen with some explanation.
504
	if ($context['first_step'])
505
	{
506
		validateToken('admin-maint');
507
		createToken('admin-maint');
508
509
		$context['sub_template'] = 'convert_entities';
510
		return;
511
	}
512
	// Otherwise use the generic "not done" template.
513
	$context['sub_template'] = 'not_done';
514
	$context['continue_post_data'] = '';
515
	$context['continue_countdown'] = 3;
516
517
	// Now we're actually going to convert...
518
	checkSession('request');
519
	validateToken('admin-maint');
520
	createToken('admin-maint');
521
	$context['not_done_token'] = 'admin-maint';
522
523
	// A list of tables ready for conversion.
524
	$tables = array(
525
		'ban_groups',
526
		'ban_items',
527
		'boards',
528
		'calendar',
529
		'calendar_holidays',
530
		'categories',
531
		'log_errors',
532
		'log_search_subjects',
533
		'membergroups',
534
		'members',
535
		'message_icons',
536
		'messages',
537
		'package_servers',
538
		'personal_messages',
539
		'pm_recipients',
540
		'polls',
541
		'poll_choices',
542
		'smileys',
543
		'themes',
544
	);
545
	$context['num_tables'] = count($tables);
546
547
	// Loop through all tables that need converting.
548
	for (; $context['table'] < $context['num_tables']; $context['table']++)
549
	{
550
		$cur_table = $tables[$context['table']];
551
		$primary_key = '';
552
		// Make sure we keep stuff unique!
553
		$primary_keys = array();
554
555
		if (function_exists('apache_reset_timeout'))
556
			@apache_reset_timeout();
557
558
		// Get a list of text columns.
559
		$columns = array();
560
		if ($db_type == 'postgresql')
561
			$request = $smcFunc['db_query']('', '
562
				SELECT column_name "Field", data_type "Type"
563
				FROM information_schema.columns
564
				WHERE table_name = {string:cur_table}
565
					AND (data_type = \'character varying\' or data_type = \'text\')',
566
				array(
567
					'cur_table' => $db_prefix . $cur_table,
568
				)
569
			);
570
		else
571
			$request = $smcFunc['db_query']('', '
572
				SHOW FULL COLUMNS
573
				FROM {db_prefix}{raw:cur_table}',
574
				array(
575
					'cur_table' => $cur_table,
576
				)
577
			);
578
		while ($column_info = $smcFunc['db_fetch_assoc']($request))
579
			if (strpos($column_info['Type'], 'text') !== false || strpos($column_info['Type'], 'char') !== false)
580
				$columns[] = strtolower($column_info['Field']);
581
582
		// Get the column with the (first) primary key.
583
		if ($db_type == 'postgresql')
584
			$request = $smcFunc['db_query']('', '
585
				SELECT a.attname "Column_name", \'PRIMARY\' "Key_name", attnum "Seq_in_index"
586
				FROM   pg_index i
587
				JOIN   pg_attribute a ON a.attrelid = i.indrelid
588
					AND a.attnum = ANY(i.indkey)
589
				WHERE  i.indrelid = {string:cur_table}::regclass
590
					AND    i.indisprimary',
591
				array(
592
					'cur_table' => $db_prefix . $cur_table,
593
				)
594
			);
595
		else
596
			$request = $smcFunc['db_query']('', '
597
				SHOW KEYS
598
				FROM {db_prefix}{raw:cur_table}',
599
				array(
600
					'cur_table' => $cur_table,
601
				)
602
			);
603
		while ($row = $smcFunc['db_fetch_assoc']($request))
604
		{
605
			if ($row['Key_name'] === 'PRIMARY')
606
			{
607
				if ((empty($primary_key) || $row['Seq_in_index'] == 1) && !in_array(strtolower($row['Column_name']), $columns))
608
					$primary_key = $row['Column_name'];
609
610
				$primary_keys[] = $row['Column_name'];
611
			}
612
		}
613
		$smcFunc['db_free_result']($request);
614
615
		// No primary key, no glory.
616
		// Same for columns. Just to be sure we've work to do!
617
		if (empty($primary_key) || empty($columns))
618
			continue;
619
620
		// Get the maximum value for the primary key.
621
		$request = $smcFunc['db_query']('', '
622
			SELECT MAX({identifier:key})
623
			FROM {db_prefix}{raw:cur_table}',
624
			array(
625
				'key' => $primary_key,
626
				'cur_table' => $cur_table,
627
			)
628
		);
629
		list($max_value) = $smcFunc['db_fetch_row']($request);
630
		$smcFunc['db_free_result']($request);
631
632
		if (empty($max_value))
633
			continue;
634
635
		while ($context['start'] <= $max_value)
636
		{
637
			// Retrieve a list of rows that has at least one entity to convert.
638
			$request = $smcFunc['db_query']('', '
639
				SELECT {raw:primary_keys}, {raw:columns}
640
				FROM {db_prefix}{raw:cur_table}
641
				WHERE {raw:primary_key} BETWEEN {int:start} AND {int:start} + 499
642
					AND {raw:like_compare}
643
				LIMIT 500',
644
				array(
645
					'primary_keys' => implode(', ', $primary_keys),
646
					'columns' => implode(', ', $columns),
647
					'cur_table' => $cur_table,
648
					'primary_key' => $primary_key,
649
					'start' => $context['start'],
650
					'like_compare' => '(' . implode(' LIKE \'%&#%\' OR ', $columns) . ' LIKE \'%&#%\')',
651
				)
652
			);
653
			while ($row = $smcFunc['db_fetch_assoc']($request))
654
			{
655
				$insertion_variables = array();
656
				$changes = array();
657
				foreach ($row as $column_name => $column_value)
658
					if ($column_name !== $primary_key && strpos($column_value, '&#') !== false)
659
					{
660
						$changes[] = $column_name . ' = {string:changes_' . $column_name . '}';
661
						$insertion_variables['changes_' . $column_name] = preg_replace_callback('~&#(\d{1,5}|x[0-9a-fA-F]{1,4});~', 'fixchardb__callback', $column_value);
662
					}
663
664
				$where = array();
665
				foreach ($primary_keys as $key)
666
				{
667
					$where[] = $key . ' = {string:where_' . $key . '}';
668
					$insertion_variables['where_' . $key] = $row[$key];
669
				}
670
671
				// Update the row.
672
				if (!empty($changes))
673
					$smcFunc['db_query']('', '
674
						UPDATE {db_prefix}' . $cur_table . '
675
						SET
676
							' . implode(',
677
							', $changes) . '
678
						WHERE ' . implode(' AND ', $where),
679
						$insertion_variables
680
					);
681
			}
682
			$smcFunc['db_free_result']($request);
683
			$context['start'] += 500;
684
685
			// After ten seconds interrupt.
686
			if (time() - $context['start_time'] > 10)
687
			{
688
				// Calculate an approximation of the percentage done.
689
				$context['continue_percent'] = round(100 * ($context['table'] + ($context['start'] / $max_value)) / $context['num_tables'], 1);
690
				$context['continue_get_data'] = '?action=admin;area=maintain;sa=database;activity=convertentities;table=' . $context['table'] . ';start=' . $context['start'] . ';' . $context['session_var'] . '=' . $context['session_id'];
691
				return;
692
			}
693
		}
694
		$context['start'] = 0;
695
	}
696
697
	// If we're here, we must be done.
698
	$context['continue_percent'] = 100;
699
	$context['continue_get_data'] = '?action=admin;area=maintain;sa=database;done=convertentities';
700
	$context['last_step'] = true;
701
	$context['continue_countdown'] = 3;
702
}
703
704
/**
705
 * Optimizes all tables in the database and lists how much was saved.
706
 * It requires the admin_forum permission.
707
 * It shows as the maintain_forum admin area.
708
 * It is accessed from ?action=admin;area=maintain;sa=database;activity=optimize.
709
 * It also updates the optimize scheduled task such that the tables are not automatically optimized again too soon.
710
 *
711
 * @uses template_optimize()
712
 */
713
function OptimizeTables()
714
{
715
	global $db_prefix, $txt, $context, $smcFunc;
716
717
	isAllowedTo('admin_forum');
718
719
	checkSession('request');
720
721
	if (!isset($_SESSION['optimized_tables']))
722
		validateToken('admin-maint');
723
	else
724
		validateToken('admin-optimize', 'post', false);
725
726
	ignore_user_abort(true);
727
	db_extend();
728
729
	$context['page_title'] = $txt['database_optimize'];
730
	$context['sub_template'] = 'optimize';
731
	$context['continue_post_data'] = '';
732
	$context['continue_countdown'] = 3;
733
734
	// Only optimize the tables related to this smf install, not all the tables in the db
735
	$real_prefix = preg_match('~^(`?)(.+?)\\1\\.(.*?)$~', $db_prefix, $match) === 1 ? $match[3] : $db_prefix;
736
737
	// Get a list of tables, as well as how many there are.
738
	$temp_tables = $smcFunc['db_list_tables'](false, $real_prefix . '%');
739
	$tables = array();
740
	foreach ($temp_tables as $table)
741
		$tables[] = array('table_name' => $table);
742
743
	// If there aren't any tables then I believe that would mean the world has exploded...
744
	$context['num_tables'] = count($tables);
745
	if ($context['num_tables'] == 0)
746
		fatal_error('You appear to be running SMF in a flat file mode... fantastic!', false);
747
748
	$_REQUEST['start'] = empty($_REQUEST['start']) ? 0 : (int) $_REQUEST['start'];
749
750
	// Try for extra time due to large tables.
751
	@set_time_limit(100);
752
753
	// For each table....
754
	$_SESSION['optimized_tables'] = !empty($_SESSION['optimized_tables']) ? $_SESSION['optimized_tables'] : array();
755
	for ($key = $_REQUEST['start']; $context['num_tables'] - 1; $key++)
756
	{
757
		if (empty($tables[$key]))
758
			break;
759
760
		// Continue?
761
		if (microtime(true) - TIME_START > 10)
762
		{
763
			$_REQUEST['start'] = $key;
764
			$context['continue_get_data'] = '?action=admin;area=maintain;sa=database;activity=optimize;start=' . $_REQUEST['start'] . ';' . $context['session_var'] . '=' . $context['session_id'];
765
			$context['continue_percent'] = round(100 * $_REQUEST['start'] / $context['num_tables']);
766
			$context['sub_template'] = 'not_done';
767
			$context['page_title'] = $txt['not_done_title'];
768
769
			createToken('admin-optimize');
770
			$context['continue_post_data'] = '<input type="hidden" name="' . $context['admin-optimize_token_var'] . '" value="' . $context['admin-optimize_token'] . '">';
771
772
			if (function_exists('apache_reset_timeout'))
773
				apache_reset_timeout();
774
775
			return;
776
		}
777
778
		// Optimize the table!  We use backticks here because it might be a custom table.
779
		$data_freed = $smcFunc['db_optimize_table']($tables[$key]['table_name']);
780
781
		if ($data_freed > 0)
782
			$_SESSION['optimized_tables'][] = array(
783
				'name' => $tables[$key]['table_name'],
784
				'data_freed' => $data_freed,
785
			);
786
	}
787
788
	// Number of tables, etc...
789
	$txt['database_numb_tables'] = sprintf($txt['database_numb_tables'], $context['num_tables']);
790
	$context['num_tables_optimized'] = count($_SESSION['optimized_tables']);
791
	$context['optimized_tables'] = $_SESSION['optimized_tables'];
792
	unset($_SESSION['optimized_tables']);
793
}
794
795
/**
796
 * Recount many forum totals that can be recounted automatically without harm.
797
 * it requires the admin_forum permission.
798
 * It shows the maintain_forum admin area.
799
 *
800
 * Totals recounted:
801
 * - fixes for topics with wrong num_replies.
802
 * - updates for num_posts and num_topics of all boards.
803
 * - recounts instant_messages but not unread_messages.
804
 * - repairs messages pointing to boards with topics pointing to other boards.
805
 * - updates the last message posted in boards and children.
806
 * - updates member count, latest member, topic count, and message count.
807
 *
808
 * The function redirects back to ?action=admin;area=maintain when complete.
809
 * It is accessed via ?action=admin;area=maintain;sa=database;activity=recount.
810
 */
811
function AdminBoardRecount()
812
{
813
	global $txt, $context, $modSettings, $sourcedir, $smcFunc;
814
815
	isAllowedTo('admin_forum');
816
	checkSession('request');
817
818
	// validate the request or the loop
819
	validateToken(!isset($_REQUEST['step']) ? 'admin-maint' : 'admin-boardrecount');
820
	$context['not_done_token'] = 'admin-boardrecount';
821
	createToken($context['not_done_token']);
822
	
823
	$context['page_title'] = $txt['not_done_title'];
824
	$context['continue_post_data'] = '';
825
	$context['continue_countdown'] = 3;
826
	$context['sub_template'] = 'not_done';
827
828
	// Try for as much time as possible.
829
	@set_time_limit(600);
830
831
	// Step the number of topics at a time so things don't time out...
832
	$request = $smcFunc['db_query']('', '
833
		SELECT MAX(id_topic)
834
		FROM {db_prefix}topics',
835
		array(
836
		)
837
	);
838
	list ($max_topics) = $smcFunc['db_fetch_row']($request);
839
	$smcFunc['db_free_result']($request);
840
841
	$increment = min(max(50, ceil($max_topics / 4)), 2000);
842
	if (empty($_REQUEST['start']))
843
		$_REQUEST['start'] = 0;
844
845
	$total_steps = 8;
846
847
	// Get each topic with a wrong reply count and fix it - let's just do some at a time, though.
848
	if (empty($_REQUEST['step']))
849
	{
850
		$_REQUEST['step'] = 0;
851
852
		while ($_REQUEST['start'] < $max_topics)
853
		{
854
			// Recount approved messages
855
			$request = $smcFunc['db_query']('', '
856
				SELECT t.id_topic, MAX(t.num_replies) AS num_replies,
857
					GREATEST(COUNT(ma.id_msg) - 1, 0) AS real_num_replies
858
				FROM {db_prefix}topics AS t
859
					LEFT JOIN {db_prefix}messages AS ma ON (ma.id_topic = t.id_topic AND ma.approved = {int:is_approved})
860
				WHERE t.id_topic > {int:start}
861
					AND t.id_topic <= {int:max_id}
862
				GROUP BY t.id_topic
863
				HAVING GREATEST(COUNT(ma.id_msg) - 1, 0) != MAX(t.num_replies)',
864
				array(
865
					'is_approved' => 1,
866
					'start' => $_REQUEST['start'],
867
					'max_id' => $_REQUEST['start'] + $increment,
868
				)
869
			);
870
			while ($row = $smcFunc['db_fetch_assoc']($request))
871
				$smcFunc['db_query']('', '
872
					UPDATE {db_prefix}topics
873
					SET num_replies = {int:num_replies}
874
					WHERE id_topic = {int:id_topic}',
875
					array(
876
						'num_replies' => $row['real_num_replies'],
877
						'id_topic' => $row['id_topic'],
878
					)
879
				);
880
			$smcFunc['db_free_result']($request);
881
882
			// Recount unapproved messages
883
			$request = $smcFunc['db_query']('', '
884
				SELECT t.id_topic, MAX(t.unapproved_posts) AS unapproved_posts,
885
					COUNT(mu.id_msg) AS real_unapproved_posts
886
				FROM {db_prefix}topics AS t
887
					LEFT JOIN {db_prefix}messages AS mu ON (mu.id_topic = t.id_topic AND mu.approved = {int:not_approved})
888
				WHERE t.id_topic > {int:start}
889
					AND t.id_topic <= {int:max_id}
890
				GROUP BY t.id_topic
891
				HAVING COUNT(mu.id_msg) != MAX(t.unapproved_posts)',
892
				array(
893
					'not_approved' => 0,
894
					'start' => $_REQUEST['start'],
895
					'max_id' => $_REQUEST['start'] + $increment,
896
				)
897
			);
898
			while ($row = $smcFunc['db_fetch_assoc']($request))
899
				$smcFunc['db_query']('', '
900
					UPDATE {db_prefix}topics
901
					SET unapproved_posts = {int:unapproved_posts}
902
					WHERE id_topic = {int:id_topic}',
903
					array(
904
						'unapproved_posts' => $row['real_unapproved_posts'],
905
						'id_topic' => $row['id_topic'],
906
					)
907
				);
908
			$smcFunc['db_free_result']($request);
909
910
			$_REQUEST['start'] += $increment;
911
912
			if (microtime(true) - TIME_START > 3)
913
			{
914
				$context['continue_get_data'] = '?action=admin;area=maintain;sa=routine;activity=recount;step=0;start=' . $_REQUEST['start'] . ';' . $context['session_var'] . '=' . $context['session_id'];
915
				$context['continue_percent'] = round((100 * $_REQUEST['start'] / $max_topics) / $total_steps);
916
917
				return;
918
			}
919
		}
920
921
		$_REQUEST['start'] = 0;
922
	}
923
924
	// Update the post count of each board.
925
	if ($_REQUEST['step'] <= 1)
926
	{
927
		if (empty($_REQUEST['start']))
928
			$smcFunc['db_query']('', '
929
				UPDATE {db_prefix}boards
930
				SET num_posts = {int:num_posts}
931
				WHERE redirect = {string:redirect}',
932
				array(
933
					'num_posts' => 0,
934
					'redirect' => '',
935
				)
936
			);
937
938
		while ($_REQUEST['start'] < $max_topics)
939
		{
940
			$request = $smcFunc['db_query']('', '
941
				SELECT m.id_board, COUNT(*) AS real_num_posts
942
				FROM {db_prefix}messages AS m
943
				WHERE m.id_topic > {int:id_topic_min}
944
					AND m.id_topic <= {int:id_topic_max}
945
					AND m.approved = {int:is_approved}
946
				GROUP BY m.id_board',
947
				array(
948
					'id_topic_min' => $_REQUEST['start'],
949
					'id_topic_max' => $_REQUEST['start'] + $increment,
950
					'is_approved' => 1,
951
				)
952
			);
953
			while ($row = $smcFunc['db_fetch_assoc']($request))
954
				$smcFunc['db_query']('', '
955
					UPDATE {db_prefix}boards
956
					SET num_posts = num_posts + {int:real_num_posts}
957
					WHERE id_board = {int:id_board}',
958
					array(
959
						'id_board' => $row['id_board'],
960
						'real_num_posts' => $row['real_num_posts'],
961
					)
962
				);
963
			$smcFunc['db_free_result']($request);
964
965
			$_REQUEST['start'] += $increment;
966
967
			if (microtime(true) - TIME_START > 3)
968
			{
969
				$context['continue_get_data'] = '?action=admin;area=maintain;sa=routine;activity=recount;step=1;start=' . $_REQUEST['start'] . ';' . $context['session_var'] . '=' . $context['session_id'];
970
				$context['continue_percent'] = round((200 + 100 * $_REQUEST['start'] / $max_topics) / $total_steps);
971
972
				return;
973
			}
974
		}
975
976
		$_REQUEST['start'] = 0;
977
	}
978
979
	// Update the topic count of each board.
980
	if ($_REQUEST['step'] <= 2)
981
	{
982
		if (empty($_REQUEST['start']))
983
			$smcFunc['db_query']('', '
984
				UPDATE {db_prefix}boards
985
				SET num_topics = {int:num_topics}',
986
				array(
987
					'num_topics' => 0,
988
				)
989
			);
990
991
		while ($_REQUEST['start'] < $max_topics)
992
		{
993
			$request = $smcFunc['db_query']('', '
994
				SELECT t.id_board, COUNT(*) AS real_num_topics
995
				FROM {db_prefix}topics AS t
996
				WHERE t.approved = {int:is_approved}
997
					AND t.id_topic > {int:id_topic_min}
998
					AND t.id_topic <= {int:id_topic_max}
999
				GROUP BY t.id_board',
1000
				array(
1001
					'is_approved' => 1,
1002
					'id_topic_min' => $_REQUEST['start'],
1003
					'id_topic_max' => $_REQUEST['start'] + $increment,
1004
				)
1005
			);
1006
			while ($row = $smcFunc['db_fetch_assoc']($request))
1007
				$smcFunc['db_query']('', '
1008
					UPDATE {db_prefix}boards
1009
					SET num_topics = num_topics + {int:real_num_topics}
1010
					WHERE id_board = {int:id_board}',
1011
					array(
1012
						'id_board' => $row['id_board'],
1013
						'real_num_topics' => $row['real_num_topics'],
1014
					)
1015
				);
1016
			$smcFunc['db_free_result']($request);
1017
1018
			$_REQUEST['start'] += $increment;
1019
1020
			if (microtime(true) - TIME_START > 3)
1021
			{
1022
				$context['continue_get_data'] = '?action=admin;area=maintain;sa=routine;activity=recount;step=2;start=' . $_REQUEST['start'] . ';' . $context['session_var'] . '=' . $context['session_id'];
1023
				$context['continue_percent'] = round((300 + 100 * $_REQUEST['start'] / $max_topics) / $total_steps);
1024
1025
				return;
1026
			}
1027
		}
1028
1029
		$_REQUEST['start'] = 0;
1030
	}
1031
1032
	// Update the unapproved post count of each board.
1033
	if ($_REQUEST['step'] <= 3)
1034
	{
1035
		if (empty($_REQUEST['start']))
1036
			$smcFunc['db_query']('', '
1037
				UPDATE {db_prefix}boards
1038
				SET unapproved_posts = {int:unapproved_posts}',
1039
				array(
1040
					'unapproved_posts' => 0,
1041
				)
1042
			);
1043
1044
		while ($_REQUEST['start'] < $max_topics)
1045
		{
1046
			$request = $smcFunc['db_query']('', '
1047
				SELECT m.id_board, COUNT(*) AS real_unapproved_posts
1048
				FROM {db_prefix}messages AS m
1049
				WHERE m.id_topic > {int:id_topic_min}
1050
					AND m.id_topic <= {int:id_topic_max}
1051
					AND m.approved = {int:is_approved}
1052
				GROUP BY m.id_board',
1053
				array(
1054
					'id_topic_min' => $_REQUEST['start'],
1055
					'id_topic_max' => $_REQUEST['start'] + $increment,
1056
					'is_approved' => 0,
1057
				)
1058
			);
1059
			while ($row = $smcFunc['db_fetch_assoc']($request))
1060
				$smcFunc['db_query']('', '
1061
					UPDATE {db_prefix}boards
1062
					SET unapproved_posts = unapproved_posts + {int:unapproved_posts}
1063
					WHERE id_board = {int:id_board}',
1064
					array(
1065
						'id_board' => $row['id_board'],
1066
						'unapproved_posts' => $row['real_unapproved_posts'],
1067
					)
1068
				);
1069
			$smcFunc['db_free_result']($request);
1070
1071
			$_REQUEST['start'] += $increment;
1072
1073
			if (microtime(true) - TIME_START > 3)
1074
			{
1075
				$context['continue_get_data'] = '?action=admin;area=maintain;sa=routine;activity=recount;step=3;start=' . $_REQUEST['start'] . ';' . $context['session_var'] . '=' . $context['session_id'];
1076
				$context['continue_percent'] = round((400 + 100 * $_REQUEST['start'] / $max_topics) / $total_steps);
1077
1078
				return;
1079
			}
1080
		}
1081
1082
		$_REQUEST['start'] = 0;
1083
	}
1084
1085
	// Update the unapproved topic count of each board.
1086
	if ($_REQUEST['step'] <= 4)
1087
	{
1088
		if (empty($_REQUEST['start']))
1089
			$smcFunc['db_query']('', '
1090
				UPDATE {db_prefix}boards
1091
				SET unapproved_topics = {int:unapproved_topics}',
1092
				array(
1093
					'unapproved_topics' => 0,
1094
				)
1095
			);
1096
1097
		while ($_REQUEST['start'] < $max_topics)
1098
		{
1099
			$request = $smcFunc['db_query']('', '
1100
				SELECT t.id_board, COUNT(*) AS real_unapproved_topics
1101
				FROM {db_prefix}topics AS t
1102
				WHERE t.approved = {int:is_approved}
1103
					AND t.id_topic > {int:id_topic_min}
1104
					AND t.id_topic <= {int:id_topic_max}
1105
				GROUP BY t.id_board',
1106
				array(
1107
					'is_approved' => 0,
1108
					'id_topic_min' => $_REQUEST['start'],
1109
					'id_topic_max' => $_REQUEST['start'] + $increment,
1110
				)
1111
			);
1112
			while ($row = $smcFunc['db_fetch_assoc']($request))
1113
				$smcFunc['db_query']('', '
1114
					UPDATE {db_prefix}boards
1115
					SET unapproved_topics = unapproved_topics + {int:real_unapproved_topics}
1116
					WHERE id_board = {int:id_board}',
1117
					array(
1118
						'id_board' => $row['id_board'],
1119
						'real_unapproved_topics' => $row['real_unapproved_topics'],
1120
					)
1121
				);
1122
			$smcFunc['db_free_result']($request);
1123
1124
			$_REQUEST['start'] += $increment;
1125
1126
			if (microtime(true) - TIME_START > 3)
1127
			{
1128
				$context['continue_get_data'] = '?action=admin;area=maintain;sa=routine;activity=recount;step=4;start=' . $_REQUEST['start'] . ';' . $context['session_var'] . '=' . $context['session_id'];
1129
				$context['continue_percent'] = round((500 + 100 * $_REQUEST['start'] / $max_topics) / $total_steps);
1130
1131
				return;
1132
			}
1133
		}
1134
1135
		$_REQUEST['start'] = 0;
1136
	}
1137
1138
	// Get all members with wrong number of personal messages.
1139
	if ($_REQUEST['step'] <= 5)
1140
	{
1141
		$request = $smcFunc['db_query']('', '
1142
			SELECT mem.id_member, COUNT(pmr.id_pm) AS real_num,
1143
				MAX(mem.instant_messages) AS instant_messages
1144
			FROM {db_prefix}members AS mem
1145
				LEFT JOIN {db_prefix}pm_recipients AS pmr ON (mem.id_member = pmr.id_member AND pmr.deleted = {int:is_not_deleted})
1146
			GROUP BY mem.id_member
1147
			HAVING COUNT(pmr.id_pm) != MAX(mem.instant_messages)',
1148
			array(
1149
				'is_not_deleted' => 0,
1150
			)
1151
		);
1152
		while ($row = $smcFunc['db_fetch_assoc']($request))
1153
			updateMemberData($row['id_member'], array('instant_messages' => $row['real_num']));
1154
		$smcFunc['db_free_result']($request);
1155
1156
		$request = $smcFunc['db_query']('', '
1157
			SELECT mem.id_member, COUNT(pmr.id_pm) AS real_num,
1158
				MAX(mem.unread_messages) AS unread_messages
1159
			FROM {db_prefix}members AS mem
1160
				LEFT JOIN {db_prefix}pm_recipients AS pmr ON (mem.id_member = pmr.id_member AND pmr.deleted = {int:is_not_deleted} AND pmr.is_read = {int:is_not_read})
1161
			GROUP BY mem.id_member
1162
			HAVING COUNT(pmr.id_pm) != MAX(mem.unread_messages)',
1163
			array(
1164
				'is_not_deleted' => 0,
1165
				'is_not_read' => 0,
1166
			)
1167
		);
1168
		while ($row = $smcFunc['db_fetch_assoc']($request))
1169
			updateMemberData($row['id_member'], array('unread_messages' => $row['real_num']));
1170
		$smcFunc['db_free_result']($request);
1171
1172
		if (microtime(true) - TIME_START > 3)
1173
		{
1174
			$context['continue_get_data'] = '?action=admin;area=maintain;sa=routine;activity=recount;step=6;start=0;' . $context['session_var'] . '=' . $context['session_id'];
1175
			$context['continue_percent'] = round(700 / $total_steps);
1176
1177
			return;
1178
		}
1179
	}
1180
1181
	// Any messages pointing to the wrong board?
1182
	if ($_REQUEST['step'] <= 6)
1183
	{
1184
		while ($_REQUEST['start'] < $modSettings['maxMsgID'])
1185
		{
1186
			$request = $smcFunc['db_query']('', '
1187
				SELECT t.id_board, m.id_msg
1188
				FROM {db_prefix}messages AS m
1189
					INNER JOIN {db_prefix}topics AS t ON (t.id_topic = m.id_topic AND t.id_board != m.id_board)
1190
				WHERE m.id_msg > {int:id_msg_min}
1191
					AND m.id_msg <= {int:id_msg_max}',
1192
				array(
1193
					'id_msg_min' => $_REQUEST['start'],
1194
					'id_msg_max' => $_REQUEST['start'] + $increment,
1195
				)
1196
			);
1197
			$boards = array();
1198
			while ($row = $smcFunc['db_fetch_assoc']($request))
1199
				$boards[$row['id_board']][] = $row['id_msg'];
1200
1201
			$smcFunc['db_free_result']($request);
1202
1203
			foreach ($boards as $board_id => $messages)
1204
				$smcFunc['db_query']('', '
1205
					UPDATE {db_prefix}messages
1206
					SET id_board = {int:id_board}
1207
					WHERE id_msg IN ({array_int:id_msg_array})',
1208
					array(
1209
						'id_msg_array' => $messages,
1210
						'id_board' => $board_id,
1211
					)
1212
				);
1213
1214
			$_REQUEST['start'] += $increment;
1215
1216
			if (microtime(true) - TIME_START > 3)
1217
			{
1218
				$context['continue_get_data'] = '?action=admin;area=maintain;sa=routine;activity=recount;step=6;start=' . $_REQUEST['start'] . ';' . $context['session_var'] . '=' . $context['session_id'];
1219
				$context['continue_percent'] = round((700 + 100 * $_REQUEST['start'] / $modSettings['maxMsgID']) / $total_steps);
1220
1221
				return;
1222
			}
1223
		}
1224
1225
		$_REQUEST['start'] = 0;
1226
	}
1227
1228
	// Update the latest message of each board.
1229
	$request = $smcFunc['db_query']('', '
1230
		SELECT m.id_board, MAX(m.id_msg) AS local_last_msg
1231
		FROM {db_prefix}messages AS m
1232
		WHERE m.approved = {int:is_approved}
1233
		GROUP BY m.id_board',
1234
		array(
1235
			'is_approved' => 1,
1236
		)
1237
	);
1238
	$realBoardCounts = array();
1239
	while ($row = $smcFunc['db_fetch_assoc']($request))
1240
		$realBoardCounts[$row['id_board']] = $row['local_last_msg'];
1241
	$smcFunc['db_free_result']($request);
1242
1243
	$request = $smcFunc['db_query']('', '
1244
		SELECT id_board, id_parent, id_last_msg, child_level, id_msg_updated
1245
		FROM {db_prefix}boards',
1246
		array(
1247
		)
1248
	);
1249
	$resort_me = array();
1250
	while ($row = $smcFunc['db_fetch_assoc']($request))
1251
	{
1252
		$row['local_last_msg'] = isset($realBoardCounts[$row['id_board']]) ? $realBoardCounts[$row['id_board']] : 0;
1253
		$resort_me[$row['child_level']][] = $row;
1254
	}
1255
	$smcFunc['db_free_result']($request);
1256
1257
	krsort($resort_me);
1258
1259
	$lastModifiedMsg = array();
1260
	foreach ($resort_me as $rows)
1261
		foreach ($rows as $row)
1262
		{
1263
			// The latest message is the latest of the current board and its children.
1264
			if (isset($lastModifiedMsg[$row['id_board']]))
1265
				$curLastModifiedMsg = max($row['local_last_msg'], $lastModifiedMsg[$row['id_board']]);
1266
			else
1267
				$curLastModifiedMsg = $row['local_last_msg'];
1268
1269
			// If what is and what should be the latest message differ, an update is necessary.
1270
			if ($row['local_last_msg'] != $row['id_last_msg'] || $curLastModifiedMsg != $row['id_msg_updated'])
1271
				$smcFunc['db_query']('', '
1272
					UPDATE {db_prefix}boards
1273
					SET id_last_msg = {int:id_last_msg}, id_msg_updated = {int:id_msg_updated}
1274
					WHERE id_board = {int:id_board}',
1275
					array(
1276
						'id_last_msg' => $row['local_last_msg'],
1277
						'id_msg_updated' => $curLastModifiedMsg,
1278
						'id_board' => $row['id_board'],
1279
					)
1280
				);
1281
1282
			// Parent boards inherit the latest modified message of their children.
1283
			if (isset($lastModifiedMsg[$row['id_parent']]))
1284
				$lastModifiedMsg[$row['id_parent']] = max($row['local_last_msg'], $lastModifiedMsg[$row['id_parent']]);
1285
			else
1286
				$lastModifiedMsg[$row['id_parent']] = $row['local_last_msg'];
1287
		}
1288
1289
	// Update all the basic statistics.
1290
	updateStats('member');
1291
	updateStats('message');
1292
	updateStats('topic');
1293
1294
	// Finally, update the latest event times.
1295
	require_once($sourcedir . '/ScheduledTasks.php');
1296
	CalculateNextTrigger();
1297
1298
	redirectexit('action=admin;area=maintain;sa=routine;done=recount');
1299
}
1300
1301
/**
1302
 * Perform a detailed version check.  A very good thing ;).
1303
 * The function parses the comment headers in all files for their version information,
1304
 * and outputs that for some javascript to check with simplemachines.org.
1305
 * It does not connect directly with simplemachines.org, but rather expects the client to.
1306
 *
1307
 * It requires the admin_forum permission.
1308
 * Uses the view_versions admin area.
1309
 * Accessed through ?action=admin;area=maintain;sa=routine;activity=version.
1310
 *
1311
 * @uses template_view_versions()
1312
 */
1313
function VersionDetail()
1314
{
1315
	global $txt, $sourcedir, $context;
1316
1317
	isAllowedTo('admin_forum');
1318
1319
	// Call the function that'll get all the version info we need.
1320
	require_once($sourcedir . '/Subs-Admin.php');
1321
	$versionOptions = array(
1322
		'include_ssi' => true,
1323
		'include_subscriptions' => true,
1324
		'include_tasks' => true,
1325
		'sort_results' => true,
1326
	);
1327
	$version_info = getFileVersions($versionOptions);
1328
1329
	// Add the new info to the template context.
1330
	$context += array(
1331
		'file_versions' => $version_info['file_versions'],
1332
		'default_template_versions' => $version_info['default_template_versions'],
1333
		'template_versions' => $version_info['template_versions'],
1334
		'default_language_versions' => $version_info['default_language_versions'],
1335
		'default_known_languages' => array_keys($version_info['default_language_versions']),
1336
		'tasks_versions' => $version_info['tasks_versions'],
1337
	);
1338
1339
	// Make it easier to manage for the template.
1340
	$context['forum_version'] = SMF_FULL_VERSION;
1341
1342
	$context['sub_template'] = 'view_versions';
1343
	$context['page_title'] = $txt['admin_version_check'];
1344
}
1345
1346
/**
1347
 * Re-attribute posts.
1348
 */
1349
function MaintainReattributePosts()
1350
{
1351
	global $sourcedir, $context, $txt;
1352
1353
	checkSession();
1354
1355
	// Find the member.
1356
	require_once($sourcedir . '/Subs-Auth.php');
1357
	$members = findMembers($_POST['to']);
1358
1359
	if (empty($members))
1360
		fatal_lang_error('reattribute_cannot_find_member');
1361
1362
	$memID = array_shift($members);
1363
	$memID = $memID['id'];
1364
1365
	$email = $_POST['type'] == 'email' ? $_POST['from_email'] : '';
1366
	$membername = $_POST['type'] == 'name' ? $_POST['from_name'] : '';
1367
1368
	// Now call the reattribute function.
1369
	require_once($sourcedir . '/Subs-Members.php');
1370
	reattributePosts($memID, $email, $membername, !empty($_POST['posts']));
1371
1372
	$context['maintenance_finished'] = $txt['maintain_reattribute_posts'];
1373
}
1374
1375
/**
1376
 * Removing old members. Done and out!
1377
 *
1378
 * @todo refactor
1379
 */
1380
function MaintainPurgeInactiveMembers()
1381
{
1382
	global $sourcedir, $context, $smcFunc, $txt;
1383
1384
	$_POST['maxdays'] = empty($_POST['maxdays']) ? 0 : (int) $_POST['maxdays'];
1385
	if (!empty($_POST['groups']) && $_POST['maxdays'] > 0)
1386
	{
1387
		checkSession();
1388
		validateToken('admin-maint');
1389
1390
		$groups = array();
1391
		foreach ($_POST['groups'] as $id => $dummy)
1392
			$groups[] = (int) $id;
1393
		$time_limit = (time() - ($_POST['maxdays'] * 24 * 3600));
1394
		$where_vars = array(
1395
			'time_limit' => $time_limit,
1396
		);
1397
		if ($_POST['del_type'] == 'activated')
1398
		{
1399
			$where = 'mem.date_registered < {int:time_limit} AND mem.is_activated = {int:is_activated}';
1400
			$where_vars['is_activated'] = 0;
1401
		}
1402
		else
1403
			$where = 'mem.last_login < {int:time_limit} AND (mem.last_login != 0 OR mem.date_registered < {int:time_limit})';
1404
1405
		// Need to get *all* groups then work out which (if any) we avoid.
1406
		$request = $smcFunc['db_query']('', '
1407
			SELECT id_group, group_name, min_posts
1408
			FROM {db_prefix}membergroups',
1409
			array(
1410
			)
1411
		);
1412
		while ($row = $smcFunc['db_fetch_assoc']($request))
1413
		{
1414
			// Avoid this one?
1415
			if (!in_array($row['id_group'], $groups))
1416
			{
1417
				// Post group?
1418
				if ($row['min_posts'] != -1)
1419
				{
1420
					$where .= ' AND mem.id_post_group != {int:id_post_group_' . $row['id_group'] . '}';
1421
					$where_vars['id_post_group_' . $row['id_group']] = $row['id_group'];
1422
				}
1423
				else
1424
				{
1425
					$where .= ' AND mem.id_group != {int:id_group_' . $row['id_group'] . '} AND FIND_IN_SET({int:id_group_' . $row['id_group'] . '}, mem.additional_groups) = 0';
1426
					$where_vars['id_group_' . $row['id_group']] = $row['id_group'];
1427
				}
1428
			}
1429
		}
1430
		$smcFunc['db_free_result']($request);
1431
1432
		// If we have ungrouped unselected we need to avoid those guys.
1433
		if (!in_array(0, $groups))
1434
		{
1435
			$where .= ' AND (mem.id_group != 0 OR mem.additional_groups != {string:blank_add_groups})';
1436
			$where_vars['blank_add_groups'] = '';
1437
		}
1438
1439
		// Select all the members we're about to murder/remove...
1440
		$request = $smcFunc['db_query']('', '
1441
			SELECT mem.id_member, COALESCE(m.id_member, 0) AS is_mod
1442
			FROM {db_prefix}members AS mem
1443
				LEFT JOIN {db_prefix}moderators AS m ON (m.id_member = mem.id_member)
1444
			WHERE ' . $where,
1445
			$where_vars
1446
		);
1447
		$members = array();
1448
		while ($row = $smcFunc['db_fetch_assoc']($request))
1449
		{
1450
			if (!$row['is_mod'] || !in_array(3, $groups))
1451
				$members[] = $row['id_member'];
1452
		}
1453
		$smcFunc['db_free_result']($request);
1454
1455
		require_once($sourcedir . '/Subs-Members.php');
1456
		deleteMembers($members);
1457
	}
1458
1459
	$context['maintenance_finished'] = $txt['maintain_members'];
1460
	createToken('admin-maint');
1461
}
1462
1463
/**
1464
 * Removing old posts doesn't take much as we really pass through.
1465
 */
1466
function MaintainRemoveOldPosts()
1467
{
1468
	global $sourcedir;
1469
1470
	validateToken('admin-maint');
1471
1472
	// Actually do what we're told!
1473
	require_once($sourcedir . '/RemoveTopic.php');
1474
	RemoveOldTopics2();
1475
}
1476
1477
/**
1478
 * Removing old drafts
1479
 */
1480
function MaintainRemoveOldDrafts()
1481
{
1482
	global $sourcedir, $smcFunc;
1483
1484
	validateToken('admin-maint');
1485
1486
	$drafts = array();
1487
1488
	// Find all of the old drafts
1489
	$request = $smcFunc['db_query']('', '
1490
		SELECT id_draft
1491
		FROM {db_prefix}user_drafts
1492
		WHERE poster_time <= {int:poster_time_old}',
1493
		array(
1494
			'poster_time_old' => time() - (86400 * $_POST['draftdays']),
1495
		)
1496
	);
1497
1498
	while ($row = $smcFunc['db_fetch_row']($request))
1499
		$drafts[] = (int) $row[0];
1500
	$smcFunc['db_free_result']($request);
1501
1502
	// If we have old drafts, remove them
1503
	if (count($drafts) > 0)
1504
	{
1505
		require_once($sourcedir . '/Drafts.php');
1506
		DeleteDraft($drafts, false);
0 ignored issues
show
Bug introduced by
$drafts of type array|integer[] is incompatible with the type integer expected by parameter $id_draft of DeleteDraft(). ( Ignorable by Annotation )

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

1506
		DeleteDraft(/** @scrutinizer ignore-type */ $drafts, false);
Loading history...
1507
	}
1508
}
1509
1510
/**
1511
 * Moves topics from one board to another.
1512
 *
1513
 * @uses template_not_done() to pause the process.
1514
 */
1515
function MaintainMassMoveTopics()
1516
{
1517
	global $smcFunc, $sourcedir, $context, $txt;
1518
1519
	// Only admins.
1520
	isAllowedTo('admin_forum');
1521
1522
	checkSession('request');
1523
	validateToken('admin-maint');
1524
1525
	// Set up to the context.
1526
	$context['page_title'] = $txt['not_done_title'];
1527
	$context['continue_countdown'] = 3;
1528
	$context['continue_post_data'] = '';
1529
	$context['continue_get_data'] = '';
1530
	$context['sub_template'] = 'not_done';
1531
	$context['start'] = empty($_REQUEST['start']) ? 0 : (int) $_REQUEST['start'];
1532
	$context['start_time'] = time();
1533
1534
	// First time we do this?
1535
	$id_board_from = isset($_REQUEST['id_board_from']) ? (int) $_REQUEST['id_board_from'] : 0;
1536
	$id_board_to = isset($_REQUEST['id_board_to']) ? (int) $_REQUEST['id_board_to'] : 0;
1537
	$max_days = isset($_REQUEST['maxdays']) ? (int) $_REQUEST['maxdays'] : 0;
1538
	$locked = isset($_POST['move_type_locked']) || isset($_GET['locked']);
1539
	$sticky = isset($_POST['move_type_sticky']) || isset($_GET['sticky']);
1540
1541
	// No boards then this is your stop.
1542
	if (empty($id_board_from) || empty($id_board_to))
1543
		return;
1544
1545
	// The big WHERE clause
1546
	$conditions = 'WHERE t.id_board = {int:id_board_from}
1547
		AND m.icon != {string:moved}';
1548
1549
	// DB parameters
1550
	$params = array(
1551
		'id_board_from' => $id_board_from,
1552
		'moved' => 'moved',
1553
	);
1554
1555
	// Only moving topics not posted in for x days?
1556
	if (!empty($max_days))
1557
	{
1558
		$conditions .= '
1559
			AND m.poster_time < {int:poster_time}';
1560
		$params['poster_time'] = time() - 3600 * 24 * $max_days;
1561
	}
1562
1563
	// Moving locked topics?
1564
	if ($locked)
1565
	{
1566
		$conditions .= '
1567
			AND t.locked = {int:locked}';
1568
		$params['locked'] = 1;
1569
	}
1570
1571
	// What about sticky topics?
1572
	if ($sticky)
1573
	{
1574
		$conditions .= '
1575
			AND t.is_sticky = {int:sticky}';
1576
		$params['sticky'] = 1;
1577
	}
1578
1579
	// How many topics are we converting?
1580
	if (!isset($_REQUEST['totaltopics']))
1581
	{
1582
		$request = $smcFunc['db_query']('', '
1583
			SELECT COUNT(*)
1584
			FROM {db_prefix}topics AS t
1585
				INNER JOIN {db_prefix}messages AS m ON (m.id_msg = t.id_last_msg)' .
1586
			$conditions,
1587
			$params
1588
		);
1589
		list ($total_topics) = $smcFunc['db_fetch_row']($request);
1590
		$smcFunc['db_free_result']($request);
1591
	}
1592
	else
1593
		$total_topics = (int) $_REQUEST['totaltopics'];
1594
1595
	// Seems like we need this here.
1596
	$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 . ';max_days=' . $max_days;
1597
1598
	if ($locked)
1599
		$context['continue_get_data'] .= ';locked';
1600
1601
	if ($sticky)
1602
		$context['continue_get_data'] .= ';sticky';
1603
1604
	$context['continue_get_data'] .= ';start=' . $context['start'] . ';' . $context['session_var'] . '=' . $context['session_id'];
1605
1606
	// We have topics to move so start the process.
1607
	if (!empty($total_topics))
1608
	{
1609
		while ($context['start'] <= $total_topics)
1610
		{
1611
			// Lets get the topics.
1612
			$request = $smcFunc['db_query']('', '
1613
				SELECT t.id_topic
1614
				FROM {db_prefix}topics AS t
1615
					INNER JOIN {db_prefix}messages AS m ON (m.id_msg = t.id_last_msg)
1616
				' . $conditions . '
1617
				LIMIT 10',
1618
				$params
1619
			);
1620
1621
			// Get the ids.
1622
			$topics = array();
1623
			while ($row = $smcFunc['db_fetch_assoc']($request))
1624
				$topics[] = $row['id_topic'];
1625
1626
			// Just return if we don't have any topics left to move.
1627
			if (empty($topics))
1628
			{
1629
				cache_put_data('board-' . $id_board_from, null, 120);
1630
				cache_put_data('board-' . $id_board_to, null, 120);
1631
				redirectexit('action=admin;area=maintain;sa=topics;done=massmove');
1632
			}
1633
1634
			// Lets move them.
1635
			require_once($sourcedir . '/MoveTopic.php');
1636
			moveTopics($topics, $id_board_to);
1637
1638
			// We've done at least ten more topics.
1639
			$context['start'] += 10;
1640
1641
			// Lets wait a while.
1642
			if (time() - $context['start_time'] > 3)
1643
			{
1644
				// What's the percent?
1645
				$context['continue_percent'] = round(100 * ($context['start'] / $total_topics), 1);
1646
				$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'] . ';' . $context['session_var'] . '=' . $context['session_id'];
1647
1648
				// Let the template system do it's thang.
1649
				return;
1650
			}
1651
		}
1652
	}
1653
1654
	// Don't confuse admins by having an out of date cache.
1655
	cache_put_data('board-' . $id_board_from, null, 120);
1656
	cache_put_data('board-' . $id_board_to, null, 120);
1657
1658
	redirectexit('action=admin;area=maintain;sa=topics;done=massmove');
1659
}
1660
1661
/**
1662
 * Recalculate all members post counts
1663
 * it requires the admin_forum permission.
1664
 *
1665
 * - recounts all posts for members found in the message table
1666
 * - updates the members post count record in the members table
1667
 * - honors the boards post count flag
1668
 * - does not count posts in the recycle bin
1669
 * - zeros post counts for all members with no posts in the message table
1670
 * - runs as a delayed loop to avoid server overload
1671
 * - uses the not_done template in Admin.template
1672
 *
1673
 * The function redirects back to action=admin;area=maintain;sa=members when complete.
1674
 * It is accessed via ?action=admin;area=maintain;sa=members;activity=recountposts
1675
 */
1676
function MaintainRecountPosts()
1677
{
1678
	global $txt, $context, $modSettings, $smcFunc;
1679
1680
	// You have to be allowed in here
1681
	isAllowedTo('admin_forum');
1682
	checkSession('request');
1683
1684
	// Set up to the context.
1685
	$context['page_title'] = $txt['not_done_title'];
1686
	$context['continue_countdown'] = 3;
1687
	$context['continue_get_data'] = '';
1688
	$context['sub_template'] = 'not_done';
1689
1690
	// init
1691
	$increment = 200;
1692
	$_REQUEST['start'] = !isset($_REQUEST['start']) ? 0 : (int) $_REQUEST['start'];
1693
1694
	// Ask for some extra time, on big boards this may take a bit
1695
	@set_time_limit(600);
1696
1697
	// Only run this query if we don't have the total number of members that have posted
1698
	if (!isset($_SESSION['total_members']))
1699
	{
1700
		validateToken('admin-maint');
1701
1702
		$request = $smcFunc['db_query']('', '
1703
			SELECT COUNT(DISTINCT m.id_member)
1704
			FROM {db_prefix}messages AS m
1705
			JOIN {db_prefix}boards AS b on m.id_board = b.id_board
1706
			WHERE m.id_member != 0
1707
				AND b.count_posts = 0',
1708
			array(
1709
			)
1710
		);
1711
1712
		// save it so we don't do this again for this task
1713
		list ($_SESSION['total_members']) = $smcFunc['db_fetch_row']($request);
1714
		$smcFunc['db_free_result']($request);
1715
	}
1716
	else
1717
		validateToken('admin-recountposts');
1718
1719
	// Lets get a group of members and determine their post count (from the boards that have post count enabled of course).
1720
	$request = $smcFunc['db_query']('', '
1721
		SELECT m.id_member, COUNT(*) AS posts
1722
		FROM {db_prefix}messages AS m
1723
			INNER JOIN {db_prefix}boards AS b ON m.id_board = b.id_board
1724
		WHERE m.id_member != {int:zero}
1725
			AND b.count_posts = {int:zero}
1726
			' . (!empty($modSettings['recycle_enable']) ? ' AND b.id_board != {int:recycle}' : '') . '
1727
		GROUP BY m.id_member
1728
		LIMIT {int:start}, {int:number}',
1729
		array(
1730
			'start' => $_REQUEST['start'],
1731
			'number' => $increment,
1732
			'recycle' => $modSettings['recycle_board'],
1733
			'zero' => 0,
1734
		)
1735
	);
1736
	$total_rows = $smcFunc['db_num_rows']($request);
1737
1738
	// Update the post count for this group
1739
	while ($row = $smcFunc['db_fetch_assoc']($request))
1740
	{
1741
		$smcFunc['db_query']('', '
1742
			UPDATE {db_prefix}members
1743
			SET posts = {int:posts}
1744
			WHERE id_member = {int:row}',
1745
			array(
1746
				'row' => $row['id_member'],
1747
				'posts' => $row['posts'],
1748
			)
1749
		);
1750
	}
1751
	$smcFunc['db_free_result']($request);
1752
1753
	// Continue?
1754
	if ($total_rows == $increment)
1755
	{
1756
		$_REQUEST['start'] += $increment;
1757
		$context['continue_get_data'] = '?action=admin;area=maintain;sa=members;activity=recountposts;start=' . $_REQUEST['start'] . ';' . $context['session_var'] . '=' . $context['session_id'];
1758
		$context['continue_percent'] = round(100 * $_REQUEST['start'] / $_SESSION['total_members']);
1759
1760
		createToken('admin-recountposts');
1761
		$context['continue_post_data'] = '<input type="hidden" name="' . $context['admin-recountposts_token_var'] . '" value="' . $context['admin-recountposts_token'] . '">';
1762
1763
		if (function_exists('apache_reset_timeout'))
1764
			apache_reset_timeout();
1765
		return;
1766
	}
1767
1768
	// final steps ... made more difficult since we don't yet support sub-selects on joins
1769
	// place all members who have posts in the message table in a temp table
1770
	$createTemporary = $smcFunc['db_query']('', '
1771
		CREATE TEMPORARY TABLE {db_prefix}tmp_maint_recountposts (
1772
			id_member mediumint(8) unsigned NOT NULL default {string:string_zero},
1773
			PRIMARY KEY (id_member)
1774
		)
1775
		SELECT m.id_member
1776
		FROM {db_prefix}messages AS m
1777
			INNER JOIN {db_prefix}boards AS b ON m.id_board = b.id_board
1778
		WHERE m.id_member != {int:zero}
1779
			AND b.count_posts = {int:zero}
1780
			' . (!empty($modSettings['recycle_enable']) ? ' AND b.id_board != {int:recycle}' : '') . '
1781
		GROUP BY m.id_member',
1782
		array(
1783
			'zero' => 0,
1784
			'string_zero' => '0',
1785
			'db_error_skip' => true,
1786
			'recycle' => !empty($modSettings['recycle_board']) ? $modSettings['recycle_board'] : 0,
1787
		)
1788
	) !== false;
1789
1790
	if ($createTemporary)
1791
	{
1792
		// outer join the members table on the temporary table finding the members that have a post count but no posts in the message table
1793
		$request = $smcFunc['db_query']('', '
1794
			SELECT mem.id_member, mem.posts
1795
			FROM {db_prefix}members AS mem
1796
				LEFT OUTER JOIN {db_prefix}tmp_maint_recountposts AS res
1797
				ON res.id_member = mem.id_member
1798
			WHERE res.id_member IS null
1799
				AND mem.posts != {int:zero}',
1800
			array(
1801
				'zero' => 0,
1802
			)
1803
		);
1804
1805
		// set the post count to zero for any delinquents we may have found
1806
		while ($row = $smcFunc['db_fetch_assoc']($request))
1807
		{
1808
			$smcFunc['db_query']('', '
1809
				UPDATE {db_prefix}members
1810
				SET posts = {int:zero}
1811
				WHERE id_member = {int:row}',
1812
				array(
1813
					'row' => $row['id_member'],
1814
					'zero' => 0,
1815
				)
1816
			);
1817
		}
1818
		$smcFunc['db_free_result']($request);
1819
	}
1820
1821
	// all done
1822
	unset($_SESSION['total_members']);
1823
	$context['maintenance_finished'] = $txt['maintain_recountposts'];
1824
	redirectexit('action=admin;area=maintain;sa=members;done=recountposts');
1825
}
1826
1827
function RebuildSettingsFile()
1828
{
1829
	global $sourcedir;
1830
1831
	isAllowedTo('admin_forum');
1832
1833
	require_once($sourcedir . '/Subs-Admin.php');
1834
	updateSettingsFile(array(), false, true);
1835
1836
	redirectexit('action=admin;area=maintain;sa=routine;done=rebuild_settings');
1837
}
1838
1839
/**
1840
 * Generates a list of integration hooks for display
1841
 * Accessed through ?action=admin;area=maintain;sa=hooks;
1842
 * Allows for removal or disabling of selected hooks
1843
 */
1844
function list_integration_hooks()
1845
{
1846
	global $boarddir, $sourcedir, $scripturl, $context, $txt;
1847
1848
	$filter_url = '';
1849
	$current_filter = '';
1850
	$hooks = get_integration_hooks();
1851
	$hooks_filters = array();
1852
1853
	if (isset($_GET['filter'], $hooks[$_GET['filter']]))
1854
	{
1855
		$filter_url = ';filter=' . $_GET['filter'];
1856
		$current_filter = $_GET['filter'];
1857
	}
1858
	$filtered_hooks = array_filter(
1859
		$hooks,
1860
		function($hook) use ($current_filter)
1861
		{
1862
			return $current_filter == '' || $current_filter == $hook;
1863
		},
1864
		ARRAY_FILTER_USE_KEY
1865
	);
1866
	ksort($hooks);
1867
1868
	foreach ($hooks as $hook => $functions)
1869
		$hooks_filters[] = '<option' . ($current_filter == $hook ? ' selected ' : '') . ' value="' . $hook . '">' . $hook . '</option>';
1870
1871
	if (!empty($hooks_filters))
1872
		$context['insert_after_template'] .= '
1873
		<script>
1874
			var hook_name_header = document.getElementById(\'header_list_integration_hooks_hook_name\');
1875
			hook_name_header.innerHTML += ' . JavaScriptEscape('<select style="margin-left:15px;" onchange="window.location=(\'' . $scripturl . '?action=admin;area=maintain;sa=hooks\' + (this.value ? \';filter=\' + this.value : \'\'));"><option value="">' . $txt['hooks_reset_filter'] . '</option>' . implode('', $hooks_filters) . '</select>') . ';
1876
		</script>';
1877
1878
	if (!empty($_REQUEST['do']) && isset($_REQUEST['hook']) && isset($_REQUEST['function']))
1879
	{
1880
		checkSession('request');
1881
		validateToken('admin-hook', 'request');
1882
1883
		if ($_REQUEST['do'] == 'remove')
1884
			remove_integration_function($_REQUEST['hook'], urldecode($_REQUEST['function']));
1885
1886
		else
1887
		{
1888
			$function_remove = urldecode($_REQUEST['function']) . (($_REQUEST['do'] == 'disable') ? '' : '!');
1889
			$function_add = urldecode($_REQUEST['function']) . (($_REQUEST['do'] == 'disable') ? '!' : '');
1890
1891
			remove_integration_function($_REQUEST['hook'], $function_remove);
1892
			add_integration_function($_REQUEST['hook'], $function_add);
1893
		}
1894
1895
		redirectexit('action=admin;area=maintain;sa=hooks' . $filter_url);
1896
	}
1897
1898
	createToken('admin-hook', 'request');
1899
1900
	$list_options = array(
1901
		'id' => 'list_integration_hooks',
1902
		'title' => $txt['hooks_title_list'],
1903
		'items_per_page' => 20,
1904
		'base_href' => $scripturl . '?action=admin;area=maintain;sa=hooks' . $filter_url . ';' . $context['session_var'] . '=' . $context['session_id'],
1905
		'default_sort_col' => 'hook_name',
1906
		'get_items' => array(
1907
			'function' => 'get_integration_hooks_data',
1908
			'params' => array(
1909
				$filtered_hooks,
1910
				strtr($boarddir, '\\', '/'),
1911
				strtr($sourcedir, '\\', '/'),
1912
			),
1913
		),
1914
		'get_count' => array(
1915
			'value' => array_reduce(
1916
				$filtered_hooks,
1917
				function($accumulator, $functions)
1918
				{
1919
					return $accumulator + count($functions);
1920
				},
1921
				0
1922
			),
1923
		),
1924
		'no_items_label' => $txt['hooks_no_hooks'],
1925
		'columns' => array(
1926
			'hook_name' => array(
1927
				'header' => array(
1928
					'value' => $txt['hooks_field_hook_name'],
1929
				),
1930
				'data' => array(
1931
					'db' => 'hook_name',
1932
				),
1933
				'sort' => array(
1934
					'default' => 'hook_name',
1935
					'reverse' => 'hook_name DESC',
1936
				),
1937
			),
1938
			'function_name' => array(
1939
				'header' => array(
1940
					'value' => $txt['hooks_field_function_name'],
1941
				),
1942
				'data' => array(
1943
					'function' => function($data) use ($txt)
1944
					{
1945
						// Show a nice icon to indicate this is an instance.
1946
						$instance = (!empty($data['instance']) ? '<span class="main_icons news" title="' . $txt['hooks_field_function_method'] . '"></span> ' : '');
1947
1948
						if (!empty($data['included_file']) && !empty($data['real_function']))
1949
							return $instance . $txt['hooks_field_function'] . ': ' . $data['real_function'] . '<br>' . $txt['hooks_field_included_file'] . ': ' . $data['included_file'];
1950
1951
						else
1952
							return $instance . $data['real_function'];
1953
					},
1954
				),
1955
				'sort' => array(
1956
					'default' => 'function_name',
1957
					'reverse' => 'function_name DESC',
1958
				),
1959
			),
1960
			'file_name' => array(
1961
				'header' => array(
1962
					'value' => $txt['hooks_field_file_name'],
1963
				),
1964
				'data' => array(
1965
					'db' => 'file_name',
1966
				),
1967
				'sort' => array(
1968
					'default' => 'file_name',
1969
					'reverse' => 'file_name DESC',
1970
				),
1971
			),
1972
			'status' => array(
1973
				'header' => array(
1974
					'value' => $txt['hooks_field_hook_exists'],
1975
					'style' => 'width:3%;',
1976
				),
1977
				'data' => array(
1978
					'function' => function($data) use ($txt, $scripturl, $context, $filter_url)
1979
					{
1980
						$change_status = array('before' => '', 'after' => '');
1981
1982
						if ($data['can_disable'])
1983
						{
1984
							$change_status['before'] = '<a href="' . $scripturl . '?action=admin;area=maintain;sa=hooks;do=' . ($data['enabled'] ? 'disable' : 'enable') . ';hook=' . $data['hook_name'] . ';function=' . urlencode($data['real_function']) . $filter_url . ';' . $context['admin-hook_token_var'] . '=' . $context['admin-hook_token'] . ';' . $context['session_var'] . '=' . $context['session_id'] . '" data-confirm="' . $txt['quickmod_confirm'] . '" class="you_sure">';
1985
							$change_status['after'] = '</a>';
1986
						}
1987
1988
						return $change_status['before'] . '<span class="main_icons post_moderation_' . $data['status'] . '" title="' . $data['img_text'] . '"></span>' . $change_status['after'];
1989
					},
1990
					'class' => 'centertext',
1991
				),
1992
				'sort' => array(
1993
					'default' => 'status',
1994
					'reverse' => 'status DESC',
1995
				),
1996
			),
1997
		),
1998
		'additional_rows' => array(
1999
			array(
2000
				'position' => 'after_title',
2001
				'value' => $txt['hooks_disable_instructions'] . '<br>
2002
					' . $txt['hooks_disable_legend'] . ':
2003
				<ul style="list-style: none;">
2004
					<li><span class="main_icons post_moderation_allow"></span> ' . $txt['hooks_disable_legend_exists'] . '</li>
2005
					<li><span class="main_icons post_moderation_moderate"></span> ' . $txt['hooks_disable_legend_disabled'] . '</li>
2006
					<li><span class="main_icons post_moderation_deny"></span> ' . $txt['hooks_disable_legend_missing'] . '</li>
2007
				</ul>'
2008
			),
2009
		),
2010
	);
2011
2012
	$list_options['columns']['remove'] = array(
2013
		'header' => array(
2014
			'value' => $txt['hooks_button_remove'],
2015
			'style' => 'width:3%',
2016
		),
2017
		'data' => array(
2018
			'function' => function($data) use ($txt, $scripturl, $context, $filter_url)
2019
			{
2020
				if (!$data['hook_exists'])
2021
					return '
2022
					<a href="' . $scripturl . '?action=admin;area=maintain;sa=hooks;do=remove;hook=' . $data['hook_name'] . ';function=' . urlencode($data['function_name']) . $filter_url . ';' . $context['admin-hook_token_var'] . '=' . $context['admin-hook_token'] . ';' . $context['session_var'] . '=' . $context['session_id'] . '" data-confirm="' . $txt['quickmod_confirm'] . '" class="you_sure">
2023
						<span class="main_icons delete" title="' . $txt['hooks_button_remove'] . '"></span>
2024
					</a>';
2025
			},
2026
			'class' => 'centertext',
2027
		),
2028
	);
2029
	$list_options['form'] = array(
2030
		'href' => $scripturl . '?action=admin;area=maintain;sa=hooks' . $filter_url . ';' . $context['session_var'] . '=' . $context['session_id'],
2031
		'name' => 'list_integration_hooks',
2032
	);
2033
2034
	require_once($sourcedir . '/Subs-List.php');
2035
	createList($list_options);
2036
2037
	$context['page_title'] = $txt['hooks_title_list'];
2038
	$context['sub_template'] = 'show_list';
2039
	$context['default_list'] = 'list_integration_hooks';
2040
}
2041
2042
/**
2043
 * Gets all of the files in a directory and its children directories
2044
 *
2045
 * @param string $dirname The path to the directory
2046
 * @return array An array containing information about the files found in the specified directory and its children
2047
 */
2048
function get_files_recursive(string $dirname): array
2049
{
2050
	return iterator_to_array(
2051
		new RecursiveIteratorIterator(
2052
			new RecursiveCallbackFilterIterator(
2053
				new RecursiveDirectoryIterator($dirname, FilesystemIterator::UNIX_PATHS),
2054
				function ($fileInfo, $currentFile, $iterator)
0 ignored issues
show
Bug introduced by
function(...) { /* ... */ } of type callable is incompatible with the type string expected by parameter $callback of RecursiveCallbackFilterIterator::__construct(). ( Ignorable by Annotation )

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

2054
				/** @scrutinizer ignore-type */ function ($fileInfo, $currentFile, $iterator)
Loading history...
2055
				{
2056
					// Allow recursion
2057
					if ($iterator->hasChildren())
2058
						return true;
2059
					return $fileInfo->getExtension() == 'php';
2060
				}
2061
			)
2062
		)
2063
	);
2064
}
2065
2066
/**
2067
 * Callback function for the integration hooks list (list_integration_hooks)
2068
 * Gets all of the hooks in the system and their status
2069
 *
2070
 * @param int $start The item to start with (for pagination purposes)
2071
 * @param int $per_page How many items to display on each page
2072
 * @param string $sort A string indicating how to sort things
2073
 * @return array An array of information about the integration hooks
2074
 */
2075
function get_integration_hooks_data($start, $per_page, $sort, $filtered_hooks, $normalized_boarddir, $normalized_sourcedir)
2076
{
2077
	global $settings, $txt, $context, $scripturl;
2078
2079
	$function_list = $sort_array = $temp_data = array();
2080
	$files = get_files_recursive($normalized_sourcedir);
2081
	foreach ($files as $currentFile => $fileInfo)
2082
		$function_list += get_defined_functions_in_file($currentFile);
2083
2084
	$sort_types = array(
2085
		'hook_name' => array('hook_name', SORT_ASC),
2086
		'hook_name DESC' => array('hook_name', SORT_DESC),
2087
		'function_name' => array('function_name', SORT_ASC),
2088
		'function_name DESC' => array('function_name', SORT_DESC),
2089
		'file_name' => array('file_name', SORT_ASC),
2090
		'file_name DESC' => array('file_name', SORT_DESC),
2091
		'status' => array('status', SORT_ASC),
2092
		'status DESC' => array('status', SORT_DESC),
2093
	);
2094
2095
	foreach ($filtered_hooks as $hook => $functions)
2096
		foreach ($functions as $rawFunc)
2097
		{
2098
			$hookParsedData = parse_integration_hook($hook, $rawFunc);
2099
2100
			// Handle hooks pointing outside the sources directory.
2101
			if ($hookParsedData['absPath'] != '' && !isset($files[$hookParsedData['absPath']]) && file_exists($hookParsedData['absPath']))
2102
				$function_list += get_defined_functions_in_file($hookParsedData['absPath']);
2103
2104
			$hook_exists = isset($function_list[$hookParsedData['call']]) || (substr($hook, -8) === '_include' && isset($files[$hookParsedData['absPath']]));
2105
			$temp = array(
2106
				'hook_name' => $hook,
2107
				'function_name' => $hookParsedData['rawData'],
2108
				'real_function' => $hookParsedData['call'],
2109
				'included_file' => $hookParsedData['hookFile'],
2110
				'file_name' => strtr($hookParsedData['absPath'] ?: ($function_list[$hookParsedData['call']] ?? ''), [$normalized_boarddir => '.']),
2111
				'instance' => $hookParsedData['object'],
2112
				'hook_exists' => $hook_exists,
2113
				'status' => $hook_exists ? ($hookParsedData['enabled'] ? 'allow' : 'moderate') : 'deny',
2114
				'img_text' => $txt['hooks_' . ($hook_exists ? ($hookParsedData['enabled'] ? 'active' : 'disabled') : 'missing')],
2115
				'enabled' => $hookParsedData['enabled'],
2116
				'can_disable' => $hookParsedData['call'] != '',
2117
			);
2118
			$sort_array[] = $temp[$sort_types[$sort][0]];
2119
			$temp_data[] = $temp;
2120
		}
2121
2122
	array_multisort($sort_array, $sort_types[$sort][1], $temp_data);
2123
2124
	return array_slice($temp_data, $start, $per_page, true);
2125
}
2126
2127
/**
2128
 * Parses modSettings to create integration hook array
2129
 *
2130
 * @return array An array of information about the integration hooks
2131
 */
2132
function get_integration_hooks()
2133
{
2134
	global $modSettings;
2135
	static $integration_hooks;
2136
2137
	if (!isset($integration_hooks))
2138
	{
2139
		$integration_hooks = array();
2140
		foreach ($modSettings as $key => $value)
2141
		{
2142
			if (!empty($value) && substr($key, 0, 10) === 'integrate_')
2143
				$integration_hooks[$key] = explode(',', $value);
2144
		}
2145
	}
2146
2147
	return $integration_hooks;
2148
}
2149
2150
/**
2151
 * Parses each hook data and returns an array.
2152
 *
2153
 * @param string $hook
2154
 * @param string $rawData A string as it was saved to the DB.
2155
 * @return array everything found in the string itself
2156
 */
2157
function parse_integration_hook(string $hook, string $rawData)
2158
{
2159
	global $boarddir, $settings, $sourcedir;
2160
2161
	// A single string can hold tons of info!
2162
	$hookData = array(
2163
		'object' => false,
2164
		'enabled' => true,
2165
		'absPath' => '',
2166
		'hookFile' => '',
2167
		'pureFunc' => '',
2168
		'method' => '',
2169
		'class' => '',
2170
		'call' => '',
2171
		'rawData' => $rawData,
2172
	);
2173
2174
	// Meh...
2175
	if (empty($rawData))
2176
		return $hookData;
2177
2178
	$modFunc = $rawData;
2179
2180
	// Any files?
2181
	if (substr($hook, -8) === '_include')
2182
		$modFunc = $modFunc . '|';
2183
	if (strpos($modFunc, '|') !== false)
2184
	{
2185
		list ($hookData['hookFile'], $modFunc) = explode('|', $modFunc);
2186
		$hookData['absPath'] = strtr(strtr(trim($hookData['hookFile']), array('$boarddir' => $boarddir, '$sourcedir' => $sourcedir, '$themedir' => $settings['theme_dir'] ?? '')), '\\', '/');
2187
	}
2188
2189
	// Hook is an instance.
2190
	if (strpos($modFunc, '#') !== false)
2191
	{
2192
		$modFunc = str_replace('#', '', $modFunc);
2193
		$hookData['object'] = true;
2194
	}
2195
2196
	// Hook is "disabled"
2197
	if (strpos($modFunc, '!') !== false)
2198
	{
2199
		$modFunc = str_replace('!', '', $modFunc);
2200
		$hookData['enabled'] = false;
2201
	}
2202
2203
	// Handling methods?
2204
	if (strpos($modFunc, '::') !== false)
2205
	{
2206
		list ($hookData['class'], $hookData['method']) = explode('::', $modFunc);
2207
		$hookData['pureFunc'] = $hookData['method'];
2208
		$hookData['call'] = $modFunc;
2209
	}
2210
2211
	else
2212
		$hookData['call'] = $hookData['pureFunc'] = $modFunc;
2213
2214
	return $hookData;
2215
}
2216
2217
function get_defined_functions_in_file(string $file): array
2218
{
2219
	$source = file_get_contents($file);
2220
	// token_get_all() is too slow so use a nice little regex instead.
2221
	preg_match_all('/\bnamespace\s++((?P>label)(?:\\\(?P>label))*+)\s*+;|\bclass\s++((?P>label))[\w\s]*+{|\bfunction\s++((?P>label))\s*+\(.*\)[:\|\w\s]*+{(?(DEFINE)(?<label>[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*+))/i', $source, $matches, PREG_SET_ORDER);
2222
2223
	$functions = array();
2224
	$namespace = '';
2225
	$class = '';
2226
2227
	foreach ($matches as $match)
2228
	{
2229
		if (!empty($match[1]))
2230
			$namespace = $match[1] . '\\';
2231
		elseif (!empty($match[2]))
2232
			$class = $namespace . $match[2] . '::';
2233
		elseif (!empty($match[3]))
2234
			$functions[$class . $match[3]] = $file;
2235
	}
2236
2237
	return $functions;
2238
}
2239
2240
/**
2241
 * Converts html entities to utf8 equivalents
2242
 * special db wrapper for mysql based on the limitation of mysql/mb3
2243
 *
2244
 * Callback function for preg_replace_callback
2245
 * Uses capture group 1 in the supplied array
2246
 * Does basic checks to keep characters inside a viewable range.
2247
 *
2248
 * @param array $matches An array of matches (relevant info should be the 2nd item in the array)
2249
 * @return string The fixed string or return the old when limitation of mysql is hit
2250
 */
2251
function fixchardb__callback($matches)
2252
{
2253
	global $smcFunc;
2254
	if (!isset($matches[1]))
2255
		return '';
2256
2257
	$num = $matches[1][0] === 'x' ? hexdec(substr($matches[1], 1)) : (int) $matches[1];
0 ignored issues
show
Bug introduced by
$matches[1] of type array is incompatible with the type string expected by parameter $string of substr(). ( Ignorable by Annotation )

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

2257
	$num = $matches[1][0] === 'x' ? hexdec(substr(/** @scrutinizer ignore-type */ $matches[1], 1)) : (int) $matches[1];
Loading history...
2258
2259
	// it's to big for mb3?
2260
	if ($num > 0xFFFF && !$smcFunc['db_mb4'])
2261
		return $matches[0];
2262
	else
2263
		return fixchar__callback($matches);
2264
}
2265
2266
?>