Completed
Pull Request — release-2.1 (#4850)
by Jeremy
08:59
created

ManageMaintenance.php ➔ MaintainDatabase()   B

Complexity

Conditions 10
Paths 16

Size

Total Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 10
nc 16
nop 0
dl 0
loc 23
rs 7.6666
c 0
b 0
f 0

How to fix   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 http://www.simplemachines.org
10
 * @copyright 2018 Simple Machines and individual contributors
11
 * @license http://www.simplemachines.org/about/smf/license.php BSD
12
 *
13
 * @version 2.1 Beta 4
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;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
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
				'logs' => 'MaintainEmptyUnimportantLogs',
56
				'cleancache' => 'MaintainCleanCache',
57
			),
58
		),
59
		'database' => array(
60
			'function' => 'MaintainDatabase',
61
			'template' => 'maintain_database',
62
			'activities' => array(
63
				'optimize' => 'OptimizeTables',
64
				'convertentities' => 'ConvertEntities',
65
				'convertmsgbody' => 'ConvertMsgBody',
66
			),
67
		),
68
		'members' => array(
69
			'function' => 'MaintainMembers',
70
			'template' => 'maintain_members',
71
			'activities' => array(
72
				'reattribute' => 'MaintainReattributePosts',
73
				'purgeinactive' => 'MaintainPurgeInactiveMembers',
74
				'recountposts' => 'MaintainRecountPosts',
75
			),
76
		),
77
		'topics' => array(
78
			'function' => 'MaintainTopics',
79
			'template' => 'maintain_topics',
80
			'activities' => array(
81
				'massmove' => 'MaintainMassMoveTopics',
82
				'pruneold' => 'MaintainRemoveOldPosts',
83
				'olddrafts' => 'MaintainRemoveOldDrafts',
84
			),
85
		),
86
		'hooks' => array(
87
			'function' => 'list_integration_hooks',
88
		),
89
		'destroy' => array(
90
			'function' => 'Destroy',
91
			'activities' => array(),
92
		),
93
	);
94
95
	call_integration_hook('integrate_manage_maintenance', array(&$subActions));
96
97
	// Yep, sub-action time!
98
	if (isset($_REQUEST['sa']) && isset($subActions[$_REQUEST['sa']]))
99
		$subAction = $_REQUEST['sa'];
100
	else
101
		$subAction = 'routine';
102
103
	// Doing something special?
104
	if (isset($_REQUEST['activity']) && isset($subActions[$subAction]['activities'][$_REQUEST['activity']]))
105
		$activity = $_REQUEST['activity'];
106
107
	// Set a few things.
108
	$context['page_title'] = $txt['maintain_title'];
109
	$context['sub_action'] = $subAction;
110
	$context['sub_template'] = !empty($subActions[$subAction]['template']) ? $subActions[$subAction]['template'] : '';
111
112
	// Finally fall through to what we are doing.
113
	call_helper($subActions[$subAction]['function']);
114
115
	// Any special activity?
116
	if (isset($activity))
117
		call_helper($subActions[$subAction]['activities'][$activity]);
118
119
	// Create a maintenance token.  Kinda hard to do it any other way.
120
	createToken('admin-maint');
121
}
122
123
/**
124
 * Supporting function for the database maintenance area.
125
 */
126
function MaintainDatabase()
127
{
128
	global $context, $db_type, $db_character_set, $modSettings, $smcFunc, $txt;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
129
130
	// Show some conversion options?
131
	$context['convert_entities'] = isset($modSettings['global_character_set']) && $modSettings['global_character_set'] === 'UTF-8';
132
133
	if ($db_type == 'mysql')
134
	{
135
		db_extend('packages');
136
137
		$colData = $smcFunc['db_list_columns']('{db_prefix}messages', true);
138
		foreach ($colData as $column)
139
			if ($column['name'] == 'body')
140
				$body_type = $column['type'];
141
142
		$context['convert_to'] = $body_type == 'text' ? 'mediumtext' : 'text';
0 ignored issues
show
Bug introduced by
The variable $body_type does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
143
		$context['convert_to_suggest'] = ($body_type != 'text' && !empty($modSettings['max_messageLength']) && $modSettings['max_messageLength'] < 65536);
144
	}
145
146
	if (isset($_GET['done']) && $_GET['done'] == 'convertentities')
147
		$context['maintenance_finished'] = $txt['entity_convert_title'];
148
}
149
150
/**
151
 * Supporting function for the routine maintenance area.
152
 */
153
function MaintainRoutine()
154
{
155
	global $context, $txt;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
156
157
	if (isset($_GET['done']) && $_GET['done'] == 'recount')
158
		$context['maintenance_finished'] = $txt['maintain_recount'];
159
}
160
161
/**
162
 * Supporting function for the members maintenance area.
163
 */
164
function MaintainMembers()
165
{
166
	global $context, $smcFunc, $txt;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
167
168
	// Get membergroups - for deleting members and the like.
169
	$result = $smcFunc['db_query']('', '
170
		SELECT id_group, group_name
171
		FROM {db_prefix}membergroups',
172
		array(
173
		)
174
	);
175
	$context['membergroups'] = array(
176
		array(
177
			'id' => 0,
178
			'name' => $txt['maintain_members_ungrouped']
179
		),
180
	);
181
	while ($row = $smcFunc['db_fetch_assoc']($result))
182
	{
183
		$context['membergroups'][] = array(
184
			'id' => $row['id_group'],
185
			'name' => $row['group_name']
186
		);
187
	}
188
	$smcFunc['db_free_result']($result);
189
190
	if (isset($_GET['done']) && $_GET['done'] == 'recountposts')
191
		$context['maintenance_finished'] = $txt['maintain_recountposts'];
192
193
	loadJavaScriptFile('suggest.js', array('defer' => false, 'minimize' => true), 'smf_suggest');
194
}
195
196
/**
197
 * Supporting function for the topics maintenance area.
198
 */
199
function MaintainTopics()
200
{
201
	global $context, $smcFunc, $txt, $sourcedir;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
202
203
	// Let's load up the boards in case they are useful.
204
	$result = $smcFunc['db_query']('order_by_board_order', '
205
		SELECT b.id_board, b.name, b.child_level, c.name AS cat_name, c.id_cat
206
		FROM {db_prefix}boards AS b
207
			LEFT JOIN {db_prefix}categories AS c ON (c.id_cat = b.id_cat)
208
		WHERE {query_see_board}
209
			AND redirect = {string:blank_redirect}',
210
		array(
211
			'blank_redirect' => '',
212
		)
213
	);
214
	$context['categories'] = array();
215
	while ($row = $smcFunc['db_fetch_assoc']($result))
216
	{
217
		if (!isset($context['categories'][$row['id_cat']]))
218
			$context['categories'][$row['id_cat']] = array(
219
				'name' => $row['cat_name'],
220
				'boards' => array()
221
			);
222
223
		$context['categories'][$row['id_cat']]['boards'][$row['id_board']] = array(
224
			'id' => $row['id_board'],
225
			'name' => $row['name'],
226
			'child_level' => $row['child_level']
227
		);
228
	}
229
	$smcFunc['db_free_result']($result);
230
231
	require_once($sourcedir . '/Subs-Boards.php');
232
	sortCategories($context['categories']);
233
234
	if (isset($_GET['done']) && $_GET['done'] == 'purgeold')
235
		$context['maintenance_finished'] = $txt['maintain_old'];
236
	elseif (isset($_GET['done']) && $_GET['done'] == 'massmove')
237
		$context['maintenance_finished'] = $txt['move_topics_maintenance'];
238
}
239
240
/**
241
 * Find and fix all errors on the forum.
242
 */
243
function MaintainFindFixErrors()
244
{
245
	global $sourcedir;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
246
247
	// Honestly, this should be done in the sub function.
248
	validateToken('admin-maint');
249
250
	require_once($sourcedir . '/RepairBoards.php');
251
	RepairBoards();
252
}
253
254
/**
255
 * Wipes the whole cache directory.
256
 * This only applies to SMF's own cache directory, though.
257
 */
258
function MaintainCleanCache()
259
{
260
	global $context, $txt;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
261
262
	checkSession();
263
	validateToken('admin-maint');
264
265
	// Just wipe the whole cache directory!
266
	clean_cache();
267
268
	$context['maintenance_finished'] = $txt['maintain_cache'];
269
}
270
271
/**
272
 * Empties all uninmportant logs
273
 */
274
function MaintainEmptyUnimportantLogs()
275
{
276
	global $context, $smcFunc, $txt;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
277
278
	checkSession();
279
	validateToken('admin-maint');
280
281
	// No one's online now.... MUHAHAHAHA :P.
282
	$smcFunc['db_query']('', '
283
		DELETE FROM {db_prefix}log_online');
284
285
	// Dump the banning logs.
286
	$smcFunc['db_query']('', '
287
		DELETE FROM {db_prefix}log_banned');
288
289
	// Start id_error back at 0 and dump the error log.
290
	$smcFunc['db_query']('truncate_table', '
291
		TRUNCATE {db_prefix}log_errors');
292
293
	// Clear out the spam log.
294
	$smcFunc['db_query']('', '
295
		DELETE FROM {db_prefix}log_floodcontrol');
296
297
	// Last but not least, the search logs!
298
	$smcFunc['db_query']('truncate_table', '
299
		TRUNCATE {db_prefix}log_search_topics');
300
301
	$smcFunc['db_query']('truncate_table', '
302
		TRUNCATE {db_prefix}log_search_messages');
303
304
	$smcFunc['db_query']('truncate_table', '
305
		TRUNCATE {db_prefix}log_search_results');
306
307
	updateSettings(array('search_pointer' => 0));
308
309
	$context['maintenance_finished'] = $txt['maintain_logs'];
310
}
311
312
/**
313
 * Oh noes! I'd document this but that would give it away
314
 */
315
function Destroy()
316
{
317
	global $context;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
318
319
	echo '<!DOCTYPE html>
320
		<html', $context['right_to_left'] ? ' dir="rtl"' : '', '><head><title>', $context['forum_name_html_safe'], ' deleted!</title></head>
321
		<body style="background-color: orange; font-family: arial, sans-serif; text-align: center;">
322
		<div style="margin-top: 8%; font-size: 400%; color: black;">Oh my, you killed ', $context['forum_name_html_safe'], '!</div>
323
		<div style="margin-top: 7%; font-size: 500%; color: red;"><strong>You lazy bum!</strong></div>
324
		</body></html>';
325
	obExit(false);
326
}
327
328
/**
329
 * Convert the column "body" of the table {db_prefix}messages from TEXT to MEDIUMTEXT and vice versa.
330
 * It requires the admin_forum permission.
331
 * This is needed only for MySQL.
332
 * During the conversion from MEDIUMTEXT to TEXT it check if any of the posts exceed the TEXT length and if so it aborts.
333
 * This action is linked from the maintenance screen (if it's applicable).
334
 * Accessed by ?action=admin;area=maintain;sa=database;activity=convertmsgbody.
335
 *
336
 * @uses the convert_msgbody sub template of the Admin template.
337
 */
338
function ConvertMsgBody()
339
{
340
	global $scripturl, $context, $txt, $db_type;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
341
	global $modSettings, $smcFunc, $time_start;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
342
343
	// Show me your badge!
344
	isAllowedTo('admin_forum');
345
346
	if ($db_type != 'mysql')
347
		return;
348
349
	db_extend('packages');
350
351
	$colData = $smcFunc['db_list_columns']('{db_prefix}messages', true);
352
	foreach ($colData as $column)
353
		if ($column['name'] == 'body')
354
			$body_type = $column['type'];
355
356
	$context['convert_to'] = $body_type == 'text' ? 'mediumtext' : 'text';
0 ignored issues
show
Bug introduced by
The variable $body_type does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
357
358
	if ($body_type == 'text' || ($body_type != 'text' && isset($_POST['do_conversion'])))
359
	{
360
		checkSession();
361
		validateToken('admin-maint');
362
363
		// Make it longer so we can do their limit.
364
		if ($body_type == 'text')
365
			$smcFunc['db_change_column']('{db_prefix}messages', 'body', array('type' => 'mediumtext'));
366
		// Shorten the column so we can have a bit (literally per record) less space occupied
367
		else
368
			$smcFunc['db_change_column']('{db_prefix}messages', 'body', array('type' => 'text'));
369
370
		// 3rd party integrations may be interested in knowning about this.
371
		call_integration_hook('integrate_convert_msgbody', array($body_type));
372
373
		$colData = $smcFunc['db_list_columns']('{db_prefix}messages', true);
374
		foreach ($colData as $column)
375
			if ($column['name'] == 'body')
376
				$body_type = $column['type'];
377
378
		$context['maintenance_finished'] = $txt[$context['convert_to'] . '_title'];
379
		$context['convert_to'] = $body_type == 'text' ? 'mediumtext' : 'text';
380
		$context['convert_to_suggest'] = ($body_type != 'text' && !empty($modSettings['max_messageLength']) && $modSettings['max_messageLength'] < 65536);
381
382
		return;
383
	}
384
	elseif ($body_type != 'text' && (!isset($_POST['do_conversion']) || isset($_POST['cont'])))
385
	{
386
		checkSession();
387
		if (empty($_REQUEST['start']))
388
			validateToken('admin-maint');
389
		else
390
			validateToken('admin-convertMsg');
391
392
		$context['page_title'] = $txt['not_done_title'];
393
		$context['continue_post_data'] = '';
394
		$context['continue_countdown'] = 3;
395
		$context['sub_template'] = 'not_done';
396
		$increment = 500;
397
		$id_msg_exceeding = isset($_POST['id_msg_exceeding']) ? explode(',', $_POST['id_msg_exceeding']) : array();
398
399
		$request = $smcFunc['db_query']('', '
400
			SELECT COUNT(*) as count
401
			FROM {db_prefix}messages',
402
			array()
403
		);
404
		list($max_msgs) = $smcFunc['db_fetch_row']($request);
405
		$smcFunc['db_free_result']($request);
406
407
		// Try for as much time as possible.
408
		@set_time_limit(600);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
409
410
		while ($_REQUEST['start'] < $max_msgs)
411
		{
412
			$request = $smcFunc['db_query']('', '
413
				SELECT /*!40001 SQL_NO_CACHE */ id_msg
414
				FROM {db_prefix}messages
415
				WHERE id_msg BETWEEN {int:start} AND {int:start} + {int:increment}
416
					AND LENGTH(body) > 65535',
417
				array(
418
					'start' => $_REQUEST['start'],
419
					'increment' => $increment - 1,
420
				)
421
			);
422
			while ($row = $smcFunc['db_fetch_assoc']($request))
423
				$id_msg_exceeding[] = $row['id_msg'];
424
			$smcFunc['db_free_result']($request);
425
426
			$_REQUEST['start'] += $increment;
427
428
			if (microtime(true) - $time_start > 3)
429
			{
430
				createToken('admin-convertMsg');
431
				$context['continue_post_data'] = '
432
					<input type="hidden" name="' . $context['admin-convertMsg_token_var'] . '" value="' . $context['admin-convertMsg_token'] . '">
433
					<input type="hidden" name="' . $context['session_var'] . '" value="' . $context['session_id'] . '">
434
					<input type="hidden" name="id_msg_exceeding" value="' . implode(',', $id_msg_exceeding) . '">';
435
436
				$context['continue_get_data'] = '?action=admin;area=maintain;sa=database;activity=convertmsgbody;start=' . $_REQUEST['start'];
437
				$context['continue_percent'] = round(100 * $_REQUEST['start'] / $max_msgs);
438
439
				return;
440
			}
441
		}
442
		createToken('admin-maint');
443
		$context['page_title'] = $txt[$context['convert_to'] . '_title'];
444
		$context['sub_template'] = 'convert_msgbody';
445
446
		if (!empty($id_msg_exceeding))
447
		{
448
			if (count($id_msg_exceeding) > 100)
449
			{
450
				$query_msg = array_slice($id_msg_exceeding, 0, 100);
451
				$context['exceeding_messages_morethan'] = sprintf($txt['exceeding_messages_morethan'], count($id_msg_exceeding));
452
			}
453
			else
454
				$query_msg = $id_msg_exceeding;
455
456
			$context['exceeding_messages'] = array();
457
			$request = $smcFunc['db_query']('', '
458
				SELECT id_msg, id_topic, subject
459
				FROM {db_prefix}messages
460
				WHERE id_msg IN ({array_int:messages})',
461
				array(
462
					'messages' => $query_msg,
463
				)
464
			);
465
			while ($row = $smcFunc['db_fetch_assoc']($request))
466
				$context['exceeding_messages'][] = '<a href="' . $scripturl . '?topic=' . $row['id_topic'] . '.msg' . $row['id_msg'] . '#msg' . $row['id_msg'] . '">' . $row['subject'] . '</a>';
467
			$smcFunc['db_free_result']($request);
468
		}
469
	}
470
}
471
472
/**
473
 * Converts HTML-entities to their UTF-8 character equivalents.
474
 * This requires the admin_forum permission.
475
 * Pre-condition: UTF-8 has been set as database and global character set.
476
 *
477
 * It is divided in steps of 10 seconds.
478
 * This action is linked from the maintenance screen (if applicable).
479
 * It is accessed by ?action=admin;area=maintain;sa=database;activity=convertentities.
480
 *
481
 * @uses Admin template, convert_entities sub-template.
482
 */
483
function ConvertEntities()
484
{
485
	global $db_character_set, $modSettings, $context, $sourcedir, $smcFunc, $db_type, $db_prefix;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
486
487
	isAllowedTo('admin_forum');
488
489
	// Check to see if UTF-8 is currently the default character set.
490
	if ($modSettings['global_character_set'] !== 'UTF-8' || !isset($db_character_set) || $db_character_set !== 'utf8')
491
		fatal_lang_error('entity_convert_only_utf8');
492
493
	// Some starting values.
494
	$context['table'] = empty($_REQUEST['table']) ? 0 : (int) $_REQUEST['table'];
495
	$context['start'] = empty($_REQUEST['start']) ? 0 : (int) $_REQUEST['start'];
496
497
	$context['start_time'] = time();
498
499
	$context['first_step'] = !isset($_REQUEST[$context['session_var']]);
500
	$context['last_step'] = false;
501
502
	// The first step is just a text screen with some explanation.
503
	if ($context['first_step'])
504
	{
505
		validateToken('admin-maint');
506
		createToken('admin-maint');
507
508
		$context['sub_template'] = 'convert_entities';
509
		return;
510
	}
511
	// Otherwise use the generic "not done" template.
512
	$context['sub_template'] = 'not_done';
513
	$context['continue_post_data'] = '';
514
	$context['continue_countdown'] = 3;
515
516
	// Now we're actually going to convert...
517
	checkSession('request');
518
	validateToken('admin-maint');
519
	createToken('admin-maint');
520
521
	// A list of tables ready for conversion.
522
	$tables = array(
523
		'ban_groups',
524
		'ban_items',
525
		'boards',
526
		'calendar',
527
		'calendar_holidays',
528
		'categories',
529
		'log_errors',
530
		'log_search_subjects',
531
		'membergroups',
532
		'members',
533
		'message_icons',
534
		'messages',
535
		'package_servers',
536
		'personal_messages',
537
		'pm_recipients',
538
		'polls',
539
		'poll_choices',
540
		'smileys',
541
		'themes',
542
	);
543
	$context['num_tables'] = count($tables);
544
545
	// Loop through all tables that need converting.
546
	for (; $context['table'] < $context['num_tables']; $context['table']++)
547
	{
548
		$cur_table = $tables[$context['table']];
549
		$primary_key = '';
550
		// Make sure we keep stuff unique!
551
		$primary_keys = array();
552
553
		if (function_exists('apache_reset_timeout'))
554
			@apache_reset_timeout();
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
555
556
		// Get a list of text columns.
557
		$columns = array();
558 View Code Duplication
		if ($db_type == 'postgresql')
559
			$request = $smcFunc['db_query']('', '
560
				SELECT column_name "Field", data_type "Type"
561
				FROM information_schema.columns 
562
				WHERE table_name = {string:cur_table}
563
				AND (data_type = \'character varying\' or data_type = \'text\')',
564
				array(
565
					'cur_table' => $db_prefix.$cur_table,
566
				)
567
			);
568
		else
569
			$request = $smcFunc['db_query']('', '
570
				SHOW FULL COLUMNS
571
				FROM {db_prefix}{raw:cur_table}',
572
				array(
573
					'cur_table' => $cur_table,
574
				)
575
			);
576
		while ($column_info = $smcFunc['db_fetch_assoc']($request))
577
			if (strpos($column_info['Type'], 'text') !== false || strpos($column_info['Type'], 'char') !== false)
578
				$columns[] = strtolower($column_info['Field']);
579
580
		// Get the column with the (first) primary key.
581 View Code Duplication
		if ($db_type == 'postgresql')
582
			$request = $smcFunc['db_query']('', '
583
				SELECT a.attname "Column_name", \'PRIMARY\' "Key_name", attnum "Seq_in_index"
584
				FROM   pg_index i
585
				JOIN   pg_attribute a ON a.attrelid = i.indrelid
586
									 AND a.attnum = ANY(i.indkey)
587
				WHERE  i.indrelid = {string:cur_table}::regclass
588
				AND    i.indisprimary',
589
				array(
590
					'cur_table' => $db_prefix.$cur_table,
591
				)
592
			);
593
		else
594
			$request = $smcFunc['db_query']('', '
595
				SHOW KEYS
596
				FROM {db_prefix}{raw:cur_table}',
597
				array(
598
					'cur_table' => $cur_table,
599
				)
600
			);
601
		while ($row = $smcFunc['db_fetch_assoc']($request))
602
		{
603
			if ($row['Key_name'] === 'PRIMARY')
604
			{
605
				if ((empty($primary_key) || $row['Seq_in_index'] == 1) && !in_array(strtolower($row['Column_name']), $columns))
606
					$primary_key = $row['Column_name'];
607
608
				$primary_keys[] = $row['Column_name'];
609
			}
610
		}
611
		$smcFunc['db_free_result']($request);
612
613
		// No primary key, no glory.
614
		// Same for columns. Just to be sure we've work to do!
615
		if (empty($primary_key) || empty($columns))
616
			continue;
617
618
		// Get the maximum value for the primary key.
619
		$request = $smcFunc['db_query']('', '
620
			SELECT MAX({identifier:key})
621
			FROM {db_prefix}{raw:cur_table}',
622
			array(
623
				'key' => $primary_key,
624
				'cur_table' => $cur_table,
625
			)
626
		);
627
		list($max_value) = $smcFunc['db_fetch_row']($request);
628
		$smcFunc['db_free_result']($request);
629
630
		if (empty($max_value))
631
			continue;
632
633
		while ($context['start'] <= $max_value)
634
		{
635
			// Retrieve a list of rows that has at least one entity to convert.
636
			$request = $smcFunc['db_query']('', '
637
				SELECT {raw:primary_keys}, {raw:columns}
638
				FROM {db_prefix}{raw:cur_table}
639
				WHERE {raw:primary_key} BETWEEN {int:start} AND {int:start} + 499
640
					AND {raw:like_compare}
641
				LIMIT 500',
642
				array(
643
					'primary_keys' => implode(', ', $primary_keys),
644
					'columns' => implode(', ', $columns),
645
					'cur_table' => $cur_table,
646
					'primary_key' => $primary_key,
647
					'start' => $context['start'],
648
					'like_compare' => '(' . implode(' LIKE \'%&#%\' OR ', $columns) . ' LIKE \'%&#%\')',
649
				)
650
			);
651
			while ($row = $smcFunc['db_fetch_assoc']($request))
652
			{
653
				$insertion_variables = array();
654
				$changes = array();
655
				foreach ($row as $column_name => $column_value)
656
					if ($column_name !== $primary_key && strpos($column_value, '&#') !== false)
657
					{
658
						$changes[] = $column_name . ' = {string:changes_' . $column_name . '}';
659
						$insertion_variables['changes_' . $column_name] = preg_replace_callback('~&#(\d{1,5}|x[0-9a-fA-F]{1,4});~', 'fixchardb__callback', $column_value);
660
					}
661
662
				$where = array();
663
				foreach ($primary_keys as $key)
664
				{
665
					$where[] = $key . ' = {string:where_' . $key . '}';
666
					$insertion_variables['where_' . $key] = $row[$key];
667
				}
668
669
				// Update the row.
670
				if (!empty($changes))
671
					$smcFunc['db_query']('', '
672
						UPDATE {db_prefix}' . $cur_table . '
673
						SET
674
							' . implode(',
675
							', $changes) . '
676
						WHERE ' . implode(' AND ', $where),
677
						$insertion_variables
678
					);
679
			}
680
			$smcFunc['db_free_result']($request);
681
			$context['start'] += 500;
682
683
			// After ten seconds interrupt.
684
			if (time() - $context['start_time'] > 10)
685
			{
686
				// Calculate an approximation of the percentage done.
687
				$context['continue_percent'] = round(100 * ($context['table'] + ($context['start'] / $max_value)) / $context['num_tables'], 1);
688
				$context['continue_get_data'] = '?action=admin;area=maintain;sa=database;activity=convertentities;table=' . $context['table'] . ';start=' . $context['start'] . ';' . $context['session_var'] . '=' . $context['session_id'];
689
				return;
690
			}
691
		}
692
		$context['start'] = 0;
693
	}
694
695
	// If we're here, we must be done.
696
	$context['continue_percent'] = 100;
697
	$context['continue_get_data'] = '?action=admin;area=maintain;sa=database;done=convertentities';
698
	$context['last_step'] = true;
699
	$context['continue_countdown'] = -1;
700
}
701
702
/**
703
 * Optimizes all tables in the database and lists how much was saved.
704
 * It requires the admin_forum permission.
705
 * It shows as the maintain_forum admin area.
706
 * It is accessed from ?action=admin;area=maintain;sa=database;activity=optimize.
707
 * It also updates the optimize scheduled task such that the tables are not automatically optimized again too soon.
708
709
 * @uses the optimize sub template
710
 */
711
function OptimizeTables()
712
{
713
	global $db_prefix, $txt, $context, $smcFunc, $time_start;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
714
715
	isAllowedTo('admin_forum');
716
717
	checkSession('request');
718
719
	if (!isset($_SESSION['optimized_tables']))
720
		validateToken('admin-maint');
721
	else
722
		validateToken('admin-optimize', 'post', false);
723
724
	ignore_user_abort(true);
725
	db_extend();
726
727
	$context['page_title'] = $txt['database_optimize'];
728
	$context['sub_template'] = 'optimize';
729
	$context['continue_post_data'] = '';
730
	$context['continue_countdown'] = 3;
731
732
	// Only optimize the tables related to this smf install, not all the tables in the db
733
	$real_prefix = preg_match('~^(`?)(.+?)\\1\\.(.*?)$~', $db_prefix, $match) === 1 ? $match[3] : $db_prefix;
734
735
	// Get a list of tables, as well as how many there are.
736
	$temp_tables = $smcFunc['db_list_tables'](false, $real_prefix . '%');
737
	$tables = array();
738
	foreach ($temp_tables as $table)
739
		$tables[] = array('table_name' => $table);
740
741
	// If there aren't any tables then I believe that would mean the world has exploded...
742
	$context['num_tables'] = count($tables);
743
	if ($context['num_tables'] == 0)
744
		fatal_error('You appear to be running SMF in a flat file mode... fantastic!', false);
0 ignored issues
show
Documentation introduced by
false is of type boolean, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
745
746
	$_REQUEST['start'] = empty($_REQUEST['start']) ? 0 : (int) $_REQUEST['start'];
747
748
	// Try for extra time due to large tables.
749
	@set_time_limit(100);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
750
751
	// For each table....
752
	$_SESSION['optimized_tables'] = !empty($_SESSION['optimized_tables']) ? $_SESSION['optimized_tables'] : array();
753
	for ($key = $_REQUEST['start']; $context['num_tables'] - 1; $key++)
754
	{
755
		if (empty($tables[$key]))
756
			break;
757
758
		// Continue?
759
		if (microtime(true) - $time_start > 10)
760
		{
761
			$_REQUEST['start'] = $key;
762
			$context['continue_get_data'] = '?action=admin;area=maintain;sa=database;activity=optimize;start=' . $_REQUEST['start'] . ';' . $context['session_var'] . '=' . $context['session_id'];
763
			$context['continue_percent'] = round(100 * $_REQUEST['start'] / $context['num_tables']);
764
			$context['sub_template'] = 'not_done';
765
			$context['page_title'] = $txt['not_done_title'];
766
767
			createToken('admin-optimize');
768
			$context['continue_post_data'] = '<input type="hidden" name="' . $context['admin-optimize_token_var'] . '" value="' . $context['admin-optimize_token'] . '">';
769
770
			if (function_exists('apache_reset_timeout'))
771
				apache_reset_timeout();
772
773
			return;
774
		}
775
776
		// Optimize the table!  We use backticks here because it might be a custom table.
777
		$data_freed = $smcFunc['db_optimize_table']($tables[$key]['table_name']);
778
779
		if ($data_freed > 0)
780
			$_SESSION['optimized_tables'][] = array(
781
				'name' => $tables[$key]['table_name'],
782
				'data_freed' => $data_freed,
783
			);
784
	}
785
786
	// Number of tables, etc...
787
	$txt['database_numb_tables'] = sprintf($txt['database_numb_tables'], $context['num_tables']);
788
	$context['num_tables_optimized'] = count($_SESSION['optimized_tables']);
789
	$context['optimized_tables'] = $_SESSION['optimized_tables'];
790
	unset($_SESSION['optimized_tables']);
791
}
792
793
/**
794
 * Recount many forum totals that can be recounted automatically without harm.
795
 * it requires the admin_forum permission.
796
 * It shows the maintain_forum admin area.
797
 *
798
 * Totals recounted:
799
 * - fixes for topics with wrong num_replies.
800
 * - updates for num_posts and num_topics of all boards.
801
 * - recounts instant_messages but not unread_messages.
802
 * - repairs messages pointing to boards with topics pointing to other boards.
803
 * - updates the last message posted in boards and children.
804
 * - updates member count, latest member, topic count, and message count.
805
 *
806
 * The function redirects back to ?action=admin;area=maintain when complete.
807
 * It is accessed via ?action=admin;area=maintain;sa=database;activity=recount.
808
 */
809
function AdminBoardRecount()
810
{
811
	global $txt, $context, $modSettings, $sourcedir;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
812
	global $time_start, $smcFunc;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
813
814
	isAllowedTo('admin_forum');
815
	checkSession('request');
816
817
	// validate the request or the loop
818
	if (!isset($_REQUEST['step']))
819
		validateToken('admin-maint');
820
	else
821
		validateToken('admin-boardrecount');
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);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
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 /*!40001 SQL_NO_CACHE */ t.id_topic, MAX(t.num_replies) AS num_replies,
857
					CASE WHEN COUNT(ma.id_msg) >= 1 THEN COUNT(ma.id_msg) - 1 ELSE 0 END 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 CASE WHEN COUNT(ma.id_msg) >= 1 THEN COUNT(ma.id_msg) - 1 ELSE 0 END != 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 /*!40001 SQL_NO_CACHE */ 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
				createToken('admin-boardrecount');
915
				$context['continue_post_data'] = '<input type="hidden" name="' . $context['admin-boardrecount_token_var'] . '" value="' . $context['admin-boardrecount_token'] . '">';
916
917
				$context['continue_get_data'] = '?action=admin;area=maintain;sa=routine;activity=recount;step=0;start=' . $_REQUEST['start'] . ';' . $context['session_var'] . '=' . $context['session_id'];
918
				$context['continue_percent'] = round((100 * $_REQUEST['start'] / $max_topics) / $total_steps);
919
920
				return;
921
			}
922
		}
923
924
		$_REQUEST['start'] = 0;
925
	}
926
927
	// Update the post count of each board.
928 View Code Duplication
	if ($_REQUEST['step'] <= 1)
929
	{
930
		if (empty($_REQUEST['start']))
931
			$smcFunc['db_query']('', '
932
				UPDATE {db_prefix}boards
933
				SET num_posts = {int:num_posts}
934
				WHERE redirect = {string:redirect}',
935
				array(
936
					'num_posts' => 0,
937
					'redirect' => '',
938
				)
939
			);
940
941
		while ($_REQUEST['start'] < $max_topics)
942
		{
943
			$request = $smcFunc['db_query']('', '
944
				SELECT /*!40001 SQL_NO_CACHE */ m.id_board, COUNT(*) AS real_num_posts
945
				FROM {db_prefix}messages AS m
946
				WHERE m.id_topic > {int:id_topic_min}
947
					AND m.id_topic <= {int:id_topic_max}
948
					AND m.approved = {int:is_approved}
949
				GROUP BY m.id_board',
950
				array(
951
					'id_topic_min' => $_REQUEST['start'],
952
					'id_topic_max' => $_REQUEST['start'] + $increment,
953
					'is_approved' => 1,
954
				)
955
			);
956
			while ($row = $smcFunc['db_fetch_assoc']($request))
957
				$smcFunc['db_query']('', '
958
					UPDATE {db_prefix}boards
959
					SET num_posts = num_posts + {int:real_num_posts}
960
					WHERE id_board = {int:id_board}',
961
					array(
962
						'id_board' => $row['id_board'],
963
						'real_num_posts' => $row['real_num_posts'],
964
					)
965
				);
966
			$smcFunc['db_free_result']($request);
967
968
			$_REQUEST['start'] += $increment;
969
970
			if (microtime(true) - $time_start > 3)
971
			{
972
				createToken('admin-boardrecount');
973
				$context['continue_post_data'] = '<input type="hidden" name="' . $context['admin-boardrecount_token_var'] . '" value="' . $context['admin-boardrecount_token'] . '">';
974
975
				$context['continue_get_data'] = '?action=admin;area=maintain;sa=routine;activity=recount;step=1;start=' . $_REQUEST['start'] . ';' . $context['session_var'] . '=' . $context['session_id'];
976
				$context['continue_percent'] = round((200 + 100 * $_REQUEST['start'] / $max_topics) / $total_steps);
977
978
				return;
979
			}
980
		}
981
982
		$_REQUEST['start'] = 0;
983
	}
984
985
	// Update the topic count of each board.
986 View Code Duplication
	if ($_REQUEST['step'] <= 2)
987
	{
988
		if (empty($_REQUEST['start']))
989
			$smcFunc['db_query']('', '
990
				UPDATE {db_prefix}boards
991
				SET num_topics = {int:num_topics}',
992
				array(
993
					'num_topics' => 0,
994
				)
995
			);
996
997
		while ($_REQUEST['start'] < $max_topics)
998
		{
999
			$request = $smcFunc['db_query']('', '
1000
				SELECT /*!40001 SQL_NO_CACHE */ t.id_board, COUNT(*) AS real_num_topics
1001
				FROM {db_prefix}topics AS t
1002
				WHERE t.approved = {int:is_approved}
1003
					AND t.id_topic > {int:id_topic_min}
1004
					AND t.id_topic <= {int:id_topic_max}
1005
				GROUP BY t.id_board',
1006
				array(
1007
					'is_approved' => 1,
1008
					'id_topic_min' => $_REQUEST['start'],
1009
					'id_topic_max' => $_REQUEST['start'] + $increment,
1010
				)
1011
			);
1012
			while ($row = $smcFunc['db_fetch_assoc']($request))
1013
				$smcFunc['db_query']('', '
1014
					UPDATE {db_prefix}boards
1015
					SET num_topics = num_topics + {int:real_num_topics}
1016
					WHERE id_board = {int:id_board}',
1017
					array(
1018
						'id_board' => $row['id_board'],
1019
						'real_num_topics' => $row['real_num_topics'],
1020
					)
1021
				);
1022
			$smcFunc['db_free_result']($request);
1023
1024
			$_REQUEST['start'] += $increment;
1025
1026
			if (microtime(true) - $time_start > 3)
1027
			{
1028
				createToken('admin-boardrecount');
1029
				$context['continue_post_data'] = '<input type="hidden" name="' . $context['admin-boardrecount_token_var'] . '" value="' . $context['admin-boardrecount_token'] . '">';
1030
1031
				$context['continue_get_data'] = '?action=admin;area=maintain;sa=routine;activity=recount;step=2;start=' . $_REQUEST['start'] . ';' . $context['session_var'] . '=' . $context['session_id'];
1032
				$context['continue_percent'] = round((300 + 100 * $_REQUEST['start'] / $max_topics) / $total_steps);
1033
1034
				return;
1035
			}
1036
		}
1037
1038
		$_REQUEST['start'] = 0;
1039
	}
1040
1041
	// Update the unapproved post count of each board.
1042 View Code Duplication
	if ($_REQUEST['step'] <= 3)
1043
	{
1044
		if (empty($_REQUEST['start']))
1045
			$smcFunc['db_query']('', '
1046
				UPDATE {db_prefix}boards
1047
				SET unapproved_posts = {int:unapproved_posts}',
1048
				array(
1049
					'unapproved_posts' => 0,
1050
				)
1051
			);
1052
1053
		while ($_REQUEST['start'] < $max_topics)
1054
		{
1055
			$request = $smcFunc['db_query']('', '
1056
				SELECT /*!40001 SQL_NO_CACHE */ m.id_board, COUNT(*) AS real_unapproved_posts
1057
				FROM {db_prefix}messages AS m
1058
				WHERE m.id_topic > {int:id_topic_min}
1059
					AND m.id_topic <= {int:id_topic_max}
1060
					AND m.approved = {int:is_approved}
1061
				GROUP BY m.id_board',
1062
				array(
1063
					'id_topic_min' => $_REQUEST['start'],
1064
					'id_topic_max' => $_REQUEST['start'] + $increment,
1065
					'is_approved' => 0,
1066
				)
1067
			);
1068
			while ($row = $smcFunc['db_fetch_assoc']($request))
1069
				$smcFunc['db_query']('', '
1070
					UPDATE {db_prefix}boards
1071
					SET unapproved_posts = unapproved_posts + {int:unapproved_posts}
1072
					WHERE id_board = {int:id_board}',
1073
					array(
1074
						'id_board' => $row['id_board'],
1075
						'unapproved_posts' => $row['real_unapproved_posts'],
1076
					)
1077
				);
1078
			$smcFunc['db_free_result']($request);
1079
1080
			$_REQUEST['start'] += $increment;
1081
1082
			if (microtime(true) - $time_start > 3)
1083
			{
1084
				createToken('admin-boardrecount');
1085
				$context['continue_post_data'] = '<input type="hidden" name="' . $context['admin-boardrecount_token_var'] . '" value="' . $context['admin-boardrecount_token'] . '">';
1086
1087
				$context['continue_get_data'] = '?action=admin;area=maintain;sa=routine;activity=recount;step=3;start=' . $_REQUEST['start'] . ';' . $context['session_var'] . '=' . $context['session_id'];
1088
				$context['continue_percent'] = round((400 + 100 * $_REQUEST['start'] / $max_topics) / $total_steps);
1089
1090
				return;
1091
			}
1092
		}
1093
1094
		$_REQUEST['start'] = 0;
1095
	}
1096
1097
	// Update the unapproved topic count of each board.
1098 View Code Duplication
	if ($_REQUEST['step'] <= 4)
1099
	{
1100
		if (empty($_REQUEST['start']))
1101
			$smcFunc['db_query']('', '
1102
				UPDATE {db_prefix}boards
1103
				SET unapproved_topics = {int:unapproved_topics}',
1104
				array(
1105
					'unapproved_topics' => 0,
1106
				)
1107
			);
1108
1109
		while ($_REQUEST['start'] < $max_topics)
1110
		{
1111
			$request = $smcFunc['db_query']('', '
1112
				SELECT /*!40001 SQL_NO_CACHE */ t.id_board, COUNT(*) AS real_unapproved_topics
1113
				FROM {db_prefix}topics AS t
1114
				WHERE t.approved = {int:is_approved}
1115
					AND t.id_topic > {int:id_topic_min}
1116
					AND t.id_topic <= {int:id_topic_max}
1117
				GROUP BY t.id_board',
1118
				array(
1119
					'is_approved' => 0,
1120
					'id_topic_min' => $_REQUEST['start'],
1121
					'id_topic_max' => $_REQUEST['start'] + $increment,
1122
				)
1123
			);
1124
			while ($row = $smcFunc['db_fetch_assoc']($request))
1125
				$smcFunc['db_query']('', '
1126
					UPDATE {db_prefix}boards
1127
					SET unapproved_topics = unapproved_topics + {int:real_unapproved_topics}
1128
					WHERE id_board = {int:id_board}',
1129
					array(
1130
						'id_board' => $row['id_board'],
1131
						'real_unapproved_topics' => $row['real_unapproved_topics'],
1132
					)
1133
				);
1134
			$smcFunc['db_free_result']($request);
1135
1136
			$_REQUEST['start'] += $increment;
1137
1138
			if (microtime(true) - $time_start > 3)
1139
			{
1140
				createToken('admin-boardrecount');
1141
				$context['continue_post_data'] = '<input type="hidden" name="' . $context['admin-boardrecount_token_var'] . '" value="' . $context['admin-boardrecount_token'] . '">';
1142
1143
				$context['continue_get_data'] = '?action=admin;area=maintain;sa=routine;activity=recount;step=4;start=' . $_REQUEST['start'] . ';' . $context['session_var'] . '=' . $context['session_id'];
1144
				$context['continue_percent'] = round((500 + 100 * $_REQUEST['start'] / $max_topics) / $total_steps);
1145
1146
				return;
1147
			}
1148
		}
1149
1150
		$_REQUEST['start'] = 0;
1151
	}
1152
1153
	// Get all members with wrong number of personal messages.
1154
	if ($_REQUEST['step'] <= 5)
1155
	{
1156
		$request = $smcFunc['db_query']('', '
1157
			SELECT /*!40001 SQL_NO_CACHE */ mem.id_member, COUNT(pmr.id_pm) AS real_num,
1158
				MAX(mem.instant_messages) AS instant_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})
1161
			GROUP BY mem.id_member
1162
			HAVING COUNT(pmr.id_pm) != MAX(mem.instant_messages)',
1163
			array(
1164
				'is_not_deleted' => 0,
1165
			)
1166
		);
1167 View Code Duplication
		while ($row = $smcFunc['db_fetch_assoc']($request))
1168
			updateMemberData($row['id_member'], array('instant_messages' => $row['real_num']));
1169
		$smcFunc['db_free_result']($request);
1170
1171
		$request = $smcFunc['db_query']('', '
1172
			SELECT /*!40001 SQL_NO_CACHE */ mem.id_member, COUNT(pmr.id_pm) AS real_num,
1173
				MAX(mem.unread_messages) AS unread_messages
1174
			FROM {db_prefix}members AS mem
1175
				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})
1176
			GROUP BY mem.id_member
1177
			HAVING COUNT(pmr.id_pm) != MAX(mem.unread_messages)',
1178
			array(
1179
				'is_not_deleted' => 0,
1180
				'is_not_read' => 0,
1181
			)
1182
		);
1183 View Code Duplication
		while ($row = $smcFunc['db_fetch_assoc']($request))
1184
			updateMemberData($row['id_member'], array('unread_messages' => $row['real_num']));
1185
		$smcFunc['db_free_result']($request);
1186
1187
		if (microtime(true) - $time_start > 3)
1188
		{
1189
			createToken('admin-boardrecount');
1190
			$context['continue_post_data'] = '<input type="hidden" name="' . $context['admin-boardrecount_token_var'] . '" value="' . $context['admin-boardrecount_token'] . '">';
1191
1192
			$context['continue_get_data'] = '?action=admin;area=maintain;sa=routine;activity=recount;step=6;start=0;' . $context['session_var'] . '=' . $context['session_id'];
1193
			$context['continue_percent'] = round(700 / $total_steps);
1194
1195
			return;
1196
		}
1197
	}
1198
1199
	// Any messages pointing to the wrong board?
1200
	if ($_REQUEST['step'] <= 6)
1201
	{
1202
		while ($_REQUEST['start'] < $modSettings['maxMsgID'])
1203
		{
1204
			$request = $smcFunc['db_query']('', '
1205
				SELECT /*!40001 SQL_NO_CACHE */ t.id_board, m.id_msg
1206
				FROM {db_prefix}messages AS m
1207
					INNER JOIN {db_prefix}topics AS t ON (t.id_topic = m.id_topic AND t.id_board != m.id_board)
1208
				WHERE m.id_msg > {int:id_msg_min}
1209
					AND m.id_msg <= {int:id_msg_max}',
1210
				array(
1211
					'id_msg_min' => $_REQUEST['start'],
1212
					'id_msg_max' => $_REQUEST['start'] + $increment,
1213
				)
1214
			);
1215
			$boards = array();
1216
			while ($row = $smcFunc['db_fetch_assoc']($request))
1217
				$boards[$row['id_board']][] = $row['id_msg'];
1218
			$smcFunc['db_free_result']($request);
1219
1220
			foreach ($boards as $board_id => $messages)
1221
				$smcFunc['db_query']('', '
1222
					UPDATE {db_prefix}messages
1223
					SET id_board = {int:id_board}
1224
					WHERE id_msg IN ({array_int:id_msg_array})',
1225
					array(
1226
						'id_msg_array' => $messages,
1227
						'id_board' => $board_id,
1228
					)
1229
				);
1230
1231
			$_REQUEST['start'] += $increment;
1232
1233
			if (microtime(true) - $time_start > 3)
1234
			{
1235
				createToken('admin-boardrecount');
1236
				$context['continue_post_data'] = '<input type="hidden" name="' . $context['admin-boardrecount_token_var'] . '" value="' . $context['admin-boardrecount_token'] . '">';
1237
1238
				$context['continue_get_data'] = '?action=admin;area=maintain;sa=routine;activity=recount;step=6;start=' . $_REQUEST['start'] . ';' . $context['session_var'] . '=' . $context['session_id'];
1239
				$context['continue_percent'] = round((700 + 100 * $_REQUEST['start'] / $modSettings['maxMsgID']) / $total_steps);
1240
1241
				return;
1242
			}
1243
		}
1244
1245
		$_REQUEST['start'] = 0;
1246
	}
1247
1248
	// Update the latest message of each board.
1249
	$request = $smcFunc['db_query']('', '
1250
		SELECT m.id_board, MAX(m.id_msg) AS local_last_msg
1251
		FROM {db_prefix}messages AS m
1252
		WHERE m.approved = {int:is_approved}
1253
		GROUP BY m.id_board',
1254
		array(
1255
			'is_approved' => 1,
1256
		)
1257
	);
1258
	$realBoardCounts = array();
1259
	while ($row = $smcFunc['db_fetch_assoc']($request))
1260
		$realBoardCounts[$row['id_board']] = $row['local_last_msg'];
1261
	$smcFunc['db_free_result']($request);
1262
1263
	$request = $smcFunc['db_query']('', '
1264
		SELECT /*!40001 SQL_NO_CACHE */ id_board, id_parent, id_last_msg, child_level, id_msg_updated
1265
		FROM {db_prefix}boards',
1266
		array(
1267
		)
1268
	);
1269
	$resort_me = array();
1270
	while ($row = $smcFunc['db_fetch_assoc']($request))
1271
	{
1272
		$row['local_last_msg'] = isset($realBoardCounts[$row['id_board']]) ? $realBoardCounts[$row['id_board']] : 0;
1273
		$resort_me[$row['child_level']][] = $row;
1274
	}
1275
	$smcFunc['db_free_result']($request);
1276
1277
	krsort($resort_me);
1278
1279
	$lastModifiedMsg = array();
1280
	foreach ($resort_me as $rows)
1281
		foreach ($rows as $row)
1282
		{
1283
			// The latest message is the latest of the current board and its children.
1284
			if (isset($lastModifiedMsg[$row['id_board']]))
1285
				$curLastModifiedMsg = max($row['local_last_msg'], $lastModifiedMsg[$row['id_board']]);
1286
			else
1287
				$curLastModifiedMsg = $row['local_last_msg'];
1288
1289
			// If what is and what should be the latest message differ, an update is necessary.
1290
			if ($row['local_last_msg'] != $row['id_last_msg'] || $curLastModifiedMsg != $row['id_msg_updated'])
1291
				$smcFunc['db_query']('', '
1292
					UPDATE {db_prefix}boards
1293
					SET id_last_msg = {int:id_last_msg}, id_msg_updated = {int:id_msg_updated}
1294
					WHERE id_board = {int:id_board}',
1295
					array(
1296
						'id_last_msg' => $row['local_last_msg'],
1297
						'id_msg_updated' => $curLastModifiedMsg,
1298
						'id_board' => $row['id_board'],
1299
					)
1300
				);
1301
1302
			// Parent boards inherit the latest modified message of their children.
1303
			if (isset($lastModifiedMsg[$row['id_parent']]))
1304
				$lastModifiedMsg[$row['id_parent']] = max($row['local_last_msg'], $lastModifiedMsg[$row['id_parent']]);
1305
			else
1306
				$lastModifiedMsg[$row['id_parent']] = $row['local_last_msg'];
1307
		}
1308
1309
	// Update all the basic statistics.
1310
	updateStats('member');
1311
	updateStats('message');
1312
	updateStats('topic');
1313
1314
	// Finally, update the latest event times.
1315
	require_once($sourcedir . '/ScheduledTasks.php');
1316
	CalculateNextTrigger();
1317
1318
	redirectexit('action=admin;area=maintain;sa=routine;done=recount');
1319
}
1320
1321
/**
1322
 * Perform a detailed version check.  A very good thing ;).
1323
 * The function parses the comment headers in all files for their version information,
1324
 * and outputs that for some javascript to check with simplemachines.org.
1325
 * It does not connect directly with simplemachines.org, but rather expects the client to.
1326
 *
1327
 * It requires the admin_forum permission.
1328
 * Uses the view_versions admin area.
1329
 * Accessed through ?action=admin;area=maintain;sa=routine;activity=version.
1330
 * @uses Admin template, view_versions sub-template.
1331
 */
1332
function VersionDetail()
1333
{
1334
	global $forum_version, $txt, $sourcedir, $context;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
1335
1336
	isAllowedTo('admin_forum');
1337
1338
	// Call the function that'll get all the version info we need.
1339
	require_once($sourcedir . '/Subs-Admin.php');
1340
	$versionOptions = array(
1341
		'include_ssi' => true,
1342
		'include_subscriptions' => true,
1343
		'include_tasks' => true,
1344
		'sort_results' => true,
1345
	);
1346
	$version_info = getFileVersions($versionOptions);
1347
1348
	// Add the new info to the template context.
1349
	$context += array(
1350
		'file_versions' => $version_info['file_versions'],
1351
		'default_template_versions' => $version_info['default_template_versions'],
1352
		'template_versions' => $version_info['template_versions'],
1353
		'default_language_versions' => $version_info['default_language_versions'],
1354
		'default_known_languages' => array_keys($version_info['default_language_versions']),
1355
		'tasks_versions' => $version_info['tasks_versions'],
1356
	);
1357
1358
	// Make it easier to manage for the template.
1359
	$context['forum_version'] = $forum_version;
1360
1361
	$context['sub_template'] = 'view_versions';
1362
	$context['page_title'] = $txt['admin_version_check'];
1363
}
1364
1365
/**
1366
 * Re-attribute posts.
1367
 */
1368
function MaintainReattributePosts()
1369
{
1370
	global $sourcedir, $context, $txt;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
1371
1372
	checkSession();
1373
1374
	// Find the member.
1375
	require_once($sourcedir . '/Subs-Auth.php');
1376
	$members = findMembers($_POST['to']);
1377
1378
	if (empty($members))
1379
		fatal_lang_error('reattribute_cannot_find_member');
1380
1381
	$memID = array_shift($members);
1382
	$memID = $memID['id'];
1383
1384
	$email = $_POST['type'] == 'email' ? $_POST['from_email'] : '';
1385
	$membername = $_POST['type'] == 'name' ? $_POST['from_name'] : '';
1386
1387
	// Now call the reattribute function.
1388
	require_once($sourcedir . '/Subs-Members.php');
1389
	reattributePosts($memID, $email, $membername, !empty($_POST['posts']));
1390
1391
	$context['maintenance_finished'] = $txt['maintain_reattribute_posts'];
1392
}
1393
1394
/**
1395
 * Removing old members. Done and out!
1396
 * @todo refactor
0 ignored issues
show
Coding Style introduced by
Comment refers to a TODO task

This check looks TODO comments that have been left in the code.

``TODO``s show that something is left unfinished and should be attended to.

Loading history...
1397
 */
1398
function MaintainPurgeInactiveMembers()
1399
{
1400
	global $sourcedir, $context, $smcFunc, $txt;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
1401
1402
	$_POST['maxdays'] = empty($_POST['maxdays']) ? 0 : (int) $_POST['maxdays'];
1403
	if (!empty($_POST['groups']) && $_POST['maxdays'] > 0)
1404
	{
1405
		checkSession();
1406
		validateToken('admin-maint');
1407
1408
		$groups = array();
1409
		foreach ($_POST['groups'] as $id => $dummy)
0 ignored issues
show
Bug introduced by
The expression $_POST['groups'] of type integer is not traversable.
Loading history...
1410
			$groups[] = (int) $id;
1411
		$time_limit = (time() - ($_POST['maxdays'] * 24 * 3600));
1412
		$where_vars = array(
1413
			'time_limit' => $time_limit,
1414
		);
1415
		if ($_POST['del_type'] == 'activated')
1416
		{
1417
			$where = 'mem.date_registered < {int:time_limit} AND mem.is_activated = {int:is_activated}';
1418
			$where_vars['is_activated'] = 0;
1419
		}
1420
		else
1421
			$where = 'mem.last_login < {int:time_limit} AND (mem.last_login != 0 OR mem.date_registered < {int:time_limit})';
1422
1423
		// Need to get *all* groups then work out which (if any) we avoid.
1424
		$request = $smcFunc['db_query']('', '
1425
			SELECT id_group, group_name, min_posts
1426
			FROM {db_prefix}membergroups',
1427
			array(
1428
			)
1429
		);
1430
		while ($row = $smcFunc['db_fetch_assoc']($request))
1431
		{
1432
			// Avoid this one?
1433
			if (!in_array($row['id_group'], $groups))
1434
			{
1435
				// Post group?
1436
				if ($row['min_posts'] != -1)
1437
				{
1438
					$where .= ' AND mem.id_post_group != {int:id_post_group_' . $row['id_group'] . '}';
1439
					$where_vars['id_post_group_' . $row['id_group']] = $row['id_group'];
1440
				}
1441
				else
1442
				{
1443
					$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';
1444
					$where_vars['id_group_' . $row['id_group']] = $row['id_group'];
1445
				}
1446
			}
1447
		}
1448
		$smcFunc['db_free_result']($request);
1449
1450
		// If we have ungrouped unselected we need to avoid those guys.
1451
		if (!in_array(0, $groups))
1452
		{
1453
			$where .= ' AND (mem.id_group != 0 OR mem.additional_groups != {string:blank_add_groups})';
1454
			$where_vars['blank_add_groups'] = '';
1455
		}
1456
1457
		// Select all the members we're about to murder/remove...
1458
		$request = $smcFunc['db_query']('', '
1459
			SELECT mem.id_member, COALESCE(m.id_member, 0) AS is_mod
1460
			FROM {db_prefix}members AS mem
1461
				LEFT JOIN {db_prefix}moderators AS m ON (m.id_member = mem.id_member)
1462
			WHERE ' . $where,
1463
			$where_vars
1464
		);
1465
		$members = array();
1466
		while ($row = $smcFunc['db_fetch_assoc']($request))
1467
		{
1468
			if (!$row['is_mod'] || !in_array(3, $groups))
1469
				$members[] = $row['id_member'];
1470
		}
1471
		$smcFunc['db_free_result']($request);
1472
1473
		require_once($sourcedir . '/Subs-Members.php');
1474
		deleteMembers($members);
1475
	}
1476
1477
	$context['maintenance_finished'] = $txt['maintain_members'];
1478
	createToken('admin-maint');
1479
}
1480
1481
/**
1482
 * Removing old posts doesn't take much as we really pass through.
1483
 */
1484
function MaintainRemoveOldPosts()
1485
{
1486
	global $sourcedir;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
1487
1488
	validateToken('admin-maint');
1489
1490
	// Actually do what we're told!
1491
	require_once($sourcedir . '/RemoveTopic.php');
1492
	RemoveOldTopics2();
1493
}
1494
1495
/**
1496
 * Removing old drafts
1497
 */
1498
function MaintainRemoveOldDrafts()
1499
{
1500
	global $sourcedir, $smcFunc;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
1501
1502
	validateToken('admin-maint');
1503
1504
	$drafts = array();
1505
1506
	// Find all of the old drafts
1507
	$request = $smcFunc['db_query']('', '
1508
		SELECT id_draft
1509
		FROM {db_prefix}user_drafts
1510
		WHERE poster_time <= {int:poster_time_old}',
1511
		array(
1512
			'poster_time_old' => time() - (86400 * $_POST['draftdays']),
1513
		)
1514
	);
1515
1516
	while ($row = $smcFunc['db_fetch_row']($request))
1517
		$drafts[] = (int) $row[0];
1518
	$smcFunc['db_free_result']($request);
1519
1520
	// If we have old drafts, remove them
1521
	if (count($drafts) > 0)
1522
	{
1523
		require_once($sourcedir . '/Drafts.php');
1524
		DeleteDraft($drafts, false);
0 ignored issues
show
Documentation introduced by
$drafts is of type array, but the function expects a integer.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1525
	}
1526
}
1527
1528
/**
1529
 * Moves topics from one board to another.
1530
 *
1531
 * @uses not_done template to pause the process.
1532
 */
1533
function MaintainMassMoveTopics()
1534
{
1535
	global $smcFunc, $sourcedir, $context, $txt;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
1536
1537
	// Only admins.
1538
	isAllowedTo('admin_forum');
1539
1540
	checkSession('request');
1541
	validateToken('admin-maint');
1542
1543
	// Set up to the context.
1544
	$context['page_title'] = $txt['not_done_title'];
1545
	$context['continue_countdown'] = 3;
1546
	$context['continue_post_data'] = '';
1547
	$context['continue_get_data'] = '';
1548
	$context['sub_template'] = 'not_done';
1549
	$context['start'] = empty($_REQUEST['start']) ? 0 : (int) $_REQUEST['start'];
1550
	$context['start_time'] = time();
1551
1552
	// First time we do this?
1553
	$id_board_from = isset($_REQUEST['id_board_from']) ? (int) $_REQUEST['id_board_from'] : 0;
1554
	$id_board_to = isset($_REQUEST['id_board_to']) ? (int) $_REQUEST['id_board_to'] : 0;
1555
	$max_days = isset($_REQUEST['maxdays']) ? (int) $_REQUEST['maxdays'] : 0;
1556
	$locked = isset($_POST['move_type_locked']) || isset($_GET['locked']);
1557
	$sticky = isset($_POST['move_type_sticky']) || isset($_GET['sticky']);
1558
1559
	// No boards then this is your stop.
1560
	if (empty($id_board_from) || empty($id_board_to))
1561
		return;
1562
1563
	// The big WHERE clause
1564
	$conditions = 'WHERE t.id_board = {int:id_board_from}
1565
		AND m.icon != {string:moved}';
1566
1567
	// DB parameters
1568
	$params = array(
1569
		'id_board_from' => $id_board_from,
1570
		'moved' => 'moved',
1571
	);
1572
1573
	// Only moving topics not posted in for x days?
1574
	if (!empty($max_days))
1575
	{
1576
		$conditions .= '
1577
			AND m.poster_time < {int:poster_time}';
1578
		$params['poster_time'] = time() - 3600 * 24 * $max_days;
1579
	}
1580
1581
	// Moving locked topics?
1582
	if ($locked)
1583
	{
1584
		$conditions .= '
1585
			AND t.locked = {int:locked}';
1586
		$params['locked'] = 1;
1587
	}
1588
1589
	// What about sticky topics?
1590
	if ($sticky)
1591
	{
1592
		$conditions .= '
1593
			AND t.is_sticky = {int:sticky}';
1594
		$params['sticky'] = 1;
1595
	}
1596
1597
	// How many topics are we converting?
1598 View Code Duplication
	if (!isset($_REQUEST['totaltopics']))
1599
	{
1600
		$request = $smcFunc['db_query']('', '
1601
			SELECT COUNT(*)
1602
			FROM {db_prefix}topics AS t
1603
				INNER JOIN {db_prefix}messages AS m ON (m.id_msg = t.id_last_msg)' .
1604
			$conditions,
1605
			$params
1606
		);
1607
		list ($total_topics) = $smcFunc['db_fetch_row']($request);
1608
		$smcFunc['db_free_result']($request);
1609
	}
1610
	else
1611
		$total_topics = (int) $_REQUEST['totaltopics'];
1612
1613
	// Seems like we need this here.
1614
	$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;
1615
1616
	if ($locked)
1617
		$context['continue_get_data'] .= ';locked';
1618
1619
	if ($sticky)
1620
		$context['continue_get_data'] .= ';sticky';
1621
1622
	$context['continue_get_data'] .= ';start=' . $context['start'] . ';' . $context['session_var'] . '=' . $context['session_id'];
1623
1624
	// We have topics to move so start the process.
1625
	if (!empty($total_topics))
1626
	{
1627
		while ($context['start'] <= $total_topics)
1628
		{
1629
			// Lets get the topics.
1630
			$request = $smcFunc['db_query']('', '
1631
				SELECT t.id_topic
1632
				FROM {db_prefix}topics AS t
1633
					INNER JOIN {db_prefix}messages AS m ON (m.id_msg = t.id_last_msg)
1634
				' . $conditions . '
1635
				LIMIT 10',
1636
				$params
1637
			);
1638
1639
			// Get the ids.
1640
			$topics = array();
1641
			while ($row = $smcFunc['db_fetch_assoc']($request))
1642
				$topics[] = $row['id_topic'];
1643
1644
			// Just return if we don't have any topics left to move.
1645
			if (empty($topics))
1646
			{
1647
				cache_put_data('board-' . $id_board_from, null, 120);
1648
				cache_put_data('board-' . $id_board_to, null, 120);
1649
				redirectexit('action=admin;area=maintain;sa=topics;done=massmove');
1650
			}
1651
1652
			// Lets move them.
1653
			require_once($sourcedir . '/MoveTopic.php');
1654
			moveTopics($topics, $id_board_to);
1655
1656
			// We've done at least ten more topics.
1657
			$context['start'] += 10;
1658
1659
			// Lets wait a while.
1660
			if (time() - $context['start_time'] > 3)
1661
			{
1662
				// What's the percent?
1663
				$context['continue_percent'] = round(100 * ($context['start'] / $total_topics), 1);
1664
				$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'];
1665
1666
				// Let the template system do it's thang.
1667
				return;
1668
			}
1669
		}
1670
	}
1671
1672
	// Don't confuse admins by having an out of date cache.
1673
	cache_put_data('board-' . $id_board_from, null, 120);
1674
	cache_put_data('board-' . $id_board_to, null, 120);
1675
1676
	redirectexit('action=admin;area=maintain;sa=topics;done=massmove');
1677
}
1678
1679
/**
1680
 * Recalculate all members post counts
1681
 * it requires the admin_forum permission.
1682
 *
1683
 * - recounts all posts for members found in the message table
1684
 * - updates the members post count record in the members table
1685
 * - honors the boards post count flag
1686
 * - does not count posts in the recycle bin
1687
 * - zeros post counts for all members with no posts in the message table
1688
 * - runs as a delayed loop to avoid server overload
1689
 * - uses the not_done template in Admin.template
1690
 *
1691
 * The function redirects back to action=admin;area=maintain;sa=members when complete.
1692
 * It is accessed via ?action=admin;area=maintain;sa=members;activity=recountposts
1693
 */
1694
function MaintainRecountPosts()
1695
{
1696
	global $txt, $context, $modSettings, $smcFunc;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
1697
1698
	// You have to be allowed in here
1699
	isAllowedTo('admin_forum');
1700
	checkSession('request');
1701
1702
	// Set up to the context.
1703
	$context['page_title'] = $txt['not_done_title'];
1704
	$context['continue_countdown'] = 3;
1705
	$context['continue_get_data'] = '';
1706
	$context['sub_template'] = 'not_done';
1707
1708
	// init
1709
	$increment = 200;
1710
	$_REQUEST['start'] = !isset($_REQUEST['start']) ? 0 : (int) $_REQUEST['start'];
1711
1712
	// Ask for some extra time, on big boards this may take a bit
1713
	@set_time_limit(600);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
1714
1715
	// Only run this query if we don't have the total number of members that have posted
1716
	if (!isset($_SESSION['total_members']))
1717
	{
1718
		validateToken('admin-maint');
1719
1720
		$request = $smcFunc['db_query']('', '
1721
			SELECT COUNT(DISTINCT m.id_member)
1722
			FROM {db_prefix}messages AS m
1723
			JOIN {db_prefix}boards AS b on m.id_board = b.id_board
1724
			WHERE m.id_member != 0
1725
				AND b.count_posts = 0',
1726
			array(
1727
			)
1728
		);
1729
1730
		// save it so we don't do this again for this task
1731
		list ($_SESSION['total_members']) = $smcFunc['db_fetch_row']($request);
1732
		$smcFunc['db_free_result']($request);
1733
	}
1734
	else
1735
		validateToken('admin-recountposts');
1736
1737
	// Lets get a group of members and determine their post count (from the boards that have post count enabled of course).
1738
	$request = $smcFunc['db_query']('', '
1739
		SELECT /*!40001 SQL_NO_CACHE */ m.id_member, COUNT(m.id_member) AS posts
1740
		FROM {db_prefix}messages AS m
1741
			INNER JOIN {db_prefix}boards AS b ON m.id_board = b.id_board
1742
		WHERE m.id_member != {int:zero}
1743
			AND b.count_posts = {int:zero}
1744
			' . (!empty($modSettings['recycle_enable']) ? ' AND b.id_board != {int:recycle}' : '') . '
1745
		GROUP BY m.id_member
1746
		LIMIT {int:start}, {int:number}',
1747
		array(
1748
			'start' => $_REQUEST['start'],
1749
			'number' => $increment,
1750
			'recycle' => $modSettings['recycle_board'],
1751
			'zero' => 0,
1752
		)
1753
	);
1754
	$total_rows = $smcFunc['db_num_rows']($request);
1755
1756
	// Update the post count for this group
1757
	while ($row = $smcFunc['db_fetch_assoc']($request))
1758
	{
1759
		$smcFunc['db_query']('', '
1760
			UPDATE {db_prefix}members
1761
			SET posts = {int:posts}
1762
			WHERE id_member = {int:row}',
1763
			array(
1764
				'row' => $row['id_member'],
1765
				'posts' => $row['posts'],
1766
			)
1767
		);
1768
	}
1769
	$smcFunc['db_free_result']($request);
1770
1771
	// Continue?
1772
	if ($total_rows == $increment)
1773
	{
1774
		$_REQUEST['start'] += $increment;
1775
		$context['continue_get_data'] = '?action=admin;area=maintain;sa=members;activity=recountposts;start=' . $_REQUEST['start'] . ';' . $context['session_var'] . '=' . $context['session_id'];
1776
		$context['continue_percent'] = round(100 * $_REQUEST['start'] / $_SESSION['total_members']);
1777
1778
		createToken('admin-recountposts');
1779
		$context['continue_post_data'] = '<input type="hidden" name="' . $context['admin-recountposts_token_var'] . '" value="' . $context['admin-recountposts_token'] . '">';
1780
1781
		if (function_exists('apache_reset_timeout'))
1782
			apache_reset_timeout();
1783
		return;
1784
	}
1785
1786
	// final steps ... made more difficult since we don't yet support sub-selects on joins
1787
	// place all members who have posts in the message table in a temp table
1788
	$createTemporary = $smcFunc['db_query']('', '
1789
		CREATE TEMPORARY TABLE {db_prefix}tmp_maint_recountposts (
1790
			id_member mediumint(8) unsigned NOT NULL default {string:string_zero},
1791
			PRIMARY KEY (id_member)
1792
		)
1793
		SELECT m.id_member
1794
		FROM {db_prefix}messages AS m
1795
			INNER JOIN {db_prefix}boards AS b ON m.id_board = b.id_board
1796
		WHERE m.id_member != {int:zero}
1797
			AND b.count_posts = {int:zero}
1798
			' . (!empty($modSettings['recycle_enable']) ? ' AND b.id_board != {int:recycle}' : '') . '
1799
		GROUP BY m.id_member',
1800
		array(
1801
			'zero' => 0,
1802
			'string_zero' => '0',
1803
			'db_error_skip' => true,
1804
			'recycle' => !empty($modSettings['recycle_board']) ? $modSettings['recycle_board'] : 0,
1805
		)
1806
	) !== false;
1807
1808 View Code Duplication
	if ($createTemporary)
1809
	{
1810
		// outer join the members table on the temporary table finding the members that have a post count but no posts in the message table
1811
		$request = $smcFunc['db_query']('', '
1812
			SELECT mem.id_member, mem.posts
1813
			FROM {db_prefix}members AS mem
1814
			LEFT OUTER JOIN {db_prefix}tmp_maint_recountposts AS res
1815
			ON res.id_member = mem.id_member
1816
			WHERE res.id_member IS null
1817
				AND mem.posts != {int:zero}',
1818
			array(
1819
				'zero' => 0,
1820
			)
1821
		);
1822
1823
		// set the post count to zero for any delinquents we may have found
1824
		while ($row = $smcFunc['db_fetch_assoc']($request))
1825
		{
1826
			$smcFunc['db_query']('', '
1827
				UPDATE {db_prefix}members
1828
				SET posts = {int:zero}
1829
				WHERE id_member = {int:row}',
1830
				array(
1831
					'row' => $row['id_member'],
1832
					'zero' => 0,
1833
				)
1834
			);
1835
		}
1836
		$smcFunc['db_free_result']($request);
1837
	}
1838
1839
	// all done
1840
	unset($_SESSION['total_members']);
1841
	$context['maintenance_finished'] = $txt['maintain_recountposts'];
1842
	redirectexit('action=admin;area=maintain;sa=members;done=recountposts');
1843
}
1844
1845
/**
1846
 * Generates a list of integration hooks for display
1847
 * Accessed through ?action=admin;area=maintain;sa=hooks;
1848
 * Allows for removal or disabling of selected hooks
1849
 */
1850
function list_integration_hooks()
1851
{
1852
	global $sourcedir, $scripturl, $context, $txt;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
1853
1854
	$context['filter_url'] = '';
1855
	$context['current_filter'] = '';
1856
	$currentHooks = get_integration_hooks();
1857
	if (isset($_GET['filter']) && in_array($_GET['filter'], array_keys($currentHooks)))
1858
	{
1859
		$context['filter_url'] = ';filter=' . $_GET['filter'];
1860
		$context['current_filter'] = $_GET['filter'];
1861
	}
1862
1863
	if (!empty($_REQUEST['do']) && isset($_REQUEST['hook']) && isset($_REQUEST['function']))
1864
	{
1865
		checkSession('request');
1866
		validateToken('admin-hook', 'request');
1867
1868
		if ($_REQUEST['do'] == 'remove')
1869
			remove_integration_function($_REQUEST['hook'], urldecode($_REQUEST['function']));
1870
1871
		else
1872
		{
1873
			$function_remove = urldecode($_REQUEST['function']) . (($_REQUEST['do'] == 'disable') ? '' : '!');
1874
			$function_add = urldecode($_REQUEST['function']) . (($_REQUEST['do'] == 'disable') ? '!' : '');
1875
1876
			remove_integration_function($_REQUEST['hook'], $function_remove);
1877
			add_integration_function($_REQUEST['hook'], $function_add);
1878
1879
			redirectexit('action=admin;area=maintain;sa=hooks' . $context['filter_url']);
1880
		}
1881
	}
1882
1883
	createToken('admin-hook', 'request');
1884
1885
	$list_options = array(
1886
		'id' => 'list_integration_hooks',
1887
		'title' => $txt['hooks_title_list'],
1888
		'items_per_page' => 20,
1889
		'base_href' => $scripturl . '?action=admin;area=maintain;sa=hooks' . $context['filter_url'] . ';' . $context['session_var'] . '=' . $context['session_id'],
1890
		'default_sort_col' => 'hook_name',
1891
		'get_items' => array(
1892
			'function' => 'get_integration_hooks_data',
1893
		),
1894
		'get_count' => array(
1895
			'function' => 'get_integration_hooks_count',
1896
		),
1897
		'no_items_label' => $txt['hooks_no_hooks'],
1898
		'columns' => array(
1899
			'hook_name' => array(
1900
				'header' => array(
1901
					'value' => $txt['hooks_field_hook_name'],
1902
				),
1903
				'data' => array(
1904
					'db' => 'hook_name',
1905
				),
1906
				'sort' =>  array(
1907
					'default' => 'hook_name',
1908
					'reverse' => 'hook_name DESC',
1909
				),
1910
			),
1911
			'function_name' => array(
1912
				'header' => array(
1913
					'value' => $txt['hooks_field_function_name'],
1914
				),
1915
				'data' => array(
1916
					'function' => function($data) use ($txt)
1917
					{
1918
						// Show a nice icon to indicate this is an instance.
1919
						$instance = (!empty($data['instance']) ? '<span class="generic_icons news" title="' . $txt['hooks_field_function_method'] . '"></span> ' : '');
1920
1921
						if (!empty($data['included_file']))
1922
							return $instance . $txt['hooks_field_function'] . ': ' . $data['real_function'] . '<br>' . $txt['hooks_field_included_file'] . ': ' . $data['included_file'];
1923
1924
						else
1925
							return $instance . $data['real_function'];
1926
					},
1927
				),
1928
				'sort' =>  array(
1929
					'default' => 'function_name',
1930
					'reverse' => 'function_name DESC',
1931
				),
1932
			),
1933
			'file_name' => array(
1934
				'header' => array(
1935
					'value' => $txt['hooks_field_file_name'],
1936
				),
1937
				'data' => array(
1938
					'db' => 'file_name',
1939
				),
1940
				'sort' =>  array(
1941
					'default' => 'file_name',
1942
					'reverse' => 'file_name DESC',
1943
				),
1944
			),
1945
			'status' => array(
1946
				'header' => array(
1947
					'value' => $txt['hooks_field_hook_exists'],
1948
					'style' => 'width:3%;',
1949
				),
1950
				'data' => array(
1951
					'function' => function($data) use ($txt, $scripturl, $context)
1952
					{
1953
						$change_status = array('before' => '', 'after' => '');
1954
1955
							$change_status['before'] = '<a href="' . $scripturl . '?action=admin;area=maintain;sa=hooks;do=' . ($data['enabled'] ? 'disable' : 'enable') . ';hook=' . $data['hook_name'] . ';function=' . urlencode($data['function_name']) . $context['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">';
1956
							$change_status['after'] = '</a>';
1957
1958
						return $change_status['before'] . '<span class="generic_icons post_moderation_' . $data['status'] . '" title="' . $data['img_text'] . '"></span>';
1959
					},
1960
					'class' => 'centertext',
1961
				),
1962
				'sort' =>  array(
1963
					'default' => 'status',
1964
					'reverse' => 'status DESC',
1965
				),
1966
			),
1967
		),
1968
		'additional_rows' => array(
1969
			array(
1970
				'position' => 'after_title',
1971
				'value' => $txt['hooks_disable_instructions'] . '<br>
1972
					' . $txt['hooks_disable_legend'] . ':
1973
				<ul style="list-style: none;">
1974
					<li><span class="generic_icons post_moderation_allow"></span> ' . $txt['hooks_disable_legend_exists'] . '</li>
1975
					<li><span class="generic_icons post_moderation_moderate"></span> ' . $txt['hooks_disable_legend_disabled'] . '</li>
1976
					<li><span class="generic_icons post_moderation_deny"></span> ' . $txt['hooks_disable_legend_missing'] . '</li>
1977
				</ul>'
1978
			),
1979
		),
1980
	);
1981
1982
	$list_options['columns']['remove'] = array(
1983
		'header' => array(
1984
			'value' => $txt['hooks_button_remove'],
1985
			'style' => 'width:3%',
1986
		),
1987
		'data' => array(
1988
			'function' => function($data) use ($txt, $scripturl, $context)
1989
			{
1990
				if (!$data['hook_exists'])
1991
					return '
1992
					<a href="' . $scripturl . '?action=admin;area=maintain;sa=hooks;do=remove;hook=' . $data['hook_name'] . ';function=' . urlencode($data['function_name']) . $context['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">
1993
						<span class="generic_icons delete" title="' . $txt['hooks_button_remove'] . '"></span>
1994
					</a>';
1995
			},
1996
			'class' => 'centertext',
1997
		),
1998
	);
1999
	$list_options['form'] = array(
2000
		'href' => $scripturl . '?action=admin;area=maintain;sa=hooks' . $context['filter_url'] . ';' . $context['session_var'] . '=' . $context['session_id'],
2001
		'name' => 'list_integration_hooks',
2002
	);
2003
2004
2005
	require_once($sourcedir . '/Subs-List.php');
2006
	createList($list_options);
2007
2008
	$context['page_title'] = $txt['hooks_title_list'];
2009
	$context['sub_template'] = 'show_list';
2010
	$context['default_list'] = 'list_integration_hooks';
2011
}
2012
2013
/**
2014
 * Gets all of the files in a directory and its children directories
2015
 *
2016
 * @param string $dir_path The path to the directory
2017
 * @return array An array containing information about the files found in the specified directory and its children
2018
 */
2019
function get_files_recursive($dir_path)
2020
{
2021
	$files = array();
2022
2023
	if ($dh = opendir($dir_path))
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $dh. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
2024
	{
2025
		while (($file = readdir($dh)) !== false)
2026
		{
2027
			if ($file != '.' && $file != '..')
2028
			{
2029
				if (is_dir($dir_path . '/' . $file))
2030
					$files = array_merge($files, get_files_recursive($dir_path . '/' . $file));
2031
				else
2032
					$files[] = array('dir' => $dir_path, 'name' => $file);
2033
			}
2034
		}
2035
	}
2036
	closedir($dh);
2037
2038
	return $files;
2039
}
2040
2041
/**
2042
 * Callback function for the integration hooks list (list_integration_hooks)
2043
 * Gets all of the hooks in the system and their status
2044
 *
2045
 * @param int $start The item to start with (for pagination purposes)
2046
 * @param int $per_page How many items to display on each page
2047
 * @param string $sort A string indicating how to sort things
2048
 * @return array An array of information about the integration hooks
2049
 */
2050
function get_integration_hooks_data($start, $per_page, $sort)
2051
{
2052
	global $boarddir, $sourcedir, $settings, $txt, $context, $scripturl;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
2053
2054
	$hooks = $temp_hooks = get_integration_hooks();
2055
	$hooks_data = $temp_data = $hook_status = array();
2056
2057
	$files = get_files_recursive($sourcedir);
2058
	if (!empty($files))
2059
	{
2060
		foreach ($files as $file)
2061
		{
2062
			if (is_file($file['dir'] . '/' . $file['name']) && substr($file['name'], -4) === '.php')
2063
			{
2064
				$fp = fopen($file['dir'] . '/' . $file['name'], 'rb');
2065
				$fc = fread($fp, filesize($file['dir'] . '/' . $file['name']));
2066
				fclose($fp);
2067
2068
				foreach ($temp_hooks as $hook => $allFunctions)
2069
				{
2070
					foreach ($allFunctions as $rawFunc)
2071
					{
2072
						// Get the hook info.
2073
						$hookParsedData = get_hook_info_from_raw($rawFunc);
2074
2075
						if (substr($hook, -8) === '_include')
2076
						{
2077
							$hook_status[$hook][$hookParsedData['pureFunc']]['exists'] = file_exists(strtr(trim($rawFunc), array('$boarddir' => $boarddir, '$sourcedir' => $sourcedir, '$themedir' => $settings['theme_dir'])));
2078
							// I need to know if there is at least one function called in this file.
2079
							$temp_data['include'][$hookParsedData['pureFunc']] = array('hook' => $hook, 'function' => $hookParsedData['pureFunc']);
2080
							unset($temp_hooks[$hook][$rawFunc]);
2081
						}
2082
						elseif (strpos(str_replace(' (', '(', $fc), 'function ' . trim($hookParsedData['pureFunc']) . '(') !== false)
2083
						{
2084
							$hook_status[$hook][$hookParsedData['pureFunc']] = $hookParsedData;
2085
							$hook_status[$hook][$hookParsedData['pureFunc']]['exists'] = true;
2086
							$hook_status[$hook][$hookParsedData['pureFunc']]['in_file'] = (!empty($file['name']) ? $file['name'] : (!empty($hookParsedData['hookFile']) ? $hookParsedData['hookFile'] : ''));
2087
2088
							// Does the hook has its own file?
2089
							if (!empty($hookParsedData['hookFile']))
2090
								$temp_data['include'][$hookParsedData['pureFunc']] = array('hook' => $hook, 'function' => $hookParsedData['pureFunc']);
2091
2092
							// I want to remember all the functions called within this file (to check later if they are enabled or disabled and decide if the integrare_*_include of that file can be disabled too)
2093
							$temp_data['function'][$file['name']][$hookParsedData['pureFunc']] = $hookParsedData['enabled'];
2094
							unset($temp_hooks[$hook][$rawFunc]);
2095
						}
2096
					}
2097
				}
2098
			}
2099
		}
2100
	}
2101
2102
	$sort_types = array(
2103
		'hook_name' => array('hook', SORT_ASC),
2104
		'hook_name DESC' => array('hook', SORT_DESC),
2105
		'function_name' => array('function', SORT_ASC),
2106
		'function_name DESC' => array('function', SORT_DESC),
2107
		'file_name' => array('file_name', SORT_ASC),
2108
		'file_name DESC' => array('file_name', SORT_DESC),
2109
		'status' => array('status', SORT_ASC),
2110
		'status DESC' => array('status', SORT_DESC),
2111
	);
2112
2113
	$sort_options = $sort_types[$sort];
2114
	$sort = array();
2115
	$hooks_filters = array();
2116
2117
	foreach ($hooks as $hook => $functions)
2118
		$hooks_filters[] = '<option' . ($context['current_filter'] == $hook ? ' selected ' : '') . ' value="' . $hook . '">' . $hook . '</option>';
2119
2120
	if (!empty($hooks_filters))
2121
		$context['insert_after_template'] .= '
2122
		<script>
2123
			var hook_name_header = document.getElementById(\'header_list_integration_hooks_hook_name\');
2124
			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>') . ';
2125
		</script>';
2126
2127
	$temp_data = array();
2128
	$id = 0;
2129
2130
	foreach ($hooks as $hook => $functions)
2131
	{
2132
		if (empty($context['filter']) || (!empty($context['filter']) && $context['filter'] == $hook))
2133
		{
2134
			foreach ($functions as $rawFunc)
2135
			{
2136
				// Get the hook info.
2137
				$hookParsedData = get_hook_info_from_raw($rawFunc);
2138
2139
				$hook_exists = !empty($hook_status[$hook][$hookParsedData['pureFunc']]['exists']);
2140
				$sort[] = $sort_options[0];
2141
2142
				$temp_data[] = array(
2143
					'id' => 'hookid_' . $id++,
0 ignored issues
show
Coding Style introduced by
Increment and decrement operators must be bracketed when used in string concatenation
Loading history...
2144
					'hook_name' => $hook,
2145
					'function_name' => $hookParsedData['rawData'],
2146
					'real_function' => $hookParsedData['pureFunc'],
2147
					'included_file' => !empty($hookParsedData['absPath']) ? $hookParsedData['absPath'] : '',
2148
					'file_name' => (isset($hook_status[$hook][$hookParsedData['pureFunc']]['in_file']) ? $hook_status[$hook][$hookParsedData['pureFunc']]['in_file'] : (!empty($hookParsedData['hookFile']) ? $hookParsedData['hookFile'] : '')),
2149
					'instance' => $hookParsedData['object'],
2150
					'hook_exists' => $hook_exists,
2151
					'status' => $hook_exists ? ($hookParsedData['enabled'] ? 'allow' : 'moderate') : 'deny',
2152
					'img_text' => $txt['hooks_' . ($hook_exists ? ($hookParsedData['enabled'] ? 'active' : 'disabled') : 'missing')],
2153
					'enabled' => $hookParsedData['enabled'],
2154
					'can_be_disabled' => !isset($hook_status[$hook][$hookParsedData['pureFunc']]['enabled']),
2155
				);
2156
			}
2157
		}
2158
	}
2159
2160
	array_multisort($sort, $sort_options[1], $temp_data);
2161
2162
	$counter = 0;
2163
	$start++;
2164
2165
	foreach ($temp_data as $data)
2166
	{
2167
		if (++$counter < $start)
2168
			continue;
2169
		elseif ($counter == $start + $per_page)
2170
			break;
2171
2172
		$hooks_data[] = $data;
2173
	}
2174
2175
	return $hooks_data;
2176
}
2177
2178
/**
2179
 * Simply returns the total count of integration hooks
2180
 * Used by the integration hooks list function (list_integration_hooks)
2181
 *
2182
 * @return int The number of hooks currently in use
2183
 */
2184
function get_integration_hooks_count()
2185
{
2186
	global $context;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
2187
2188
	$hooks = get_integration_hooks();
2189
	$hooks_count = 0;
2190
2191
	$context['filter'] = false;
2192
	if (isset($_GET['filter']))
2193
		$context['filter'] = $_GET['filter'];
2194
2195
	foreach ($hooks as $hook => $functions)
2196
	{
2197
		if (empty($context['filter']) || (!empty($context['filter']) && $context['filter'] == $hook))
2198
			$hooks_count += count($functions);
2199
	}
2200
2201
	return $hooks_count;
2202
}
2203
2204
/**
2205
 * Parses modSettings to create integration hook array
2206
 *
2207
 * @return array An array of information about the integration hooks
2208
 */
2209
function get_integration_hooks()
2210
{
2211
	global $modSettings;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
2212
	static $integration_hooks;
2213
2214
	if (!isset($integration_hooks))
2215
	{
2216
		$integration_hooks = array();
2217
		foreach ($modSettings as $key => $value)
2218
		{
2219
			if (!empty($value) && substr($key, 0, 10) === 'integrate_')
2220
				$integration_hooks[$key] = explode(',', $value);
2221
		}
2222
	}
2223
2224
	return $integration_hooks;
2225
}
2226
2227
/**
2228
 * Parses each hook data and returns an array.
2229
 *
2230
 * @param string $rawData A string as it was saved to the DB.
2231
 * @return array everything found in the string itself
2232
 */
2233
function get_hook_info_from_raw($rawData)
2234
{
2235
	global $boarddir, $settings, $sourcedir;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
2236
2237
	// A single string can hold tons of info!
2238
	$hookData = array(
2239
		'object' => false,
2240
		'enabled' => true,
2241
		'fileExists' => false,
2242
		'absPath' => '',
2243
		'hookFile' => '',
2244
		'pureFunc' => '',
2245
		'method' => '',
2246
		'class' => '',
2247
		'rawData' => $rawData,
2248
	);
2249
2250
	// Meh...
2251
	if (empty($rawData))
2252
		return $hookData;
2253
2254
	// For convenience purposes only!
2255
	$modFunc = $rawData;
2256
2257
	// Any files?
2258
	if (strpos($modFunc, '|') !== false)
2259
	{
2260
		list ($hookData['hookFile'], $modFunc) = explode('|', $modFunc);
2261
2262
		// Does the file exists? who knows!
2263
		if (empty($settings['theme_dir']))
2264
			$hookData['absPath'] = strtr(trim($hookData['hookFile']), array('$boarddir' => $boarddir, '$sourcedir' => $sourcedir));
2265
2266
		else
2267
			$hookData['absPath'] = strtr(trim($hookData['hookFile']), array('$boarddir' => $boarddir, '$sourcedir' => $sourcedir, '$themedir' => $settings['theme_dir']));
2268
2269
		$hookData['fileExists'] = file_exists($hookData['absPath']);
2270
		$hookData['hookFile'] = basename($hookData['hookFile']);
2271
	}
2272
2273
	// Hook is an instance.
2274 View Code Duplication
	if (strpos($modFunc, '#') !== false)
2275
	{
2276
		$modFunc = str_replace('#', '', $modFunc);
2277
		$hookData['object'] = true;
2278
	}
2279
2280
	// Hook is "disabled"
2281 View Code Duplication
	if (strpos($modFunc, '!') !== false)
2282
	{
2283
		$modFunc = str_replace('!', '', $modFunc);
2284
		$hookData['enabled'] = false;
2285
	}
2286
2287
	// Handling methods?
2288
	if (strpos($modFunc, '::') !== false)
2289
	{
2290
		list ($hookData['class'], $hookData['method']) = explode('::', $modFunc);
2291
		$hookData['pureFunc'] = $hookData['method'];
2292
	}
2293
2294
	else
2295
		$hookData['pureFunc'] = $modFunc;
2296
2297
	return $hookData;
2298
}
2299
2300
/**
2301
 * Converts html entities to utf8 equivalents
2302
 * special db wrapper for mysql based on the limitation of mysql/mb3
2303
 *
2304
 * Callback function for preg_replace_callback
2305
 * Uses capture group 1 in the supplied array
2306
 * Does basic checks to keep characters inside a viewable range.
2307
 *
2308
 * @param array $matches An array of matches (relevant info should be the 2nd item in the array)
2309
 * @return string The fixed string or return the old when limitation of mysql is hit
2310
 */
2311
function fixchardb__callback($matches)
2312
{
2313
	global $smcFunc;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
2314
	if (!isset($matches[1]))
2315
		return '';
2316
2317
	$num = $matches[1][0] === 'x' ? hexdec(substr($matches[1], 1)) : (int) $matches[1];
2318
	
2319
	// it's to big for mb3?
2320
	if ($num > 0xFFFF && !$smcFunc['db_mb4'])
2321
		return $matches[0];
2322
	else
2323
		return fixchar__callback($matches);
2324
}
2325
2326
?>