Completed
Push — release-2.1 ( f70233...9fa566 )
by Colin
10:32
created

ManageMaintenance.php ➔ ManageMaintenance()   B

Complexity

Conditions 7
Paths 16

Size

Total Lines 99

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
nc 16
nop 0
dl 0
loc 99
rs 7.0884
c 0
b 0
f 0

How to fix   Long Method   

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, $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')
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
	$context['not_done_token'] = 'admin-maint';
521
522
	// A list of tables ready for conversion.
523
	$tables = array(
524
		'ban_groups',
525
		'ban_items',
526
		'boards',
527
		'calendar',
528
		'calendar_holidays',
529
		'categories',
530
		'log_errors',
531
		'log_search_subjects',
532
		'membergroups',
533
		'members',
534
		'message_icons',
535
		'messages',
536
		'package_servers',
537
		'personal_messages',
538
		'pm_recipients',
539
		'polls',
540
		'poll_choices',
541
		'smileys',
542
		'themes',
543
	);
544
	$context['num_tables'] = count($tables);
545
546
	// Loop through all tables that need converting.
547
	for (; $context['table'] < $context['num_tables']; $context['table']++)
548
	{
549
		$cur_table = $tables[$context['table']];
550
		$primary_key = '';
551
		// Make sure we keep stuff unique!
552
		$primary_keys = array();
553
554
		if (function_exists('apache_reset_timeout'))
555
			@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...
556
557
		// Get a list of text columns.
558
		$columns = array();
559 View Code Duplication
		if ($db_type == 'postgresql')
560
			$request = $smcFunc['db_query']('', '
561
				SELECT column_name "Field", data_type "Type"
562
				FROM information_schema.columns 
563
				WHERE table_name = {string:cur_table}
564
				AND (data_type = \'character varying\' or data_type = \'text\')',
565
				array(
566
					'cur_table' => $db_prefix.$cur_table,
567
				)
568
			);
569
		else
570
			$request = $smcFunc['db_query']('', '
571
				SHOW FULL COLUMNS
572
				FROM {db_prefix}{raw:cur_table}',
573
				array(
574
					'cur_table' => $cur_table,
575
				)
576
			);
577
		while ($column_info = $smcFunc['db_fetch_assoc']($request))
578
			if (strpos($column_info['Type'], 'text') !== false || strpos($column_info['Type'], 'char') !== false)
579
				$columns[] = strtolower($column_info['Field']);
580
581
		// Get the column with the (first) primary key.
582 View Code Duplication
		if ($db_type == 'postgresql')
583
			$request = $smcFunc['db_query']('', '
584
				SELECT a.attname "Column_name", \'PRIMARY\' "Key_name", attnum "Seq_in_index"
585
				FROM   pg_index i
586
				JOIN   pg_attribute a ON a.attrelid = i.indrelid
587
									 AND a.attnum = ANY(i.indkey)
588
				WHERE  i.indrelid = {string:cur_table}::regclass
589
				AND    i.indisprimary',
590
				array(
591
					'cur_table' => $db_prefix.$cur_table,
592
				)
593
			);
594
		else
595
			$request = $smcFunc['db_query']('', '
596
				SHOW KEYS
597
				FROM {db_prefix}{raw:cur_table}',
598
				array(
599
					'cur_table' => $cur_table,
600
				)
601
			);
602
		while ($row = $smcFunc['db_fetch_assoc']($request))
603
		{
604
			if ($row['Key_name'] === 'PRIMARY')
605
			{
606
				if ((empty($primary_key) || $row['Seq_in_index'] == 1) && !in_array(strtolower($row['Column_name']), $columns))
607
					$primary_key = $row['Column_name'];
608
609
				$primary_keys[] = $row['Column_name'];
610
			}
611
		}
612
		$smcFunc['db_free_result']($request);
613
614
		// No primary key, no glory.
615
		// Same for columns. Just to be sure we've work to do!
616
		if (empty($primary_key) || empty($columns))
617
			continue;
618
619
		// Get the maximum value for the primary key.
620
		$request = $smcFunc['db_query']('', '
621
			SELECT MAX({identifier:key})
622
			FROM {db_prefix}{raw:cur_table}',
623
			array(
624
				'key' => $primary_key,
625
				'cur_table' => $cur_table,
626
			)
627
		);
628
		list($max_value) = $smcFunc['db_fetch_row']($request);
629
		$smcFunc['db_free_result']($request);
630
631
		if (empty($max_value))
632
			continue;
633
634
		while ($context['start'] <= $max_value)
635
		{
636
			// Retrieve a list of rows that has at least one entity to convert.
637
			$request = $smcFunc['db_query']('', '
638
				SELECT {raw:primary_keys}, {raw:columns}
639
				FROM {db_prefix}{raw:cur_table}
640
				WHERE {raw:primary_key} BETWEEN {int:start} AND {int:start} + 499
641
					AND {raw:like_compare}
642
				LIMIT 500',
643
				array(
644
					'primary_keys' => implode(', ', $primary_keys),
645
					'columns' => implode(', ', $columns),
646
					'cur_table' => $cur_table,
647
					'primary_key' => $primary_key,
648
					'start' => $context['start'],
649
					'like_compare' => '(' . implode(' LIKE \'%&#%\' OR ', $columns) . ' LIKE \'%&#%\')',
650
				)
651
			);
652
			while ($row = $smcFunc['db_fetch_assoc']($request))
653
			{
654
				$insertion_variables = array();
655
				$changes = array();
656
				foreach ($row as $column_name => $column_value)
657
					if ($column_name !== $primary_key && strpos($column_value, '&#') !== false)
658
					{
659
						$changes[] = $column_name . ' = {string:changes_' . $column_name . '}';
660
						$insertion_variables['changes_' . $column_name] = preg_replace_callback('~&#(\d{1,5}|x[0-9a-fA-F]{1,4});~', 'fixchardb__callback', $column_value);
661
					}
662
663
				$where = array();
664
				foreach ($primary_keys as $key)
665
				{
666
					$where[] = $key . ' = {string:where_' . $key . '}';
667
					$insertion_variables['where_' . $key] = $row[$key];
668
				}
669
670
				// Update the row.
671
				if (!empty($changes))
672
					$smcFunc['db_query']('', '
673
						UPDATE {db_prefix}' . $cur_table . '
674
						SET
675
							' . implode(',
676
							', $changes) . '
677
						WHERE ' . implode(' AND ', $where),
678
						$insertion_variables
679
					);
680
			}
681
			$smcFunc['db_free_result']($request);
682
			$context['start'] += 500;
683
684
			// After ten seconds interrupt.
685
			if (time() - $context['start_time'] > 10)
686
			{
687
				// Calculate an approximation of the percentage done.
688
				$context['continue_percent'] = round(100 * ($context['table'] + ($context['start'] / $max_value)) / $context['num_tables'], 1);
689
				$context['continue_get_data'] = '?action=admin;area=maintain;sa=database;activity=convertentities;table=' . $context['table'] . ';start=' . $context['start'] . ';' . $context['session_var'] . '=' . $context['session_id'];
690
				return;
691
			}
692
		}
693
		$context['start'] = 0;
694
	}
695
696
	// If we're here, we must be done.
697
	$context['continue_percent'] = 100;
698
	$context['continue_get_data'] = '?action=admin;area=maintain;sa=database;done=convertentities';
699
	$context['last_step'] = true;
700
	$context['continue_countdown'] = 3;
701
}
702
703
/**
704
 * Optimizes all tables in the database and lists how much was saved.
705
 * It requires the admin_forum permission.
706
 * It shows as the maintain_forum admin area.
707
 * It is accessed from ?action=admin;area=maintain;sa=database;activity=optimize.
708
 * It also updates the optimize scheduled task such that the tables are not automatically optimized again too soon.
709
710
 * @uses the optimize sub template
711
 */
712
function OptimizeTables()
713
{
714
	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...
715
716
	isAllowedTo('admin_forum');
717
718
	checkSession('request');
719
720
	if (!isset($_SESSION['optimized_tables']))
721
		validateToken('admin-maint');
722
	else
723
		validateToken('admin-optimize', 'post', false);
724
725
	ignore_user_abort(true);
726
	db_extend();
727
728
	$context['page_title'] = $txt['database_optimize'];
729
	$context['sub_template'] = 'optimize';
730
	$context['continue_post_data'] = '';
731
	$context['continue_countdown'] = 3;
732
733
	// Only optimize the tables related to this smf install, not all the tables in the db
734
	$real_prefix = preg_match('~^(`?)(.+?)\\1\\.(.*?)$~', $db_prefix, $match) === 1 ? $match[3] : $db_prefix;
735
736
	// Get a list of tables, as well as how many there are.
737
	$temp_tables = $smcFunc['db_list_tables'](false, $real_prefix . '%');
738
	$tables = array();
739
	foreach ($temp_tables as $table)
740
		$tables[] = array('table_name' => $table);
741
742
	// If there aren't any tables then I believe that would mean the world has exploded...
743
	$context['num_tables'] = count($tables);
744
	if ($context['num_tables'] == 0)
745
		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...
746
747
	$_REQUEST['start'] = empty($_REQUEST['start']) ? 0 : (int) $_REQUEST['start'];
748
749
	// Try for extra time due to large tables.
750
	@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...
751
752
	// For each table....
753
	$_SESSION['optimized_tables'] = !empty($_SESSION['optimized_tables']) ? $_SESSION['optimized_tables'] : array();
754
	for ($key = $_REQUEST['start']; $context['num_tables'] - 1; $key++)
755
	{
756
		if (empty($tables[$key]))
757
			break;
758
759
		// Continue?
760
		if (microtime(true) - $time_start > 10)
761
		{
762
			$_REQUEST['start'] = $key;
763
			$context['continue_get_data'] = '?action=admin;area=maintain;sa=database;activity=optimize;start=' . $_REQUEST['start'] . ';' . $context['session_var'] . '=' . $context['session_id'];
764
			$context['continue_percent'] = round(100 * $_REQUEST['start'] / $context['num_tables']);
765
			$context['sub_template'] = 'not_done';
766
			$context['page_title'] = $txt['not_done_title'];
767
768
			createToken('admin-optimize');
769
			$context['continue_post_data'] = '<input type="hidden" name="' . $context['admin-optimize_token_var'] . '" value="' . $context['admin-optimize_token'] . '">';
770
771
			if (function_exists('apache_reset_timeout'))
772
				apache_reset_timeout();
773
774
			return;
775
		}
776
777
		// Optimize the table!  We use backticks here because it might be a custom table.
778
		$data_freed = $smcFunc['db_optimize_table']($tables[$key]['table_name']);
779
780
		if ($data_freed > 0)
781
			$_SESSION['optimized_tables'][] = array(
782
				'name' => $tables[$key]['table_name'],
783
				'data_freed' => $data_freed,
784
			);
785
	}
786
787
	// Number of tables, etc...
788
	$txt['database_numb_tables'] = sprintf($txt['database_numb_tables'], $context['num_tables']);
789
	$context['num_tables_optimized'] = count($_SESSION['optimized_tables']);
790
	$context['optimized_tables'] = $_SESSION['optimized_tables'];
791
	unset($_SESSION['optimized_tables']);
792
}
793
794
/**
795
 * Recount many forum totals that can be recounted automatically without harm.
796
 * it requires the admin_forum permission.
797
 * It shows the maintain_forum admin area.
798
 *
799
 * Totals recounted:
800
 * - fixes for topics with wrong num_replies.
801
 * - updates for num_posts and num_topics of all boards.
802
 * - recounts instant_messages but not unread_messages.
803
 * - repairs messages pointing to boards with topics pointing to other boards.
804
 * - updates the last message posted in boards and children.
805
 * - updates member count, latest member, topic count, and message count.
806
 *
807
 * The function redirects back to ?action=admin;area=maintain when complete.
808
 * It is accessed via ?action=admin;area=maintain;sa=database;activity=recount.
809
 */
810
function AdminBoardRecount()
811
{
812
	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...
813
	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...
814
815
	isAllowedTo('admin_forum');
816
	checkSession('request');
817
818
	// validate the request or the loop
819
	if (!isset($_REQUEST['step']))
820
		validateToken('admin-maint');
821
	else
822
		validateToken('admin-boardrecount');
823
824
	$context['page_title'] = $txt['not_done_title'];
825
	$context['continue_post_data'] = '';
826
	$context['continue_countdown'] = 3;
827
	$context['sub_template'] = 'not_done';
828
829
	// Try for as much time as possible.
830
	@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...
831
832
	// Step the number of topics at a time so things don't time out...
833
	$request = $smcFunc['db_query']('', '
834
		SELECT MAX(id_topic)
835
		FROM {db_prefix}topics',
836
		array(
837
		)
838
	);
839
	list ($max_topics) = $smcFunc['db_fetch_row']($request);
840
	$smcFunc['db_free_result']($request);
841
842
	$increment = min(max(50, ceil($max_topics / 4)), 2000);
843
	if (empty($_REQUEST['start']))
844
		$_REQUEST['start'] = 0;
845
846
	$total_steps = 8;
847
848
	// Get each topic with a wrong reply count and fix it - let's just do some at a time, though.
849
	if (empty($_REQUEST['step']))
850
	{
851
		$_REQUEST['step'] = 0;
852
853
		while ($_REQUEST['start'] < $max_topics)
854
		{
855
			// Recount approved messages
856
			$request = $smcFunc['db_query']('', '
857
				SELECT /*!40001 SQL_NO_CACHE */ t.id_topic, MAX(t.num_replies) AS num_replies,
858
					CASE WHEN COUNT(ma.id_msg) >= 1 THEN COUNT(ma.id_msg) - 1 ELSE 0 END AS real_num_replies
859
				FROM {db_prefix}topics AS t
860
					LEFT JOIN {db_prefix}messages AS ma ON (ma.id_topic = t.id_topic AND ma.approved = {int:is_approved})
861
				WHERE t.id_topic > {int:start}
862
					AND t.id_topic <= {int:max_id}
863
				GROUP BY t.id_topic
864
				HAVING CASE WHEN COUNT(ma.id_msg) >= 1 THEN COUNT(ma.id_msg) - 1 ELSE 0 END != MAX(t.num_replies)',
865
				array(
866
					'is_approved' => 1,
867
					'start' => $_REQUEST['start'],
868
					'max_id' => $_REQUEST['start'] + $increment,
869
				)
870
			);
871
			while ($row = $smcFunc['db_fetch_assoc']($request))
872
				$smcFunc['db_query']('', '
873
					UPDATE {db_prefix}topics
874
					SET num_replies = {int:num_replies}
875
					WHERE id_topic = {int:id_topic}',
876
					array(
877
						'num_replies' => $row['real_num_replies'],
878
						'id_topic' => $row['id_topic'],
879
					)
880
				);
881
			$smcFunc['db_free_result']($request);
882
883
			// Recount unapproved messages
884
			$request = $smcFunc['db_query']('', '
885
				SELECT /*!40001 SQL_NO_CACHE */ t.id_topic, MAX(t.unapproved_posts) AS unapproved_posts,
886
					COUNT(mu.id_msg) AS real_unapproved_posts
887
				FROM {db_prefix}topics AS t
888
					LEFT JOIN {db_prefix}messages AS mu ON (mu.id_topic = t.id_topic AND mu.approved = {int:not_approved})
889
				WHERE t.id_topic > {int:start}
890
					AND t.id_topic <= {int:max_id}
891
				GROUP BY t.id_topic
892
				HAVING COUNT(mu.id_msg) != MAX(t.unapproved_posts)',
893
				array(
894
					'not_approved' => 0,
895
					'start' => $_REQUEST['start'],
896
					'max_id' => $_REQUEST['start'] + $increment,
897
				)
898
			);
899
			while ($row = $smcFunc['db_fetch_assoc']($request))
900
				$smcFunc['db_query']('', '
901
					UPDATE {db_prefix}topics
902
					SET unapproved_posts = {int:unapproved_posts}
903
					WHERE id_topic = {int:id_topic}',
904
					array(
905
						'unapproved_posts' => $row['real_unapproved_posts'],
906
						'id_topic' => $row['id_topic'],
907
					)
908
				);
909
			$smcFunc['db_free_result']($request);
910
911
			$_REQUEST['start'] += $increment;
912
913
			if (microtime(true) - $time_start > 3)
914
			{
915
				createToken('admin-boardrecount');
916
				$context['continue_post_data'] = '<input type="hidden" name="' . $context['admin-boardrecount_token_var'] . '" value="' . $context['admin-boardrecount_token'] . '">';
917
918
				$context['continue_get_data'] = '?action=admin;area=maintain;sa=routine;activity=recount;step=0;start=' . $_REQUEST['start'] . ';' . $context['session_var'] . '=' . $context['session_id'];
919
				$context['continue_percent'] = round((100 * $_REQUEST['start'] / $max_topics) / $total_steps);
920
921
				return;
922
			}
923
		}
924
925
		$_REQUEST['start'] = 0;
926
	}
927
928
	// Update the post count of each board.
929 View Code Duplication
	if ($_REQUEST['step'] <= 1)
930
	{
931
		if (empty($_REQUEST['start']))
932
			$smcFunc['db_query']('', '
933
				UPDATE {db_prefix}boards
934
				SET num_posts = {int:num_posts}
935
				WHERE redirect = {string:redirect}',
936
				array(
937
					'num_posts' => 0,
938
					'redirect' => '',
939
				)
940
			);
941
942
		while ($_REQUEST['start'] < $max_topics)
943
		{
944
			$request = $smcFunc['db_query']('', '
945
				SELECT /*!40001 SQL_NO_CACHE */ m.id_board, COUNT(*) AS real_num_posts
946
				FROM {db_prefix}messages AS m
947
				WHERE m.id_topic > {int:id_topic_min}
948
					AND m.id_topic <= {int:id_topic_max}
949
					AND m.approved = {int:is_approved}
950
				GROUP BY m.id_board',
951
				array(
952
					'id_topic_min' => $_REQUEST['start'],
953
					'id_topic_max' => $_REQUEST['start'] + $increment,
954
					'is_approved' => 1,
955
				)
956
			);
957
			while ($row = $smcFunc['db_fetch_assoc']($request))
958
				$smcFunc['db_query']('', '
959
					UPDATE {db_prefix}boards
960
					SET num_posts = num_posts + {int:real_num_posts}
961
					WHERE id_board = {int:id_board}',
962
					array(
963
						'id_board' => $row['id_board'],
964
						'real_num_posts' => $row['real_num_posts'],
965
					)
966
				);
967
			$smcFunc['db_free_result']($request);
968
969
			$_REQUEST['start'] += $increment;
970
971
			if (microtime(true) - $time_start > 3)
972
			{
973
				createToken('admin-boardrecount');
974
				$context['continue_post_data'] = '<input type="hidden" name="' . $context['admin-boardrecount_token_var'] . '" value="' . $context['admin-boardrecount_token'] . '">';
975
976
				$context['continue_get_data'] = '?action=admin;area=maintain;sa=routine;activity=recount;step=1;start=' . $_REQUEST['start'] . ';' . $context['session_var'] . '=' . $context['session_id'];
977
				$context['continue_percent'] = round((200 + 100 * $_REQUEST['start'] / $max_topics) / $total_steps);
978
979
				return;
980
			}
981
		}
982
983
		$_REQUEST['start'] = 0;
984
	}
985
986
	// Update the topic count of each board.
987 View Code Duplication
	if ($_REQUEST['step'] <= 2)
988
	{
989
		if (empty($_REQUEST['start']))
990
			$smcFunc['db_query']('', '
991
				UPDATE {db_prefix}boards
992
				SET num_topics = {int:num_topics}',
993
				array(
994
					'num_topics' => 0,
995
				)
996
			);
997
998
		while ($_REQUEST['start'] < $max_topics)
999
		{
1000
			$request = $smcFunc['db_query']('', '
1001
				SELECT /*!40001 SQL_NO_CACHE */ t.id_board, COUNT(*) AS real_num_topics
1002
				FROM {db_prefix}topics AS t
1003
				WHERE t.approved = {int:is_approved}
1004
					AND t.id_topic > {int:id_topic_min}
1005
					AND t.id_topic <= {int:id_topic_max}
1006
				GROUP BY t.id_board',
1007
				array(
1008
					'is_approved' => 1,
1009
					'id_topic_min' => $_REQUEST['start'],
1010
					'id_topic_max' => $_REQUEST['start'] + $increment,
1011
				)
1012
			);
1013
			while ($row = $smcFunc['db_fetch_assoc']($request))
1014
				$smcFunc['db_query']('', '
1015
					UPDATE {db_prefix}boards
1016
					SET num_topics = num_topics + {int:real_num_topics}
1017
					WHERE id_board = {int:id_board}',
1018
					array(
1019
						'id_board' => $row['id_board'],
1020
						'real_num_topics' => $row['real_num_topics'],
1021
					)
1022
				);
1023
			$smcFunc['db_free_result']($request);
1024
1025
			$_REQUEST['start'] += $increment;
1026
1027
			if (microtime(true) - $time_start > 3)
1028
			{
1029
				createToken('admin-boardrecount');
1030
				$context['continue_post_data'] = '<input type="hidden" name="' . $context['admin-boardrecount_token_var'] . '" value="' . $context['admin-boardrecount_token'] . '">';
1031
1032
				$context['continue_get_data'] = '?action=admin;area=maintain;sa=routine;activity=recount;step=2;start=' . $_REQUEST['start'] . ';' . $context['session_var'] . '=' . $context['session_id'];
1033
				$context['continue_percent'] = round((300 + 100 * $_REQUEST['start'] / $max_topics) / $total_steps);
1034
1035
				return;
1036
			}
1037
		}
1038
1039
		$_REQUEST['start'] = 0;
1040
	}
1041
1042
	// Update the unapproved post count of each board.
1043 View Code Duplication
	if ($_REQUEST['step'] <= 3)
1044
	{
1045
		if (empty($_REQUEST['start']))
1046
			$smcFunc['db_query']('', '
1047
				UPDATE {db_prefix}boards
1048
				SET unapproved_posts = {int:unapproved_posts}',
1049
				array(
1050
					'unapproved_posts' => 0,
1051
				)
1052
			);
1053
1054
		while ($_REQUEST['start'] < $max_topics)
1055
		{
1056
			$request = $smcFunc['db_query']('', '
1057
				SELECT /*!40001 SQL_NO_CACHE */ m.id_board, COUNT(*) AS real_unapproved_posts
1058
				FROM {db_prefix}messages AS m
1059
				WHERE m.id_topic > {int:id_topic_min}
1060
					AND m.id_topic <= {int:id_topic_max}
1061
					AND m.approved = {int:is_approved}
1062
				GROUP BY m.id_board',
1063
				array(
1064
					'id_topic_min' => $_REQUEST['start'],
1065
					'id_topic_max' => $_REQUEST['start'] + $increment,
1066
					'is_approved' => 0,
1067
				)
1068
			);
1069
			while ($row = $smcFunc['db_fetch_assoc']($request))
1070
				$smcFunc['db_query']('', '
1071
					UPDATE {db_prefix}boards
1072
					SET unapproved_posts = unapproved_posts + {int:unapproved_posts}
1073
					WHERE id_board = {int:id_board}',
1074
					array(
1075
						'id_board' => $row['id_board'],
1076
						'unapproved_posts' => $row['real_unapproved_posts'],
1077
					)
1078
				);
1079
			$smcFunc['db_free_result']($request);
1080
1081
			$_REQUEST['start'] += $increment;
1082
1083
			if (microtime(true) - $time_start > 3)
1084
			{
1085
				createToken('admin-boardrecount');
1086
				$context['continue_post_data'] = '<input type="hidden" name="' . $context['admin-boardrecount_token_var'] . '" value="' . $context['admin-boardrecount_token'] . '">';
1087
1088
				$context['continue_get_data'] = '?action=admin;area=maintain;sa=routine;activity=recount;step=3;start=' . $_REQUEST['start'] . ';' . $context['session_var'] . '=' . $context['session_id'];
1089
				$context['continue_percent'] = round((400 + 100 * $_REQUEST['start'] / $max_topics) / $total_steps);
1090
1091
				return;
1092
			}
1093
		}
1094
1095
		$_REQUEST['start'] = 0;
1096
	}
1097
1098
	// Update the unapproved topic count of each board.
1099 View Code Duplication
	if ($_REQUEST['step'] <= 4)
1100
	{
1101
		if (empty($_REQUEST['start']))
1102
			$smcFunc['db_query']('', '
1103
				UPDATE {db_prefix}boards
1104
				SET unapproved_topics = {int:unapproved_topics}',
1105
				array(
1106
					'unapproved_topics' => 0,
1107
				)
1108
			);
1109
1110
		while ($_REQUEST['start'] < $max_topics)
1111
		{
1112
			$request = $smcFunc['db_query']('', '
1113
				SELECT /*!40001 SQL_NO_CACHE */ t.id_board, COUNT(*) AS real_unapproved_topics
1114
				FROM {db_prefix}topics AS t
1115
				WHERE t.approved = {int:is_approved}
1116
					AND t.id_topic > {int:id_topic_min}
1117
					AND t.id_topic <= {int:id_topic_max}
1118
				GROUP BY t.id_board',
1119
				array(
1120
					'is_approved' => 0,
1121
					'id_topic_min' => $_REQUEST['start'],
1122
					'id_topic_max' => $_REQUEST['start'] + $increment,
1123
				)
1124
			);
1125
			while ($row = $smcFunc['db_fetch_assoc']($request))
1126
				$smcFunc['db_query']('', '
1127
					UPDATE {db_prefix}boards
1128
					SET unapproved_topics = unapproved_topics + {int:real_unapproved_topics}
1129
					WHERE id_board = {int:id_board}',
1130
					array(
1131
						'id_board' => $row['id_board'],
1132
						'real_unapproved_topics' => $row['real_unapproved_topics'],
1133
					)
1134
				);
1135
			$smcFunc['db_free_result']($request);
1136
1137
			$_REQUEST['start'] += $increment;
1138
1139
			if (microtime(true) - $time_start > 3)
1140
			{
1141
				createToken('admin-boardrecount');
1142
				$context['continue_post_data'] = '<input type="hidden" name="' . $context['admin-boardrecount_token_var'] . '" value="' . $context['admin-boardrecount_token'] . '">';
1143
1144
				$context['continue_get_data'] = '?action=admin;area=maintain;sa=routine;activity=recount;step=4;start=' . $_REQUEST['start'] . ';' . $context['session_var'] . '=' . $context['session_id'];
1145
				$context['continue_percent'] = round((500 + 100 * $_REQUEST['start'] / $max_topics) / $total_steps);
1146
1147
				return;
1148
			}
1149
		}
1150
1151
		$_REQUEST['start'] = 0;
1152
	}
1153
1154
	// Get all members with wrong number of personal messages.
1155
	if ($_REQUEST['step'] <= 5)
1156
	{
1157
		$request = $smcFunc['db_query']('', '
1158
			SELECT /*!40001 SQL_NO_CACHE */ mem.id_member, COUNT(pmr.id_pm) AS real_num,
1159
				MAX(mem.instant_messages) AS instant_messages
1160
			FROM {db_prefix}members AS mem
1161
				LEFT JOIN {db_prefix}pm_recipients AS pmr ON (mem.id_member = pmr.id_member AND pmr.deleted = {int:is_not_deleted})
1162
			GROUP BY mem.id_member
1163
			HAVING COUNT(pmr.id_pm) != MAX(mem.instant_messages)',
1164
			array(
1165
				'is_not_deleted' => 0,
1166
			)
1167
		);
1168 View Code Duplication
		while ($row = $smcFunc['db_fetch_assoc']($request))
1169
			updateMemberData($row['id_member'], array('instant_messages' => $row['real_num']));
1170
		$smcFunc['db_free_result']($request);
1171
1172
		$request = $smcFunc['db_query']('', '
1173
			SELECT /*!40001 SQL_NO_CACHE */ mem.id_member, COUNT(pmr.id_pm) AS real_num,
1174
				MAX(mem.unread_messages) AS unread_messages
1175
			FROM {db_prefix}members AS mem
1176
				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})
1177
			GROUP BY mem.id_member
1178
			HAVING COUNT(pmr.id_pm) != MAX(mem.unread_messages)',
1179
			array(
1180
				'is_not_deleted' => 0,
1181
				'is_not_read' => 0,
1182
			)
1183
		);
1184 View Code Duplication
		while ($row = $smcFunc['db_fetch_assoc']($request))
1185
			updateMemberData($row['id_member'], array('unread_messages' => $row['real_num']));
1186
		$smcFunc['db_free_result']($request);
1187
1188
		if (microtime(true) - $time_start > 3)
1189
		{
1190
			createToken('admin-boardrecount');
1191
			$context['continue_post_data'] = '<input type="hidden" name="' . $context['admin-boardrecount_token_var'] . '" value="' . $context['admin-boardrecount_token'] . '">';
1192
1193
			$context['continue_get_data'] = '?action=admin;area=maintain;sa=routine;activity=recount;step=6;start=0;' . $context['session_var'] . '=' . $context['session_id'];
1194
			$context['continue_percent'] = round(700 / $total_steps);
1195
1196
			return;
1197
		}
1198
	}
1199
1200
	// Any messages pointing to the wrong board?
1201
	if ($_REQUEST['step'] <= 6)
1202
	{
1203
		while ($_REQUEST['start'] < $modSettings['maxMsgID'])
1204
		{
1205
			$request = $smcFunc['db_query']('', '
1206
				SELECT /*!40001 SQL_NO_CACHE */ t.id_board, m.id_msg
1207
				FROM {db_prefix}messages AS m
1208
					INNER JOIN {db_prefix}topics AS t ON (t.id_topic = m.id_topic AND t.id_board != m.id_board)
1209
				WHERE m.id_msg > {int:id_msg_min}
1210
					AND m.id_msg <= {int:id_msg_max}',
1211
				array(
1212
					'id_msg_min' => $_REQUEST['start'],
1213
					'id_msg_max' => $_REQUEST['start'] + $increment,
1214
				)
1215
			);
1216
			$boards = array();
1217
			while ($row = $smcFunc['db_fetch_assoc']($request))
1218
				$boards[$row['id_board']][] = $row['id_msg'];
1219
			$smcFunc['db_free_result']($request);
1220
1221
			foreach ($boards as $board_id => $messages)
1222
				$smcFunc['db_query']('', '
1223
					UPDATE {db_prefix}messages
1224
					SET id_board = {int:id_board}
1225
					WHERE id_msg IN ({array_int:id_msg_array})',
1226
					array(
1227
						'id_msg_array' => $messages,
1228
						'id_board' => $board_id,
1229
					)
1230
				);
1231
1232
			$_REQUEST['start'] += $increment;
1233
1234
			if (microtime(true) - $time_start > 3)
1235
			{
1236
				createToken('admin-boardrecount');
1237
				$context['continue_post_data'] = '<input type="hidden" name="' . $context['admin-boardrecount_token_var'] . '" value="' . $context['admin-boardrecount_token'] . '">';
1238
1239
				$context['continue_get_data'] = '?action=admin;area=maintain;sa=routine;activity=recount;step=6;start=' . $_REQUEST['start'] . ';' . $context['session_var'] . '=' . $context['session_id'];
1240
				$context['continue_percent'] = round((700 + 100 * $_REQUEST['start'] / $modSettings['maxMsgID']) / $total_steps);
1241
1242
				return;
1243
			}
1244
		}
1245
1246
		$_REQUEST['start'] = 0;
1247
	}
1248
1249
	// Update the latest message of each board.
1250
	$request = $smcFunc['db_query']('', '
1251
		SELECT m.id_board, MAX(m.id_msg) AS local_last_msg
1252
		FROM {db_prefix}messages AS m
1253
		WHERE m.approved = {int:is_approved}
1254
		GROUP BY m.id_board',
1255
		array(
1256
			'is_approved' => 1,
1257
		)
1258
	);
1259
	$realBoardCounts = array();
1260
	while ($row = $smcFunc['db_fetch_assoc']($request))
1261
		$realBoardCounts[$row['id_board']] = $row['local_last_msg'];
1262
	$smcFunc['db_free_result']($request);
1263
1264
	$request = $smcFunc['db_query']('', '
1265
		SELECT /*!40001 SQL_NO_CACHE */ id_board, id_parent, id_last_msg, child_level, id_msg_updated
1266
		FROM {db_prefix}boards',
1267
		array(
1268
		)
1269
	);
1270
	$resort_me = array();
1271
	while ($row = $smcFunc['db_fetch_assoc']($request))
1272
	{
1273
		$row['local_last_msg'] = isset($realBoardCounts[$row['id_board']]) ? $realBoardCounts[$row['id_board']] : 0;
1274
		$resort_me[$row['child_level']][] = $row;
1275
	}
1276
	$smcFunc['db_free_result']($request);
1277
1278
	krsort($resort_me);
1279
1280
	$lastModifiedMsg = array();
1281
	foreach ($resort_me as $rows)
1282
		foreach ($rows as $row)
1283
		{
1284
			// The latest message is the latest of the current board and its children.
1285
			if (isset($lastModifiedMsg[$row['id_board']]))
1286
				$curLastModifiedMsg = max($row['local_last_msg'], $lastModifiedMsg[$row['id_board']]);
1287
			else
1288
				$curLastModifiedMsg = $row['local_last_msg'];
1289
1290
			// If what is and what should be the latest message differ, an update is necessary.
1291
			if ($row['local_last_msg'] != $row['id_last_msg'] || $curLastModifiedMsg != $row['id_msg_updated'])
1292
				$smcFunc['db_query']('', '
1293
					UPDATE {db_prefix}boards
1294
					SET id_last_msg = {int:id_last_msg}, id_msg_updated = {int:id_msg_updated}
1295
					WHERE id_board = {int:id_board}',
1296
					array(
1297
						'id_last_msg' => $row['local_last_msg'],
1298
						'id_msg_updated' => $curLastModifiedMsg,
1299
						'id_board' => $row['id_board'],
1300
					)
1301
				);
1302
1303
			// Parent boards inherit the latest modified message of their children.
1304
			if (isset($lastModifiedMsg[$row['id_parent']]))
1305
				$lastModifiedMsg[$row['id_parent']] = max($row['local_last_msg'], $lastModifiedMsg[$row['id_parent']]);
1306
			else
1307
				$lastModifiedMsg[$row['id_parent']] = $row['local_last_msg'];
1308
		}
1309
1310
	// Update all the basic statistics.
1311
	updateStats('member');
1312
	updateStats('message');
1313
	updateStats('topic');
1314
1315
	// Finally, update the latest event times.
1316
	require_once($sourcedir . '/ScheduledTasks.php');
1317
	CalculateNextTrigger();
1318
1319
	redirectexit('action=admin;area=maintain;sa=routine;done=recount');
1320
}
1321
1322
/**
1323
 * Perform a detailed version check.  A very good thing ;).
1324
 * The function parses the comment headers in all files for their version information,
1325
 * and outputs that for some javascript to check with simplemachines.org.
1326
 * It does not connect directly with simplemachines.org, but rather expects the client to.
1327
 *
1328
 * It requires the admin_forum permission.
1329
 * Uses the view_versions admin area.
1330
 * Accessed through ?action=admin;area=maintain;sa=routine;activity=version.
1331
 * @uses Admin template, view_versions sub-template.
1332
 */
1333
function VersionDetail()
1334
{
1335
	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...
1336
1337
	isAllowedTo('admin_forum');
1338
1339
	// Call the function that'll get all the version info we need.
1340
	require_once($sourcedir . '/Subs-Admin.php');
1341
	$versionOptions = array(
1342
		'include_ssi' => true,
1343
		'include_subscriptions' => true,
1344
		'include_tasks' => true,
1345
		'sort_results' => true,
1346
	);
1347
	$version_info = getFileVersions($versionOptions);
1348
1349
	// Add the new info to the template context.
1350
	$context += array(
1351
		'file_versions' => $version_info['file_versions'],
1352
		'default_template_versions' => $version_info['default_template_versions'],
1353
		'template_versions' => $version_info['template_versions'],
1354
		'default_language_versions' => $version_info['default_language_versions'],
1355
		'default_known_languages' => array_keys($version_info['default_language_versions']),
1356
		'tasks_versions' => $version_info['tasks_versions'],
1357
	);
1358
1359
	// Make it easier to manage for the template.
1360
	$context['forum_version'] = $forum_version;
1361
1362
	$context['sub_template'] = 'view_versions';
1363
	$context['page_title'] = $txt['admin_version_check'];
1364
}
1365
1366
/**
1367
 * Re-attribute posts.
1368
 */
1369
function MaintainReattributePosts()
1370
{
1371
	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...
1372
1373
	checkSession();
1374
1375
	// Find the member.
1376
	require_once($sourcedir . '/Subs-Auth.php');
1377
	$members = findMembers($_POST['to']);
1378
1379
	if (empty($members))
1380
		fatal_lang_error('reattribute_cannot_find_member');
1381
1382
	$memID = array_shift($members);
1383
	$memID = $memID['id'];
1384
1385
	$email = $_POST['type'] == 'email' ? $_POST['from_email'] : '';
1386
	$membername = $_POST['type'] == 'name' ? $_POST['from_name'] : '';
1387
1388
	// Now call the reattribute function.
1389
	require_once($sourcedir . '/Subs-Members.php');
1390
	reattributePosts($memID, $email, $membername, !empty($_POST['posts']));
1391
1392
	$context['maintenance_finished'] = $txt['maintain_reattribute_posts'];
1393
}
1394
1395
/**
1396
 * Removing old members. Done and out!
1397
 * @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...
1398
 */
1399
function MaintainPurgeInactiveMembers()
1400
{
1401
	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...
1402
1403
	$_POST['maxdays'] = empty($_POST['maxdays']) ? 0 : (int) $_POST['maxdays'];
1404
	if (!empty($_POST['groups']) && $_POST['maxdays'] > 0)
1405
	{
1406
		checkSession();
1407
		validateToken('admin-maint');
1408
1409
		$groups = array();
1410
		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...
1411
			$groups[] = (int) $id;
1412
		$time_limit = (time() - ($_POST['maxdays'] * 24 * 3600));
1413
		$where_vars = array(
1414
			'time_limit' => $time_limit,
1415
		);
1416
		if ($_POST['del_type'] == 'activated')
1417
		{
1418
			$where = 'mem.date_registered < {int:time_limit} AND mem.is_activated = {int:is_activated}';
1419
			$where_vars['is_activated'] = 0;
1420
		}
1421
		else
1422
			$where = 'mem.last_login < {int:time_limit} AND (mem.last_login != 0 OR mem.date_registered < {int:time_limit})';
1423
1424
		// Need to get *all* groups then work out which (if any) we avoid.
1425
		$request = $smcFunc['db_query']('', '
1426
			SELECT id_group, group_name, min_posts
1427
			FROM {db_prefix}membergroups',
1428
			array(
1429
			)
1430
		);
1431
		while ($row = $smcFunc['db_fetch_assoc']($request))
1432
		{
1433
			// Avoid this one?
1434
			if (!in_array($row['id_group'], $groups))
1435
			{
1436
				// Post group?
1437
				if ($row['min_posts'] != -1)
1438
				{
1439
					$where .= ' AND mem.id_post_group != {int:id_post_group_' . $row['id_group'] . '}';
1440
					$where_vars['id_post_group_' . $row['id_group']] = $row['id_group'];
1441
				}
1442
				else
1443
				{
1444
					$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';
1445
					$where_vars['id_group_' . $row['id_group']] = $row['id_group'];
1446
				}
1447
			}
1448
		}
1449
		$smcFunc['db_free_result']($request);
1450
1451
		// If we have ungrouped unselected we need to avoid those guys.
1452
		if (!in_array(0, $groups))
1453
		{
1454
			$where .= ' AND (mem.id_group != 0 OR mem.additional_groups != {string:blank_add_groups})';
1455
			$where_vars['blank_add_groups'] = '';
1456
		}
1457
1458
		// Select all the members we're about to murder/remove...
1459
		$request = $smcFunc['db_query']('', '
1460
			SELECT mem.id_member, COALESCE(m.id_member, 0) AS is_mod
1461
			FROM {db_prefix}members AS mem
1462
				LEFT JOIN {db_prefix}moderators AS m ON (m.id_member = mem.id_member)
1463
			WHERE ' . $where,
1464
			$where_vars
1465
		);
1466
		$members = array();
1467
		while ($row = $smcFunc['db_fetch_assoc']($request))
1468
		{
1469
			if (!$row['is_mod'] || !in_array(3, $groups))
1470
				$members[] = $row['id_member'];
1471
		}
1472
		$smcFunc['db_free_result']($request);
1473
1474
		require_once($sourcedir . '/Subs-Members.php');
1475
		deleteMembers($members);
1476
	}
1477
1478
	$context['maintenance_finished'] = $txt['maintain_members'];
1479
	createToken('admin-maint');
1480
}
1481
1482
/**
1483
 * Removing old posts doesn't take much as we really pass through.
1484
 */
1485
function MaintainRemoveOldPosts()
1486
{
1487
	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...
1488
1489
	validateToken('admin-maint');
1490
1491
	// Actually do what we're told!
1492
	require_once($sourcedir . '/RemoveTopic.php');
1493
	RemoveOldTopics2();
1494
}
1495
1496
/**
1497
 * Removing old drafts
1498
 */
1499
function MaintainRemoveOldDrafts()
1500
{
1501
	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...
1502
1503
	validateToken('admin-maint');
1504
1505
	$drafts = array();
1506
1507
	// Find all of the old drafts
1508
	$request = $smcFunc['db_query']('', '
1509
		SELECT id_draft
1510
		FROM {db_prefix}user_drafts
1511
		WHERE poster_time <= {int:poster_time_old}',
1512
		array(
1513
			'poster_time_old' => time() - (86400 * $_POST['draftdays']),
1514
		)
1515
	);
1516
1517
	while ($row = $smcFunc['db_fetch_row']($request))
1518
		$drafts[] = (int) $row[0];
1519
	$smcFunc['db_free_result']($request);
1520
1521
	// If we have old drafts, remove them
1522
	if (count($drafts) > 0)
1523
	{
1524
		require_once($sourcedir . '/Drafts.php');
1525
		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...
1526
	}
1527
}
1528
1529
/**
1530
 * Moves topics from one board to another.
1531
 *
1532
 * @uses not_done template to pause the process.
1533
 */
1534
function MaintainMassMoveTopics()
1535
{
1536
	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...
1537
1538
	// Only admins.
1539
	isAllowedTo('admin_forum');
1540
1541
	checkSession('request');
1542
	validateToken('admin-maint');
1543
1544
	// Set up to the context.
1545
	$context['page_title'] = $txt['not_done_title'];
1546
	$context['continue_countdown'] = 3;
1547
	$context['continue_post_data'] = '';
1548
	$context['continue_get_data'] = '';
1549
	$context['sub_template'] = 'not_done';
1550
	$context['start'] = empty($_REQUEST['start']) ? 0 : (int) $_REQUEST['start'];
1551
	$context['start_time'] = time();
1552
1553
	// First time we do this?
1554
	$id_board_from = isset($_REQUEST['id_board_from']) ? (int) $_REQUEST['id_board_from'] : 0;
1555
	$id_board_to = isset($_REQUEST['id_board_to']) ? (int) $_REQUEST['id_board_to'] : 0;
1556
	$max_days = isset($_REQUEST['maxdays']) ? (int) $_REQUEST['maxdays'] : 0;
1557
	$locked = isset($_POST['move_type_locked']) || isset($_GET['locked']);
1558
	$sticky = isset($_POST['move_type_sticky']) || isset($_GET['sticky']);
1559
1560
	// No boards then this is your stop.
1561
	if (empty($id_board_from) || empty($id_board_to))
1562
		return;
1563
1564
	// The big WHERE clause
1565
	$conditions = 'WHERE t.id_board = {int:id_board_from}
1566
		AND m.icon != {string:moved}';
1567
1568
	// DB parameters
1569
	$params = array(
1570
		'id_board_from' => $id_board_from,
1571
		'moved' => 'moved',
1572
	);
1573
1574
	// Only moving topics not posted in for x days?
1575
	if (!empty($max_days))
1576
	{
1577
		$conditions .= '
1578
			AND m.poster_time < {int:poster_time}';
1579
		$params['poster_time'] = time() - 3600 * 24 * $max_days;
1580
	}
1581
1582
	// Moving locked topics?
1583
	if ($locked)
1584
	{
1585
		$conditions .= '
1586
			AND t.locked = {int:locked}';
1587
		$params['locked'] = 1;
1588
	}
1589
1590
	// What about sticky topics?
1591
	if ($sticky)
1592
	{
1593
		$conditions .= '
1594
			AND t.is_sticky = {int:sticky}';
1595
		$params['sticky'] = 1;
1596
	}
1597
1598
	// How many topics are we converting?
1599 View Code Duplication
	if (!isset($_REQUEST['totaltopics']))
1600
	{
1601
		$request = $smcFunc['db_query']('', '
1602
			SELECT COUNT(*)
1603
			FROM {db_prefix}topics AS t
1604
				INNER JOIN {db_prefix}messages AS m ON (m.id_msg = t.id_last_msg)' .
1605
			$conditions,
1606
			$params
1607
		);
1608
		list ($total_topics) = $smcFunc['db_fetch_row']($request);
1609
		$smcFunc['db_free_result']($request);
1610
	}
1611
	else
1612
		$total_topics = (int) $_REQUEST['totaltopics'];
1613
1614
	// Seems like we need this here.
1615
	$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;
1616
1617
	if ($locked)
1618
		$context['continue_get_data'] .= ';locked';
1619
1620
	if ($sticky)
1621
		$context['continue_get_data'] .= ';sticky';
1622
1623
	$context['continue_get_data'] .= ';start=' . $context['start'] . ';' . $context['session_var'] . '=' . $context['session_id'];
1624
1625
	// We have topics to move so start the process.
1626
	if (!empty($total_topics))
1627
	{
1628
		while ($context['start'] <= $total_topics)
1629
		{
1630
			// Lets get the topics.
1631
			$request = $smcFunc['db_query']('', '
1632
				SELECT t.id_topic
1633
				FROM {db_prefix}topics AS t
1634
					INNER JOIN {db_prefix}messages AS m ON (m.id_msg = t.id_last_msg)
1635
				' . $conditions . '
1636
				LIMIT 10',
1637
				$params
1638
			);
1639
1640
			// Get the ids.
1641
			$topics = array();
1642
			while ($row = $smcFunc['db_fetch_assoc']($request))
1643
				$topics[] = $row['id_topic'];
1644
1645
			// Just return if we don't have any topics left to move.
1646
			if (empty($topics))
1647
			{
1648
				cache_put_data('board-' . $id_board_from, null, 120);
1649
				cache_put_data('board-' . $id_board_to, null, 120);
1650
				redirectexit('action=admin;area=maintain;sa=topics;done=massmove');
1651
			}
1652
1653
			// Lets move them.
1654
			require_once($sourcedir . '/MoveTopic.php');
1655
			moveTopics($topics, $id_board_to);
1656
1657
			// We've done at least ten more topics.
1658
			$context['start'] += 10;
1659
1660
			// Lets wait a while.
1661
			if (time() - $context['start_time'] > 3)
1662
			{
1663
				// What's the percent?
1664
				$context['continue_percent'] = round(100 * ($context['start'] / $total_topics), 1);
1665
				$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'];
1666
1667
				// Let the template system do it's thang.
1668
				return;
1669
			}
1670
		}
1671
	}
1672
1673
	// Don't confuse admins by having an out of date cache.
1674
	cache_put_data('board-' . $id_board_from, null, 120);
1675
	cache_put_data('board-' . $id_board_to, null, 120);
1676
1677
	redirectexit('action=admin;area=maintain;sa=topics;done=massmove');
1678
}
1679
1680
/**
1681
 * Recalculate all members post counts
1682
 * it requires the admin_forum permission.
1683
 *
1684
 * - recounts all posts for members found in the message table
1685
 * - updates the members post count record in the members table
1686
 * - honors the boards post count flag
1687
 * - does not count posts in the recycle bin
1688
 * - zeros post counts for all members with no posts in the message table
1689
 * - runs as a delayed loop to avoid server overload
1690
 * - uses the not_done template in Admin.template
1691
 *
1692
 * The function redirects back to action=admin;area=maintain;sa=members when complete.
1693
 * It is accessed via ?action=admin;area=maintain;sa=members;activity=recountposts
1694
 */
1695
function MaintainRecountPosts()
1696
{
1697
	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...
1698
1699
	// You have to be allowed in here
1700
	isAllowedTo('admin_forum');
1701
	checkSession('request');
1702
1703
	// Set up to the context.
1704
	$context['page_title'] = $txt['not_done_title'];
1705
	$context['continue_countdown'] = 3;
1706
	$context['continue_get_data'] = '';
1707
	$context['sub_template'] = 'not_done';
1708
1709
	// init
1710
	$increment = 200;
1711
	$_REQUEST['start'] = !isset($_REQUEST['start']) ? 0 : (int) $_REQUEST['start'];
1712
1713
	// Ask for some extra time, on big boards this may take a bit
1714
	@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...
1715
1716
	// Only run this query if we don't have the total number of members that have posted
1717
	if (!isset($_SESSION['total_members']))
1718
	{
1719
		validateToken('admin-maint');
1720
1721
		$request = $smcFunc['db_query']('', '
1722
			SELECT COUNT(DISTINCT m.id_member)
1723
			FROM {db_prefix}messages AS m
1724
			JOIN {db_prefix}boards AS b on m.id_board = b.id_board
1725
			WHERE m.id_member != 0
1726
				AND b.count_posts = 0',
1727
			array(
1728
			)
1729
		);
1730
1731
		// save it so we don't do this again for this task
1732
		list ($_SESSION['total_members']) = $smcFunc['db_fetch_row']($request);
1733
		$smcFunc['db_free_result']($request);
1734
	}
1735
	else
1736
		validateToken('admin-recountposts');
1737
1738
	// Lets get a group of members and determine their post count (from the boards that have post count enabled of course).
1739
	$request = $smcFunc['db_query']('', '
1740
		SELECT /*!40001 SQL_NO_CACHE */ m.id_member, COUNT(m.id_member) AS posts
1741
		FROM {db_prefix}messages AS m
1742
			INNER JOIN {db_prefix}boards AS b ON m.id_board = b.id_board
1743
		WHERE m.id_member != {int:zero}
1744
			AND b.count_posts = {int:zero}
1745
			' . (!empty($modSettings['recycle_enable']) ? ' AND b.id_board != {int:recycle}' : '') . '
1746
		GROUP BY m.id_member
1747
		LIMIT {int:start}, {int:number}',
1748
		array(
1749
			'start' => $_REQUEST['start'],
1750
			'number' => $increment,
1751
			'recycle' => $modSettings['recycle_board'],
1752
			'zero' => 0,
1753
		)
1754
	);
1755
	$total_rows = $smcFunc['db_num_rows']($request);
1756
1757
	// Update the post count for this group
1758
	while ($row = $smcFunc['db_fetch_assoc']($request))
1759
	{
1760
		$smcFunc['db_query']('', '
1761
			UPDATE {db_prefix}members
1762
			SET posts = {int:posts}
1763
			WHERE id_member = {int:row}',
1764
			array(
1765
				'row' => $row['id_member'],
1766
				'posts' => $row['posts'],
1767
			)
1768
		);
1769
	}
1770
	$smcFunc['db_free_result']($request);
1771
1772
	// Continue?
1773
	if ($total_rows == $increment)
1774
	{
1775
		$_REQUEST['start'] += $increment;
1776
		$context['continue_get_data'] = '?action=admin;area=maintain;sa=members;activity=recountposts;start=' . $_REQUEST['start'] . ';' . $context['session_var'] . '=' . $context['session_id'];
1777
		$context['continue_percent'] = round(100 * $_REQUEST['start'] / $_SESSION['total_members']);
1778
1779
		createToken('admin-recountposts');
1780
		$context['continue_post_data'] = '<input type="hidden" name="' . $context['admin-recountposts_token_var'] . '" value="' . $context['admin-recountposts_token'] . '">';
1781
1782
		if (function_exists('apache_reset_timeout'))
1783
			apache_reset_timeout();
1784
		return;
1785
	}
1786
1787
	// final steps ... made more difficult since we don't yet support sub-selects on joins
1788
	// place all members who have posts in the message table in a temp table
1789
	$createTemporary = $smcFunc['db_query']('', '
1790
		CREATE TEMPORARY TABLE {db_prefix}tmp_maint_recountposts (
1791
			id_member mediumint(8) unsigned NOT NULL default {string:string_zero},
1792
			PRIMARY KEY (id_member)
1793
		)
1794
		SELECT m.id_member
1795
		FROM {db_prefix}messages AS m
1796
			INNER JOIN {db_prefix}boards AS b ON m.id_board = b.id_board
1797
		WHERE m.id_member != {int:zero}
1798
			AND b.count_posts = {int:zero}
1799
			' . (!empty($modSettings['recycle_enable']) ? ' AND b.id_board != {int:recycle}' : '') . '
1800
		GROUP BY m.id_member',
1801
		array(
1802
			'zero' => 0,
1803
			'string_zero' => '0',
1804
			'db_error_skip' => true,
1805
			'recycle' => !empty($modSettings['recycle_board']) ? $modSettings['recycle_board'] : 0,
1806
		)
1807
	) !== false;
1808
1809 View Code Duplication
	if ($createTemporary)
1810
	{
1811
		// outer join the members table on the temporary table finding the members that have a post count but no posts in the message table
1812
		$request = $smcFunc['db_query']('', '
1813
			SELECT mem.id_member, mem.posts
1814
			FROM {db_prefix}members AS mem
1815
			LEFT OUTER JOIN {db_prefix}tmp_maint_recountposts AS res
1816
			ON res.id_member = mem.id_member
1817
			WHERE res.id_member IS null
1818
				AND mem.posts != {int:zero}',
1819
			array(
1820
				'zero' => 0,
1821
			)
1822
		);
1823
1824
		// set the post count to zero for any delinquents we may have found
1825
		while ($row = $smcFunc['db_fetch_assoc']($request))
1826
		{
1827
			$smcFunc['db_query']('', '
1828
				UPDATE {db_prefix}members
1829
				SET posts = {int:zero}
1830
				WHERE id_member = {int:row}',
1831
				array(
1832
					'row' => $row['id_member'],
1833
					'zero' => 0,
1834
				)
1835
			);
1836
		}
1837
		$smcFunc['db_free_result']($request);
1838
	}
1839
1840
	// all done
1841
	unset($_SESSION['total_members']);
1842
	$context['maintenance_finished'] = $txt['maintain_recountposts'];
1843
	redirectexit('action=admin;area=maintain;sa=members;done=recountposts');
1844
}
1845
1846
/**
1847
 * Generates a list of integration hooks for display
1848
 * Accessed through ?action=admin;area=maintain;sa=hooks;
1849
 * Allows for removal or disabling of selected hooks
1850
 */
1851
function list_integration_hooks()
1852
{
1853
	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...
1854
1855
	$context['filter_url'] = '';
1856
	$context['current_filter'] = '';
1857
	$currentHooks = get_integration_hooks();
1858
	if (isset($_GET['filter']) && in_array($_GET['filter'], array_keys($currentHooks)))
1859
	{
1860
		$context['filter_url'] = ';filter=' . $_GET['filter'];
1861
		$context['current_filter'] = $_GET['filter'];
1862
	}
1863
1864
	if (!empty($_REQUEST['do']) && isset($_REQUEST['hook']) && isset($_REQUEST['function']))
1865
	{
1866
		checkSession('request');
1867
		validateToken('admin-hook', 'request');
1868
1869
		if ($_REQUEST['do'] == 'remove')
1870
			remove_integration_function($_REQUEST['hook'], urldecode($_REQUEST['function']));
1871
1872
		else
1873
		{
1874
			$function_remove = urldecode($_REQUEST['function']) . (($_REQUEST['do'] == 'disable') ? '' : '!');
1875
			$function_add = urldecode($_REQUEST['function']) . (($_REQUEST['do'] == 'disable') ? '!' : '');
1876
1877
			remove_integration_function($_REQUEST['hook'], $function_remove);
1878
			add_integration_function($_REQUEST['hook'], $function_add);
1879
1880
			redirectexit('action=admin;area=maintain;sa=hooks' . $context['filter_url']);
1881
		}
1882
	}
1883
1884
	createToken('admin-hook', 'request');
1885
1886
	$list_options = array(
1887
		'id' => 'list_integration_hooks',
1888
		'title' => $txt['hooks_title_list'],
1889
		'items_per_page' => 20,
1890
		'base_href' => $scripturl . '?action=admin;area=maintain;sa=hooks' . $context['filter_url'] . ';' . $context['session_var'] . '=' . $context['session_id'],
1891
		'default_sort_col' => 'hook_name',
1892
		'get_items' => array(
1893
			'function' => 'get_integration_hooks_data',
1894
		),
1895
		'get_count' => array(
1896
			'function' => 'get_integration_hooks_count',
1897
		),
1898
		'no_items_label' => $txt['hooks_no_hooks'],
1899
		'columns' => array(
1900
			'hook_name' => array(
1901
				'header' => array(
1902
					'value' => $txt['hooks_field_hook_name'],
1903
				),
1904
				'data' => array(
1905
					'db' => 'hook_name',
1906
				),
1907
				'sort' =>  array(
1908
					'default' => 'hook_name',
1909
					'reverse' => 'hook_name DESC',
1910
				),
1911
			),
1912
			'function_name' => array(
1913
				'header' => array(
1914
					'value' => $txt['hooks_field_function_name'],
1915
				),
1916
				'data' => array(
1917
					'function' => function($data) use ($txt)
1918
					{
1919
						// Show a nice icon to indicate this is an instance.
1920
						$instance = (!empty($data['instance']) ? '<span class="generic_icons news" title="' . $txt['hooks_field_function_method'] . '"></span> ' : '');
1921
1922
						if (!empty($data['included_file']))
1923
							return $instance . $txt['hooks_field_function'] . ': ' . $data['real_function'] . '<br>' . $txt['hooks_field_included_file'] . ': ' . $data['included_file'];
1924
1925
						else
1926
							return $instance . $data['real_function'];
1927
					},
1928
				),
1929
				'sort' =>  array(
1930
					'default' => 'function_name',
1931
					'reverse' => 'function_name DESC',
1932
				),
1933
			),
1934
			'file_name' => array(
1935
				'header' => array(
1936
					'value' => $txt['hooks_field_file_name'],
1937
				),
1938
				'data' => array(
1939
					'db' => 'file_name',
1940
				),
1941
				'sort' =>  array(
1942
					'default' => 'file_name',
1943
					'reverse' => 'file_name DESC',
1944
				),
1945
			),
1946
			'status' => array(
1947
				'header' => array(
1948
					'value' => $txt['hooks_field_hook_exists'],
1949
					'style' => 'width:3%;',
1950
				),
1951
				'data' => array(
1952
					'function' => function($data) use ($txt, $scripturl, $context)
1953
					{
1954
						$change_status = array('before' => '', 'after' => '');
1955
1956
							$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">';
1957
							$change_status['after'] = '</a>';
1958
1959
						return $change_status['before'] . '<span class="generic_icons post_moderation_' . $data['status'] . '" title="' . $data['img_text'] . '"></span>';
1960
					},
1961
					'class' => 'centertext',
1962
				),
1963
				'sort' =>  array(
1964
					'default' => 'status',
1965
					'reverse' => 'status DESC',
1966
				),
1967
			),
1968
		),
1969
		'additional_rows' => array(
1970
			array(
1971
				'position' => 'after_title',
1972
				'value' => $txt['hooks_disable_instructions'] . '<br>
1973
					' . $txt['hooks_disable_legend'] . ':
1974
				<ul style="list-style: none;">
1975
					<li><span class="generic_icons post_moderation_allow"></span> ' . $txt['hooks_disable_legend_exists'] . '</li>
1976
					<li><span class="generic_icons post_moderation_moderate"></span> ' . $txt['hooks_disable_legend_disabled'] . '</li>
1977
					<li><span class="generic_icons post_moderation_deny"></span> ' . $txt['hooks_disable_legend_missing'] . '</li>
1978
				</ul>'
1979
			),
1980
		),
1981
	);
1982
1983
	$list_options['columns']['remove'] = array(
1984
		'header' => array(
1985
			'value' => $txt['hooks_button_remove'],
1986
			'style' => 'width:3%',
1987
		),
1988
		'data' => array(
1989
			'function' => function($data) use ($txt, $scripturl, $context)
1990
			{
1991
				if (!$data['hook_exists'])
1992
					return '
1993
					<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">
1994
						<span class="generic_icons delete" title="' . $txt['hooks_button_remove'] . '"></span>
1995
					</a>';
1996
			},
1997
			'class' => 'centertext',
1998
		),
1999
	);
2000
	$list_options['form'] = array(
2001
		'href' => $scripturl . '?action=admin;area=maintain;sa=hooks' . $context['filter_url'] . ';' . $context['session_var'] . '=' . $context['session_id'],
2002
		'name' => 'list_integration_hooks',
2003
	);
2004
2005
2006
	require_once($sourcedir . '/Subs-List.php');
2007
	createList($list_options);
2008
2009
	$context['page_title'] = $txt['hooks_title_list'];
2010
	$context['sub_template'] = 'show_list';
2011
	$context['default_list'] = 'list_integration_hooks';
2012
}
2013
2014
/**
2015
 * Gets all of the files in a directory and its children directories
2016
 *
2017
 * @param string $dir_path The path to the directory
2018
 * @return array An array containing information about the files found in the specified directory and its children
2019
 */
2020
function get_files_recursive($dir_path)
2021
{
2022
	$files = array();
2023
2024
	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...
2025
	{
2026
		while (($file = readdir($dh)) !== false)
2027
		{
2028
			if ($file != '.' && $file != '..')
2029
			{
2030
				if (is_dir($dir_path . '/' . $file))
2031
					$files = array_merge($files, get_files_recursive($dir_path . '/' . $file));
2032
				else
2033
					$files[] = array('dir' => $dir_path, 'name' => $file);
2034
			}
2035
		}
2036
	}
2037
	closedir($dh);
2038
2039
	return $files;
2040
}
2041
2042
/**
2043
 * Callback function for the integration hooks list (list_integration_hooks)
2044
 * Gets all of the hooks in the system and their status
2045
 *
2046
 * @param int $start The item to start with (for pagination purposes)
2047
 * @param int $per_page How many items to display on each page
2048
 * @param string $sort A string indicating how to sort things
2049
 * @return array An array of information about the integration hooks
2050
 */
2051
function get_integration_hooks_data($start, $per_page, $sort)
2052
{
2053
	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...
2054
2055
	$hooks = $temp_hooks = get_integration_hooks();
2056
	$hooks_data = $temp_data = $hook_status = array();
2057
2058
	$files = get_files_recursive($sourcedir);
2059
	if (!empty($files))
2060
	{
2061
		foreach ($files as $file)
2062
		{
2063
			if (is_file($file['dir'] . '/' . $file['name']) && substr($file['name'], -4) === '.php')
2064
			{
2065
				$fp = fopen($file['dir'] . '/' . $file['name'], 'rb');
2066
				$fc = fread($fp, filesize($file['dir'] . '/' . $file['name']));
2067
				fclose($fp);
2068
2069
				foreach ($temp_hooks as $hook => $allFunctions)
2070
				{
2071
					foreach ($allFunctions as $rawFunc)
2072
					{
2073
						// Get the hook info.
2074
						$hookParsedData = get_hook_info_from_raw($rawFunc);
2075
2076
						if (substr($hook, -8) === '_include')
2077
						{
2078
							$hook_status[$hook][$hookParsedData['pureFunc']]['exists'] = file_exists(strtr(trim($rawFunc), array('$boarddir' => $boarddir, '$sourcedir' => $sourcedir, '$themedir' => $settings['theme_dir'])));
2079
							// I need to know if there is at least one function called in this file.
2080
							$temp_data['include'][$hookParsedData['pureFunc']] = array('hook' => $hook, 'function' => $hookParsedData['pureFunc']);
2081
							unset($temp_hooks[$hook][$rawFunc]);
2082
						}
2083
						elseif (strpos(str_replace(' (', '(', $fc), 'function ' . trim($hookParsedData['pureFunc']) . '(') !== false)
2084
						{
2085
							$hook_status[$hook][$hookParsedData['pureFunc']] = $hookParsedData;
2086
							$hook_status[$hook][$hookParsedData['pureFunc']]['exists'] = true;
2087
							$hook_status[$hook][$hookParsedData['pureFunc']]['in_file'] = (!empty($file['name']) ? $file['name'] : (!empty($hookParsedData['hookFile']) ? $hookParsedData['hookFile'] : ''));
2088
2089
							// Does the hook has its own file?
2090
							if (!empty($hookParsedData['hookFile']))
2091
								$temp_data['include'][$hookParsedData['pureFunc']] = array('hook' => $hook, 'function' => $hookParsedData['pureFunc']);
2092
2093
							// 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)
2094
							$temp_data['function'][$file['name']][$hookParsedData['pureFunc']] = $hookParsedData['enabled'];
2095
							unset($temp_hooks[$hook][$rawFunc]);
2096
						}
2097
					}
2098
				}
2099
			}
2100
		}
2101
	}
2102
2103
	$sort_types = array(
2104
		'hook_name' => array('hook', SORT_ASC),
2105
		'hook_name DESC' => array('hook', SORT_DESC),
2106
		'function_name' => array('function', SORT_ASC),
2107
		'function_name DESC' => array('function', SORT_DESC),
2108
		'file_name' => array('file_name', SORT_ASC),
2109
		'file_name DESC' => array('file_name', SORT_DESC),
2110
		'status' => array('status', SORT_ASC),
2111
		'status DESC' => array('status', SORT_DESC),
2112
	);
2113
2114
	$sort_options = $sort_types[$sort];
2115
	$sort = array();
2116
	$hooks_filters = array();
2117
2118
	foreach ($hooks as $hook => $functions)
2119
		$hooks_filters[] = '<option' . ($context['current_filter'] == $hook ? ' selected ' : '') . ' value="' . $hook . '">' . $hook . '</option>';
2120
2121
	if (!empty($hooks_filters))
2122
		$context['insert_after_template'] .= '
2123
		<script>
2124
			var hook_name_header = document.getElementById(\'header_list_integration_hooks_hook_name\');
2125
			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>') . ';
2126
		</script>';
2127
2128
	$temp_data = array();
2129
	$id = 0;
2130
2131
	foreach ($hooks as $hook => $functions)
2132
	{
2133
		if (empty($context['filter']) || (!empty($context['filter']) && $context['filter'] == $hook))
2134
		{
2135
			foreach ($functions as $rawFunc)
2136
			{
2137
				// Get the hook info.
2138
				$hookParsedData = get_hook_info_from_raw($rawFunc);
2139
2140
				$hook_exists = !empty($hook_status[$hook][$hookParsedData['pureFunc']]['exists']);
2141
				$sort[] = $sort_options[0];
2142
2143
				$temp_data[] = array(
2144
					'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...
2145
					'hook_name' => $hook,
2146
					'function_name' => $hookParsedData['rawData'],
2147
					'real_function' => $hookParsedData['pureFunc'],
2148
					'included_file' => !empty($hookParsedData['absPath']) ? $hookParsedData['absPath'] : '',
2149
					'file_name' => (isset($hook_status[$hook][$hookParsedData['pureFunc']]['in_file']) ? $hook_status[$hook][$hookParsedData['pureFunc']]['in_file'] : (!empty($hookParsedData['hookFile']) ? $hookParsedData['hookFile'] : '')),
2150
					'instance' => $hookParsedData['object'],
2151
					'hook_exists' => $hook_exists,
2152
					'status' => $hook_exists ? ($hookParsedData['enabled'] ? 'allow' : 'moderate') : 'deny',
2153
					'img_text' => $txt['hooks_' . ($hook_exists ? ($hookParsedData['enabled'] ? 'active' : 'disabled') : 'missing')],
2154
					'enabled' => $hookParsedData['enabled'],
2155
					'can_be_disabled' => !isset($hook_status[$hook][$hookParsedData['pureFunc']]['enabled']),
2156
				);
2157
			}
2158
		}
2159
	}
2160
2161
	array_multisort($sort, $sort_options[1], $temp_data);
2162
2163
	$counter = 0;
2164
	$start++;
2165
2166
	foreach ($temp_data as $data)
2167
	{
2168
		if (++$counter < $start)
2169
			continue;
2170
		elseif ($counter == $start + $per_page)
2171
			break;
2172
2173
		$hooks_data[] = $data;
2174
	}
2175
2176
	return $hooks_data;
2177
}
2178
2179
/**
2180
 * Simply returns the total count of integration hooks
2181
 * Used by the integration hooks list function (list_integration_hooks)
2182
 *
2183
 * @return int The number of hooks currently in use
2184
 */
2185
function get_integration_hooks_count()
2186
{
2187
	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...
2188
2189
	$hooks = get_integration_hooks();
2190
	$hooks_count = 0;
2191
2192
	$context['filter'] = false;
2193
	if (isset($_GET['filter']))
2194
		$context['filter'] = $_GET['filter'];
2195
2196
	foreach ($hooks as $hook => $functions)
2197
	{
2198
		if (empty($context['filter']) || (!empty($context['filter']) && $context['filter'] == $hook))
2199
			$hooks_count += count($functions);
2200
	}
2201
2202
	return $hooks_count;
2203
}
2204
2205
/**
2206
 * Parses modSettings to create integration hook array
2207
 *
2208
 * @return array An array of information about the integration hooks
2209
 */
2210
function get_integration_hooks()
2211
{
2212
	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...
2213
	static $integration_hooks;
2214
2215
	if (!isset($integration_hooks))
2216
	{
2217
		$integration_hooks = array();
2218
		foreach ($modSettings as $key => $value)
2219
		{
2220
			if (!empty($value) && substr($key, 0, 10) === 'integrate_')
2221
				$integration_hooks[$key] = explode(',', $value);
2222
		}
2223
	}
2224
2225
	return $integration_hooks;
2226
}
2227
2228
/**
2229
 * Parses each hook data and returns an array.
2230
 *
2231
 * @param string $rawData A string as it was saved to the DB.
2232
 * @return array everything found in the string itself
2233
 */
2234
function get_hook_info_from_raw($rawData)
2235
{
2236
	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...
2237
2238
	// A single string can hold tons of info!
2239
	$hookData = array(
2240
		'object' => false,
2241
		'enabled' => true,
2242
		'fileExists' => false,
2243
		'absPath' => '',
2244
		'hookFile' => '',
2245
		'pureFunc' => '',
2246
		'method' => '',
2247
		'class' => '',
2248
		'rawData' => $rawData,
2249
	);
2250
2251
	// Meh...
2252
	if (empty($rawData))
2253
		return $hookData;
2254
2255
	// For convenience purposes only!
2256
	$modFunc = $rawData;
2257
2258
	// Any files?
2259
	if (strpos($modFunc, '|') !== false)
2260
	{
2261
		list ($hookData['hookFile'], $modFunc) = explode('|', $modFunc);
2262
2263
		// Does the file exists? who knows!
2264
		if (empty($settings['theme_dir']))
2265
			$hookData['absPath'] = strtr(trim($hookData['hookFile']), array('$boarddir' => $boarddir, '$sourcedir' => $sourcedir));
2266
2267
		else
2268
			$hookData['absPath'] = strtr(trim($hookData['hookFile']), array('$boarddir' => $boarddir, '$sourcedir' => $sourcedir, '$themedir' => $settings['theme_dir']));
2269
2270
		$hookData['fileExists'] = file_exists($hookData['absPath']);
2271
		$hookData['hookFile'] = basename($hookData['hookFile']);
2272
	}
2273
2274
	// Hook is an instance.
2275 View Code Duplication
	if (strpos($modFunc, '#') !== false)
2276
	{
2277
		$modFunc = str_replace('#', '', $modFunc);
2278
		$hookData['object'] = true;
2279
	}
2280
2281
	// Hook is "disabled"
2282 View Code Duplication
	if (strpos($modFunc, '!') !== false)
2283
	{
2284
		$modFunc = str_replace('!', '', $modFunc);
2285
		$hookData['enabled'] = false;
2286
	}
2287
2288
	// Handling methods?
2289
	if (strpos($modFunc, '::') !== false)
2290
	{
2291
		list ($hookData['class'], $hookData['method']) = explode('::', $modFunc);
2292
		$hookData['pureFunc'] = $hookData['method'];
2293
	}
2294
2295
	else
2296
		$hookData['pureFunc'] = $modFunc;
2297
2298
	return $hookData;
2299
}
2300
2301
/**
2302
 * Converts html entities to utf8 equivalents
2303
 * special db wrapper for mysql based on the limitation of mysql/mb3
2304
 *
2305
 * Callback function for preg_replace_callback
2306
 * Uses capture group 1 in the supplied array
2307
 * Does basic checks to keep characters inside a viewable range.
2308
 *
2309
 * @param array $matches An array of matches (relevant info should be the 2nd item in the array)
2310
 * @return string The fixed string or return the old when limitation of mysql is hit
2311
 */
2312
function fixchardb__callback($matches)
2313
{
2314
	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...
2315
	if (!isset($matches[1]))
2316
		return '';
2317
2318
	$num = $matches[1][0] === 'x' ? hexdec(substr($matches[1], 1)) : (int) $matches[1];
2319
	
2320
	// it's to big for mb3?
2321
	if ($num > 0xFFFF && !$smcFunc['db_mb4'])
2322
		return $matches[0];
2323
	else
2324
		return fixchar__callback($matches);
2325
}
2326
2327
?>