Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 239
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 / 237
0.00% covered (danger)
0.00%
0 / 11
5402
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 / 46
0.00% covered (danger)
0.00%
0 / 1
462
 validate_path
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
56
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            $options = $this->storage_helper->get_provider_options($this->storage_helper->get_current_provider($storage_name));
382
383            $modified = false;
384
385            // Check if provider have been modified
386            if ($this->request->variable([$storage_name, 'provider'], '') != $this->storage_helper->get_current_provider($storage_name))
387            {
388                $modified = true;
389            }
390            else
391            {
392                // Check if options have been modified
393                foreach (array_keys($options) as $definition)
394                {
395                    if ($this->request->variable([$storage_name, $definition], '') != $this->storage_helper->get_current_definition($storage_name, $definition))
396                    {
397                        $modified = true;
398                        break;
399                    }
400                }
401            }
402
403            if ($modified)
404            {
405                $modified_storages[] = $storage_name;
406            }
407        }
408
409        return $modified_storages;
410    }
411
412    /**
413     * Fill template variables to show storage stats in settings page
414     *
415     * @return void
416     */
417    protected function storage_stats(): void
418    {
419        // Top table with stats of each storage
420        $storage_stats = [];
421        foreach ($this->storage_collection as $storage)
422        {
423            try
424            {
425                $free_space = get_formatted_filesize($storage->free_space());
426            }
427            catch (storage_exception $e)
428            {
429                $free_space = $this->lang->lang('STORAGE_UNKNOWN');
430            }
431
432            $storage_stats[] = [
433                'name' => $this->lang->lang('STORAGE_' . strtoupper($storage->get_name()) . '_TITLE'),
434                'files' => $storage->get_num_files(),
435                'size' => get_formatted_filesize($storage->get_size()),
436                'free_space' => $free_space,
437            ];
438        }
439
440        $this->template->assign_vars([
441            'STORAGE_STATS' => $storage_stats,
442        ]);
443    }
444
445    /**
446     * Display progress page
447     */
448    protected function display_progress_page() : void
449    {
450        $u_action = append_sid($this->u_action . '&amp;action=update&amp;hash=' . generate_link_hash('acp_storage'));
451        meta_refresh(1, $u_action);
452
453        adm_page_header($this->lang->lang('STORAGE_UPDATE_IN_PROGRESS'));
454        $this->template->set_filenames([
455                'body'    => 'acp_storage_update_progress.html'
456        ]);
457
458        $this->template->assign_vars([
459                'INDEXING_TITLE'        => $this->lang->lang('STORAGE_UPDATE_IN_PROGRESS'),
460                'INDEXING_EXPLAIN'        => $this->lang->lang('STORAGE_UPDATE_IN_PROGRESS_EXPLAIN'),
461                'INDEXING_PROGRESS_BAR'    => $this->get_storage_update_progress(),
462        ]);
463        adm_page_footer();
464    }
465
466    /**
467     * Get storage update progress to show progress bar
468     *
469     * @return array
470     */
471    protected function get_storage_update_progress(): array
472    {
473        $file_index = $this->state_helper->file_index();
474        $stage_is_copy = $this->state_helper->storage_index() < count($this->state_helper->storages());
475        $storage_name = $this->state_helper->storages()[$stage_is_copy ? $this->state_helper->storage_index() : $this->state_helper->remove_storage_index()];
476
477        $sql = 'SELECT COUNT(file_id) as done_count
478            FROM ' . $this->storage_table . '
479            WHERE file_id <= ' . $file_index . "
480                AND storage = '" . $this->db->sql_escape($storage_name) . "'";
481        $result = $this->db->sql_query($sql);
482        $done_count = (int) $this->db->sql_fetchfield('done_count');
483        $this->db->sql_freeresult($result);
484
485        $sql = 'SELECT COUNT(file_id) as remain_count
486            FROM ' . $this->storage_table . "
487            WHERE file_id > ' . $file_index . '
488                AND storage = '" . $this->db->sql_escape($storage_name) . "'";
489        $result = $this->db->sql_query($sql);
490        $remain_count = (int) $this->db->sql_fetchfield('remain_count');
491        $this->db->sql_freeresult($result);
492
493        $total_count = $done_count + $remain_count;
494        $percent = $total_count > 0 ? $done_count / $total_count : 0;
495
496        $steps = $this->state_helper->storage_index() + $this->state_helper->remove_storage_index() + $percent;
497        $multiplier = $this->state_helper->update_type() === update_type::MOVE ? 2 : 1;
498        $steps_total = count($this->state_helper->storages()) * $multiplier;
499
500        return [
501            'VALUE'            => $steps,
502            'TOTAL'            => $steps_total,
503            'PERCENTAGE'    => $steps / $steps_total * 100,
504        ];
505    }
506
507    /**
508     * Validates data
509     *
510     * @param string $storage_name Storage name
511     * @param array $messages Reference to messages array
512     */
513    protected function validate_data(string $storage_name, array &$messages): void
514    {
515        $storage_title = $this->lang->lang('STORAGE_' . strtoupper($storage_name) . '_TITLE');
516
517        // Check if provider exists
518        try
519        {
520            $new_provider = $this->provider_collection->get_by_class($this->request->variable([$storage_name, 'provider'], ''));
521        }
522        catch (\Exception $e)
523        {
524            $messages[] = $this->lang->lang('STORAGE_PROVIDER_NOT_EXISTS', $storage_title);
525            return;
526        }
527
528        // Check if provider is available
529        if (!$new_provider->is_available())
530        {
531            $messages[] = $this->lang->lang('STORAGE_PROVIDER_NOT_AVAILABLE', $storage_title);
532            return;
533        }
534
535        $this->validate_path($storage_name, $messages);
536
537        // Check options
538        $new_options = $this->storage_helper->get_provider_options($this->request->variable([$storage_name, 'provider'], ''));
539
540        foreach ($new_options as $definition_key => $definition_value)
541        {
542            $provider = $this->provider_collection->get_by_class($this->request->variable([$storage_name, 'provider'], ''));
543            $definition_title = $this->lang->lang('STORAGE_ADAPTER_' . strtoupper($provider->get_name()) . '_OPTION_' . strtoupper($definition_key));
544
545            $value = $this->request->variable([$storage_name, $definition_key], '');
546
547            switch ($definition_value['tag'])
548            {
549                case 'text':
550                    if ($definition_value['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['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 ($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['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['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        $current_provider = $this->storage_helper->get_current_provider($storage_name);
623        $options = $this->storage_helper->get_provider_options($current_provider);
624
625        if ($this->provider_collection->get_by_class($current_provider)->get_name() == 'local' && isset($options['path']))
626        {
627            $path = $this->request->is_set_post('submit') ? $this->request->variable([$storage_name, 'path'], '') : $this->storage_helper->get_current_definition($storage_name, 'path');
628
629            if (empty($path))
630            {
631                $messages[] = $this->lang->lang('STORAGE_PATH_NOT_SET', $this->lang->lang('STORAGE_' . strtoupper($storage_name) . '_TITLE'));
632            }
633            else if (!$this->filesystem->exists($this->phpbb_root_path . $path) || !$this->filesystem->is_writable($this->phpbb_root_path . $path))
634            {
635                $messages[] = $this->lang->lang('STORAGE_PATH_NOT_EXISTS', $this->lang->lang('STORAGE_' . strtoupper($storage_name) . '_TITLE'));
636            }
637        }
638    }
639
640}