Completed
Push — release-2.1 ( 61d523...503c62 )
by Colin
17:13 queued 08:20
created

ManageMaintenance.php ➔ fixchardb__callback()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 9
nc 5
nop 1
dl 0
loc 14
rs 8.8571
c 0
b 0
f 0
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
				'convertutf8' => 'ConvertUtf8',
66
				'convertmsgbody' => 'ConvertMsgBody',
67
			),
68
		),
69
		'members' => array(
70
			'function' => 'MaintainMembers',
71
			'template' => 'maintain_members',
72
			'activities' => array(
73
				'reattribute' => 'MaintainReattributePosts',
74
				'purgeinactive' => 'MaintainPurgeInactiveMembers',
75
				'recountposts' => 'MaintainRecountPosts',
76
			),
77
		),
78
		'topics' => array(
79
			'function' => 'MaintainTopics',
80
			'template' => 'maintain_topics',
81
			'activities' => array(
82
				'massmove' => 'MaintainMassMoveTopics',
83
				'pruneold' => 'MaintainRemoveOldPosts',
84
				'olddrafts' => 'MaintainRemoveOldDrafts',
85
			),
86
		),
87
		'hooks' => array(
88
			'function' => 'list_integration_hooks',
89
		),
90
		'destroy' => array(
91
			'function' => 'Destroy',
92
			'activities' => array(),
93
		),
94
	);
95
96
	call_integration_hook('integrate_manage_maintenance', array(&$subActions));
97
98
	// Yep, sub-action time!
99
	if (isset($_REQUEST['sa']) && isset($subActions[$_REQUEST['sa']]))
100
		$subAction = $_REQUEST['sa'];
101
	else
102
		$subAction = 'routine';
103
104
	// Doing something special?
105
	if (isset($_REQUEST['activity']) && isset($subActions[$subAction]['activities'][$_REQUEST['activity']]))
106
		$activity = $_REQUEST['activity'];
107
108
	// Set a few things.
109
	$context['page_title'] = $txt['maintain_title'];
110
	$context['sub_action'] = $subAction;
111
	$context['sub_template'] = !empty($subActions[$subAction]['template']) ? $subActions[$subAction]['template'] : '';
112
113
	// Finally fall through to what we are doing.
114
	call_helper($subActions[$subAction]['function']);
115
116
	// Any special activity?
117
	if (isset($activity))
118
		call_helper($subActions[$subAction]['activities'][$activity]);
119
120
	//converted to UTF-8? show a small maintenance info
121 View Code Duplication
	if (isset($_GET['done']) && $_GET['done'] == 'convertutf8')
122
		$context['maintenance_finished'] = $txt['utf8_title'];
123
124
	// Create a maintenance token.  Kinda hard to do it any other way.
125
	createToken('admin-maint');
126
}
127
128
/**
129
 * Supporting function for the database maintenance area.
130
 */
131
function MaintainDatabase()
132
{
133
	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...
134
135
	// Show some conversion options?
136
	$context['convert_utf8'] = ($db_type == 'mysql') && (!isset($db_character_set) || $db_character_set !== 'utf8' || empty($modSettings['global_character_set']) || $modSettings['global_character_set'] !== 'UTF-8') && version_compare('4.1.2', preg_replace('~\-.+?$~', '', $smcFunc['db_server_info']()), '<=');
137
	$context['convert_entities'] = isset($db_character_set, $modSettings['global_character_set']) && $db_character_set === 'utf8' && $modSettings['global_character_set'] === 'UTF-8';
138
139
	if ($db_type == 'mysql')
140
	{
141
		db_extend('packages');
142
143
		$colData = $smcFunc['db_list_columns']('{db_prefix}messages', true);
144
		foreach ($colData as $column)
145
			if ($column['name'] == 'body')
146
				$body_type = $column['type'];
147
148
		$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...
149
		$context['convert_to_suggest'] = ($body_type != 'text' && !empty($modSettings['max_messageLength']) && $modSettings['max_messageLength'] < 65536);
150
	}
151
152 View Code Duplication
	if (isset($_GET['done']) && $_GET['done'] == 'convertutf8')
153
		$context['maintenance_finished'] = $txt['utf8_title'];
154
	if (isset($_GET['done']) && $_GET['done'] == 'convertentities')
155
		$context['maintenance_finished'] = $txt['entity_convert_title'];
156
}
157
158
/**
159
 * Supporting function for the routine maintenance area.
160
 */
161
function MaintainRoutine()
162
{
163
	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...
164
165
	if (isset($_GET['done']) && $_GET['done'] == 'recount')
166
		$context['maintenance_finished'] = $txt['maintain_recount'];
167
}
168
169
/**
170
 * Supporting function for the members maintenance area.
171
 */
172
function MaintainMembers()
173
{
174
	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...
175
176
	// Get membergroups - for deleting members and the like.
177
	$result = $smcFunc['db_query']('', '
178
		SELECT id_group, group_name
179
		FROM {db_prefix}membergroups',
180
		array(
181
		)
182
	);
183
	$context['membergroups'] = array(
184
		array(
185
			'id' => 0,
186
			'name' => $txt['maintain_members_ungrouped']
187
		),
188
	);
189
	while ($row = $smcFunc['db_fetch_assoc']($result))
190
	{
191
		$context['membergroups'][] = array(
192
			'id' => $row['id_group'],
193
			'name' => $row['group_name']
194
		);
195
	}
196
	$smcFunc['db_free_result']($result);
197
198
	if (isset($_GET['done']) && $_GET['done'] == 'recountposts')
199
		$context['maintenance_finished'] = $txt['maintain_recountposts'];
200
201
	loadJavaScriptFile('suggest.js', array('defer' => false, 'minimize' => true), 'smf_suggest');
202
}
203
204
/**
205
 * Supporting function for the topics maintenance area.
206
 */
207
function MaintainTopics()
208
{
209
	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...
210
211
	// Let's load up the boards in case they are useful.
212
	$result = $smcFunc['db_query']('order_by_board_order', '
213
		SELECT b.id_board, b.name, b.child_level, c.name AS cat_name, c.id_cat
214
		FROM {db_prefix}boards AS b
215
			LEFT JOIN {db_prefix}categories AS c ON (c.id_cat = b.id_cat)
216
		WHERE {query_see_board}
217
			AND redirect = {string:blank_redirect}',
218
		array(
219
			'blank_redirect' => '',
220
		)
221
	);
222
	$context['categories'] = array();
223
	while ($row = $smcFunc['db_fetch_assoc']($result))
224
	{
225
		if (!isset($context['categories'][$row['id_cat']]))
226
			$context['categories'][$row['id_cat']] = array(
227
				'name' => $row['cat_name'],
228
				'boards' => array()
229
			);
230
231
		$context['categories'][$row['id_cat']]['boards'][$row['id_board']] = array(
232
			'id' => $row['id_board'],
233
			'name' => $row['name'],
234
			'child_level' => $row['child_level']
235
		);
236
	}
237
	$smcFunc['db_free_result']($result);
238
239
	require_once($sourcedir . '/Subs-Boards.php');
240
	sortCategories($context['categories']);
241
242
	if (isset($_GET['done']) && $_GET['done'] == 'purgeold')
243
		$context['maintenance_finished'] = $txt['maintain_old'];
244
	elseif (isset($_GET['done']) && $_GET['done'] == 'massmove')
245
		$context['maintenance_finished'] = $txt['move_topics_maintenance'];
246
}
247
248
/**
249
 * Find and fix all errors on the forum.
250
 */
251
function MaintainFindFixErrors()
252
{
253
	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...
254
255
	// Honestly, this should be done in the sub function.
256
	validateToken('admin-maint');
257
258
	require_once($sourcedir . '/RepairBoards.php');
259
	RepairBoards();
260
}
261
262
/**
263
 * Wipes the whole cache directory.
264
 * This only applies to SMF's own cache directory, though.
265
 */
266
function MaintainCleanCache()
267
{
268
	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...
269
270
	checkSession();
271
	validateToken('admin-maint');
272
273
	// Just wipe the whole cache directory!
274
	clean_cache();
275
276
	$context['maintenance_finished'] = $txt['maintain_cache'];
277
}
278
279
/**
280
 * Empties all uninmportant logs
281
 */
282
function MaintainEmptyUnimportantLogs()
283
{
284
	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...
285
286
	checkSession();
287
	validateToken('admin-maint');
288
289
	// No one's online now.... MUHAHAHAHA :P.
290
	$smcFunc['db_query']('', '
291
		DELETE FROM {db_prefix}log_online');
292
293
	// Dump the banning logs.
294
	$smcFunc['db_query']('', '
295
		DELETE FROM {db_prefix}log_banned');
296
297
	// Start id_error back at 0 and dump the error log.
298
	$smcFunc['db_query']('truncate_table', '
299
		TRUNCATE {db_prefix}log_errors');
300
301
	// Clear out the spam log.
302
	$smcFunc['db_query']('', '
303
		DELETE FROM {db_prefix}log_floodcontrol');
304
305
	// Last but not least, the search logs!
306
	$smcFunc['db_query']('truncate_table', '
307
		TRUNCATE {db_prefix}log_search_topics');
308
309
	$smcFunc['db_query']('truncate_table', '
310
		TRUNCATE {db_prefix}log_search_messages');
311
312
	$smcFunc['db_query']('truncate_table', '
313
		TRUNCATE {db_prefix}log_search_results');
314
315
	updateSettings(array('search_pointer' => 0));
316
317
	$context['maintenance_finished'] = $txt['maintain_logs'];
318
}
319
320
/**
321
 * Oh noes! I'd document this but that would give it away
322
 */
323
function Destroy()
324
{
325
	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...
326
327
	echo '<!DOCTYPE html>
328
		<html', $context['right_to_left'] ? ' dir="rtl"' : '', '><head><title>', $context['forum_name_html_safe'], ' deleted!</title></head>
329
		<body style="background-color: orange; font-family: arial, sans-serif; text-align: center;">
330
		<div style="margin-top: 8%; font-size: 400%; color: black;">Oh my, you killed ', $context['forum_name_html_safe'], '!</div>
331
		<div style="margin-top: 7%; font-size: 500%; color: red;"><strong>You lazy bum!</strong></div>
332
		</body></html>';
333
	obExit(false);
334
}
335
336
/**
337
 * Convert the column "body" of the table {db_prefix}messages from TEXT to MEDIUMTEXT and vice versa.
338
 * It requires the admin_forum permission.
339
 * This is needed only for MySQL.
340
 * During the conversion from MEDIUMTEXT to TEXT it check if any of the posts exceed the TEXT length and if so it aborts.
341
 * This action is linked from the maintenance screen (if it's applicable).
342
 * Accessed by ?action=admin;area=maintain;sa=database;activity=convertmsgbody.
343
 *
344
 * @uses the convert_msgbody sub template of the Admin template.
345
 */
346
function ConvertMsgBody()
347
{
348
	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...
349
	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...
350
351
	// Show me your badge!
352
	isAllowedTo('admin_forum');
353
354
	if ($db_type != 'mysql')
355
		return;
356
357
	db_extend('packages');
358
359
	$colData = $smcFunc['db_list_columns']('{db_prefix}messages', true);
360
	foreach ($colData as $column)
361
		if ($column['name'] == 'body')
362
			$body_type = $column['type'];
363
364
	$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...
365
366
	if ($body_type == 'text' || ($body_type != 'text' && isset($_POST['do_conversion'])))
367
	{
368
		checkSession();
369
		validateToken('admin-maint');
370
371
		// Make it longer so we can do their limit.
372
		if ($body_type == 'text')
373
			$smcFunc['db_change_column']('{db_prefix}messages', 'body', array('type' => 'mediumtext'));
374
		// Shorten the column so we can have a bit (literally per record) less space occupied
375
		else
376
			$smcFunc['db_change_column']('{db_prefix}messages', 'body', array('type' => 'text'));
377
378
		// 3rd party integrations may be interested in knowning about this.
379
		call_integration_hook('integrate_convert_msgbody', array($body_type));
380
381
		$colData = $smcFunc['db_list_columns']('{db_prefix}messages', true);
382
		foreach ($colData as $column)
383
			if ($column['name'] == 'body')
384
				$body_type = $column['type'];
385
386
		$context['maintenance_finished'] = $txt[$context['convert_to'] . '_title'];
387
		$context['convert_to'] = $body_type == 'text' ? 'mediumtext' : 'text';
388
		$context['convert_to_suggest'] = ($body_type != 'text' && !empty($modSettings['max_messageLength']) && $modSettings['max_messageLength'] < 65536);
389
390
		return;
391
	}
392
	elseif ($body_type != 'text' && (!isset($_POST['do_conversion']) || isset($_POST['cont'])))
393
	{
394
		checkSession();
395
		if (empty($_REQUEST['start']))
396
			validateToken('admin-maint');
397
		else
398
			validateToken('admin-convertMsg');
399
400
		$context['page_title'] = $txt['not_done_title'];
401
		$context['continue_post_data'] = '';
402
		$context['continue_countdown'] = 3;
403
		$context['sub_template'] = 'not_done';
404
		$increment = 500;
405
		$id_msg_exceeding = isset($_POST['id_msg_exceeding']) ? explode(',', $_POST['id_msg_exceeding']) : array();
406
407
		$request = $smcFunc['db_query']('', '
408
			SELECT COUNT(*) as count
409
			FROM {db_prefix}messages',
410
			array()
411
		);
412
		list($max_msgs) = $smcFunc['db_fetch_row']($request);
413
		$smcFunc['db_free_result']($request);
414
415
		// Try for as much time as possible.
416
		@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...
417
418
		while ($_REQUEST['start'] < $max_msgs)
419
		{
420
			$request = $smcFunc['db_query']('', '
421
				SELECT /*!40001 SQL_NO_CACHE */ id_msg
422
				FROM {db_prefix}messages
423
				WHERE id_msg BETWEEN {int:start} AND {int:start} + {int:increment}
424
					AND LENGTH(body) > 65535',
425
				array(
426
					'start' => $_REQUEST['start'],
427
					'increment' => $increment - 1,
428
				)
429
			);
430
			while ($row = $smcFunc['db_fetch_assoc']($request))
431
				$id_msg_exceeding[] = $row['id_msg'];
432
			$smcFunc['db_free_result']($request);
433
434
			$_REQUEST['start'] += $increment;
435
436
			if (microtime(true) - $time_start > 3)
437
			{
438
				createToken('admin-convertMsg');
439
				$context['continue_post_data'] = '
440
					<input type="hidden" name="' . $context['admin-convertMsg_token_var'] . '" value="' . $context['admin-convertMsg_token'] . '">
441
					<input type="hidden" name="' . $context['session_var'] . '" value="' . $context['session_id'] . '">
442
					<input type="hidden" name="id_msg_exceeding" value="' . implode(',', $id_msg_exceeding) . '">';
443
444
				$context['continue_get_data'] = '?action=admin;area=maintain;sa=database;activity=convertmsgbody;start=' . $_REQUEST['start'];
445
				$context['continue_percent'] = round(100 * $_REQUEST['start'] / $max_msgs);
446
447
				return;
448
			}
449
		}
450
		createToken('admin-maint');
451
		$context['page_title'] = $txt[$context['convert_to'] . '_title'];
452
		$context['sub_template'] = 'convert_msgbody';
453
454
		if (!empty($id_msg_exceeding))
455
		{
456
			if (count($id_msg_exceeding) > 100)
457
			{
458
				$query_msg = array_slice($id_msg_exceeding, 0, 100);
459
				$context['exceeding_messages_morethan'] = sprintf($txt['exceeding_messages_morethan'], count($id_msg_exceeding));
460
			}
461
			else
462
				$query_msg = $id_msg_exceeding;
463
464
			$context['exceeding_messages'] = array();
465
			$request = $smcFunc['db_query']('', '
466
				SELECT id_msg, id_topic, subject
467
				FROM {db_prefix}messages
468
				WHERE id_msg IN ({array_int:messages})',
469
				array(
470
					'messages' => $query_msg,
471
				)
472
			);
473
			while ($row = $smcFunc['db_fetch_assoc']($request))
474
				$context['exceeding_messages'][] = '<a href="' . $scripturl . '?topic=' . $row['id_topic'] . '.msg' . $row['id_msg'] . '#msg' . $row['id_msg'] . '">' . $row['subject'] . '</a>';
475
			$smcFunc['db_free_result']($request);
476
		}
477
	}
478
}
479
480
/**
481
 * Converts HTML-entities to their UTF-8 character equivalents.
482
 * This requires the admin_forum permission.
483
 * Pre-condition: UTF-8 has been set as database and global character set.
484
 *
485
 * It is divided in steps of 10 seconds.
486
 * This action is linked from the maintenance screen (if applicable).
487
 * It is accessed by ?action=admin;area=maintain;sa=database;activity=convertentities.
488
 *
489
 * @uses Admin template, convert_entities sub-template.
490
 */
491
function ConvertEntities()
492
{
493
	global $db_character_set, $modSettings, $context, $sourcedir, $smcFunc, $db_type, $db_prefix;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

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

1. Pass all data via parameters

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

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

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

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