Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 240
0.00% covered (danger)
0.00%
0 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
acp_storage
0.00% covered (danger)
0.00%
0 / 238
0.00% covered (danger)
0.00%
0 / 11
5550
0.00% covered (danger)
0.00%
0 / 1
 main
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
6
 settings
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
56
 update_action
0.00% covered (danger)
0.00%
0 / 49
0.00% covered (danger)
0.00%
0 / 1
182
 update_inprogress
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 settings_form
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
72
 get_modified_storages
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
42
 storage_stats
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
12
 display_progress_page
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
2
 get_storage_update_progress
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
20
 validate_data
0.00% covered (danger)
0.00%
0 / 45
0.00% covered (danger)
0.00%
0 / 1
462
 validate_path
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
72
1<?php
2/**
3*
4* This file is part of the phpBB Forum Software package.
5*
6* @copyright (c) phpBB Limited <https://www.phpbb.com>
7* @license GNU General Public License, version 2 (GPL-2.0)
8*
9* For full copyright and license information, please see
10* the docs/CREDITS.txt file.
11*
12*/
13
14use phpbb\db\driver\driver_interface;
15use phpbb\di\service_collection;
16use phpbb\language\language;
17use phpbb\log\log_interface;
18use phpbb\request\request;
19use phpbb\storage\exception\storage_exception;
20use phpbb\storage\helper;
21use phpbb\storage\state_helper;
22use phpbb\storage\update_type;
23use phpbb\template\template;
24use phpbb\user;
25
26/**
27* @ignore
28*/
29if (!defined('IN_PHPBB'))
30{
31    exit;
32}
33
34class acp_storage
35{
36    /** @var driver_interface */
37    protected $db;
38
39    /** @var language */
40    protected $lang;
41
42    /** @var log_interface */
43    protected $log;
44
45    /** @var request */
46    protected $request;
47
48    /** @var template */
49    protected $template;
50
51    /** @var user */
52    protected $user;
53
54    /** @var service_collection */
55    protected $provider_collection;
56
57    /** @var service_collection */
58    protected $storage_collection;
59
60    /** @var \phpbb\filesystem\filesystem */
61    protected $filesystem;
62
63    /** @var string */
64    public $page_title;
65
66    /** @var string */
67    public $phpbb_root_path;
68
69    /** @var string */
70    public $tpl_name;
71
72    /** @var string */
73    public $u_action;
74
75    /** @var state_helper */
76    private $state_helper;
77
78    /** @var helper */
79    private $storage_helper;
80
81    /** @var string */
82    private $storage_table;
83
84    /**
85     * @param string $id
86     * @param string $mode
87     */
88    public function main(string $id, string $mode): void
89    {
90        global $phpbb_container, $phpbb_dispatcher, $phpbb_root_path;
91
92        $this->db = $phpbb_container->get('dbal.conn');
93        $this->lang = $phpbb_container->get('language');
94        $this->log = $phpbb_container->get('log');
95        $this->request = $phpbb_container->get('request');
96        $this->template = $phpbb_container->get('template');
97        $this->user = $phpbb_container->get('user');
98        $this->provider_collection = $phpbb_container->get('storage.provider_collection');
99        $this->storage_collection = $phpbb_container->get('storage.storage_collection');
100        $this->filesystem = $phpbb_container->get('filesystem');
101        $this->phpbb_root_path = $phpbb_root_path;
102        $this->state_helper = $phpbb_container->get('storage.state_helper');
103        $this->storage_helper = $phpbb_container->get('storage.helper');
104        $this->storage_table = $phpbb_container->getParameter('tables.storage');
105
106        // Add necessary language files
107        $this->lang->add_lang(['acp/storage']);
108
109        /**
110         * Add language strings
111         *
112         * @event core.acp_storage_load
113         * @since 4.0.0-a1
114         */
115        $phpbb_dispatcher->trigger_event('core.acp_storage_load');
116
117        switch ($mode)
118        {
119            case 'settings':
120                $this->settings($id, $mode);
121            break;
122        }
123    }
124
125    /**
126     * Method to route the request to the correct page
127     *
128     * @param string $id
129     * @param string $mode
130     */
131    private function settings(string $id, string $mode): void
132    {
133        $action = $this->request->variable('action', '');
134        if ($action && !$this->request->is_set_post('cancel'))
135        {
136            switch ($action)
137            {
138                case 'update':
139                    $this->update_action();
140                break;
141
142                default:
143                    trigger_error('NO_ACTION', E_USER_ERROR);
144            }
145        }
146        else
147        {
148            // If clicked to cancel (acp_storage_update_progress form)
149            if ($this->request->is_set_post('cancel'))
150            {
151                $this->state_helper->clear_state();
152            }
153
154            // There is an updating in progress, show the form to continue or cancel
155            if ($this->state_helper->is_action_in_progress())
156            {
157                $this->update_inprogress();
158            }
159            else
160            {
161                $this->settings_form();
162            }
163        }
164    }
165
166    /**
167     * Page to update storage settings and move files
168     *
169     * @return void
170     */
171    private function update_action(): void
172    {
173        if (!check_link_hash($this->request->variable('hash', ''), 'acp_storage'))
174        {
175            trigger_error($this->lang->lang('FORM_INVALID') . adm_back_link($this->u_action), E_USER_WARNING);
176        }
177
178        // If update_type is copy or move, copy files from the old to the new storage
179        if (in_array($this->state_helper->update_type(), [update_type::COPY, update_type::MOVE], true))
180        {
181            $i = 0;
182            foreach ($this->state_helper->storages() as $storage_name)
183            {
184                // Skip storages that have already copied files
185                if ($this->state_helper->storage_index() > $i++)
186                {
187                    continue;
188                }
189
190                $sql = 'SELECT file_id, file_path
191                        FROM ' . $this->storage_table . "
192                        WHERE  storage = '" . $this->db->sql_escape($storage_name) . "'
193                            AND file_id > " . $this->state_helper->file_index();
194                $result = $this->db->sql_query($sql);
195
196                while ($row = $this->db->sql_fetchrow($result))
197                {
198                    if (!still_on_time())
199                    {
200                        $this->db->sql_freeresult($result);
201                        $this->display_progress_page();
202                        return;
203                    }
204
205                    // Copy file from old adapter to the new one
206                    $this->storage_helper->copy_file_to_new_adapter($storage_name, $row['file_path']);
207
208                    $this->state_helper->set_file_index($row['file_id']); // update last file index copied
209                }
210
211                $this->db->sql_freeresult($result);
212
213                // Copied all files of a storage, increase storage index and reset file index
214                $this->state_helper->set_storage_index($this->state_helper->storage_index()+1);
215                $this->state_helper->set_file_index(0);
216            }
217
218            // If update_type is move files, remove the old files
219            if ($this->state_helper->update_type() === update_type::MOVE)
220            {
221                $i = 0;
222                foreach ($this->state_helper->storages() as $storage_name)
223                {
224                    // Skip storages that have already moved files
225                    if ($this->state_helper->remove_storage_index() > $i++)
226                    {
227                        continue;
228                    }
229
230                    $sql = 'SELECT file_id, file_path
231                            FROM ' . $this->storage_table . "
232                            WHERE  storage = '" . $this->db->sql_escape($storage_name) . "'
233                                AND file_id > " . $this->state_helper->file_index();
234                    $result = $this->db->sql_query($sql);
235
236                    while ($row = $this->db->sql_fetchrow($result))
237                    {
238                        if (!still_on_time())
239                        {
240                            $this->db->sql_freeresult($result);
241                            $this->display_progress_page();
242                            return;
243                        }
244
245                        // remove file from old (current) adapter
246                        $current_adapter = $this->storage_helper->get_current_adapter($storage_name);
247                        $current_adapter->delete($row['file_path']);
248
249                        $this->state_helper->set_file_index($row['file_id']);
250                    }
251
252                    $this->db->sql_freeresult($result);
253
254                    // Remove all files of a storage, increase storage index and reset file index
255                    $this->state_helper->set_remove_storage_index($this->state_helper->remove_storage_index() + 1);
256                    $this->state_helper->set_file_index(0);
257                }
258            }
259        }
260
261        // Here all files have been copied/moved, so save new configuration
262        foreach ($this->state_helper->storages() as $storage_name)
263        {
264            $this->storage_helper->update_storage_config($storage_name);
265        }
266
267        $storages = $this->state_helper->storages();
268        $this->state_helper->clear_state();
269        $this->log->add('admin', $this->user->data['user_id'], $this->user->ip, 'LOG_STORAGE_UPDATE', false, [implode(', ', $storages)]);
270        trigger_error($this->lang->lang('STORAGE_UPDATE_SUCCESSFUL') . adm_back_link($this->u_action));
271    }
272
273    /**
274     * Page that show a form with the progress bar, and a button to continue or cancel
275     *
276     * @return void
277     */
278    private function update_inprogress(): void
279    {
280        // Template from adm/style
281        $this->tpl_name = 'acp_storage_update_inprogress';
282
283        // Set page title
284        $this->page_title = 'STORAGE_TITLE';
285
286        $this->template->assign_vars([
287            'U_ACTION'    => $this->u_action . '&amp;action=update&amp;hash=' . generate_link_hash('acp_storage'),
288            'CONTINUE_PROGRESS' => $this->get_storage_update_progress(),
289        ]);
290    }
291
292    /**
293     * Main settings page, shows a form with all the storages and their configuration options
294     *
295     * @return void
296     */
297    private function settings_form(): void
298    {
299        $form_key = 'acp_storage';
300        add_form_key($form_key);
301
302        // Process form and create a "state" for the update,
303        // then show a confirm form
304        if ($this->request->is_set_post('submit'))
305        {
306            if (!check_form_key($form_key) || !check_link_hash($this->request->variable('hash', ''), 'acp_storage'))
307            {
308                trigger_error($this->lang->lang('FORM_INVALID') . adm_back_link($this->u_action), E_USER_WARNING);
309            }
310
311            $modified_storages = $this->get_modified_storages();
312
313            // validate submited paths if they are local
314            $messages = [];
315            foreach ($modified_storages as $storage_name)
316            {
317                $this->validate_data($storage_name, $messages);
318            }
319            if (!empty($messages))
320            {
321                trigger_error(implode('<br>', $messages) . adm_back_link($this->u_action), E_USER_WARNING);
322            }
323
324            // Start process and show progress
325            if (!empty($modified_storages))
326            {
327                // Create state
328                $this->state_helper->init(update_type::from((int) $this->request->variable('update_type', update_type::CONFIG->value)), $modified_storages, $this->request);
329
330                // Start displaying progress on first submit
331                $this->display_progress_page();
332                return;
333            }
334
335            // If there is no changes
336            trigger_error($this->lang->lang('STORAGE_NO_CHANGES') . adm_back_link($this->u_action), E_USER_WARNING);
337        }
338
339        // Template from adm/style
340        $this->tpl_name = 'acp_storage';
341
342        // Set page title
343        $this->page_title = 'STORAGE_TITLE';
344
345        $this->storage_stats(); // Show table with storage stats
346
347        // Validate local paths to check if everything is fine
348        $messages = [];
349        foreach ($this->storage_collection as $storage)
350        {
351            $this->validate_path($storage->get_name(), $messages);
352        }
353
354        $this->template->assign_vars([
355            'STORAGES'                        => $this->storage_collection,
356            'PROVIDERS'                     => $this->provider_collection,
357
358            'ERROR_MESSAGES'                => $messages,
359
360            'U_ACTION'                        => $this->u_action . '&amp;hash=' . generate_link_hash('acp_storage'),
361
362            'STORAGE_UPDATE_TYPE_CONFIG'    => update_type::CONFIG->value,
363            'STORAGE_UPDATE_TYPE_COPY'        => update_type::COPY->value,
364            'STORAGE_UPDATE_TYPE_MOVE'        => update_type::MOVE->value,
365        ]);
366    }
367
368    /**
369     * When submit the settings form, check which storages have been modified
370     * to update only those.
371     *
372     * @return array
373     */
374    private function get_modified_storages(): array
375    {
376        $modified_storages = [];
377
378        foreach ($this->storage_collection as $storage)
379        {
380            $storage_name = $storage->get_name();
381
382            $modified = false;
383
384            // Check if provider have been modified
385            if ($this->request->variable([$storage_name, 'provider'], '') != $this->storage_helper->get_current_provider($storage_name))
386            {
387                $modified = true;
388            }
389            else
390            {
391                $options = $this->storage_helper->get_provider_options($this->storage_helper->get_current_provider($storage_name));
392
393                // Check if options have been modified
394                foreach (array_keys($options) as $definition)
395                {
396                    if ($this->request->variable([$storage_name, $definition], '') != $this->storage_helper->get_current_definition($storage_name, $definition))
397                    {
398                        $modified = true;
399                        break;
400                    }
401                }
402            }
403
404            if ($modified)
405            {
406                $modified_storages[] = $storage_name;
407            }
408        }
409
410        return $modified_storages;
411    }
412
413    /**
414     * Fill template variables to show storage stats in settings page
415     *
416     * @return void
417     */
418    protected function storage_stats(): void
419    {
420        // Top table with stats of each storage
421        $storage_stats = [];
422        foreach ($this->storage_collection as $storage)
423        {
424            try
425            {
426                $free_space = get_formatted_filesize($storage->free_space());
427            }
428            catch (storage_exception $e)
429            {
430                $free_space = $this->lang->lang('STORAGE_UNKNOWN');
431            }
432
433            $storage_stats[] = [
434                'name' => $this->lang->lang('STORAGE_' . strtoupper($storage->get_name()) . '_TITLE'),
435                'files' => $storage->total_files(),
436                'size' => get_formatted_filesize($storage->total_size()),
437                'free_space' => $free_space,
438            ];
439        }
440
441        $this->template->assign_vars([
442            'STORAGE_STATS' => $storage_stats,
443        ]);
444    }
445
446    /**
447     * Display progress page
448     */
449    protected function display_progress_page() : void
450    {
451        $u_action = append_sid($this->u_action . '&amp;action=update&amp;hash=' . generate_link_hash('acp_storage'));
452        meta_refresh(1, $u_action);
453
454        adm_page_header($this->lang->lang('STORAGE_UPDATE_IN_PROGRESS'));
455        $this->template->set_filenames([
456                'body'    => 'acp_storage_update_progress.html'
457        ]);
458
459        $this->template->assign_vars([
460                'INDEXING_TITLE'        => $this->lang->lang('STORAGE_UPDATE_IN_PROGRESS'),
461                'INDEXING_EXPLAIN'        => $this->lang->lang('STORAGE_UPDATE_IN_PROGRESS_EXPLAIN'),
462                'INDEXING_PROGRESS_BAR'    => $this->get_storage_update_progress(),
463        ]);
464        adm_page_footer();
465    }
466
467    /**
468     * Get storage update progress to show progress bar
469     *
470     * @return array
471     */
472    protected function get_storage_update_progress(): array
473    {
474        $file_index = $this->state_helper->file_index();
475        $stage_is_copy = $this->state_helper->storage_index() < count($this->state_helper->storages());
476        $storage_name = $this->state_helper->storages()[$stage_is_copy ? $this->state_helper->storage_index() : $this->state_helper->remove_storage_index()];
477
478        $sql = 'SELECT COUNT(file_id) as done_count
479            FROM ' . $this->storage_table . '
480            WHERE file_id <= ' . $file_index . "
481                AND storage = '" . $this->db->sql_escape($storage_name) . "'";
482        $result = $this->db->sql_query($sql);
483        $done_count = (int) $this->db->sql_fetchfield('done_count');
484        $this->db->sql_freeresult($result);
485
486        $sql = 'SELECT COUNT(file_id) as remain_count
487            FROM ' . $this->storage_table . "
488            WHERE file_id > ' . $file_index . '
489                AND storage = '" . $this->db->sql_escape($storage_name) . "'";
490        $result = $this->db->sql_query($sql);
491        $remain_count = (int) $this->db->sql_fetchfield('remain_count');
492        $this->db->sql_freeresult($result);
493
494        $total_count = $done_count + $remain_count;
495        $percent = $total_count > 0 ? $done_count / $total_count : 0;
496
497        $steps = $this->state_helper->storage_index() + $this->state_helper->remove_storage_index() + $percent;
498        $multiplier = $this->state_helper->update_type() === update_type::MOVE ? 2 : 1;
499        $steps_total = count($this->state_helper->storages()) * $multiplier;
500
501        return [
502            'VALUE'            => $steps,
503            'TOTAL'            => $steps_total,
504            'PERCENTAGE'    => $steps / $steps_total * 100,
505        ];
506    }
507
508    /**
509     * Validates data
510     *
511     * @param string $storage_name Storage name
512     * @param array $messages Reference to messages array
513     */
514    protected function validate_data(string $storage_name, array &$messages): void
515    {
516        $storage_title = $this->lang->lang('STORAGE_' . strtoupper($storage_name) . '_TITLE');
517
518        // Check if provider exists
519        try
520        {
521            $new_provider = $this->provider_collection->get_by_class($this->request->variable([$storage_name, 'provider'], ''));
522        }
523        catch (\Exception $e)
524        {
525            $messages[] = $this->lang->lang('STORAGE_PROVIDER_NOT_EXISTS', $storage_title);
526            return;
527        }
528
529        // Check if provider is available
530        if (!$new_provider->is_available())
531        {
532            $messages[] = $this->lang->lang('STORAGE_PROVIDER_NOT_AVAILABLE', $storage_title);
533            return;
534        }
535
536        $this->validate_path($storage_name, $messages);
537
538        // Check options
539        $new_provider = $this->provider_collection->get_by_class($this->request->variable([$storage_name, 'provider'], ''));
540
541        foreach ($new_provider->get_options() as $definition_key => $definition_value)
542        {
543
544            $definition_title = $definition_value['title'];
545            $value = $this->request->variable([$storage_name, $definition_key], '');
546
547            switch ($definition_value['form_macro']['tag'])
548            {
549                case 'text':
550                    if ($definition_value['form_macro']['type'] === 'email' && filter_var($value, FILTER_VALIDATE_EMAIL))
551                    {
552                        $messages[] = $this->lang->lang('STORAGE_FORM_TYPE_EMAIL_INCORRECT_FORMAT', $definition_title, $storage_title);
553                    }
554
555                    $maxlength = $definition_value['form_macro']['max'] ?? 255;
556                    if (strlen($value) > $maxlength)
557                    {
558                        $messages[] = $this->lang->lang('STORAGE_FORM_TYPE_TEXT_TOO_LONG', $definition_title, $storage_title);
559                    }
560
561                    if ($new_provider->get_name() === 'local' && $definition_key === 'path')
562                    {
563                        $path = $value;
564
565                        if (empty($path))
566                        {
567                            $messages[] = $this->lang->lang('STORAGE_PATH_NOT_SET', $this->lang->lang('STORAGE_' . strtoupper($storage_name) . '_TITLE'));
568                        }
569                        else if (!$this->filesystem->exists($this->phpbb_root_path . $path) || !$this->filesystem->is_writable($this->phpbb_root_path . $path))
570                        {
571                            $messages[] = $this->lang->lang('STORAGE_PATH_NOT_EXISTS', $this->lang->lang('STORAGE_' . strtoupper($storage_name) . '_TITLE'));
572                        }
573                    }
574                break;
575
576                case 'radio':
577                    $found = false;
578                    foreach ($definition_value['form_macro']['buttons'] as $button)
579                    {
580                        if ($button['value'] == $value)
581                        {
582                            $found = true;
583                            break;
584                        }
585                    }
586
587                    if (!$found)
588                    {
589                        $messages[] = $this->lang->lang('STORAGE_FORM_TYPE_SELECT_NOT_AVAILABLE', $definition_title, $storage_title);
590                    }
591                break;
592
593                case 'select':
594                    $found = false;
595                    foreach ($definition_value['form_macro']['options'] as $option)
596                    {
597                        if ($option['value'] == $value)
598                        {
599                            $found = true;
600                            break;
601                        }
602                    }
603
604                    if (!$found)
605                    {
606                        $messages[] = $this->lang->lang('STORAGE_FORM_TYPE_SELECT_NOT_AVAILABLE', $definition_title, $storage_title);
607                    }
608                break;
609            }
610        }
611    }
612
613    /**
614     * Validates path when the filesystem is local
615     *
616     * @param string $storage_name Storage name
617     * @param array $messages Error messages array
618     * @return void
619     */
620    protected function validate_path(string $storage_name, array &$messages) : void
621    {
622        if ($this->request->is_set_post('submit'))
623        {
624            $provider = $this->request->variable([$storage_name, 'provider'], '');
625        }
626        else
627        {
628            $provider = $this->storage_helper->get_current_provider($storage_name);
629        }
630
631        $options = $this->storage_helper->get_provider_options($provider);
632
633        if ($this->provider_collection->get_by_class($provider)->get_name() === 'local' && isset($options['path']))
634        {
635            $path = $this->request->is_set_post('submit') ? $this->request->variable([$storage_name, 'path'], '') : $this->storage_helper->get_current_definition($storage_name, 'path');
636
637            if (empty($path))
638            {
639                $messages[] = $this->lang->lang('STORAGE_PATH_NOT_SET', $this->lang->lang('STORAGE_' . strtoupper($storage_name) . '_TITLE'));
640            }
641            else if (!$this->filesystem->exists($this->phpbb_root_path . $path) || !$this->filesystem->is_writable($this->phpbb_root_path . $path))
642            {
643                $messages[] = $this->lang->lang('STORAGE_PATH_NOT_EXISTS', $this->lang->lang('STORAGE_' . strtoupper($storage_name) . '_TITLE'));
644            }
645        }
646    }
647
648}