1 | <?php |
||||
2 | |||||
3 | /** |
||||
4 | * Forum maintenance. Important stuff. |
||||
5 | * |
||||
6 | * @package ElkArte Forum |
||||
7 | * @copyright ElkArte Forum contributors |
||||
8 | * @license BSD http://opensource.org/licenses/BSD-3-Clause (see accompanying LICENSE.txt file) |
||||
9 | * |
||||
10 | * This file contains code covered by: |
||||
11 | * copyright: 2011 Simple Machines (http://www.simplemachines.org) |
||||
12 | * |
||||
13 | * @version 2.0 dev |
||||
14 | * |
||||
15 | */ |
||||
16 | |||||
17 | namespace ElkArte\AdminController; |
||||
18 | |||||
19 | use ElkArte\AbstractController; |
||||
20 | use ElkArte\Action; |
||||
21 | use ElkArte\Cache\Cache; |
||||
22 | use ElkArte\Debug; |
||||
23 | use ElkArte\EventManager; |
||||
24 | use ElkArte\Exceptions\Exception; |
||||
25 | use ElkArte\Helper\DataValidator; |
||||
26 | use ElkArte\Http\FtpConnection; |
||||
27 | use ElkArte\Languages\Txt; |
||||
28 | use ElkArte\User; |
||||
29 | |||||
30 | /** |
||||
31 | * Entry point class for all maintenance, routine, members, database, |
||||
32 | * attachments, topics and hooks |
||||
33 | * |
||||
34 | * @package Maintenance |
||||
35 | */ |
||||
36 | class Maintenance extends AbstractController |
||||
37 | { |
||||
38 | /** @var int Maximum topic counter */ |
||||
39 | public $max_topics; |
||||
40 | |||||
41 | /** @var int How many actions to take for a maintenance actions */ |
||||
42 | public $increment; |
||||
43 | |||||
44 | /** @var int Total steps for a given maintenance action */ |
||||
45 | public $total_steps; |
||||
46 | |||||
47 | /** @var int reStart pointer for paused maintenance actions */ |
||||
48 | public $start; |
||||
49 | |||||
50 | /** @var int Loop counter for paused maintenance actions */ |
||||
51 | public $step; |
||||
52 | |||||
53 | /** |
||||
54 | * Main dispatcher, the maintenance access point. |
||||
55 | * |
||||
56 | * What it does: |
||||
57 | * |
||||
58 | * - This, as usual, checks permissions, loads language files, |
||||
59 | * and forwards to the actual workers. |
||||
60 | * |
||||
61 | * @see AbstractController::action_index |
||||
62 | */ |
||||
63 | public function action_index() |
||||
64 | { |
||||
65 | global $txt, $context; |
||||
66 | |||||
67 | // You absolutely must be an admin by here! |
||||
68 | isAllowedTo('admin_forum'); |
||||
69 | |||||
70 | // Need something to talk about? |
||||
71 | Txt::load('Maintenance'); |
||||
72 | theme()->getTemplates()->load('Maintenance'); |
||||
73 | |||||
74 | // This uses admin tabs - as it should! |
||||
75 | // Create the tabs |
||||
76 | $context[$context['admin_menu_name']]['object']->prepareTabData([ |
||||
77 | 'title' => 'maintain_title', |
||||
78 | 'description' => 'maintain_info', |
||||
79 | 'class' => 'i-cog'] |
||||
80 | ); |
||||
81 | |||||
82 | // So many things you can do - but frankly I won't let you - just these! |
||||
83 | $subActions = array( |
||||
84 | 'routine' => array( |
||||
85 | 'controller' => $this, |
||||
86 | 'function' => 'action_routine', |
||||
87 | 'activities' => array( |
||||
88 | 'version' => 'action_version_display', |
||||
89 | 'repair' => 'action_repair_display', |
||||
90 | 'recount' => 'action_recount_display', |
||||
91 | 'logs' => 'action_logs_display', |
||||
92 | 'cleancache' => 'action_cleancache_display', |
||||
93 | ), |
||||
94 | ), |
||||
95 | 'database' => array( |
||||
96 | 'controller' => $this, |
||||
97 | 'function' => 'action_database', |
||||
98 | 'activities' => array( |
||||
99 | 'optimize' => 'action_optimize_display', |
||||
100 | 'backup' => 'action_backup_display', |
||||
101 | 'convertmsgbody' => 'action_convertmsgbody_display', |
||||
102 | ), |
||||
103 | ), |
||||
104 | 'members' => array( |
||||
105 | 'controller' => $this, |
||||
106 | 'function' => 'action_members', |
||||
107 | 'activities' => array( |
||||
108 | 'reattribute' => 'action_reattribute_display', |
||||
109 | 'purgeinactive' => 'action_purgeinactive_display', |
||||
110 | 'recountposts' => 'action_recountposts_display', |
||||
111 | ), |
||||
112 | ), |
||||
113 | 'topics' => array( |
||||
114 | 'controller' => $this, |
||||
115 | 'function' => 'action_topics', |
||||
116 | 'activities' => array( |
||||
117 | 'massmove' => 'action_massmove_display', |
||||
118 | 'pruneold' => 'action_pruneold_display', |
||||
119 | ), |
||||
120 | ), |
||||
121 | 'hooks' => array( |
||||
122 | 'controller' => $this, |
||||
123 | 'function' => 'action_hooks', |
||||
124 | ), |
||||
125 | 'attachments' => array( |
||||
126 | 'controller' => ManageAttachments::class, |
||||
127 | 'function' => 'action_maintenance', |
||||
128 | ), |
||||
129 | ); |
||||
130 | |||||
131 | // Set up the action handler |
||||
132 | $action = new Action('manage_maintenance'); |
||||
133 | |||||
134 | // Yep, sub-action time and call integrate_sa_manage_maintenance as well |
||||
135 | $subAction = $action->initialize($subActions, 'routine'); |
||||
136 | |||||
137 | // Doing something special, does it exist? |
||||
138 | $activity = $this->_req->getQuery('activity', 'trim|strval', ''); |
||||
139 | |||||
140 | // Set a few things. |
||||
141 | $context[$context['admin_menu_name']]['current_subsection'] = $subAction; |
||||
142 | $context['page_title'] = $txt['maintain_title']; |
||||
143 | $context['sub_action'] = $subAction; |
||||
144 | |||||
145 | // Finally fall through to what we are doing. |
||||
146 | $action->dispatch($subAction); |
||||
147 | |||||
148 | // Any special activity defined, then go to it. |
||||
149 | if (isset($subActions[$subAction]['activities'][$activity])) |
||||
150 | { |
||||
151 | if (is_string($subActions[$subAction]['activities'][$activity]) && method_exists($this, $subActions[$subAction]['activities'][$activity])) |
||||
152 | { |
||||
153 | $this->{$subActions[$subAction]['activities'][$activity]}(); |
||||
154 | } |
||||
155 | elseif (is_string($subActions[$subAction]['activities'][$activity])) |
||||
156 | { |
||||
157 | $subActions[$subAction]['activities'][$activity](); |
||||
158 | } |
||||
159 | elseif (is_array($subActions[$subAction]['activities'][$activity])) |
||||
160 | { |
||||
161 | $activity_obj = new $subActions[$subAction]['activities'][$activity]['class'](); |
||||
162 | $activity_obj->{$subActions[$subAction]['activities'][$activity]['method']}(); |
||||
163 | } |
||||
164 | else |
||||
165 | { |
||||
166 | $subActions[$subAction]['activities'][$activity](); |
||||
167 | } |
||||
168 | } |
||||
169 | |||||
170 | // Create a maintenance token. Kinda hard to do it any other way. |
||||
171 | createToken('admin-maint'); |
||||
172 | } |
||||
173 | |||||
174 | /** |
||||
175 | * Supporting function for the routine maintenance area. |
||||
176 | * |
||||
177 | * @event integrate_routine_maintenance, passed $context['routine_actions'] array to allow |
||||
178 | * addons to add more options |
||||
179 | * @uses Template Maintenance, sub template maintain_routine |
||||
180 | */ |
||||
181 | public function action_routine() |
||||
182 | { |
||||
183 | global $context, $txt; |
||||
184 | |||||
185 | if ($this->_req->compareQuery('done', 'recount', 'trim|strval')) |
||||
186 | { |
||||
187 | $context['maintenance_finished'] = $txt['maintain_recount']; |
||||
188 | } |
||||
189 | |||||
190 | // set up the sub-template |
||||
191 | $context['sub_template'] = 'maintain_routine'; |
||||
192 | $context['routine_actions'] = array( |
||||
193 | 'version' => array( |
||||
194 | 'url' => getUrl('admin', ['action' => 'admin', 'area' => 'maintain', 'sa' => 'routine', 'activity' => 'version']), |
||||
195 | 'title' => $txt['maintain_version'], |
||||
196 | 'description' => $txt['maintain_version_info'], |
||||
197 | 'submit' => $txt['maintain_run_now'], |
||||
198 | 'hidden' => array( |
||||
199 | 'session_var' => 'session_id', |
||||
200 | ) |
||||
201 | ), |
||||
202 | 'repair' => array( |
||||
203 | 'url' => getUrl('admin', ['action' => 'admin', 'area' => 'repairboards']), |
||||
204 | 'title' => $txt['maintain_errors'], |
||||
205 | 'description' => $txt['maintain_errors_info'], |
||||
206 | 'submit' => $txt['maintain_run_now'], |
||||
207 | 'hidden' => array( |
||||
208 | 'session_var' => 'session_id', |
||||
209 | 'admin-maint_token_var' => 'admin-maint_token', |
||||
210 | ) |
||||
211 | ), |
||||
212 | 'recount' => array( |
||||
213 | 'url' => getUrl('admin', ['action' => 'admin', 'area' => 'maintain', 'sa' => 'routine', 'activity' => 'recount']), |
||||
214 | 'title' => $txt['maintain_recount'], |
||||
215 | 'description' => $txt['maintain_recount_info'], |
||||
216 | 'submit' => $txt['maintain_run_now'], |
||||
217 | 'hidden' => array( |
||||
218 | 'session_var' => 'session_id', |
||||
219 | 'admin-maint_token_var' => 'admin-maint_token', |
||||
220 | ) |
||||
221 | ), |
||||
222 | 'logs' => array( |
||||
223 | 'url' => getUrl('admin', ['action' => 'admin', 'area' => 'maintain', 'sa' => 'routine', 'activity' => 'logs']), |
||||
224 | 'title' => $txt['maintain_logs'], |
||||
225 | 'description' => $txt['maintain_logs_info'], |
||||
226 | 'submit' => $txt['maintain_run_now'], |
||||
227 | 'hidden' => array( |
||||
228 | 'session_var' => 'session_id', |
||||
229 | 'admin-maint_token_var' => 'admin-maint_token', |
||||
230 | ) |
||||
231 | ), |
||||
232 | 'cleancache' => array( |
||||
233 | 'url' => getUrl('admin', ['action' => 'admin', 'area' => 'maintain', 'sa' => 'routine', 'activity' => 'cleancache']), |
||||
234 | 'title' => $txt['maintain_cache'], |
||||
235 | 'description' => $txt['maintain_cache_info'], |
||||
236 | 'submit' => $txt['maintain_run_now'], |
||||
237 | 'hidden' => array( |
||||
238 | 'session_var' => 'session_id', |
||||
239 | 'admin-maint_token_var' => 'admin-maint_token', |
||||
240 | ) |
||||
241 | ), |
||||
242 | ); |
||||
243 | |||||
244 | call_integration_hook('integrate_routine_maintenance', array(&$context['routine_actions'])); |
||||
245 | } |
||||
246 | |||||
247 | /** |
||||
248 | * Supporting function for the members maintenance area. |
||||
249 | */ |
||||
250 | public function action_members() |
||||
251 | { |
||||
252 | global $context, $txt; |
||||
253 | |||||
254 | require_once(SUBSDIR . '/Membergroups.subs.php'); |
||||
255 | |||||
256 | // Get all membergroups - for deleting members and the like. |
||||
257 | $context['membergroups'] = getBasicMembergroupData(array('all')); |
||||
258 | |||||
259 | // Show that we completed this action |
||||
260 | if ($this->_req->compareQuery('done', 'recountposts', 'trim|strval')) |
||||
261 | { |
||||
262 | $context['maintenance_finished'] = array( |
||||
263 | 'errors' => array(sprintf($txt['maintain_done'], $txt['maintain_recountposts'])), |
||||
264 | ); |
||||
265 | } |
||||
266 | |||||
267 | loadJavascriptFile('suggest.js', array('defer' => true)); |
||||
268 | |||||
269 | // Set up the sub-template |
||||
270 | $context['sub_template'] = 'maintain_members'; |
||||
271 | } |
||||
272 | |||||
273 | /** |
||||
274 | * Supporting function for the topics maintenance area. |
||||
275 | * |
||||
276 | * @event integrate_topics_maintenance, passed $context['topics_actions'] to allow addons |
||||
277 | * to add additonal topic maintance functions |
||||
278 | * @uses GenericBoards template, sub template maintain_topics |
||||
279 | */ |
||||
280 | public function action_topics() |
||||
281 | { |
||||
282 | global $context, $txt; |
||||
283 | |||||
284 | require_once(SUBSDIR . '/Boards.subs.php'); |
||||
285 | |||||
286 | // Let's load up the boards in case they are useful. |
||||
287 | $context += getBoardList(array('not_redirection' => true)); |
||||
288 | |||||
289 | // Include a list of boards per category for easy toggling. |
||||
290 | foreach ($context['categories'] as $cat => &$category) |
||||
291 | { |
||||
292 | $context['boards_in_category'][$cat] = count($category['boards']); |
||||
293 | $category['child_ids'] = array_keys($category['boards']); |
||||
294 | } |
||||
295 | |||||
296 | // @todo Hacky! |
||||
297 | $txt['choose_board'] = $txt['maintain_old_all']; |
||||
298 | $context['boards_check_all'] = true; |
||||
299 | theme()->getTemplates()->load('GenericBoards'); |
||||
300 | |||||
301 | $context['topics_actions'] = array( |
||||
302 | 'pruneold' => array( |
||||
303 | 'url' => getUrl('admin', ['action' => 'admin', 'area' => 'maintain', 'sa' => 'topics', 'activity' => 'pruneold']), |
||||
304 | 'title' => $txt['maintain_old'], |
||||
305 | 'submit' => $txt['maintain_old_remove'], |
||||
306 | 'confirm' => $txt['maintain_old_confirm'], |
||||
307 | 'hidden' => array( |
||||
308 | 'session_var' => 'session_id', |
||||
309 | 'admin-maint_token_var' => 'admin-maint_token', |
||||
310 | ) |
||||
311 | ), |
||||
312 | 'massmove' => array( |
||||
313 | 'url' => getUrl('admin', ['action' => 'admin', 'area' => 'maintain', 'sa' => 'topics', 'activity' => 'massmove']), |
||||
314 | 'title' => $txt['move_topics_maintenance'], |
||||
315 | 'submit' => $txt['move_topics_now'], |
||||
316 | 'confirm' => $txt['move_topics_confirm'], |
||||
317 | 'hidden' => array( |
||||
318 | 'session_var' => 'session_id', |
||||
319 | 'admin-maint_token_var' => 'admin-maint_token', |
||||
320 | ) |
||||
321 | ), |
||||
322 | ); |
||||
323 | |||||
324 | call_integration_hook('integrate_topics_maintenance', array(&$context['topics_actions'])); |
||||
325 | |||||
326 | if ($this->_req->compareQuery('done', 'purgeold', 'trim|strval')) |
||||
327 | { |
||||
328 | $context['maintenance_finished'] = array( |
||||
329 | 'errors' => array(sprintf($txt['maintain_done'], $txt['maintain_old'])), |
||||
330 | ); |
||||
331 | } |
||||
332 | elseif ($this->_req->compareQuery('done', 'massmove', 'trim|strval')) |
||||
333 | { |
||||
334 | $context['maintenance_finished'] = array( |
||||
335 | 'errors' => array(sprintf($txt['maintain_done'], $txt['move_topics_maintenance'])), |
||||
336 | ); |
||||
337 | } |
||||
338 | |||||
339 | // Set up the sub-template |
||||
340 | $context['sub_template'] = 'maintain_topics'; |
||||
341 | } |
||||
342 | |||||
343 | /** |
||||
344 | * Find and try to fix all errors on the forum. |
||||
345 | * |
||||
346 | * - Forwards to repair boards controller. |
||||
347 | */ |
||||
348 | public function action_repair_display() |
||||
349 | { |
||||
350 | // Honestly, this should be done in the sub function. |
||||
351 | validateToken('admin-maint'); |
||||
352 | |||||
353 | $controller = new RepairBoards(new EventManager()); |
||||
354 | $controller->setUser(User::$info); |
||||
355 | $controller->pre_dispatch(); |
||||
356 | $controller->action_repairboards(); |
||||
357 | } |
||||
358 | |||||
359 | /** |
||||
360 | * Wipes the current cache entries as best it can. |
||||
361 | * |
||||
362 | * - This only applies to our own cache entries, opcache and data. |
||||
363 | * - This action, like other maintenance tasks, may be called automatically |
||||
364 | * by the task scheduler or manually by the admin in Maintenance area. |
||||
365 | */ |
||||
366 | public function action_cleancache_display() |
||||
367 | { |
||||
368 | global $context, $txt; |
||||
369 | |||||
370 | checkSession(); |
||||
371 | validateToken('admin-maint'); |
||||
372 | |||||
373 | // Just wipe the whole cache directory! |
||||
374 | Cache::instance()->clean(); |
||||
375 | |||||
376 | // Change the PWA stale so it will refresh (if enabled) |
||||
377 | setPWACacheStale(true); |
||||
378 | |||||
379 | $context['maintenance_finished'] = $txt['maintain_cache']; |
||||
380 | } |
||||
381 | |||||
382 | /** |
||||
383 | * Empties all unimportant logs. |
||||
384 | * |
||||
385 | * - This action may be called periodically, by the tasks scheduler, |
||||
386 | * or manually by the admin in Maintenance area. |
||||
387 | */ |
||||
388 | public function action_logs_display() |
||||
389 | { |
||||
390 | global $context, $txt; |
||||
391 | |||||
392 | require_once(SUBSDIR . '/Maintenance.subs.php'); |
||||
393 | |||||
394 | checkSession(); |
||||
395 | validateToken('admin-maint'); |
||||
396 | |||||
397 | // Maintenance time was scheduled! |
||||
398 | // When there is no intelligent life on this planet. |
||||
399 | // Apart from me, I mean. |
||||
400 | flushLogTables(); |
||||
401 | |||||
402 | updateSettings(array('search_pointer' => 0)); |
||||
403 | |||||
404 | $context['maintenance_finished'] = $txt['maintain_logs']; |
||||
405 | } |
||||
406 | |||||
407 | /** |
||||
408 | * Convert the column "body" of the table {db_prefix}messages from TEXT to |
||||
409 | * MEDIUMTEXT and vice versa. |
||||
410 | * |
||||
411 | * What it does: |
||||
412 | * |
||||
413 | * - It requires the admin_forum permission. |
||||
414 | * - This is needed only for MySQL. |
||||
415 | * - During the conversion from MEDIUMTEXT to TEXT it check if any of the |
||||
416 | * posts exceed the TEXT length and if so it aborts. |
||||
417 | * - This action is linked from the maintenance screen (if it's applicable). |
||||
418 | * - Accessed by ?action=admin;area=maintain;sa=database;activity=convertmsgbody. |
||||
419 | * |
||||
420 | * @uses the convert_msgbody sub template of the Admin template. |
||||
421 | */ |
||||
422 | public function action_convertmsgbody_display() |
||||
423 | { |
||||
424 | global $context, $txt, $modSettings, $time_start; |
||||
425 | |||||
426 | // Show me your badge! |
||||
427 | isAllowedTo('admin_forum'); |
||||
428 | $db = database(); |
||||
429 | |||||
430 | if ($db->supportMediumtext() === false) |
||||
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||||
431 | { |
||||
432 | return; |
||||
433 | } |
||||
434 | |||||
435 | $body_type = ''; |
||||
436 | |||||
437 | // Find the body column "type" from the message table |
||||
438 | $colData = getMessageTableColumns(); |
||||
439 | foreach ($colData as $column) |
||||
440 | { |
||||
441 | if ($column['name'] === 'body') |
||||
442 | { |
||||
443 | $body_type = $column['type']; |
||||
444 | break; |
||||
445 | } |
||||
446 | } |
||||
447 | |||||
448 | $context['convert_to'] = $body_type === 'text' ? 'mediumtext' : 'text'; |
||||
449 | if ($body_type === 'text' || isset($this->_req->post->do_conversion)) |
||||
450 | { |
||||
451 | checkSession(); |
||||
452 | validateToken('admin-maint'); |
||||
453 | |||||
454 | // Make it longer so we can do their limit. |
||||
455 | if ($body_type === 'text') |
||||
456 | { |
||||
457 | resizeMessageTableBody('mediumtext'); |
||||
458 | } |
||||
459 | // Shorten the column so we can have a bit (literally per record) less space occupied |
||||
460 | else |
||||
461 | { |
||||
462 | resizeMessageTableBody('text'); |
||||
463 | } |
||||
464 | |||||
465 | $colData = getMessageTableColumns(); |
||||
466 | foreach ($colData as $column) |
||||
467 | { |
||||
468 | if ($column['name'] === 'body') |
||||
469 | { |
||||
470 | $body_type = $column['type']; |
||||
471 | } |
||||
472 | } |
||||
473 | |||||
474 | $context['maintenance_finished'] = $txt[$context['convert_to'] . '_title']; |
||||
475 | $context['convert_to'] = $body_type === 'text' ? 'mediumtext' : 'text'; |
||||
476 | $context['convert_to_suggest'] = ($body_type !== 'text' && !empty($modSettings['max_messageLength']) && $modSettings['max_messageLength'] < 65536); |
||||
477 | |||||
478 | return; |
||||
479 | } |
||||
480 | |||||
481 | if (!isset($this->_req->post->do_conversion) || isset($this->_req->post->cont)) |
||||
482 | { |
||||
483 | checkSession(); |
||||
484 | if (empty($this->_req->query->start)) |
||||
485 | { |
||||
486 | validateToken('admin-maint'); |
||||
487 | } |
||||
488 | else |
||||
489 | { |
||||
490 | validateToken('admin-convertMsg'); |
||||
491 | } |
||||
492 | |||||
493 | $context['page_title'] = $txt['not_done_title']; |
||||
494 | $context['continue_post_data'] = ''; |
||||
495 | $context['continue_countdown'] = 3; |
||||
496 | $context['sub_template'] = 'not_done'; |
||||
497 | |||||
498 | $increment = 500; |
||||
499 | $id_msg_exceeding = isset($this->_req->post->id_msg_exceeding) ? explode(',', $this->_req->post->id_msg_exceeding) : array(); |
||||
500 | $max_msgs = countMessages(); |
||||
501 | $start = $this->_req->query->start; |
||||
502 | |||||
503 | // Try for as much time as possible. |
||||
504 | detectServer()->setTimeLimit(600); |
||||
505 | while ($start < $max_msgs) |
||||
506 | { |
||||
507 | $id_msg_exceeding = detectExceedingMessages($start, $increment); |
||||
508 | |||||
509 | $start += $increment; |
||||
510 | |||||
511 | if (microtime(true) - $time_start > 3) |
||||
512 | { |
||||
513 | createToken('admin-convertMsg'); |
||||
514 | $context['continue_post_data'] = ' |
||||
515 | <input type="hidden" name="' . $context['admin-convertMsg_token_var'] . '" value="' . $context['admin-convertMsg_token'] . '" /> |
||||
516 | <input type="hidden" name="' . $context['session_var'] . '" value="' . $context['session_id'] . '" /> |
||||
517 | <input type="hidden" name="id_msg_exceeding" value="' . implode(',', $id_msg_exceeding) . '" />'; |
||||
518 | $context['continue_get_data'] = '?action=admin;area=maintain;sa=database;activity=convertmsgbody;start=' . $start; |
||||
519 | $context['continue_percent'] = round(100 * $start / $max_msgs); |
||||
520 | $context['not_done_title'] = $txt['not_done_title'] . ' (' . $context['continue_percent'] . '%)'; |
||||
521 | |||||
522 | return; |
||||
523 | } |
||||
524 | } |
||||
525 | |||||
526 | createToken('admin-maint'); |
||||
527 | $context['page_title'] = $txt[$context['convert_to'] . '_title']; |
||||
528 | $context['sub_template'] = 'convert_msgbody'; |
||||
529 | |||||
530 | if (!empty($id_msg_exceeding)) |
||||
531 | { |
||||
532 | if (count($id_msg_exceeding) > 100) |
||||
533 | { |
||||
534 | $query_msg = array_slice($id_msg_exceeding, 0, 100); |
||||
535 | $context['exceeding_messages_morethan'] = sprintf($txt['exceeding_messages_morethan'], count($id_msg_exceeding)); |
||||
536 | } |
||||
537 | else |
||||
538 | { |
||||
539 | $query_msg = $id_msg_exceeding; |
||||
540 | } |
||||
541 | |||||
542 | $context['exceeding_messages'] = getExceedingMessages($query_msg); |
||||
543 | } |
||||
544 | } |
||||
545 | } |
||||
546 | |||||
547 | /** |
||||
548 | * Optimizes all tables in the database and lists how much was saved. |
||||
549 | * |
||||
550 | * What it does: |
||||
551 | * |
||||
552 | * - It requires the admin_forum permission. |
||||
553 | * - It shows as the maintain_forum admin area. |
||||
554 | * - It is accessed from ?action=admin;area=maintain;sa=database;activity=optimize. |
||||
555 | * - It also updates the optimize scheduled task such that the tables are not automatically optimized again too soon. |
||||
556 | */ |
||||
557 | public function action_optimize_display() |
||||
558 | { |
||||
559 | global $txt, $context; |
||||
560 | |||||
561 | isAllowedTo('admin_forum'); |
||||
562 | |||||
563 | // Some validation |
||||
564 | checkSession('post'); |
||||
565 | validateToken('admin-maint'); |
||||
566 | |||||
567 | ignore_user_abort(true); |
||||
568 | |||||
569 | require_once(SUBSDIR . '/Maintenance.subs.php'); |
||||
570 | |||||
571 | $context['page_title'] = $txt['database_optimize']; |
||||
572 | $context['sub_template'] = 'optimize'; |
||||
573 | |||||
574 | $tables = getElkTables(); |
||||
575 | |||||
576 | // If there aren't any tables then I believe that would mean the world has exploded... |
||||
577 | $context['num_tables'] = count($tables); |
||||
578 | if ($context['num_tables'] === 0) |
||||
579 | { |
||||
580 | throw new Exception('You appear to be running ElkArte in a flat file mode... fantastic!', false); |
||||
581 | } |
||||
582 | |||||
583 | // For each table.... |
||||
584 | $context['optimized_tables'] = array(); |
||||
585 | $db_table = db_table(); |
||||
586 | |||||
587 | foreach ($tables as $table) |
||||
588 | { |
||||
589 | // Optimize the table! We use backticks here because it might be a custom table. |
||||
590 | $data_freed = $db_table->optimize($table['table_name']); |
||||
591 | |||||
592 | if ($data_freed > 0) |
||||
593 | { |
||||
594 | $context['optimized_tables'][] = array( |
||||
595 | 'name' => $table['table_name'], |
||||
596 | 'data_freed' => $data_freed, |
||||
597 | ); |
||||
598 | } |
||||
599 | } |
||||
600 | |||||
601 | // Number of tables, etc.... |
||||
602 | $txt['database_numb_tables'] = sprintf($txt['database_numb_tables'], $context['num_tables']); |
||||
603 | $context['num_tables_optimized'] = count($context['optimized_tables']); |
||||
604 | |||||
605 | // Check that we don't auto optimise again too soon! |
||||
606 | require_once(SUBSDIR . '/ScheduledTasks.subs.php'); |
||||
607 | calculateNextTrigger('auto_optimize', true); |
||||
608 | } |
||||
609 | |||||
610 | /** |
||||
611 | * Recount many forum totals that can be recounted automatically without harm. |
||||
612 | * |
||||
613 | * What it does: |
||||
614 | * |
||||
615 | * - it requires the admin_forum permission. |
||||
616 | * - It shows the maintain_forum admin area. |
||||
617 | * - The function redirects back to ?action=admin;area=maintain when complete. |
||||
618 | * - It is accessed via ?action=admin;area=maintain;sa=database;activity=recount. |
||||
619 | * |
||||
620 | * Totals recounted: |
||||
621 | * - fixes for topics with wrong num_replies. |
||||
622 | * - updates for num_posts and num_topics of all boards. |
||||
623 | * - recounts personal_messages but not unread_messages. |
||||
624 | * - repairs messages pointing to boards with topics pointing to other boards. |
||||
625 | * - updates the last message posted in boards and children. |
||||
626 | * - updates member count, latest member, topic count, and message count. |
||||
627 | */ |
||||
628 | public function action_recount_display() |
||||
629 | { |
||||
630 | global $txt, $context, $modSettings, $time_start; |
||||
631 | |||||
632 | isAllowedTo('admin_forum'); |
||||
633 | checkSession(); |
||||
634 | |||||
635 | // Functions |
||||
636 | require_once(SUBSDIR . '/Maintenance.subs.php'); |
||||
637 | require_once(SUBSDIR . '/Topic.subs.php'); |
||||
638 | |||||
639 | // Validate the request or the loop |
||||
640 | if (!isset($this->_req->query->step)) |
||||
641 | { |
||||
642 | validateToken('admin-maint'); |
||||
643 | } |
||||
644 | else |
||||
645 | { |
||||
646 | validateToken('admin-boardrecount'); |
||||
647 | } |
||||
648 | |||||
649 | // For the loop template |
||||
650 | $context['page_title'] = $txt['not_done_title']; |
||||
651 | $context['continue_post_data'] = ''; |
||||
652 | $context['continue_countdown'] = 3; |
||||
653 | $context['sub_template'] = 'not_done'; |
||||
654 | |||||
655 | // Try for as much time as possible. |
||||
656 | detectServer()->setTimeLimit(600); |
||||
657 | |||||
658 | // Step the number of topics at a time so things don't time out... |
||||
659 | $this->max_topics = getMaxTopicID(); |
||||
660 | $this->increment = (int) min(max(50, ceil($this->max_topics / 4)), 2000); |
||||
661 | |||||
662 | // An 8 step process, should be 12 for the admin |
||||
663 | $this->total_steps = 8; |
||||
664 | $this->start = $this->_req->getQuery('start', 'inval', 0); |
||||
665 | $this->step = $this->_req->getQuery('step', 'intval', 0); |
||||
666 | |||||
667 | // Get each topic with a wrong reply count and fix it |
||||
668 | if (empty($this->step)) |
||||
669 | { |
||||
670 | // let's just do some at a time, though. |
||||
671 | while ($this->start < $this->max_topics) |
||||
672 | { |
||||
673 | recountApprovedMessages($this->start, $this->increment); |
||||
674 | recountUnapprovedMessages($this->start, $this->increment); |
||||
675 | $this->start += $this->increment; |
||||
676 | |||||
677 | if (microtime(true) - $time_start > 3) |
||||
678 | { |
||||
679 | $percent = round((100 * $this->start / $this->max_topics) / $this->total_steps); |
||||
680 | $this->_buildContinue($percent, 0); |
||||
681 | |||||
682 | return; |
||||
683 | } |
||||
684 | } |
||||
685 | |||||
686 | // Done with step 0, reset start for the next one |
||||
687 | $this->start = 0; |
||||
688 | } |
||||
689 | |||||
690 | // Update the post count of each board. |
||||
691 | if ($this->step <= 1) |
||||
692 | { |
||||
693 | if (empty($this->start)) |
||||
694 | { |
||||
695 | resetBoardsCounter('num_posts'); |
||||
696 | } |
||||
697 | |||||
698 | while ($this->start < $this->max_topics) |
||||
699 | { |
||||
700 | // Recount the posts |
||||
701 | updateBoardsCounter('posts', $this->start, $this->increment); |
||||
702 | $this->start += $this->increment; |
||||
703 | |||||
704 | if (microtime(true) - $time_start > 3) |
||||
705 | { |
||||
706 | $percent = round((200 + 100 * $this->start / $this->max_topics) / $this->total_steps); |
||||
707 | $this->_buildContinue($percent, 1); |
||||
708 | |||||
709 | return; |
||||
710 | } |
||||
711 | } |
||||
712 | |||||
713 | // Done with step 1, reset start for the next one |
||||
714 | $this->start = 0; |
||||
715 | } |
||||
716 | |||||
717 | // Update the topic count of each board. |
||||
718 | if ($this->step <= 2) |
||||
719 | { |
||||
720 | if (empty($this->start)) |
||||
721 | { |
||||
722 | resetBoardsCounter('num_topics'); |
||||
723 | } |
||||
724 | |||||
725 | while ($this->start < $this->max_topics) |
||||
726 | { |
||||
727 | updateBoardsCounter('topics', $this->start, $this->increment); |
||||
728 | $this->start += $this->increment; |
||||
729 | |||||
730 | if (microtime(true) - $time_start > 3) |
||||
731 | { |
||||
732 | $percent = round((300 + 100 * $this->start / $this->max_topics) / $this->total_steps); |
||||
733 | $this->_buildContinue($percent, 2); |
||||
734 | |||||
735 | return; |
||||
736 | } |
||||
737 | } |
||||
738 | |||||
739 | // Done with step 2, reset start for the next one |
||||
740 | $this->start = 0; |
||||
741 | } |
||||
742 | |||||
743 | // Update the unapproved post count of each board. |
||||
744 | if ($this->step <= 3) |
||||
745 | { |
||||
746 | if (empty($this->start)) |
||||
747 | { |
||||
748 | resetBoardsCounter('unapproved_posts'); |
||||
749 | } |
||||
750 | |||||
751 | while ($this->start < $this->max_topics) |
||||
752 | { |
||||
753 | updateBoardsCounter('unapproved_posts', $this->start, $this->increment); |
||||
754 | $this->start += $this->increment; |
||||
755 | |||||
756 | if (microtime(true) - $time_start > 3) |
||||
757 | { |
||||
758 | $percent = round((400 + 100 * $this->start / $this->max_topics) / $this->total_steps); |
||||
759 | $this->_buildContinue($percent, 3); |
||||
760 | |||||
761 | return; |
||||
762 | } |
||||
763 | } |
||||
764 | |||||
765 | // Done with step 3, reset start for the next one |
||||
766 | $this->start = 0; |
||||
767 | } |
||||
768 | |||||
769 | // Update the unapproved topic count of each board. |
||||
770 | if ($this->step <= 4) |
||||
771 | { |
||||
772 | if (empty($this->start)) |
||||
773 | { |
||||
774 | resetBoardsCounter('unapproved_topics'); |
||||
775 | } |
||||
776 | |||||
777 | while ($this->start < $this->max_topics) |
||||
778 | { |
||||
779 | updateBoardsCounter('unapproved_topics', $this->start, $this->increment); |
||||
780 | $this->start += $this->increment; |
||||
781 | |||||
782 | if (microtime(true) - $time_start > 3) |
||||
783 | { |
||||
784 | $percent = round((500 + 100 * $this->start / $this->max_topics) / $this->total_steps); |
||||
785 | $this->_buildContinue($percent, 4); |
||||
786 | |||||
787 | return; |
||||
788 | } |
||||
789 | } |
||||
790 | |||||
791 | // Done with step 4, reset start for the next one |
||||
792 | $this->start = 0; |
||||
793 | } |
||||
794 | |||||
795 | // Get all members with wrong number of personal messages. |
||||
796 | if ($this->step <= 5) |
||||
797 | { |
||||
798 | updatePersonalMessagesCounter(); |
||||
799 | |||||
800 | // Done with step 5, reset start for the next one |
||||
801 | $this->start = 0; |
||||
802 | if (microtime(true) - $time_start > 3) |
||||
803 | { |
||||
804 | $percent = round(700 / $this->total_steps); |
||||
805 | $this->_buildContinue($percent, 6); |
||||
806 | |||||
807 | return; |
||||
808 | } |
||||
809 | } |
||||
810 | |||||
811 | // Any messages pointing to the wrong board? |
||||
812 | if ($this->step <= 6) |
||||
813 | { |
||||
814 | while ($this->start < $modSettings['maxMsgID']) |
||||
815 | { |
||||
816 | updateMessagesBoardID($this->_req->query->start, $this->increment); |
||||
817 | $this->start += $this->increment; |
||||
818 | |||||
819 | if (microtime(true) - $time_start > 3) |
||||
820 | { |
||||
821 | $percent = round((700 + 100 * $this->start / $modSettings['maxMsgID']) / $this->total_steps); |
||||
822 | $this->_buildContinue($percent, 6); |
||||
823 | |||||
824 | return; |
||||
825 | } |
||||
826 | } |
||||
827 | |||||
828 | // Done with step 6, reset start for the next one |
||||
829 | $this->start = 0; |
||||
830 | } |
||||
831 | |||||
832 | updateBoardsLastMessage(); |
||||
833 | |||||
834 | // Update all the basic statistics. |
||||
835 | require_once(SUBSDIR . '/Members.subs.php'); |
||||
836 | updateMemberStats(); |
||||
837 | require_once(SUBSDIR . '/Messages.subs.php'); |
||||
838 | updateMessageStats(); |
||||
839 | require_once(SUBSDIR . '/Topic.subs.php'); |
||||
840 | updateTopicStats(); |
||||
841 | |||||
842 | // Finally, update the latest event times. |
||||
843 | require_once(SUBSDIR . '/ScheduledTasks.subs.php'); |
||||
844 | calculateNextTrigger(); |
||||
845 | |||||
846 | // Ta-da |
||||
847 | redirectexit('action=admin;area=maintain;sa=routine;done=recount'); |
||||
848 | } |
||||
849 | |||||
850 | /** |
||||
851 | * Helper function for teh recount process, build the continue values for |
||||
852 | * the template |
||||
853 | * |
||||
854 | * @param int $percent percent done |
||||
855 | * @param int $step step we are on |
||||
856 | */ |
||||
857 | private function _buildContinue($percent, $step) |
||||
858 | { |
||||
859 | global $context, $txt; |
||||
860 | |||||
861 | createToken('admin-boardrecount'); |
||||
862 | |||||
863 | $context['continue_post_data'] = ' |
||||
864 | <input type="hidden" name="' . $context['admin-boardrecount_token_var'] . '" value="' . $context['admin-boardrecount_token'] . '" /> |
||||
865 | <input type="hidden" name="' . $context['session_var'] . '" value="' . $context['session_id'] . '" />'; |
||||
866 | $context['continue_get_data'] = '?action=admin;area=maintain;sa=routine;activity=recount;step=' . $step . ';start=' . $this->start; |
||||
867 | $context['continue_percent'] = $percent; |
||||
868 | $context['not_done_title'] = $txt['not_done_title'] . ' (' . $context['continue_percent'] . '%)'; |
||||
869 | } |
||||
870 | |||||
871 | /** |
||||
872 | * Re-attribute posts to the user sent from the maintenance page. |
||||
873 | */ |
||||
874 | public function action_reattribute_display() |
||||
875 | { |
||||
876 | global $context, $txt; |
||||
877 | |||||
878 | checkSession(); |
||||
879 | |||||
880 | $validator = new DataValidator(); |
||||
881 | $validator->sanitation_rules(array('posts' => 'empty', 'type' => 'trim', 'from_email' => 'trim', 'from_name' => 'trim', 'to' => 'trim')); |
||||
882 | $validator->validation_rules(array('from_email' => 'valid_email', 'from_name' => 'required', 'to' => 'required', 'type' => 'contains[name,email]')); |
||||
883 | $validator->validate($this->_req->post); |
||||
884 | |||||
885 | // Fetch the Mr. Clean values |
||||
886 | $our_post = array_replace((array) $this->_req->post, $validator->validation_data()); |
||||
887 | |||||
888 | // Do we have a valid set of options to continue? |
||||
889 | if (($our_post['type'] === 'name' && !empty($our_post['from_name'])) || ($our_post['type'] === 'email' && !$validator->validation_errors('from_email'))) |
||||
890 | { |
||||
891 | // Find the member. |
||||
892 | require_once(SUBSDIR . '/Auth.subs.php'); |
||||
893 | $members = findMembers($our_post['to']); |
||||
894 | |||||
895 | // No members, no further |
||||
896 | if (empty($members)) |
||||
897 | { |
||||
898 | throw new Exception('reattribute_cannot_find_member'); |
||||
899 | } |
||||
900 | |||||
901 | $memID = array_shift($members); |
||||
902 | $memID = $memID['id']; |
||||
903 | |||||
904 | $email = $our_post['type'] === 'email' ? $our_post['from_email'] : ''; |
||||
905 | $memberName = $our_post['type'] === 'name' ? $our_post['from_name'] : ''; |
||||
906 | |||||
907 | // Now call the reattribute function. |
||||
908 | require_once(SUBSDIR . '/Members.subs.php'); |
||||
909 | reattributePosts($memID, $email, $memberName, !$our_post['posts']); |
||||
910 | |||||
911 | $context['maintenance_finished'] = array( |
||||
912 | 'errors' => array(sprintf($txt['maintain_done'], $txt['maintain_reattribute_posts'])), |
||||
913 | ); |
||||
914 | } |
||||
915 | else |
||||
916 | { |
||||
917 | // Show them the correct error |
||||
918 | if ($our_post['type'] === 'name' && empty($our_post['from_name'])) |
||||
919 | { |
||||
920 | $error = $validator->validation_errors(array('from_name', 'to')); |
||||
921 | } |
||||
922 | else |
||||
923 | { |
||||
924 | $error = $validator->validation_errors(array('from_email', 'to')); |
||||
925 | } |
||||
926 | |||||
927 | $context['maintenance_finished'] = array( |
||||
928 | 'errors' => $error, |
||||
929 | 'type' => 'minor', |
||||
930 | ); |
||||
931 | } |
||||
932 | } |
||||
933 | |||||
934 | /** |
||||
935 | * Handling function for the backup stuff. |
||||
936 | * |
||||
937 | * - It requires an administrator and the session hash by post. |
||||
938 | * - This method simply forwards to DumpDatabase2(). |
||||
939 | */ |
||||
940 | public function action_backup_display() |
||||
941 | { |
||||
942 | validateToken('admin-maint'); |
||||
943 | |||||
944 | // Administrators only! |
||||
945 | if (!allowedTo('admin_forum')) |
||||
946 | { |
||||
947 | throw new Exception('no_dump_database', 'critical'); |
||||
948 | } |
||||
949 | |||||
950 | checkSession('post'); |
||||
951 | |||||
952 | // Validate access |
||||
953 | if (!defined('I_KNOW_IT_MAY_BE_UNSAFE') && $this->_validate_access() === false) |
||||
954 | { |
||||
955 | return $this->action_database(); |
||||
956 | } |
||||
957 | |||||
958 | require_once(SUBSDIR . '/Admin.subs.php'); |
||||
959 | emailAdmins('admin_backup_database', array( |
||||
960 | 'BAK_REALNAME' => $this->user->name |
||||
961 | )); |
||||
962 | |||||
963 | logAction('database_backup', array('member' => $this->user->id), 'admin'); |
||||
964 | require_once(SOURCEDIR . '/DumpDatabase.php'); |
||||
965 | DumpDatabase2(); |
||||
966 | |||||
967 | // Should not get here as DumpDatabase2 exits |
||||
968 | return true; |
||||
969 | } |
||||
970 | |||||
971 | /** |
||||
972 | * Validates the user can make an FTP connection with the supplied uid/pass |
||||
973 | * |
||||
974 | * - Used as an extra layer of security when performing backups |
||||
975 | */ |
||||
976 | private function _validate_access() |
||||
977 | { |
||||
978 | global $context, $txt; |
||||
979 | |||||
980 | $ftp = new FtpConnection($this->_req->post->ftp_server, $this->_req->post->ftp_port, $this->_req->post->ftp_username, $this->_req->post->ftp_password); |
||||
981 | |||||
982 | // No errors on the connection, id/pass are good |
||||
983 | // I know, I know... but a lot of people want to type /home/xyz/... which is wrong, but logical. |
||||
984 | if ($ftp->error === false && !$ftp->chdir($this->_req->post->ftp_path)) |
||||
985 | { |
||||
986 | $ftp->chdir(preg_replace('~^/home[2]?/[^/]+~', '', $this->_req->post->ftp_path)); |
||||
987 | } |
||||
988 | |||||
989 | // If we had an error... |
||||
990 | if ($ftp->error !== false) |
||||
991 | { |
||||
992 | Txt::load('Packages'); |
||||
993 | $ftp_error = $ftp->last_message ?? $txt['package_ftp_' . $ftp->error] ?? ''; |
||||
0 ignored issues
–
show
Are you sure
$ftp->error of type string|true can be used in concatenation ?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
994 | |||||
995 | // Fill the boxes for a FTP connection with data from the previous attempt |
||||
996 | $context['package_ftp'] = array( |
||||
997 | 'form_elements_only' => 1, |
||||
998 | 'server' => $this->_req->post->ftp_server, |
||||
999 | 'port' => $this->_req->post->ftp_port, |
||||
1000 | 'username' => $this->_req->post->ftp_username, |
||||
1001 | 'path' => $this->_req->post->ftp_path, |
||||
1002 | 'error' => empty($ftp_error) ? null : $ftp_error, |
||||
1003 | ); |
||||
1004 | |||||
1005 | return false; |
||||
1006 | } |
||||
1007 | |||||
1008 | return true; |
||||
1009 | } |
||||
1010 | |||||
1011 | /** |
||||
1012 | * Supporting function for the database maintenance area. |
||||
1013 | */ |
||||
1014 | public function action_database() |
||||
1015 | { |
||||
1016 | global $context, $modSettings, $maintenance; |
||||
1017 | |||||
1018 | // We need this, really.. |
||||
1019 | require_once(SUBSDIR . '/Maintenance.subs.php'); |
||||
1020 | |||||
1021 | // Set up the sub-template |
||||
1022 | $context['sub_template'] = 'maintain_database'; |
||||
1023 | $db = database(); |
||||
1024 | |||||
1025 | if ($db->supportMediumtext()) |
||||
1026 | { |
||||
1027 | $body_type = fetchBodyType(); |
||||
1028 | |||||
1029 | $context['convert_to'] = $body_type === 'text' ? 'mediumtext' : 'text'; |
||||
1030 | $context['convert_to_suggest'] = ($body_type !== 'text' && !empty($modSettings['max_messageLength']) && $modSettings['max_messageLength'] < 65536); |
||||
1031 | } |
||||
1032 | |||||
1033 | // Check few things to give advices before make a backup |
||||
1034 | // If safe mod is enable the external tool is *always* the best (and probably the only) solution |
||||
1035 | $context['safe_mode_enable'] = false; |
||||
1036 | |||||
1037 | // This is just a...guess |
||||
1038 | $messages = countMessages(); |
||||
1039 | |||||
1040 | // 256 is what we use in the backup script |
||||
1041 | detectServer()->setMemoryLimit('256M'); |
||||
1042 | $memory_limit = memoryReturnBytes(ini_get('memory_limit')) / (1024 * 1024); |
||||
1043 | |||||
1044 | // Zip limit is set to more or less 1/4th the size of the available memory * 1500 |
||||
1045 | // 1500 is an estimate of the number of messages that generates a database of 1 MB (yeah I know IT'S AN ESTIMATION!!!) |
||||
1046 | // Why that? Because the only reliable zip package is the one sent out the first time, |
||||
1047 | // so when the backup takes 1/5th (just to stay on the safe side) of the memory available |
||||
1048 | $zip_limit = $memory_limit * 1500 / 5; |
||||
1049 | |||||
1050 | // Here is more tricky: it depends on many factors, but the main idea is that |
||||
1051 | // if it takes "too long" the backup is not reliable. So, I know that on my computer it take |
||||
1052 | // 20 minutes to backup 2.5 GB, of course my computer is not representative, so I'll multiply by 4 the time. |
||||
1053 | // I would consider "too long" 5 minutes (I know it can be a long time, but let's start with that): |
||||
1054 | // 80 minutes for a 2.5 GB and a 5 minutes limit means 160 MB approx |
||||
1055 | $plain_limit = 240000; |
||||
1056 | |||||
1057 | // Last thing: are we able to gain time? |
||||
1058 | $current_time_limit = (int) ini_get('max_execution_time'); |
||||
1059 | $new_time_limit = detectServer()->setTimeLimit(159); //something strange just to be sure |
||||
1060 | detectServer()->setTimeLimit($current_time_limit); |
||||
1061 | |||||
1062 | $context['use_maintenance'] = 0; |
||||
1063 | |||||
1064 | // External tool if: |
||||
1065 | // * cannot change the execution time OR |
||||
1066 | // * cannot reset timeout |
||||
1067 | if (empty($new_time_limit) || ($current_time_limit == $new_time_limit && !function_exists('apache_reset_timeout'))) |
||||
1068 | { |
||||
1069 | $context['suggested_method'] = 'use_external_tool'; |
||||
1070 | } |
||||
1071 | elseif ($zip_limit < $plain_limit && $messages < $zip_limit) |
||||
1072 | { |
||||
1073 | $context['suggested_method'] = 'zipped_file'; |
||||
1074 | } |
||||
1075 | elseif ($zip_limit > $plain_limit || ($zip_limit < $plain_limit && $plain_limit < $messages)) |
||||
1076 | { |
||||
1077 | $context['suggested_method'] = 'use_external_tool'; |
||||
1078 | $context['use_maintenance'] = empty($maintenance) ? 2 : 0; |
||||
1079 | } |
||||
1080 | else |
||||
1081 | { |
||||
1082 | $context['use_maintenance'] = 1; |
||||
1083 | $context['suggested_method'] = 'plain_text'; |
||||
1084 | } |
||||
1085 | |||||
1086 | theme()->getTemplates()->load('Packages'); |
||||
1087 | Txt::load('Packages'); |
||||
1088 | |||||
1089 | // $context['package_ftp'] may be set action_backup_display when an error occur |
||||
1090 | if (!isset($context['package_ftp'])) |
||||
1091 | { |
||||
1092 | $context['package_ftp'] = array( |
||||
1093 | 'form_elements_only' => true, |
||||
1094 | 'server' => '', |
||||
1095 | 'port' => '', |
||||
1096 | 'username' => $modSettings['package_username'] ?? '', |
||||
1097 | 'path' => '', |
||||
1098 | 'error' => '', |
||||
1099 | ); |
||||
1100 | } |
||||
1101 | |||||
1102 | $context['skip_security'] = defined('I_KNOW_IT_MAY_BE_UNSAFE'); |
||||
1103 | } |
||||
1104 | |||||
1105 | /** |
||||
1106 | * Removing old and inactive members. |
||||
1107 | */ |
||||
1108 | public function action_purgeinactive_display() |
||||
1109 | { |
||||
1110 | global $context, $txt; |
||||
1111 | |||||
1112 | checkSession(); |
||||
1113 | validateToken('admin-maint'); |
||||
1114 | |||||
1115 | // Start with checking and cleaning what was sent |
||||
1116 | $validator = new DataValidator(); |
||||
1117 | $validator->sanitation_rules(array('maxdays' => 'intval')); |
||||
1118 | $validator->validation_rules(array('maxdays' => 'required', 'groups' => 'isarray', 'del_type' => 'required')); |
||||
1119 | |||||
1120 | // Validator says, you can pass or not |
||||
1121 | if ($validator->validate($this->_req->post)) |
||||
1122 | { |
||||
1123 | // Get the clean data |
||||
1124 | $our_post = array_replace((array) $this->_req->post, $validator->validation_data()); |
||||
1125 | |||||
1126 | require_once(SUBSDIR . '/Maintenance.subs.php'); |
||||
1127 | require_once(SUBSDIR . '/Members.subs.php'); |
||||
1128 | |||||
1129 | $groups = array(); |
||||
1130 | foreach ($our_post['groups'] as $id => $dummy) |
||||
1131 | { |
||||
1132 | $groups[] = (int) $id; |
||||
1133 | } |
||||
1134 | |||||
1135 | $time_limit = (time() - ($our_post['maxdays'] * 24 * 3600)); |
||||
1136 | $members = purgeMembers($our_post['del_type'], $groups, $time_limit); |
||||
1137 | deleteMembers($members); |
||||
1138 | |||||
1139 | $context['maintenance_finished'] = array( |
||||
1140 | 'errors' => array(sprintf($txt['maintain_done'], $txt['maintain_members'])), |
||||
1141 | ); |
||||
1142 | } |
||||
1143 | else |
||||
1144 | { |
||||
1145 | $context['maintenance_finished'] = array( |
||||
1146 | 'errors' => $validator->validation_errors(), |
||||
1147 | 'type' => 'minor', |
||||
1148 | ); |
||||
1149 | } |
||||
1150 | } |
||||
1151 | |||||
1152 | /** |
||||
1153 | * This method takes care of removal of old posts. |
||||
1154 | * They're very very old, perhaps even older. |
||||
1155 | */ |
||||
1156 | public function action_pruneold_display() |
||||
1157 | { |
||||
1158 | validateToken('admin-maint'); |
||||
1159 | |||||
1160 | isAllowedTo('admin_forum'); |
||||
1161 | checkSession('post', 'admin'); |
||||
1162 | |||||
1163 | // No boards at all? Forget it then :/. |
||||
1164 | if (empty($this->_req->post->boards)) |
||||
1165 | { |
||||
1166 | redirectexit('action=admin;area=maintain;sa=topics'); |
||||
1167 | } |
||||
1168 | |||||
1169 | $boards = array_keys($this->_req->post->boards); |
||||
1170 | |||||
1171 | if (!isset($this->_req->post->delete_type) || !in_array($this->_req->post->delete_type, array('moved', 'nothing', 'locked'))) |
||||
1172 | { |
||||
1173 | $delete_type = 'nothing'; |
||||
1174 | } |
||||
1175 | else |
||||
1176 | { |
||||
1177 | $delete_type = $this->_req->post->delete_type; |
||||
1178 | } |
||||
1179 | |||||
1180 | $exclude_stickies = isset($this->_req->post->delete_old_not_sticky); |
||||
1181 | |||||
1182 | // @todo what is the minimum for maxdays? Maybe throw an error? |
||||
1183 | $older_than = time() - 3600 * 24 * max($this->_req->getPost('maxdays', 'intval', 0), 1); |
||||
1184 | |||||
1185 | require_once(SUBSDIR . '/Topic.subs.php'); |
||||
1186 | removeOldTopics($boards, $delete_type, $exclude_stickies, $older_than); |
||||
1187 | |||||
1188 | // Log an action into the moderation log. |
||||
1189 | logAction('pruned', array('days' => max($this->_req->getPost('maxdays', 'intval', 0), 1))); |
||||
1190 | |||||
1191 | redirectexit('action=admin;area=maintain;sa=topics;done=purgeold'); |
||||
1192 | } |
||||
1193 | |||||
1194 | /** |
||||
1195 | * Moves topics from one board to another. |
||||
1196 | * |
||||
1197 | * @uses not_done template to pause the process. |
||||
1198 | */ |
||||
1199 | public function action_massmove_display() |
||||
1200 | { |
||||
1201 | global $context, $txt, $time_start; |
||||
1202 | |||||
1203 | // Only admins. |
||||
1204 | isAllowedTo('admin_forum'); |
||||
1205 | |||||
1206 | // And valid requests |
||||
1207 | checkSession(); |
||||
1208 | |||||
1209 | // Set up to the context. |
||||
1210 | $context['page_title'] = $txt['not_done_title']; |
||||
1211 | $context['continue_countdown'] = 3; |
||||
1212 | $context['continue_post_data'] = ''; |
||||
1213 | $context['continue_get_data'] = ''; |
||||
1214 | $context['sub_template'] = 'not_done'; |
||||
1215 | $context['start'] = $this->_req->getQuery('start', 'intval', 0); |
||||
1216 | |||||
1217 | // First time we do this? |
||||
1218 | $id_board_from = $this->_req->getPost('id_board_from', 'intval', $this->_req->getQuery('id_board_from', 'intval', 0)); |
||||
1219 | $id_board_to = $this->_req->getPost('id_board_to', 'intval', $this->_req->getQuery('id_board_to', 'intval', 0)); |
||||
1220 | |||||
1221 | // No boards then this is your stop. |
||||
1222 | if (empty($id_board_from) || empty($id_board_to)) |
||||
1223 | { |
||||
1224 | return; |
||||
1225 | } |
||||
1226 | |||||
1227 | // These will be needed |
||||
1228 | require_once(SUBSDIR . '/Maintenance.subs.php'); |
||||
1229 | require_once(SUBSDIR . '/Topic.subs.php'); |
||||
1230 | |||||
1231 | // How many topics are we moving? |
||||
1232 | $total_topics = $this->_req->getQuery('totaltopics', 'intval', 0); |
||||
1233 | if (empty($total_topics) || $context['start'] === 0) |
||||
1234 | { |
||||
1235 | validateToken('admin-maint'); |
||||
1236 | $total_topics = countTopicsFromBoard($id_board_from); |
||||
1237 | } |
||||
1238 | else |
||||
1239 | { |
||||
1240 | $total_topics = (int) $this->_req->query->totaltopics; |
||||
1241 | validateToken('admin_movetopics'); |
||||
1242 | } |
||||
1243 | |||||
1244 | // We have topics to move so start the process. |
||||
1245 | if (!empty($total_topics)) |
||||
1246 | { |
||||
1247 | while ($context['start'] <= $total_topics) |
||||
1248 | { |
||||
1249 | // Lets get the next 10 topics. |
||||
1250 | $topics = getTopicsToMove($id_board_from); |
||||
1251 | |||||
1252 | // Just return if we don't have any topics left to move. |
||||
1253 | if (empty($topics)) |
||||
1254 | { |
||||
1255 | break; |
||||
1256 | } |
||||
1257 | |||||
1258 | // Lets move them. |
||||
1259 | moveTopics($topics, $id_board_to); |
||||
1260 | |||||
1261 | // Increase the counter |
||||
1262 | $context['start'] += 10; |
||||
1263 | |||||
1264 | // If this is really taking some time, show the pause screen |
||||
1265 | if (microtime(true) - $time_start > 3) |
||||
1266 | { |
||||
1267 | // What's the percent? |
||||
1268 | $context['continue_percent'] = round(100 * ($context['start'] / $total_topics), 1); |
||||
1269 | |||||
1270 | // Set up for the form |
||||
1271 | $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']; |
||||
1272 | $context['continue_post_data'] = ' |
||||
1273 | <input type="hidden" name="' . $context['session_var'] . '" value="' . $context['session_id'] . '" />'; |
||||
1274 | |||||
1275 | // Let the template system do it's thang. |
||||
1276 | return; |
||||
1277 | } |
||||
1278 | } |
||||
1279 | } |
||||
1280 | |||||
1281 | // Don't confuse admins by having an out of date cache. |
||||
1282 | Cache::instance()->remove('board-' . $id_board_from); |
||||
1283 | Cache::instance()->remove('board-' . $id_board_to); |
||||
1284 | |||||
1285 | redirectexit('action=admin;area=maintain;sa=topics;done=massmove'); |
||||
1286 | } |
||||
1287 | |||||
1288 | /** |
||||
1289 | * Generates a list of integration hooks for display |
||||
1290 | * |
||||
1291 | * - Accessed through ?action=admin;area=maintain;sa=hooks; |
||||
1292 | * - Allows for removal or disabling of selected hooks |
||||
1293 | */ |
||||
1294 | public function action_hooks() |
||||
1295 | { |
||||
1296 | global $context, $txt; |
||||
1297 | |||||
1298 | require_once(SUBSDIR . '/AddonSettings.subs.php'); |
||||
1299 | |||||
1300 | $context['filter_url'] = ''; |
||||
1301 | $context['current_filter'] = ''; |
||||
1302 | |||||
1303 | // Get the list of the current system hooks, filter them if needed |
||||
1304 | $currentHooks = get_integration_hooks(); |
||||
1305 | if (isset($this->_req->query->filter) && array_key_exists($this->_req->query->filter, $currentHooks)) |
||||
1306 | { |
||||
1307 | $context['filter_url'] = ';filter=' . $this->_req->query->filter; |
||||
1308 | $context['current_filter'] = $this->_req->query->filter; |
||||
1309 | } |
||||
1310 | |||||
1311 | $list_options = array( |
||||
1312 | 'id' => 'list_integration_hooks', |
||||
1313 | 'title' => $txt['maintain_sub_hooks_list'], |
||||
1314 | 'items_per_page' => 20, |
||||
1315 | 'base_href' => getUrl('admin', ['action' => 'admin', 'area' => 'maintain', 'sa' => 'hooks', $context['filter_url'], '{session_data}']), |
||||
1316 | 'default_sort_col' => 'hook_name', |
||||
1317 | 'get_items' => array( |
||||
1318 | 'function' => fn($start, $items_per_page, $sort) => $this->list_getIntegrationHooks($start, $items_per_page, $sort), |
||||
1319 | ), |
||||
1320 | 'get_count' => array( |
||||
1321 | 'function' => fn() => $this->list_getIntegrationHooksCount(), |
||||
1322 | ), |
||||
1323 | 'no_items_label' => $txt['hooks_no_hooks'], |
||||
1324 | 'columns' => array( |
||||
1325 | 'hook_name' => array( |
||||
1326 | 'header' => array( |
||||
1327 | 'value' => $txt['hooks_field_hook_name'], |
||||
1328 | ), |
||||
1329 | 'data' => array( |
||||
1330 | 'db' => 'hook_name', |
||||
1331 | ), |
||||
1332 | 'sort' => array( |
||||
1333 | 'default' => 'hook_name', |
||||
1334 | 'reverse' => 'hook_name DESC', |
||||
1335 | ), |
||||
1336 | ), |
||||
1337 | 'function_name' => array( |
||||
1338 | 'header' => array( |
||||
1339 | 'value' => $txt['hooks_field_function_name'], |
||||
1340 | ), |
||||
1341 | 'data' => array( |
||||
1342 | 'function' => static function ($data) { |
||||
1343 | global $txt; |
||||
1344 | |||||
1345 | if (!empty($data['included_file'])) |
||||
1346 | { |
||||
1347 | return $txt['hooks_field_function'] . ': ' . $data['real_function'] . '<br />' . $txt['hooks_field_included_file'] . ': ' . $data['included_file']; |
||||
1348 | } |
||||
1349 | |||||
1350 | return $data['real_function']; |
||||
1351 | }, |
||||
1352 | ), |
||||
1353 | 'sort' => array( |
||||
1354 | 'default' => 'function_name', |
||||
1355 | 'reverse' => 'function_name DESC', |
||||
1356 | ), |
||||
1357 | ), |
||||
1358 | 'file_name' => array( |
||||
1359 | 'header' => array( |
||||
1360 | 'value' => $txt['hooks_field_file_name'], |
||||
1361 | ), |
||||
1362 | 'data' => array( |
||||
1363 | 'db' => 'file_name', |
||||
1364 | ), |
||||
1365 | 'sort' => array( |
||||
1366 | 'default' => 'file_name', |
||||
1367 | 'reverse' => 'file_name DESC', |
||||
1368 | ), |
||||
1369 | ), |
||||
1370 | 'status' => array( |
||||
1371 | 'header' => array( |
||||
1372 | 'value' => $txt['hooks_field_hook_exists'], |
||||
1373 | 'class' => 'nowrap', |
||||
1374 | ), |
||||
1375 | 'data' => array( |
||||
1376 | 'function' => static fn($data) => '<i class="icon i-post_moderation_' . $data['status'] . '" title="' . $data['img_text'] . '"></i>', |
||||
1377 | 'class' => 'centertext', |
||||
1378 | ), |
||||
1379 | 'sort' => array( |
||||
1380 | 'default' => 'status', |
||||
1381 | 'reverse' => 'status DESC', |
||||
1382 | ), |
||||
1383 | ), |
||||
1384 | ), |
||||
1385 | 'additional_rows' => array( |
||||
1386 | array( |
||||
1387 | 'position' => 'after_title', |
||||
1388 | 'value' => $txt['hooks_disable_legend'] . ': |
||||
1389 | <ul> |
||||
1390 | <li> |
||||
1391 | <i class="icon i-post_moderation_allow" title="' . $txt['hooks_active'] . '"></i>' . $txt['hooks_disable_legend_exists'] . ' |
||||
1392 | </li> |
||||
1393 | <li> |
||||
1394 | <i class="icon i-post_moderation_deny col" title="' . $txt['hooks_missing'] . '"></i>' . $txt['hooks_disable_legend_missing'] . ' |
||||
1395 | </li> |
||||
1396 | </ul>' |
||||
1397 | ), |
||||
1398 | ), |
||||
1399 | ); |
||||
1400 | |||||
1401 | createList($list_options); |
||||
1402 | |||||
1403 | $context['page_title'] = $txt['maintain_sub_hooks_list']; |
||||
1404 | $context['sub_template'] = 'show_list'; |
||||
1405 | $context['default_list'] = 'list_integration_hooks'; |
||||
1406 | } |
||||
1407 | |||||
1408 | /** |
||||
1409 | * Callback for createList(). Called by action_hooks |
||||
1410 | * |
||||
1411 | * @param int $start The item to start with (for pagination purposes) |
||||
1412 | * @param int $items_per_page The number of items to show per page |
||||
1413 | * @param string $sort A string indicating how to sort the results |
||||
1414 | * |
||||
1415 | * @return array |
||||
1416 | */ |
||||
1417 | public function list_getIntegrationHooks($start, $items_per_page, $sort) |
||||
1418 | { |
||||
1419 | return list_integration_hooks_data($start, $items_per_page, $sort); |
||||
1420 | } |
||||
1421 | |||||
1422 | /** |
||||
1423 | * Simply returns the total count of integration hooks |
||||
1424 | * Callback for createList(). |
||||
1425 | * |
||||
1426 | * @return int |
||||
1427 | */ |
||||
1428 | public function list_getIntegrationHooksCount() |
||||
1429 | { |
||||
1430 | global $context; |
||||
1431 | |||||
1432 | $context['filter'] = $this->_req->getQuery('filter', 'trim|strval', false); |
||||
1433 | |||||
1434 | return integration_hooks_count($context['filter']); |
||||
1435 | } |
||||
1436 | |||||
1437 | /** |
||||
1438 | * Recalculate all members post counts |
||||
1439 | * |
||||
1440 | * What it does: |
||||
1441 | * |
||||
1442 | * - It requires the admin_forum permission. |
||||
1443 | * - Recounts all posts for members found in the message table |
||||
1444 | * - Updates the members post count record in the members table |
||||
1445 | * - Honors the boards post count flag |
||||
1446 | * - Does not count posts in the recycle bin |
||||
1447 | * - Zeros post counts for all members with no posts in the message table |
||||
1448 | * - Runs as a delayed loop to avoid server overload |
||||
1449 | * - Uses the not_done template in Admin.template |
||||
1450 | * - Redirects back to action=admin;area=maintain;sa=members when complete. |
||||
1451 | * - Accessed via ?action=admin;area=maintain;sa=members;activity=recountposts |
||||
1452 | */ |
||||
1453 | public function action_recountposts_display() |
||||
1454 | { |
||||
1455 | global $txt, $context; |
||||
1456 | |||||
1457 | // Check the session |
||||
1458 | checkSession(); |
||||
1459 | |||||
1460 | // Set up to the context for the pause screen |
||||
1461 | $context['page_title'] = $txt['not_done_title']; |
||||
1462 | $context['continue_countdown'] = 3; |
||||
1463 | $context['continue_get_data'] = ''; |
||||
1464 | $context['sub_template'] = 'not_done'; |
||||
1465 | |||||
1466 | // Init, do 200 members in a bunch |
||||
1467 | $increment = 200; |
||||
1468 | $start = $this->_req->getQuery('start', 'intval', 0); |
||||
1469 | |||||
1470 | // Ask for some extra time, on big boards this may take a bit |
||||
1471 | detectServer()->setTimeLimit(600); |
||||
1472 | Debug::instance()->off(); |
||||
1473 | |||||
1474 | // The functions here will come in handy |
||||
1475 | require_once(SUBSDIR . '/Maintenance.subs.php'); |
||||
1476 | |||||
1477 | // Only run this query if we don't have the total number of members that have posted |
||||
1478 | if (!isset($_SESSION['total_members']) || $start === 0) |
||||
1479 | { |
||||
1480 | validateToken('admin-maint'); |
||||
1481 | $total_members = countContributors(); |
||||
1482 | $_SESSION['total_member'] = $total_members; |
||||
1483 | } |
||||
1484 | else |
||||
1485 | { |
||||
1486 | validateToken('admin-recountposts'); |
||||
1487 | $total_members = $this->_req->session->total_members; |
||||
1488 | } |
||||
1489 | |||||
1490 | // Lets get the next group of members and determine their post count |
||||
1491 | // (from the boards that have post count enabled of course). |
||||
1492 | $total_rows = updateMembersPostCount($start, $increment); |
||||
1493 | |||||
1494 | // Continue? |
||||
1495 | if ($total_rows === $increment) |
||||
1496 | { |
||||
1497 | createToken('admin-recountposts'); |
||||
1498 | |||||
1499 | $start += $increment; |
||||
1500 | $context['continue_get_data'] = '?action=admin;area=maintain;sa=members;activity=recountposts;start=' . $start; |
||||
1501 | $context['continue_percent'] = round(100 * $start / $total_members); |
||||
1502 | $context['not_done_title'] = $txt['not_done_title'] . ' (' . $context['continue_percent'] . '%)'; |
||||
1503 | $context['continue_post_data'] = '<input type="hidden" name="' . $context['admin-recountposts_token_var'] . '" value="' . $context['admin-recountposts_token'] . '" /> |
||||
1504 | <input type="hidden" name="' . $context['session_var'] . '" value="' . $context['session_id'] . '" />'; |
||||
1505 | |||||
1506 | Debug::instance()->on(); |
||||
1507 | |||||
1508 | return; |
||||
1509 | } |
||||
1510 | |||||
1511 | // No countable posts? set posts counter to 0 |
||||
1512 | updateZeroPostMembers(); |
||||
1513 | |||||
1514 | Debug::instance()->on(); |
||||
1515 | |||||
1516 | // All done, clean up and go back to maintenance |
||||
1517 | unset($_SESSION['total_members']); |
||||
1518 | redirectexit('action=admin;area=maintain;sa=members;done=recountposts'); |
||||
1519 | } |
||||
1520 | } |
||||
1521 |