Passed
Pull Request — release-2.1 (#6409)
by
unknown
06:46
created

ConvertUtf8mb4()   F

Complexity

Conditions 14
Paths 8192

Size

Total Lines 171
Code Lines 75

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 14
eloc 75
nc 8192
nop 0
dl 0
loc 171
rs 2.2254
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

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

1691
		DeleteDraft(/** @scrutinizer ignore-type */ $drafts, false);
Loading history...
1692
	}
1693
}
1694
1695
/**
1696
 * Moves topics from one board to another.
1697
 *
1698
 * @uses template_not_done() to pause the process.
1699
 */
1700
function MaintainMassMoveTopics()
1701
{
1702
	global $smcFunc, $sourcedir, $context, $txt;
1703
1704
	// Only admins.
1705
	isAllowedTo('admin_forum');
1706
1707
	checkSession('request');
1708
	validateToken('admin-maint');
1709
1710
	// Set up to the context.
1711
	$context['page_title'] = $txt['not_done_title'];
1712
	$context['continue_countdown'] = 3;
1713
	$context['continue_post_data'] = '';
1714
	$context['continue_get_data'] = '';
1715
	$context['sub_template'] = 'not_done';
1716
	$context['start'] = empty($_REQUEST['start']) ? 0 : (int) $_REQUEST['start'];
1717
	$context['start_time'] = time();
1718
1719
	// First time we do this?
1720
	$id_board_from = isset($_REQUEST['id_board_from']) ? (int) $_REQUEST['id_board_from'] : 0;
1721
	$id_board_to = isset($_REQUEST['id_board_to']) ? (int) $_REQUEST['id_board_to'] : 0;
1722
	$max_days = isset($_REQUEST['maxdays']) ? (int) $_REQUEST['maxdays'] : 0;
1723
	$locked = isset($_POST['move_type_locked']) || isset($_GET['locked']);
1724
	$sticky = isset($_POST['move_type_sticky']) || isset($_GET['sticky']);
1725
1726
	// No boards then this is your stop.
1727
	if (empty($id_board_from) || empty($id_board_to))
1728
		return;
1729
1730
	// The big WHERE clause
1731
	$conditions = 'WHERE t.id_board = {int:id_board_from}
1732
		AND m.icon != {string:moved}';
1733
1734
	// DB parameters
1735
	$params = array(
1736
		'id_board_from' => $id_board_from,
1737
		'moved' => 'moved',
1738
	);
1739
1740
	// Only moving topics not posted in for x days?
1741
	if (!empty($max_days))
1742
	{
1743
		$conditions .= '
1744
			AND m.poster_time < {int:poster_time}';
1745
		$params['poster_time'] = time() - 3600 * 24 * $max_days;
1746
	}
1747
1748
	// Moving locked topics?
1749
	if ($locked)
1750
	{
1751
		$conditions .= '
1752
			AND t.locked = {int:locked}';
1753
		$params['locked'] = 1;
1754
	}
1755
1756
	// What about sticky topics?
1757
	if ($sticky)
1758
	{
1759
		$conditions .= '
1760
			AND t.is_sticky = {int:sticky}';
1761
		$params['sticky'] = 1;
1762
	}
1763
1764
	// How many topics are we converting?
1765
	if (!isset($_REQUEST['totaltopics']))
1766
	{
1767
		$request = $smcFunc['db_query']('', '
1768
			SELECT COUNT(*)
1769
			FROM {db_prefix}topics AS t
1770
				INNER JOIN {db_prefix}messages AS m ON (m.id_msg = t.id_last_msg)' .
1771
			$conditions,
1772
			$params
1773
		);
1774
		list ($total_topics) = $smcFunc['db_fetch_row']($request);
1775
		$smcFunc['db_free_result']($request);
1776
	}
1777
	else
1778
		$total_topics = (int) $_REQUEST['totaltopics'];
1779
1780
	// Seems like we need this here.
1781
	$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;
1782
1783
	if ($locked)
1784
		$context['continue_get_data'] .= ';locked';
1785
1786
	if ($sticky)
1787
		$context['continue_get_data'] .= ';sticky';
1788
1789
	$context['continue_get_data'] .= ';start=' . $context['start'] . ';' . $context['session_var'] . '=' . $context['session_id'];
1790
1791
	// We have topics to move so start the process.
1792
	if (!empty($total_topics))
1793
	{
1794
		while ($context['start'] <= $total_topics)
1795
		{
1796
			// Lets get the topics.
1797
			$request = $smcFunc['db_query']('', '
1798
				SELECT t.id_topic
1799
				FROM {db_prefix}topics AS t
1800
					INNER JOIN {db_prefix}messages AS m ON (m.id_msg = t.id_last_msg)
1801
				' . $conditions . '
1802
				LIMIT 10',
1803
				$params
1804
			);
1805
1806
			// Get the ids.
1807
			$topics = array();
1808
			while ($row = $smcFunc['db_fetch_assoc']($request))
1809
				$topics[] = $row['id_topic'];
1810
1811
			// Just return if we don't have any topics left to move.
1812
			if (empty($topics))
1813
			{
1814
				cache_put_data('board-' . $id_board_from, null, 120);
1815
				cache_put_data('board-' . $id_board_to, null, 120);
1816
				redirectexit('action=admin;area=maintain;sa=topics;done=massmove');
1817
			}
1818
1819
			// Lets move them.
1820
			require_once($sourcedir . '/MoveTopic.php');
1821
			moveTopics($topics, $id_board_to);
1822
1823
			// We've done at least ten more topics.
1824
			$context['start'] += 10;
1825
1826
			// Lets wait a while.
1827
			if (time() - $context['start_time'] > 3)
1828
			{
1829
				// What's the percent?
1830
				$context['continue_percent'] = round(100 * ($context['start'] / $total_topics), 1);
1831
				$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'];
1832
1833
				// Let the template system do it's thang.
1834
				return;
1835
			}
1836
		}
1837
	}
1838
1839
	// Don't confuse admins by having an out of date cache.
1840
	cache_put_data('board-' . $id_board_from, null, 120);
1841
	cache_put_data('board-' . $id_board_to, null, 120);
1842
1843
	redirectexit('action=admin;area=maintain;sa=topics;done=massmove');
1844
}
1845
1846
/**
1847
 * Recalculate all members post counts
1848
 * it requires the admin_forum permission.
1849
 *
1850
 * - recounts all posts for members found in the message table
1851
 * - updates the members post count record in the members table
1852
 * - honors the boards post count flag
1853
 * - does not count posts in the recycle bin
1854
 * - zeros post counts for all members with no posts in the message table
1855
 * - runs as a delayed loop to avoid server overload
1856
 * - uses the not_done template in Admin.template
1857
 *
1858
 * The function redirects back to action=admin;area=maintain;sa=members when complete.
1859
 * It is accessed via ?action=admin;area=maintain;sa=members;activity=recountposts
1860
 */
1861
function MaintainRecountPosts()
1862
{
1863
	global $txt, $context, $modSettings, $smcFunc;
1864
1865
	// You have to be allowed in here
1866
	isAllowedTo('admin_forum');
1867
	checkSession('request');
1868
1869
	// Set up to the context.
1870
	$context['page_title'] = $txt['not_done_title'];
1871
	$context['continue_countdown'] = 3;
1872
	$context['continue_get_data'] = '';
1873
	$context['sub_template'] = 'not_done';
1874
1875
	// init
1876
	$increment = 200;
1877
	$_REQUEST['start'] = !isset($_REQUEST['start']) ? 0 : (int) $_REQUEST['start'];
1878
1879
	// Ask for some extra time, on big boards this may take a bit
1880
	@set_time_limit(600);
1881
1882
	// Only run this query if we don't have the total number of members that have posted
1883
	if (!isset($_SESSION['total_members']))
1884
	{
1885
		validateToken('admin-maint');
1886
1887
		$request = $smcFunc['db_query']('', '
1888
			SELECT COUNT(DISTINCT m.id_member)
1889
			FROM {db_prefix}messages AS m
1890
			JOIN {db_prefix}boards AS b on m.id_board = b.id_board
1891
			WHERE m.id_member != 0
1892
				AND b.count_posts = 0',
1893
			array(
1894
			)
1895
		);
1896
1897
		// save it so we don't do this again for this task
1898
		list ($_SESSION['total_members']) = $smcFunc['db_fetch_row']($request);
1899
		$smcFunc['db_free_result']($request);
1900
	}
1901
	else
1902
		validateToken('admin-recountposts');
1903
1904
	// Lets get a group of members and determine their post count (from the boards that have post count enabled of course).
1905
	$request = $smcFunc['db_query']('', '
1906
		SELECT m.id_member, COUNT(*) AS posts
1907
		FROM {db_prefix}messages AS m
1908
			INNER JOIN {db_prefix}boards AS b ON m.id_board = b.id_board
1909
		WHERE m.id_member != {int:zero}
1910
			AND b.count_posts = {int:zero}
1911
			' . (!empty($modSettings['recycle_enable']) ? ' AND b.id_board != {int:recycle}' : '') . '
1912
		GROUP BY m.id_member
1913
		LIMIT {int:start}, {int:number}',
1914
		array(
1915
			'start' => $_REQUEST['start'],
1916
			'number' => $increment,
1917
			'recycle' => $modSettings['recycle_board'],
1918
			'zero' => 0,
1919
		)
1920
	);
1921
	$total_rows = $smcFunc['db_num_rows']($request);
1922
1923
	// Update the post count for this group
1924
	while ($row = $smcFunc['db_fetch_assoc']($request))
1925
	{
1926
		$smcFunc['db_query']('', '
1927
			UPDATE {db_prefix}members
1928
			SET posts = {int:posts}
1929
			WHERE id_member = {int:row}',
1930
			array(
1931
				'row' => $row['id_member'],
1932
				'posts' => $row['posts'],
1933
			)
1934
		);
1935
	}
1936
	$smcFunc['db_free_result']($request);
1937
1938
	// Continue?
1939
	if ($total_rows == $increment)
1940
	{
1941
		$_REQUEST['start'] += $increment;
1942
		$context['continue_get_data'] = '?action=admin;area=maintain;sa=members;activity=recountposts;start=' . $_REQUEST['start'] . ';' . $context['session_var'] . '=' . $context['session_id'];
1943
		$context['continue_percent'] = round(100 * $_REQUEST['start'] / $_SESSION['total_members']);
1944
1945
		createToken('admin-recountposts');
1946
		$context['continue_post_data'] = '<input type="hidden" name="' . $context['admin-recountposts_token_var'] . '" value="' . $context['admin-recountposts_token'] . '">';
1947
1948
		if (function_exists('apache_reset_timeout'))
1949
			apache_reset_timeout();
1950
		return;
1951
	}
1952
1953
	// final steps ... made more difficult since we don't yet support sub-selects on joins
1954
	// place all members who have posts in the message table in a temp table
1955
	$createTemporary = $smcFunc['db_query']('', '
1956
		CREATE TEMPORARY TABLE {db_prefix}tmp_maint_recountposts (
1957
			id_member mediumint(8) unsigned NOT NULL default {string:string_zero},
1958
			PRIMARY KEY (id_member)
1959
		)
1960
		SELECT m.id_member
1961
		FROM {db_prefix}messages AS m
1962
			INNER JOIN {db_prefix}boards AS b ON m.id_board = b.id_board
1963
		WHERE m.id_member != {int:zero}
1964
			AND b.count_posts = {int:zero}
1965
			' . (!empty($modSettings['recycle_enable']) ? ' AND b.id_board != {int:recycle}' : '') . '
1966
		GROUP BY m.id_member',
1967
		array(
1968
			'zero' => 0,
1969
			'string_zero' => '0',
1970
			'db_error_skip' => true,
1971
			'recycle' => !empty($modSettings['recycle_board']) ? $modSettings['recycle_board'] : 0,
1972
		)
1973
	) !== false;
1974
1975
	if ($createTemporary)
1976
	{
1977
		// outer join the members table on the temporary table finding the members that have a post count but no posts in the message table
1978
		$request = $smcFunc['db_query']('', '
1979
			SELECT mem.id_member, mem.posts
1980
			FROM {db_prefix}members AS mem
1981
				LEFT OUTER JOIN {db_prefix}tmp_maint_recountposts AS res
1982
				ON res.id_member = mem.id_member
1983
			WHERE res.id_member IS null
1984
				AND mem.posts != {int:zero}',
1985
			array(
1986
				'zero' => 0,
1987
			)
1988
		);
1989
1990
		// set the post count to zero for any delinquents we may have found
1991
		while ($row = $smcFunc['db_fetch_assoc']($request))
1992
		{
1993
			$smcFunc['db_query']('', '
1994
				UPDATE {db_prefix}members
1995
				SET posts = {int:zero}
1996
				WHERE id_member = {int:row}',
1997
				array(
1998
					'row' => $row['id_member'],
1999
					'zero' => 0,
2000
				)
2001
			);
2002
		}
2003
		$smcFunc['db_free_result']($request);
2004
	}
2005
2006
	// all done
2007
	unset($_SESSION['total_members']);
2008
	$context['maintenance_finished'] = $txt['maintain_recountposts'];
2009
	redirectexit('action=admin;area=maintain;sa=members;done=recountposts');
2010
}
2011
2012
function RebuildSettingsFile()
2013
{
2014
	global $sourcedir;
2015
2016
	isAllowedTo('admin_forum');
2017
2018
	require_once($sourcedir . '/Subs-Admin.php');
2019
	updateSettingsFile(array(), false, true);
2020
2021
	redirectexit('action=admin;area=maintain;sa=routine;done=rebuild_settings');
2022
}
2023
2024
/**
2025
 * Generates a list of integration hooks for display
2026
 * Accessed through ?action=admin;area=maintain;sa=hooks;
2027
 * Allows for removal or disabling of selected hooks
2028
 */
2029
function list_integration_hooks()
2030
{
2031
	global $boarddir, $sourcedir, $scripturl, $context, $txt;
2032
2033
	$filter_url = '';
2034
	$current_filter = '';
2035
	$hooks = get_integration_hooks();
2036
	$hooks_filters = array();
2037
2038
	if (isset($_GET['filter'], $hooks[$_GET['filter']]))
2039
	{
2040
		$filter_url = ';filter=' . $_GET['filter'];
2041
		$current_filter = $_GET['filter'];
2042
	}
2043
	$filtered_hooks = array_filter(
2044
		$hooks,
2045
		function($hook) use ($current_filter)
2046
		{
2047
			return $current_filter == '' || $current_filter == $hook;
2048
		},
2049
		ARRAY_FILTER_USE_KEY
2050
	);
2051
	ksort($hooks);
2052
2053
	foreach ($hooks as $hook => $functions)
2054
		$hooks_filters[] = '<option' . ($current_filter == $hook ? ' selected ' : '') . ' value="' . $hook . '">' . $hook . '</option>';
2055
2056
	if (!empty($hooks_filters))
2057
		$context['insert_after_template'] .= '
2058
		<script>
2059
			var hook_name_header = document.getElementById(\'header_list_integration_hooks_hook_name\');
2060
			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>') . ';
2061
		</script>';
2062
2063
	if (!empty($_REQUEST['do']) && isset($_REQUEST['hook']) && isset($_REQUEST['function']))
2064
	{
2065
		checkSession('request');
2066
		validateToken('admin-hook', 'request');
2067
2068
		if ($_REQUEST['do'] == 'remove')
2069
			remove_integration_function($_REQUEST['hook'], urldecode($_REQUEST['function']));
2070
2071
		else
2072
		{
2073
			$function_remove = urldecode($_REQUEST['function']) . (($_REQUEST['do'] == 'disable') ? '' : '!');
2074
			$function_add = urldecode($_REQUEST['function']) . (($_REQUEST['do'] == 'disable') ? '!' : '');
2075
2076
			remove_integration_function($_REQUEST['hook'], $function_remove);
2077
			add_integration_function($_REQUEST['hook'], $function_add);
2078
		}
2079
2080
		redirectexit('action=admin;area=maintain;sa=hooks' . $filter_url);
2081
	}
2082
2083
	createToken('admin-hook', 'request');
2084
2085
	$list_options = array(
2086
		'id' => 'list_integration_hooks',
2087
		'title' => $txt['hooks_title_list'],
2088
		'items_per_page' => 20,
2089
		'base_href' => $scripturl . '?action=admin;area=maintain;sa=hooks' . $filter_url . ';' . $context['session_var'] . '=' . $context['session_id'],
2090
		'default_sort_col' => 'hook_name',
2091
		'get_items' => array(
2092
			'function' => 'get_integration_hooks_data',
2093
			'params' => array(
2094
				$filtered_hooks,
2095
				strtr($boarddir, '\\', '/'),
2096
				strtr($sourcedir, '\\', '/'),
2097
			),
2098
		),
2099
		'get_count' => array(
2100
			'value' => array_reduce(
2101
				$filtered_hooks,
2102
				function($accumulator, $functions)
2103
				{
2104
					return $accumulator + count($functions);
2105
				},
2106
				0
2107
			),
2108
		),
2109
		'no_items_label' => $txt['hooks_no_hooks'],
2110
		'columns' => array(
2111
			'hook_name' => array(
2112
				'header' => array(
2113
					'value' => $txt['hooks_field_hook_name'],
2114
				),
2115
				'data' => array(
2116
					'db' => 'hook_name',
2117
				),
2118
				'sort' => array(
2119
					'default' => 'hook_name',
2120
					'reverse' => 'hook_name DESC',
2121
				),
2122
			),
2123
			'function_name' => array(
2124
				'header' => array(
2125
					'value' => $txt['hooks_field_function_name'],
2126
				),
2127
				'data' => array(
2128
					'function' => function($data) use ($txt)
2129
					{
2130
						// Show a nice icon to indicate this is an instance.
2131
						$instance = (!empty($data['instance']) ? '<span class="main_icons news" title="' . $txt['hooks_field_function_method'] . '"></span> ' : '');
2132
2133
						if (!empty($data['included_file']) && !empty($data['real_function']))
2134
							return $instance . $txt['hooks_field_function'] . ': ' . $data['real_function'] . '<br>' . $txt['hooks_field_included_file'] . ': ' . $data['included_file'];
2135
2136
						else
2137
							return $instance . $data['real_function'];
2138
					},
2139
				),
2140
				'sort' => array(
2141
					'default' => 'function_name',
2142
					'reverse' => 'function_name DESC',
2143
				),
2144
			),
2145
			'file_name' => array(
2146
				'header' => array(
2147
					'value' => $txt['hooks_field_file_name'],
2148
				),
2149
				'data' => array(
2150
					'db' => 'file_name',
2151
				),
2152
				'sort' => array(
2153
					'default' => 'file_name',
2154
					'reverse' => 'file_name DESC',
2155
				),
2156
			),
2157
			'status' => array(
2158
				'header' => array(
2159
					'value' => $txt['hooks_field_hook_exists'],
2160
					'style' => 'width:3%;',
2161
				),
2162
				'data' => array(
2163
					'function' => function($data) use ($txt, $scripturl, $context, $filter_url)
2164
					{
2165
						$change_status = array('before' => '', 'after' => '');
2166
2167
						if ($data['can_disable'])
2168
						{
2169
							$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">';
2170
							$change_status['after'] = '</a>';
2171
						}
2172
2173
						return $change_status['before'] . '<span class="main_icons post_moderation_' . $data['status'] . '" title="' . $data['img_text'] . '"></span>' . $change_status['after'];
2174
					},
2175
					'class' => 'centertext',
2176
				),
2177
				'sort' => array(
2178
					'default' => 'status',
2179
					'reverse' => 'status DESC',
2180
				),
2181
			),
2182
		),
2183
		'additional_rows' => array(
2184
			array(
2185
				'position' => 'after_title',
2186
				'value' => $txt['hooks_disable_instructions'] . '<br>
2187
					' . $txt['hooks_disable_legend'] . ':
2188
				<ul style="list-style: none;">
2189
					<li><span class="main_icons post_moderation_allow"></span> ' . $txt['hooks_disable_legend_exists'] . '</li>
2190
					<li><span class="main_icons post_moderation_moderate"></span> ' . $txt['hooks_disable_legend_disabled'] . '</li>
2191
					<li><span class="main_icons post_moderation_deny"></span> ' . $txt['hooks_disable_legend_missing'] . '</li>
2192
				</ul>'
2193
			),
2194
		),
2195
	);
2196
2197
	$list_options['columns']['remove'] = array(
2198
		'header' => array(
2199
			'value' => $txt['hooks_button_remove'],
2200
			'style' => 'width:3%',
2201
		),
2202
		'data' => array(
2203
			'function' => function($data) use ($txt, $scripturl, $context, $filter_url)
2204
			{
2205
				if (!$data['hook_exists'])
2206
					return '
2207
					<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">
2208
						<span class="main_icons delete" title="' . $txt['hooks_button_remove'] . '"></span>
2209
					</a>';
2210
			},
2211
			'class' => 'centertext',
2212
		),
2213
	);
2214
	$list_options['form'] = array(
2215
		'href' => $scripturl . '?action=admin;area=maintain;sa=hooks' . $filter_url . ';' . $context['session_var'] . '=' . $context['session_id'],
2216
		'name' => 'list_integration_hooks',
2217
	);
2218
2219
	require_once($sourcedir . '/Subs-List.php');
2220
	createList($list_options);
2221
2222
	$context['page_title'] = $txt['hooks_title_list'];
2223
	$context['sub_template'] = 'show_list';
2224
	$context['default_list'] = 'list_integration_hooks';
2225
}
2226
2227
/**
2228
 * Gets all of the files in a directory and its children directories
2229
 *
2230
 * @param string $dirname The path to the directory
2231
 * @return array An array containing information about the files found in the specified directory and its children
2232
 */
2233
function get_files_recursive(string $dirname): array
2234
{
2235
	return iterator_to_array(
2236
		new RecursiveIteratorIterator(
2237
			new RecursiveCallbackFilterIterator(
2238
				new RecursiveDirectoryIterator($dirname, FilesystemIterator::UNIX_PATHS),
2239
				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

2239
				/** @scrutinizer ignore-type */ function ($fileInfo, $currentFile, $iterator)
Loading history...
2240
				{
2241
					// Allow recursion
2242
					if ($iterator->hasChildren())
2243
						return true;
2244
					return $fileInfo->getExtension() == 'php';
2245
				}
2246
			)
2247
		)
2248
	);
2249
}
2250
2251
/**
2252
 * Callback function for the integration hooks list (list_integration_hooks)
2253
 * Gets all of the hooks in the system and their status
2254
 *
2255
 * @param int $start The item to start with (for pagination purposes)
2256
 * @param int $per_page How many items to display on each page
2257
 * @param string $sort A string indicating how to sort things
2258
 * @return array An array of information about the integration hooks
2259
 */
2260
function get_integration_hooks_data($start, $per_page, $sort, $filtered_hooks, $normalized_boarddir, $normalized_sourcedir)
2261
{
2262
	global $settings, $txt, $context, $scripturl;
2263
2264
	$function_list = $sort_array = $temp_data = array();
2265
	$files = get_files_recursive($normalized_sourcedir);
2266
	foreach ($files as $currentFile => $fileInfo)
2267
		$function_list += get_defined_functions_in_file($currentFile);
2268
2269
	$sort_types = array(
2270
		'hook_name' => array('hook_name', SORT_ASC),
2271
		'hook_name DESC' => array('hook_name', SORT_DESC),
2272
		'function_name' => array('function_name', SORT_ASC),
2273
		'function_name DESC' => array('function_name', SORT_DESC),
2274
		'file_name' => array('file_name', SORT_ASC),
2275
		'file_name DESC' => array('file_name', SORT_DESC),
2276
		'status' => array('status', SORT_ASC),
2277
		'status DESC' => array('status', SORT_DESC),
2278
	);
2279
2280
	foreach ($filtered_hooks as $hook => $functions)
2281
		foreach ($functions as $rawFunc)
2282
		{
2283
			$hookParsedData = parse_integration_hook($hook, $rawFunc);
2284
2285
			// Handle hooks pointing outside the sources directory.
2286
			if ($hookParsedData['absPath'] != '' && !isset($files[$hookParsedData['absPath']]) && file_exists($hookParsedData['absPath']))
2287
				$function_list += get_defined_functions_in_file($hookParsedData['absPath']);
2288
2289
			$hook_exists = isset($function_list[$hookParsedData['call']]) || (substr($hook, -8) === '_include' && isset($files[$hookParsedData['absPath']]));
2290
			$temp = array(
2291
				'hook_name' => $hook,
2292
				'function_name' => $hookParsedData['rawData'],
2293
				'real_function' => $hookParsedData['call'],
2294
				'included_file' => $hookParsedData['hookFile'],
2295
				'file_name' => strtr($hookParsedData['absPath'] ?: ($function_list[$hookParsedData['call']] ?? ''), [$normalized_boarddir => '.']),
2296
				'instance' => $hookParsedData['object'],
2297
				'hook_exists' => $hook_exists,
2298
				'status' => $hook_exists ? ($hookParsedData['enabled'] ? 'allow' : 'moderate') : 'deny',
2299
				'img_text' => $txt['hooks_' . ($hook_exists ? ($hookParsedData['enabled'] ? 'active' : 'disabled') : 'missing')],
2300
				'enabled' => $hookParsedData['enabled'],
2301
				'can_disable' => $hookParsedData['call'] != '',
2302
			);
2303
			$sort_array[] = $temp[$sort_types[$sort][0]];
2304
			$temp_data[] = $temp;
2305
		}
2306
2307
	array_multisort($sort_array, $sort_types[$sort][1], $temp_data);
2308
2309
	return array_slice($temp_data, $start, $per_page, true);
2310
}
2311
2312
/**
2313
 * Parses modSettings to create integration hook array
2314
 *
2315
 * @return array An array of information about the integration hooks
2316
 */
2317
function get_integration_hooks()
2318
{
2319
	global $modSettings;
2320
	static $integration_hooks;
2321
2322
	if (!isset($integration_hooks))
2323
	{
2324
		$integration_hooks = array();
2325
		foreach ($modSettings as $key => $value)
2326
		{
2327
			if (!empty($value) && substr($key, 0, 10) === 'integrate_')
2328
				$integration_hooks[$key] = explode(',', $value);
2329
		}
2330
	}
2331
2332
	return $integration_hooks;
2333
}
2334
2335
/**
2336
 * Parses each hook data and returns an array.
2337
 *
2338
 * @param string $hook
2339
 * @param string $rawData A string as it was saved to the DB.
2340
 * @return array everything found in the string itself
2341
 */
2342
function parse_integration_hook(string $hook, string $rawData)
2343
{
2344
	global $boarddir, $settings, $sourcedir;
2345
2346
	// A single string can hold tons of info!
2347
	$hookData = array(
2348
		'object' => false,
2349
		'enabled' => true,
2350
		'absPath' => '',
2351
		'hookFile' => '',
2352
		'pureFunc' => '',
2353
		'method' => '',
2354
		'class' => '',
2355
		'call' => '',
2356
		'rawData' => $rawData,
2357
	);
2358
2359
	// Meh...
2360
	if (empty($rawData))
2361
		return $hookData;
2362
2363
	$modFunc = $rawData;
2364
2365
	// Any files?
2366
	if (substr($hook, -8) === '_include')
2367
		$modFunc = $modFunc . '|';
2368
	if (strpos($modFunc, '|') !== false)
2369
	{
2370
		list ($hookData['hookFile'], $modFunc) = explode('|', $modFunc);
2371
		$hookData['absPath'] = strtr(strtr(trim($hookData['hookFile']), array('$boarddir' => $boarddir, '$sourcedir' => $sourcedir, '$themedir' => $settings['theme_dir'] ?? '')), '\\', '/');
2372
	}
2373
2374
	// Hook is an instance.
2375
	if (strpos($modFunc, '#') !== false)
2376
	{
2377
		$modFunc = str_replace('#', '', $modFunc);
2378
		$hookData['object'] = true;
2379
	}
2380
2381
	// Hook is "disabled"
2382
	if (strpos($modFunc, '!') !== false)
2383
	{
2384
		$modFunc = str_replace('!', '', $modFunc);
2385
		$hookData['enabled'] = false;
2386
	}
2387
2388
	// Handling methods?
2389
	if (strpos($modFunc, '::') !== false)
2390
	{
2391
		list ($hookData['class'], $hookData['method']) = explode('::', $modFunc);
2392
		$hookData['pureFunc'] = $hookData['method'];
2393
		$hookData['call'] = $modFunc;
2394
	}
2395
2396
	else
2397
		$hookData['call'] = $hookData['pureFunc'] = $modFunc;
2398
2399
	return $hookData;
2400
}
2401
2402
function get_defined_functions_in_file(string $file): array
2403
{
2404
	$source = file_get_contents($file);
2405
	// token_get_all() is too slow so use a nice little regex instead.
2406
	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);
2407
2408
	$functions = array();
2409
	$namespace = '';
2410
	$class = '';
2411
2412
	foreach ($matches as $match)
2413
	{
2414
		if (!empty($match[1]))
2415
			$namespace = $match[1] . '\\';
2416
		elseif (!empty($match[2]))
2417
			$class = $namespace . $match[2] . '::';
2418
		elseif (!empty($match[3]))
2419
			$functions[$class . $match[3]] = $file;
2420
	}
2421
2422
	return $functions;
2423
}
2424
2425
/**
2426
 * Converts html entities to utf8 equivalents
2427
 * special db wrapper for mysql based on the limitation of mysql/mb3
2428
 *
2429
 * Callback function for preg_replace_callback
2430
 * Uses capture group 1 in the supplied array
2431
 * Does basic checks to keep characters inside a viewable range.
2432
 *
2433
 * @param array $matches An array of matches (relevant info should be the 2nd item in the array)
2434
 * @return string The fixed string or return the old when limitation of mysql is hit
2435
 */
2436
function fixchardb__callback($matches)
2437
{
2438
	global $smcFunc;
2439
	if (!isset($matches[1]))
2440
		return '';
2441
2442
	$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

2442
	$num = $matches[1][0] === 'x' ? hexdec(substr(/** @scrutinizer ignore-type */ $matches[1], 1)) : (int) $matches[1];
Loading history...
2443
2444
	// it's to big for mb3?
2445
	if ($num > 0xFFFF && !$smcFunc['db_mb4'])
2446
		return $matches[0];
2447
	else
2448
		return fixchar__callback($matches);
2449
}
2450
2451
?>