diff --git a/.gitignore b/.gitignore index 69f93652be..6972ca1516 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,7 @@ # Excludes test / dev files /phpunit.xml +/.phpunit.result.cache /phpBB/composer.phar /tests/phpbb_unit_tests.sqlite* /tests/test_config*.php diff --git a/phpBB/adm/style/acp_storage.html b/phpBB/adm/style/acp_storage.html index 3886f006d1..86c9298828 100644 --- a/phpBB/adm/style/acp_storage.html +++ b/phpBB/adm/style/acp_storage.html @@ -16,31 +16,32 @@ - {% for storage in STORAGE_STATS %} - - {{ storage.name }} - {{ storage.files }} - {{ storage.size }} - {{ storage.free_space }} - - {% endfor %} + {% for storage in STORAGE_STATS %} + + {{ storage.name }} + {{ storage.files }} + {{ storage.size }} + {{ storage.free_space }} + + {% endfor %} -{% if S_ERROR %} +{% if ERROR_MESSAGES is not empty %}

{{ lang('WARNING') }}

-

{{ ERROR_MSG }}

+ {% for ERROR_MESSAGE in ERROR_MESSAGES %} +

{{ ERROR_MESSAGE }}

+ {% endfor %}
{% endif %}
- {% for storage in STORAGES %}
{{ lang('STORAGE_' ~ storage.get_name | upper ~ '_TITLE') }}
-

{{ lang('STORAGE_SELECT_DESC') }}
+

{{ lang('STORAGE_SELECT_DESC') }}
- {% elseif input_type == 'textarea' %} - - {% elseif input_type == 'radio' %} - {% for option_name, option_value in options['options'] %} - {{ lang(option_name) }} + {% set input_name = storage.get_name ~ '[' ~ name ~ ']' %} + {% set input_value = attribute(config, 'storage\\' ~ storage.get_name ~ '\\config\\' ~ name) %} + + {% if options.tag == 'input' %} + {{ FormsInput(options | merge({"name": input_name, "value": input_value})) }} + {% elseif options.tag == 'textarea' %} + {{ FormsTextarea(options | merge({"name": input_name, "content": input_value})) }} + {% elseif options.tag == 'radio' %} + {% set buttons = [] %} + + {% for button in options.buttons %} + {% set new_button = button | merge({"name": input_name, "checked": button.value == input_value}) %} + {% set buttons = buttons | merge([new_button]) %} {% endfor %} - {% elseif input_type == 'select' %} - + + {{ FormsRadioButtons(options | merge({"buttons": buttons})) }} + {% elseif options.tag == 'select' %} + {% set select_options = [] %} + + {% for option in options.options %} + {% set new_option = option | merge({"selected": option.value == input_value}) %} + {% set select_options = select_options | merge([new_option]) %} + {% endfor %} + + {{ FormsSelect(options | merge({"name": input_name, "options": select_options})) }} {% endif %}
@@ -97,6 +105,17 @@ {% endfor %} {% endfor %} +
+
+
+
+ + + +
+
+
+
{{ lang('SUBMIT') }}   diff --git a/phpBB/adm/style/acp_storage_update_inprogress.html b/phpBB/adm/style/acp_storage_update_inprogress.html new file mode 100644 index 0000000000..5f7614cb2c --- /dev/null +++ b/phpBB/adm/style/acp_storage_update_inprogress.html @@ -0,0 +1,32 @@ +{% include 'overall_header.html' %} + + + +

{{ lang('STORAGE_TITLE') }}

+ +

{{ lang('STORAGE_TITLE_EXPLAIN') }}

+ + +
+ {{ lang('SUBMIT') }} + + {% if CONTINUE_PROGRESS %} +
+
+
+ {{ CONTINUE_PROGRESS.PERCENTAGE|number_format(2) ~ ' %' }} +
+ {% endif %} + +

+   + +

+ {{ S_FORM_TOKEN }} +
+ + +{% include 'overall_footer.html' %} diff --git a/phpBB/adm/style/acp_storage_update_progress.html b/phpBB/adm/style/acp_storage_update_progress.html new file mode 100644 index 0000000000..17ba67a037 --- /dev/null +++ b/phpBB/adm/style/acp_storage_update_progress.html @@ -0,0 +1,20 @@ +{% include 'overall_header.html' %} + + + +
+

{{ INDEXING_TITLE }}

+

+ {{ INDEXING_EXPLAIN }} + {% if INDEXING_PROGRESS_BAR %} +
+
+ {{ INDEXING_PROGRESS_BAR.PERCENTAGE|number_format(2) ~ ' %' }} + {% endif %} +

+
+ +{% include 'overall_footer.html' %} diff --git a/phpBB/config/default/container/services_storage.yml b/phpBB/config/default/container/services_storage.yml index 4ef28ccf3b..496c30de5d 100644 --- a/phpBB/config/default/container/services_storage.yml +++ b/phpBB/config/default/container/services_storage.yml @@ -107,3 +107,20 @@ services: - '@storage.attachment' - '@symfony_request' - '@user' + +# Helpers + storage.state_helper: + class: phpbb\storage\state_helper + arguments: + - '@config' + - '@config_text' + - '@storage.provider_collection' + + storage.helper: + class: phpbb\storage\helper + arguments: + - '@config' + - '@storage.adapter.factory' + - '@storage.state_helper' + - '@storage.provider_collection' + - '@storage.adapter_collection' diff --git a/phpBB/includes/acp/acp_database.php b/phpBB/includes/acp/acp_database.php index 7b8dcc3ee5..85943dc5e4 100644 --- a/phpBB/includes/acp/acp_database.php +++ b/phpBB/includes/acp/acp_database.php @@ -287,7 +287,7 @@ class acp_database fclose($fp); fclose($stream); } - catch (\phpbb\storage\exception\exception $e) + catch (\phpbb\storage\exception\storage_exception $e) { trigger_error($user->lang['RESTORE_DOWNLOAD_FAIL'] . adm_back_link($this->u_action)); } diff --git a/phpBB/includes/acp/acp_storage.php b/phpBB/includes/acp/acp_storage.php index 3ea1036167..2adf833562 100644 --- a/phpBB/includes/acp/acp_storage.php +++ b/phpBB/includes/acp/acp_storage.php @@ -11,6 +11,18 @@ * */ +use phpbb\db\driver\driver_interface; +use phpbb\di\service_collection; +use phpbb\language\language; +use phpbb\log\log_interface; +use phpbb\request\request; +use phpbb\storage\exception\storage_exception; +use phpbb\storage\helper; +use phpbb\storage\state_helper; +use phpbb\storage\update_type; +use phpbb\template\template; +use phpbb\user; + /** * @ignore */ @@ -21,22 +33,28 @@ if (!defined('IN_PHPBB')) class acp_storage { - /** @var \phpbb\config\config $config */ - protected $config; + /** @var driver_interface */ + protected $db; - /** @var \phpbb\language\language $lang */ + /** @var language */ protected $lang; - /** @var \phpbb\request\request */ + /** @var log_interface */ + protected $log; + + /** @var request */ protected $request; - /** @var \phpbb\template\template */ + /** @var template */ protected $template; - /** @var \phpbb\di\service_collection */ + /** @var user */ + protected $user; + + /** @var service_collection */ protected $provider_collection; - /** @var \phpbb\di\service_collection */ + /** @var service_collection */ protected $storage_collection; /** @var \phpbb\filesystem\filesystem */ @@ -54,133 +72,359 @@ class acp_storage /** @var string */ public $u_action; + /** @var state_helper */ + private $state_helper; + + /** @var helper */ + private $storage_helper; + + /** @var string */ + private $storage_table; + /** * @param string $id * @param string $mode */ - public function main($id, $mode) + public function main(string $id, string $mode): void { global $phpbb_container, $phpbb_dispatcher, $phpbb_root_path; - $this->config = $phpbb_container->get('config'); - $this->filesystem = $phpbb_container->get('filesystem'); + $this->db = $phpbb_container->get('dbal.conn'); $this->lang = $phpbb_container->get('language'); + $this->log = $phpbb_container->get('log'); $this->request = $phpbb_container->get('request'); $this->template = $phpbb_container->get('template'); + $this->user = $phpbb_container->get('user'); $this->provider_collection = $phpbb_container->get('storage.provider_collection'); $this->storage_collection = $phpbb_container->get('storage.storage_collection'); + $this->filesystem = $phpbb_container->get('filesystem'); $this->phpbb_root_path = $phpbb_root_path; + $this->state_helper = $phpbb_container->get('storage.state_helper'); + $this->storage_helper = $phpbb_container->get('storage.helper'); + $this->storage_table = $phpbb_container->getParameter('tables.storage'); - // Add necesary language files + // Add necessary language files $this->lang->add_lang(['acp/storage']); /** * Add language strings * * @event core.acp_storage_load - * @since 3.3.0-a1 + * @since 4.0.0-a1 */ $phpbb_dispatcher->trigger_event('core.acp_storage_load'); - $this->overview($id, $mode); + switch ($mode) + { + case 'settings': + $this->settings($id, $mode); + break; + } } /** + * Method to route the request to the correct page + * * @param string $id * @param string $mode */ - public function overview($id, $mode) + private function settings(string $id, string $mode): void + { + $action = $this->request->variable('action', ''); + if ($action && !$this->request->is_set_post('cancel')) + { + switch ($action) + { + case 'update': + $this->update_action(); + break; + + default: + trigger_error('NO_ACTION', E_USER_ERROR); + } + } + else + { + // If clicked to cancel (acp_storage_update_progress form) + if ($this->request->is_set_post('cancel')) + { + $this->state_helper->clear_state(); + } + + // There is an updating in progress, show the form to continue or cancel + if ($this->state_helper->is_action_in_progress()) + { + $this->update_inprogress(); + } + else + { + $this->settings_form(); + } + } + } + + /** + * Page to update storage settings and move files + * + * @return void + */ + private function update_action(): void + { + if (!check_link_hash($this->request->variable('hash', ''), 'acp_storage')) + { + trigger_error($this->lang->lang('FORM_INVALID') . adm_back_link($this->u_action), E_USER_WARNING); + } + + // If update_type is copy or move, copy files from the old to the new storage + if (in_array($this->state_helper->update_type(), [update_type::COPY, update_type::MOVE], true)) + { + $i = 0; + foreach ($this->state_helper->storages() as $storage_name) + { + // Skip storages that have already copied files + if ($this->state_helper->storage_index() > $i++) + { + continue; + } + + $sql = 'SELECT file_id, file_path + FROM ' . $this->storage_table . " + WHERE storage = '" . $this->db->sql_escape($storage_name) . "' + AND file_id > " . $this->state_helper->file_index(); + $result = $this->db->sql_query($sql); + + while ($row = $this->db->sql_fetchrow($result)) + { + if (!still_on_time()) + { + $this->db->sql_freeresult($result); + $this->display_progress_page(); + return; + } + + // Copy file from old adapter to the new one + $this->storage_helper->copy_file_to_new_adapter($storage_name, $row['file_path']); + + $this->state_helper->set_file_index($row['file_id']); // update last file index copied + } + + $this->db->sql_freeresult($result); + + // Copied all files of a storage, increase storage index and reset file index + $this->state_helper->set_storage_index($this->state_helper->storage_index()+1); + $this->state_helper->set_file_index(0); + } + + // If update_type is move files, remove the old files + if ($this->state_helper->update_type() === update_type::MOVE) + { + $i = 0; + foreach ($this->state_helper->storages() as $storage_name) + { + // Skip storages that have already moved files + if ($this->state_helper->remove_storage_index() > $i++) + { + continue; + } + + $sql = 'SELECT file_id, file_path + FROM ' . $this->storage_table . " + WHERE storage = '" . $this->db->sql_escape($storage_name) . "' + AND file_id > " . $this->state_helper->file_index(); + $result = $this->db->sql_query($sql); + + while ($row = $this->db->sql_fetchrow($result)) + { + if (!still_on_time()) + { + $this->db->sql_freeresult($result); + $this->display_progress_page(); + return; + } + + // remove file from old (current) adapter + $current_adapter = $this->storage_helper->get_current_adapter($storage_name); + $current_adapter->delete($row['file_path']); + + $this->state_helper->set_file_index($row['file_id']); + } + + $this->db->sql_freeresult($result); + + // Remove all files of a storage, increase storage index and reset file index + $this->state_helper->set_remove_storage_index($this->state_helper->remove_storage_index() + 1); + $this->state_helper->set_file_index(0); + } + } + } + + // Here all files have been copied/moved, so save new configuration + foreach ($this->state_helper->storages() as $storage_name) + { + $this->storage_helper->update_storage_config($storage_name); + } + + $storages = $this->state_helper->storages(); + $this->state_helper->clear_state(); + $this->log->add('admin', $this->user->data['user_id'], $this->user->ip, 'LOG_STORAGE_UPDATE', false, [implode(', ', $storages)]); + trigger_error($this->lang->lang('STORAGE_UPDATE_SUCCESSFUL') . adm_back_link($this->u_action)); + } + + /** + * Page that show a form with the progress bar, and a button to continue or cancel + * + * @return void + */ + private function update_inprogress(): void + { + // Template from adm/style + $this->tpl_name = 'acp_storage_update_inprogress'; + + // Set page title + $this->page_title = 'STORAGE_TITLE'; + + $this->template->assign_vars([ + 'U_ACTION' => $this->u_action . '&action=update&hash=' . generate_link_hash('acp_storage'), + 'CONTINUE_PROGRESS' => $this->get_storage_update_progress(), + ]); + } + + /** + * Main settings page, shows a form with all the storages and their configuration options + * + * @return void + */ + private function settings_form(): void { $form_key = 'acp_storage'; add_form_key($form_key); + // Process form and create a "state" for the update, + // then show a confirm form + if ($this->request->is_set_post('submit')) + { + if (!check_form_key($form_key) || !check_link_hash($this->request->variable('hash', ''), 'acp_storage')) + { + trigger_error($this->lang->lang('FORM_INVALID') . adm_back_link($this->u_action), E_USER_WARNING); + } + + $modified_storages = $this->get_modified_storages(); + + // validate submited paths if they are local + $messages = []; + foreach ($modified_storages as $storage_name) + { + $this->validate_data($storage_name, $messages); + } + if (!empty($messages)) + { + trigger_error(implode('
', $messages) . adm_back_link($this->u_action), E_USER_WARNING); + } + + // Start process and show progress + if (!empty($modified_storages)) + { + // Create state + $this->state_helper->init(update_type::from((int) $this->request->variable('update_type', update_type::CONFIG->value)), $modified_storages, $this->request); + + // Start displaying progress on first submit + $this->display_progress_page(); + return; + } + + // If there is no changes + trigger_error($this->lang->lang('STORAGE_NO_CHANGES') . adm_back_link($this->u_action), E_USER_WARNING); + } + // Template from adm/style $this->tpl_name = 'acp_storage'; // Set page title $this->page_title = 'STORAGE_TITLE'; + $this->storage_stats(); // Show table with storage stats + + // Validate local paths to check if everything is fine $messages = []; - if ($this->request->is_set_post('submit')) + foreach ($this->storage_collection as $storage) { - $modified_storages = []; - - if (!check_form_key($form_key)) - { - $messages[] = $this->lang->lang('FORM_INVALID'); - } - - foreach ($this->storage_collection as $storage) - { - $storage_name = $storage->get_name(); - - $options = $this->get_provider_options($this->get_current_provider($storage_name)); - - $this->validate_path($storage_name, $options, $messages); - - $modified = false; - - // Check if provider have been modified - if ($this->get_new_provider($storage_name) != $this->get_current_provider($storage_name)) - { - $modified = true; - } - - // Check if options have been modified - if (!$modified) - { - foreach (array_keys($options) as $definition) - { - if ($this->get_new_definition($storage_name, $definition) != $this->get_current_definition($storage_name, $definition)) - { - $modified = true; - break; - } - } - } - - // If the storage have been modified, validate options - if ($modified) - { - $modified_storages[] = $storage_name; - $this->validate_data($storage_name, $messages); - } - } - - if (!empty($modified_storages)) - { - if (empty($messages)) - { - foreach ($modified_storages as $storage_name) - { - $this->update_storage_config($storage_name); - } - - trigger_error($this->lang->lang('STORAGE_UPDATE_SUCCESSFUL') . adm_back_link($this->u_action), E_USER_NOTICE); - } - else - { - trigger_error(implode('
', $messages) . adm_back_link($this->u_action), E_USER_WARNING); - } - } - - // If there is no changes - trigger_error($this->lang->lang('STORAGE_NO_CHANGES') . adm_back_link($this->u_action), E_USER_WARNING); + $this->validate_path($storage->get_name(), $messages); } - $storage_stats = []; + $this->template->assign_vars([ + 'STORAGES' => $this->storage_collection, + 'PROVIDERS' => $this->provider_collection, + + 'ERROR_MESSAGES' => $messages, + + 'U_ACTION' => $this->u_action . '&hash=' . generate_link_hash('acp_storage'), + + 'STORAGE_UPDATE_TYPE_CONFIG' => update_type::CONFIG->value, + 'STORAGE_UPDATE_TYPE_COPY' => update_type::COPY->value, + 'STORAGE_UPDATE_TYPE_MOVE' => update_type::MOVE->value, + ]); + } + + /** + * When submit the settings form, check which storages have been modified + * to update only those. + * + * @return array + */ + private function get_modified_storages(): array + { + $modified_storages = []; + foreach ($this->storage_collection as $storage) { $storage_name = $storage->get_name(); - $options = $this->get_provider_options($this->get_current_provider($storage_name)); + $options = $this->storage_helper->get_provider_options($this->storage_helper->get_current_provider($storage_name)); - $this->validate_path($storage_name, $options, $messages); + $modified = false; + // Check if provider have been modified + if ($this->request->variable([$storage_name, 'provider'], '') != $this->storage_helper->get_current_provider($storage_name)) + { + $modified = true; + } + else + { + // Check if options have been modified + foreach (array_keys($options) as $definition) + { + if ($this->request->variable([$storage_name, $definition], '') != $this->storage_helper->get_current_definition($storage_name, $definition)) + { + $modified = true; + break; + } + } + } + + if ($modified) + { + $modified_storages[] = $storage_name; + } + } + + return $modified_storages; + } + + /** + * Fill template variables to show storage stats in settings page + * + * @return void + */ + protected function storage_stats(): void + { + // Top table with stats of each storage + $storage_stats = []; + foreach ($this->storage_collection as $storage) + { try { $free_space = get_formatted_filesize($storage->free_space()); } - catch (\phpbb\storage\exception\exception $e) + catch (storage_exception $e) { $free_space = $this->lang->lang('STORAGE_UNKNOWN'); } @@ -194,70 +438,70 @@ class acp_storage } $this->template->assign_vars([ - 'STORAGES' => $this->storage_collection, 'STORAGE_STATS' => $storage_stats, - 'PROVIDERS' => $this->provider_collection, - - 'ERROR_MSG' => implode('
', $messages), - 'S_ERROR' => !empty($messages), ]); } /** - * Get the current provider from config - * - * @param string $storage_name Storage name - * @return string The current provider + * Display progress page */ - protected function get_current_provider($storage_name) + protected function display_progress_page() : void { - return $this->config['storage\\' . $storage_name . '\\provider']; + $u_action = append_sid($this->u_action . '&action=update&hash=' . generate_link_hash('acp_storage')); + meta_refresh(1, $u_action); + + adm_page_header($this->lang->lang('STORAGE_UPDATE_IN_PROGRESS')); + $this->template->set_filenames([ + 'body' => 'acp_storage_update_progress.html' + ]); + + $this->template->assign_vars([ + 'INDEXING_TITLE' => $this->lang->lang('STORAGE_UPDATE_IN_PROGRESS'), + 'INDEXING_EXPLAIN' => $this->lang->lang('STORAGE_UPDATE_IN_PROGRESS_EXPLAIN'), + 'INDEXING_PROGRESS_BAR' => $this->get_storage_update_progress(), + ]); + adm_page_footer(); } /** - * Get the new provider from the request + * Get storage update progress to show progress bar * - * @param string $storage_name Storage name - * @return string The new provider + * @return array */ - protected function get_new_provider($storage_name) + protected function get_storage_update_progress(): array { - return $this->request->variable([$storage_name, 'provider'], ''); - } + $file_index = $this->state_helper->file_index(); + $stage_is_copy = $this->state_helper->storage_index() < count($this->state_helper->storages()); + $storage_name = $this->state_helper->storages()[$stage_is_copy ? $this->state_helper->storage_index() : $this->state_helper->remove_storage_index()]; - /** - * Get adapter definitions from a provider - * - * @param string $provider Provider class - * @return array Adapter definitions - */ - protected function get_provider_options($provider) - { - return $this->provider_collection->get_by_class($provider)->get_options(); - } + $sql = 'SELECT COUNT(file_id) as done_count + FROM ' . $this->storage_table . ' + WHERE file_id <= ' . $file_index . " + AND storage = '" . $this->db->sql_escape($storage_name) . "'"; + $result = $this->db->sql_query($sql); + $done_count = (int) $this->db->sql_fetchfield('done_count'); + $this->db->sql_freeresult($result); - /** - * Get the current value of the definition of a storage from config - * - * @param string $storage_name Storage name - * @param string $definition Definition - * @return string Definition value - */ - protected function get_current_definition($storage_name, $definition) - { - return $this->config['storage\\' . $storage_name . '\\config\\' . $definition]; - } + $sql = 'SELECT COUNT(file_id) as remain_count + FROM ' . $this->storage_table . " + WHERE file_id > ' . $file_index . ' + AND storage = '" . $this->db->sql_escape($storage_name) . "'"; + $result = $this->db->sql_query($sql); + $remain_count = (int) $this->db->sql_fetchfield('remain_count'); + $this->db->sql_freeresult($result); - /** - * Get the new value of the definition of a storage from the request - * - * @param string $storage_name Storage name - * @param string $definition Definition - * @return string Definition value - */ - protected function get_new_definition($storage_name, $definition) - { - return $this->request->variable([$storage_name, $definition], ''); + $total_count = $done_count + $remain_count; + $percent = $total_count > 0 ? $done_count / $total_count : 0; + + $steps = $this->state_helper->storage_index() + $this->state_helper->remove_storage_index() + $percent; + $multiplier = $this->state_helper->update_type() === update_type::MOVE ? 2 : 1; + $steps_total = count($this->state_helper->storages()) * $multiplier; + + return [ + 'VALUE' => $steps, + 'TOTAL' => $steps_total, + 'PERCENTAGE' => $steps / $steps_total * 100, + ]; } /** @@ -266,14 +510,14 @@ class acp_storage * @param string $storage_name Storage name * @param array $messages Reference to messages array */ - protected function validate_data($storage_name, &$messages) + protected function validate_data(string $storage_name, array &$messages): void { $storage_title = $this->lang->lang('STORAGE_' . strtoupper($storage_name) . '_TITLE'); // Check if provider exists try { - $new_provider = $this->provider_collection->get_by_class($this->get_new_provider($storage_name)); + $new_provider = $this->provider_collection->get_by_class($this->request->variable([$storage_name, 'provider'], '')); } catch (\Exception $e) { @@ -288,91 +532,109 @@ class acp_storage return; } + $this->validate_path($storage_name, $messages); + // Check options - $new_options = $this->get_provider_options($this->get_new_provider($storage_name)); + $new_options = $this->storage_helper->get_provider_options($this->request->variable([$storage_name, 'provider'], '')); foreach ($new_options as $definition_key => $definition_value) { - $provider = $this->provider_collection->get_by_class($this->get_new_provider($storage_name)); + $provider = $this->provider_collection->get_by_class($this->request->variable([$storage_name, 'provider'], '')); $definition_title = $this->lang->lang('STORAGE_ADAPTER_' . strtoupper($provider->get_name()) . '_OPTION_' . strtoupper($definition_key)); - $value = $this->get_new_definition($storage_name, $definition_key); + $value = $this->request->variable([$storage_name, $definition_key], ''); - switch ($definition_value['type']) + switch ($definition_value['tag']) { - case 'email': - if (!filter_var($value, FILTER_VALIDATE_EMAIL)) + case 'text': + if ($definition_value['type'] == 'email' && filter_var($value, FILTER_VALIDATE_EMAIL)) { $messages[] = $this->lang->lang('STORAGE_FORM_TYPE_EMAIL_INCORRECT_FORMAT', $definition_title, $storage_title); } - case 'text': - case 'password': - $maxlength = isset($definition_value['maxlength']) ? $definition_value['maxlength'] : 255; + + $maxlength = $definition_value['max'] ?? 255; if (strlen($value) > $maxlength) { $messages[] = $this->lang->lang('STORAGE_FORM_TYPE_TEXT_TOO_LONG', $definition_title, $storage_title); } - break; + + if ($provider->get_name() == 'local' && $definition_key == 'path') + { + $path = $value; + + if (empty($path)) + { + $messages[] = $this->lang->lang('STORAGE_PATH_NOT_SET', $this->lang->lang('STORAGE_' . strtoupper($storage_name) . '_TITLE')); + } + else if (!$this->filesystem->exists($this->phpbb_root_path . $path) || !$this->filesystem->is_writable($this->phpbb_root_path . $path)) + { + $messages[] = $this->lang->lang('STORAGE_PATH_NOT_EXISTS', $this->lang->lang('STORAGE_' . strtoupper($storage_name) . '_TITLE')); + } + } + break; + case 'radio': - case 'select': - if (!in_array($value, array_values($definition_value['options']))) + $found = false; + foreach ($definition_value['buttons'] as $button) + { + if ($button['value'] == $value) + { + $found = true; + break; + } + } + + if (!$found) { $messages[] = $this->lang->lang('STORAGE_FORM_TYPE_SELECT_NOT_AVAILABLE', $definition_title, $storage_title); } - break; + break; + + case 'select': + $found = false; + foreach ($definition_value['options'] as $option) + { + if ($option['value'] == $value) + { + $found = true; + break; + } + } + + if (!$found) + { + $messages[] = $this->lang->lang('STORAGE_FORM_TYPE_SELECT_NOT_AVAILABLE', $definition_title, $storage_title); + } + break; } } } /** - * Updates an storage with the info provided in the form + * Validates path when the filesystem is local * * @param string $storage_name Storage name - */ - protected function update_storage_config($storage_name) - { - $current_options = $this->get_provider_options($this->get_current_provider($storage_name)); - - // Remove old storage config - foreach (array_keys($current_options) as $definition) - { - $this->config->delete('storage\\' . $storage_name . '\\config\\' . $definition); - } - - // Update provider - $this->config->set('storage\\' . $storage_name . '\\provider', $this->get_new_provider($storage_name)); - - // Set new storage config - $new_options = $this->get_provider_options($this->get_new_provider($storage_name)); - - foreach (array_keys($new_options) as $definition) - { - $this->config->set('storage\\' . $storage_name . '\\config\\' . $definition, $this->get_new_definition($storage_name, $definition)); - } - } - - /** - * Validates path - * - * @param string $storage_name Storage name - * @param array $options Storage provider configuration keys - * @param array $messages Reference to error messages array + * @param array $messages Error messages array * @return void */ - protected function validate_path($storage_name, $options, &$messages) + protected function validate_path(string $storage_name, array &$messages) : void { - if ($this->provider_collection->get_by_class($this->get_current_provider($storage_name))->get_name() == 'local' && isset($options['path'])) + $current_provider = $this->storage_helper->get_current_provider($storage_name); + $options = $this->storage_helper->get_provider_options($current_provider); + + if ($this->provider_collection->get_by_class($current_provider)->get_name() == 'local' && isset($options['path'])) { - $path = $this->request->is_set_post('submit') ? $this->get_new_definition($storage_name, 'path') : $this->get_current_definition($storage_name, 'path'); + $path = $this->request->is_set_post('submit') ? $this->request->variable([$storage_name, 'path'], '') : $this->storage_helper->get_current_definition($storage_name, 'path'); if (empty($path)) { $messages[] = $this->lang->lang('STORAGE_PATH_NOT_SET', $this->lang->lang('STORAGE_' . strtoupper($storage_name) . '_TITLE')); } - else if (!$this->filesystem->is_writable($this->phpbb_root_path . $path) || !$this->filesystem->exists($this->phpbb_root_path . $path)) + else if (!$this->filesystem->exists($this->phpbb_root_path . $path) || !$this->filesystem->is_writable($this->phpbb_root_path . $path)) { $messages[] = $this->lang->lang('STORAGE_PATH_NOT_EXISTS', $this->lang->lang('STORAGE_' . strtoupper($storage_name) . '_TITLE')); } } } + } diff --git a/phpBB/includes/functions_user.php b/phpBB/includes/functions_user.php index c41e54098e..737332f4fa 100644 --- a/phpBB/includes/functions_user.php +++ b/phpBB/includes/functions_user.php @@ -1789,7 +1789,7 @@ function avatar_delete($mode, $row, $clean_db = false) return true; } - catch (\phpbb\storage\exception\exception $e) + catch (\phpbb\storage\exception\storage_exception $e) { // Fail is covered by return statement below } @@ -2131,7 +2131,7 @@ function group_correct_avatar($group_id, $old_entry) WHERE group_id = $group_id"; $db->sql_query($sql); } - catch (\phpbb\storage\exception\exception $e) + catch (\phpbb\storage\exception\storage_exception $e) { // If rename fail, dont execute the query } diff --git a/phpBB/language/en/acp/common.php b/phpBB/language/en/acp/common.php index bcbbe5d931..62c08f0226 100644 --- a/phpBB/language/en/acp/common.php +++ b/phpBB/language/en/acp/common.php @@ -768,6 +768,8 @@ $lang = array_merge($lang, array( 'LOG_STYLE_EDIT_DETAILS' => 'Edited style
» %s', 'LOG_STYLE_EXPORT' => 'Exported style
» %s', + 'LOG_STORAGE_UPDATE' => 'Storage updated
» %s', + 'LOG_UPDATE_DATABASE' => 'Updated Database from version %1$s to version %2$s', 'LOG_UPDATE_PHPBB' => 'Updated phpBB from version %1$s to version %2$s', diff --git a/phpBB/language/en/acp/storage.php b/phpBB/language/en/acp/storage.php index 98ad84df53..9f928ee58e 100644 --- a/phpBB/language/en/acp/storage.php +++ b/phpBB/language/en/acp/storage.php @@ -36,18 +36,26 @@ if (empty($lang) || !is_array($lang)) // equally where a string contains only two placeholders which are used to wrap text // in a url you again do not need to specify an order e.g., 'Click %sHERE%s' is fine -$lang = array_merge($lang, array( +$lang = array_merge($lang, [ // Template - 'STORAGE_TITLE' => 'Storage Settings', - 'STORAGE_TITLE_EXPLAIN' => 'Change storage providers for the file storage types of phpBB. Choose local or remote providers to store files added to or created by phpBB.', - 'STORAGE_SELECT' => 'Select storage', - 'STORAGE_SELECT_DESC' => 'Select a storage from the list.', - 'STORAGE_NAME' => 'Storage name', - 'STORAGE_NUM_FILES' => 'Number of files', - 'STORAGE_SIZE' => 'Size', - 'STORAGE_FREE' => 'Available space', - 'STORAGE_UNKNOWN' => 'Unknown', + 'STORAGE_TITLE' => 'Storage Settings', + 'STORAGE_TITLE_EXPLAIN' => 'Change storage providers for the file storage types of phpBB. Choose local or remote providers to store files added to or created by phpBB.', + 'STORAGE_SELECT' => 'Select storage', + 'STORAGE_SELECT_DESC' => 'Select a storage from the list.', + 'STORAGE_NAME' => 'Storage name', + 'STORAGE_NUM_FILES' => 'Number of files', + 'STORAGE_SIZE' => 'Size', + 'STORAGE_FREE' => 'Available space', + 'STORAGE_UNKNOWN' => 'Unknown', + 'STORAGE_UPDATE_TYPE' => 'Update type', + 'STORAGE_UPDATE_TYPE_CONFIG' => 'Update configuration only', + 'STORAGE_UPDATE_TYPE_COPY' => 'Update configuration and copy files', + 'STORAGE_UPDATE_TYPE_MOVE' => 'Update configuration and move files', + + // Template progress bar + 'STORAGE_UPDATE_IN_PROGRESS' => 'Storage update in progress', + 'STORAGE_UPDATE_IN_PROGRESS_EXPLAIN' => 'Files are being moved between storages. This can take some minutes.', // Storage names 'STORAGE_ATTACHMENT_TITLE' => 'Attachments storage', @@ -69,4 +77,4 @@ $lang = array_merge($lang, array( 'STORAGE_PATH_NOT_EXISTS' => '“%1$s” path does not exist or is not writable.', 'STORAGE_PATH_NOT_SET' => '“%1$s” path is not set.', -)); +]); diff --git a/phpBB/phpbb/attachment/delete.php b/phpBB/phpbb/attachment/delete.php index 80fd6b62d4..2620a7a94f 100644 --- a/phpBB/phpbb/attachment/delete.php +++ b/phpBB/phpbb/attachment/delete.php @@ -464,7 +464,7 @@ class delete return true; } } - catch (\phpbb\storage\exception\exception $exception) + catch (\phpbb\storage\exception\storage_exception $exception) { // Fail is covered by return statement below } diff --git a/phpBB/phpbb/attachment/upload.php b/phpBB/phpbb/attachment/upload.php index d5b961de5f..c0b0c490c4 100644 --- a/phpBB/phpbb/attachment/upload.php +++ b/phpBB/phpbb/attachment/upload.php @@ -351,7 +351,7 @@ class upload return false; } } - catch (\phpbb\storage\exception\exception $e) + catch (\phpbb\storage\exception\storage_exception $e) { // Do nothing } diff --git a/phpBB/phpbb/avatar/driver/upload.php b/phpBB/phpbb/avatar/driver/upload.php index 14b9e4026c..51425b506f 100644 --- a/phpBB/phpbb/avatar/driver/upload.php +++ b/phpBB/phpbb/avatar/driver/upload.php @@ -19,7 +19,7 @@ use phpbb\event\dispatcher_interface; use phpbb\files\factory; use phpbb\path_helper; use phpbb\routing\helper; -use phpbb\storage\exception\exception as storage_exception; +use phpbb\storage\exception\storage_exception; use phpbb\storage\storage; /** diff --git a/phpBB/phpbb/db/migration/data/v400/storage_track.php b/phpBB/phpbb/db/migration/data/v400/storage_track.php index 767eee5c0d..7cf05503d6 100644 --- a/phpBB/phpbb/db/migration/data/v400/storage_track.php +++ b/phpBB/phpbb/db/migration/data/v400/storage_track.php @@ -14,7 +14,7 @@ namespace phpbb\db\migration\data\v400; use phpbb\db\migration\container_aware_migration; -use phpbb\storage\exception\exception; +use phpbb\storage\exception\storage_exception; use phpbb\storage\storage; class storage_track extends container_aware_migration @@ -97,7 +97,7 @@ class storage_track extends container_aware_migration { $storage->track_file($this->config['avatar_salt'] . '_' . ($avatar_group ? 'g' : '') . $filename . '.' . $ext); } - catch (exception $e) + catch (storage_exception $e) { // If file doesn't exist, don't track it } @@ -121,7 +121,7 @@ class storage_track extends container_aware_migration { $storage->track_file($row['physical_filename']); } - catch (exception $e) + catch (storage_exception $e) { // If file doesn't exist, don't track it } @@ -132,7 +132,7 @@ class storage_track extends container_aware_migration { $storage->track_file('thumb_' . $row['physical_filename']); } - catch (exception $e) + catch (storage_exception $e) { // If file doesn't exist, don't track it } @@ -157,7 +157,7 @@ class storage_track extends container_aware_migration { $storage->track_file($row['filename']); } - catch (exception $e) + catch (storage_exception $e) { // If file doesn't exist, don't track it } diff --git a/phpBB/phpbb/files/filespec_storage.php b/phpBB/phpbb/files/filespec_storage.php index e775935756..83678718b5 100644 --- a/phpBB/phpbb/files/filespec_storage.php +++ b/phpBB/phpbb/files/filespec_storage.php @@ -453,7 +453,7 @@ class filespec_storage fclose($fp); } } - catch (\phpbb\storage\exception\exception $e) + catch (\phpbb\storage\exception\storage_exception $e) { $this->error[] = $this->language->lang($this->upload->error_prefix . 'GENERAL_UPLOAD_ERROR', $this->destination_file); $this->file_moved = false; diff --git a/phpBB/phpbb/storage/adapter/adapter_interface.php b/phpBB/phpbb/storage/adapter/adapter_interface.php index 5bd6525a73..725b954c2d 100644 --- a/phpBB/phpbb/storage/adapter/adapter_interface.php +++ b/phpBB/phpbb/storage/adapter/adapter_interface.php @@ -13,7 +13,7 @@ namespace phpbb\storage\adapter; -use phpbb\storage\exception\exception; +use phpbb\storage\exception\storage_exception; interface adapter_interface { @@ -29,7 +29,7 @@ interface adapter_interface * * @param string $path * @param string $content - * @throws exception When the file cannot be written + * @throws storage_exception When the file cannot be written */ public function put_contents(string $path, string $content): void; @@ -39,7 +39,7 @@ interface adapter_interface * @param string $path The file to read * * @return string Returns file contents - * @throws exception When cannot read file contents + * @throws storage_exception When cannot read file contents */ public function get_contents(string $path): string; @@ -57,7 +57,7 @@ interface adapter_interface * * @param string $path file/directory to remove * - * @throws exception When removal fails. + * @throws storage_exception When removal fails. */ public function delete(string $path): void; @@ -67,7 +67,7 @@ interface adapter_interface * @param string $path_orig The original file/direcotry * @param string $path_dest The target file/directory * - * @throws exception When file/directory cannot be renamed + * @throws storage_exception When file/directory cannot be renamed */ public function rename(string $path_orig, string $path_dest): void; @@ -77,7 +77,7 @@ interface adapter_interface * @param string $path_orig The original filename * @param string $path_dest The target filename * - * @throws exception When the file cannot be copied + * @throws storage_exception When the file cannot be copied */ public function copy(string $path_orig, string $path_dest): void; @@ -94,7 +94,7 @@ interface adapter_interface * Get space available in bytes * * @return float Returns available space - * @throws exception When unable to retrieve available storage space + * @throws storage_exception When unable to retrieve available storage space */ public function free_space(): float; } diff --git a/phpBB/phpbb/storage/adapter/local.php b/phpBB/phpbb/storage/adapter/local.php index be4b3399d6..48722cf5b2 100644 --- a/phpBB/phpbb/storage/adapter/local.php +++ b/phpBB/phpbb/storage/adapter/local.php @@ -14,7 +14,7 @@ namespace phpbb\storage\adapter; use phpbb\storage\stream_interface; -use phpbb\storage\exception\exception; +use phpbb\storage\exception\storage_exception; use phpbb\filesystem\exception\filesystem_exception; use phpbb\filesystem\filesystem; use phpbb\filesystem\helper as filesystem_helper; @@ -117,7 +117,7 @@ class local implements adapter_interface, stream_interface } catch (filesystem_exception $e) { - throw new exception('STORAGE_CANNOT_WRITE_FILE', $path, array(), $e); + throw new storage_exception('STORAGE_CANNOT_WRITE_FILE', $path, array(), $e); } } @@ -130,7 +130,7 @@ class local implements adapter_interface, stream_interface if ($content === false) { - throw new exception('STORAGE_CANNOT_READ_FILE', $path); + throw new storage_exception('STORAGE_CANNOT_READ_FILE', $path); } return $content; @@ -155,7 +155,7 @@ class local implements adapter_interface, stream_interface } catch (filesystem_exception $e) { - throw new exception('STORAGE_CANNOT_DELETE', $path, array(), $e); + throw new storage_exception('STORAGE_CANNOT_DELETE', $path, array(), $e); } } @@ -172,7 +172,7 @@ class local implements adapter_interface, stream_interface } catch (filesystem_exception $e) { - throw new exception('STORAGE_CANNOT_RENAME', $path_orig, array(), $e); + throw new storage_exception('STORAGE_CANNOT_RENAME', $path_orig, array(), $e); } } @@ -189,7 +189,7 @@ class local implements adapter_interface, stream_interface } catch (filesystem_exception $e) { - throw new exception('STORAGE_CANNOT_COPY', $path_orig, array(), $e); + throw new storage_exception('STORAGE_CANNOT_COPY', $path_orig, array(), $e); } } @@ -198,7 +198,7 @@ class local implements adapter_interface, stream_interface * * @param string $path The directory path * - * @throws exception On any directory creation failure + * @throws storage_exception On any directory creation failure */ protected function create_dir(string $path): void { @@ -208,7 +208,7 @@ class local implements adapter_interface, stream_interface } catch (filesystem_exception $e) { - throw new exception('STORAGE_CANNOT_CREATE_DIR', $path, array(), $e); + throw new storage_exception('STORAGE_CANNOT_CREATE_DIR', $path, array(), $e); } } @@ -217,7 +217,7 @@ class local implements adapter_interface, stream_interface * * @param string $path The file path * - * @throws exception On any directory creation failure + * @throws storage_exception On any directory creation failure */ protected function ensure_directory_exists(string $path): void { @@ -264,7 +264,7 @@ class local implements adapter_interface, stream_interface if (!$stream) { - throw new exception('STORAGE_CANNOT_OPEN_FILE', $path); + throw new storage_exception('STORAGE_CANNOT_OPEN_FILE', $path); } return $stream; @@ -281,13 +281,13 @@ class local implements adapter_interface, stream_interface if (!$stream) { - throw new exception('STORAGE_CANNOT_CREATE_FILE', $path); + throw new storage_exception('STORAGE_CANNOT_CREATE_FILE', $path); } if (stream_copy_to_stream($resource, $stream) === false) { fclose($stream); - throw new exception('STORAGE_CANNOT_COPY_RESOURCE'); + throw new storage_exception('STORAGE_CANNOT_COPY_RESOURCE'); } fclose($stream); @@ -298,11 +298,9 @@ class local implements adapter_interface, stream_interface * * @param string $path The file * - * @throws exception When cannot get size - * * @return array Properties - * @throws exception When cannot get size * + * @throws storage_exception When cannot get size */ public function file_size(string $path): array { @@ -310,7 +308,7 @@ class local implements adapter_interface, stream_interface if ($size === null) { - throw new exception('STORAGE_CANNOT_GET_FILESIZE'); + throw new storage_exception('STORAGE_CANNOT_GET_FILESIZE'); } return ['size' => $size]; @@ -392,12 +390,12 @@ class local implements adapter_interface, stream_interface if ($free_space === false) { - throw new exception('STORAGE_CANNOT_GET_FREE_SPACE'); + throw new storage_exception('STORAGE_CANNOT_GET_FREE_SPACE'); } } else { - throw new exception('STORAGE_CANNOT_GET_FREE_SPACE'); + throw new storage_exception('STORAGE_CANNOT_GET_FREE_SPACE'); } return $free_space; diff --git a/phpBB/phpbb/storage/adapter_factory.php b/phpBB/phpbb/storage/adapter_factory.php index 5d08bfa7c2..61ea879482 100644 --- a/phpBB/phpbb/storage/adapter_factory.php +++ b/phpBB/phpbb/storage/adapter_factory.php @@ -15,7 +15,7 @@ namespace phpbb\storage; use phpbb\config\config; use phpbb\di\service_collection; -use phpbb\storage\exception\exception; +use phpbb\storage\exception\storage_exception; class adapter_factory { @@ -53,41 +53,44 @@ class adapter_factory * * @param string $storage_name * - * @return \phpbb\storage\adapter\adapter_interface + * @return mixed */ - public function get($storage_name) + public function get(string $storage_name): mixed + { + $provider_class = $this->config['storage\\' . $storage_name . '\\provider']; + $provider = $this->providers->get_by_class($provider_class); + + $options = []; + foreach (array_keys($provider->get_options()) as $definition) + { + /** @psalm-suppress InvalidArrayOffset */ + $options[$definition] = $this->config['storage\\' . $storage_name . '\\config\\' . $definition]; + } + + return $this->get_with_options($storage_name, $options); + } + + /** + * Obtains a configured adapters for a given storage with custom options + * + * @param string $storage_name + * @param array $options + * + * @return mixed + */ + public function get_with_options(string $storage_name, array $options): mixed { $provider_class = $this->config['storage\\' . $storage_name . '\\provider']; $provider = $this->providers->get_by_class($provider_class); if (!$provider->is_available()) { - throw new exception('STORAGE_ADAPTER_NOT_AVAILABLE'); + throw new storage_exception('STORAGE_ADAPTER_NOT_AVAILABLE'); } $adapter = $this->adapters->get_by_class($provider->get_adapter_class()); - $adapter->configure($this->build_options($storage_name, $provider->get_options())); + $adapter->configure($options); return $adapter; } - - /** - * Obtains configuration for a given storage - * - * @param string $storage_name - * @param array $definitions - * - * @return array Returns storage configuration values - */ - public function build_options($storage_name, array $definitions) - { - $options = []; - - foreach (array_keys($definitions) as $definition) - { - $options[$definition] = $this->config['storage\\' . $storage_name . '\\config\\' . $definition]; - } - - return $options; - } } diff --git a/phpBB/phpbb/storage/controller/controller.php b/phpBB/phpbb/storage/controller/controller.php index 54f95aa224..d1ea9f29f7 100644 --- a/phpBB/phpbb/storage/controller/controller.php +++ b/phpBB/phpbb/storage/controller/controller.php @@ -16,7 +16,7 @@ namespace phpbb\storage\controller; use phpbb\cache\service; use phpbb\db\driver\driver_interface; use phpbb\exception\http_exception; -use phpbb\storage\exception\exception; +use phpbb\storage\exception\storage_exception; use phpbb\storage\storage; use Symfony\Component\HttpFoundation\Request as symfony_request; use Symfony\Component\HttpFoundation\Response; @@ -63,7 +63,7 @@ class controller * @return Response a Symfony response object * * @throws http_exception when can't access $file - * @throws exception when there is an error reading the file + * @throws storage_exception when there is an error reading the file */ public function handle(string $file): Response { @@ -120,7 +120,7 @@ class controller * @param string $file File path * * @return void - * @throws exception when there is an error reading the file + * @throws storage_exception when there is an error reading the file */ protected function prepare(StreamedResponse $response, string $file): void { @@ -133,7 +133,7 @@ class controller { $content_type = $file_info->get('mimetype'); } - catch (exception $e) + catch (storage_exception $e) { $content_type = 'application/octet-stream'; } @@ -148,7 +148,7 @@ class controller { $response->headers->set('Content-Length', $file_info->get('size')); } - catch (exception $e) + catch (storage_exception $e) { // Just don't send this header } diff --git a/phpBB/phpbb/storage/exception/action_in_progress_exception.php b/phpBB/phpbb/storage/exception/action_in_progress_exception.php new file mode 100644 index 0000000000..1e6f01495e --- /dev/null +++ b/phpBB/phpbb/storage/exception/action_in_progress_exception.php @@ -0,0 +1,18 @@ + + * @license GNU General Public License, version 2 (GPL-2.0) + * + * For full copyright and license information, please see + * the docs/CREDITS.txt file. + * + */ + +namespace phpbb\storage\exception; + +class action_in_progress_exception extends storage_exception +{ +} diff --git a/phpBB/phpbb/storage/exception/no_action_in_progress_exception.php b/phpBB/phpbb/storage/exception/no_action_in_progress_exception.php new file mode 100644 index 0000000000..5b5e63fc07 --- /dev/null +++ b/phpBB/phpbb/storage/exception/no_action_in_progress_exception.php @@ -0,0 +1,18 @@ + + * @license GNU General Public License, version 2 (GPL-2.0) + * + * For full copyright and license information, please see + * the docs/CREDITS.txt file. + * + */ + +namespace phpbb\storage\exception; + +class no_action_in_progress_exception extends storage_exception +{ +} diff --git a/phpBB/phpbb/storage/exception/exception.php b/phpBB/phpbb/storage/exception/storage_exception.php similarity index 96% rename from phpBB/phpbb/storage/exception/exception.php rename to phpBB/phpbb/storage/exception/storage_exception.php index 8268530c16..08c0cfa4ef 100644 --- a/phpBB/phpbb/storage/exception/exception.php +++ b/phpBB/phpbb/storage/exception/storage_exception.php @@ -15,7 +15,7 @@ namespace phpbb\storage\exception; use phpbb\exception\runtime_exception; -class exception extends runtime_exception +class storage_exception extends runtime_exception { /** * Constructor diff --git a/phpBB/phpbb/storage/file_info.php b/phpBB/phpbb/storage/file_info.php index 2e93846838..3bb6b28131 100644 --- a/phpBB/phpbb/storage/file_info.php +++ b/phpBB/phpbb/storage/file_info.php @@ -13,7 +13,7 @@ namespace phpbb\storage; -use phpbb\storage\exception\exception; +use phpbb\storage\exception\storage_exception; use phpbb\storage\adapter\adapter_interface; class file_info @@ -66,7 +66,7 @@ class file_info { if (!method_exists($this->adapter, 'file_' . $name)) { - throw new exception('STORAGE_METHOD_NOT_IMPLEMENTED'); + throw new storage_exception('STORAGE_METHOD_NOT_IMPLEMENTED'); } $this->properties = array_merge($this->properties, call_user_func([$this->adapter, 'file_' . $name], $this->path)); diff --git a/phpBB/phpbb/storage/helper.php b/phpBB/phpbb/storage/helper.php new file mode 100644 index 0000000000..66921b60a7 --- /dev/null +++ b/phpBB/phpbb/storage/helper.php @@ -0,0 +1,230 @@ + + * @license GNU General Public License, version 2 (GPL-2.0) + * + * For full copyright and license information, please see + * the docs/CREDITS.txt file. + * + */ + +namespace phpbb\storage; + +use phpbb\config\config; +use phpbb\di\service_collection; + +class helper +{ + /** @var config */ + protected $config; + + /** @var adapter_factory */ + protected $adapter_factory; + + /** @var state_helper */ + protected $state_helper; + + /** @var service_collection */ + protected $provider_collection; + + /** @var service_collection */ + protected $adapter_collection; + + /** + * Constructor + * + * @param config $config + * @param adapter_factory $adapter_factory + * @param state_helper $state_helper + * @param service_collection $provider_collection + * @param service_collection $adapter_collection + */ + public function __construct(config $config, adapter_factory $adapter_factory, state_helper $state_helper, service_collection $provider_collection, service_collection $adapter_collection) + { + $this->config = $config; + $this->adapter_factory = $adapter_factory; + $this->state_helper = $state_helper; + $this->provider_collection = $provider_collection; + $this->adapter_collection = $adapter_collection; + } + + /** + * Get adapter definitions from a provider + * + * @param string $provider_class Provider class + * + * @return array Adapter definitions + */ + public function get_provider_options(string $provider_class) : array + { + return $this->provider_collection->get_by_class($provider_class)->get_options(); + } + + /** + * Get the current provider from config + * + * @param string $storage_name Storage name + * + * @return string The current provider + */ + public function get_current_provider(string $storage_name) : string + { + return (string) $this->config['storage\\' . $storage_name . '\\provider']; + } + + /** + * Get the current value of the definition of a storage from config + * + * @param string $storage_name Storage name + * @param string $definition Definition + * + * @return string Definition value + */ + public function get_current_definition(string $storage_name, string $definition) : string + { + return (string) $this->config['storage\\' . $storage_name . '\\config\\' . $definition]; + } + + /** + * Get current storage adapter + * + * @param string $storage_name Storage adapter name + * + * @return object Storage adapter instance + */ + public function get_current_adapter(string $storage_name): object + { + static $adapters = []; + + if (!isset($adapters[$storage_name])) + { + $adapters[$storage_name] = $this->adapter_factory->get($storage_name); + } + + return $adapters[$storage_name]; + } + + /** + * Get new storage adapter + * + * @param string $storage_name + * + * @return mixed Storage adapter instance + */ + public function get_new_adapter(string $storage_name): mixed + { + static $adapters = []; + + if (!isset($adapters[$storage_name])) + { + $provider_class = $this->state_helper->new_provider($storage_name); + $definitions = array_keys($this->get_provider_options($provider_class)); + + $options = []; + foreach ($definitions as $definition) + { + $options[$definition] = $this->state_helper->new_definition_value($storage_name, $definition); + } + + $adapters[$storage_name] = $this->adapter_factory->get_with_options($storage_name, $options); + } + + return $adapters[$storage_name]; + } + + /** + * Delete configuration options for a given storage + * + * @param string $storage_name + * + * @return void + */ + public function delete_storage_options(string $storage_name): void + { + $provider = $this->get_current_provider($storage_name); + $options = $this->get_provider_options($provider); + + foreach (array_keys($options) as $definition) + { + $this->config->delete('storage\\' . $storage_name . '\\config\\' . $definition); + } + } + + /** + * Set a provider in configuration for a given storage + * + * @param string $storage_name + * @param string $provider + * + * @return void + */ + public function set_storage_provider(string $storage_name, string $provider): void + { + $this->config->set('storage\\' . $storage_name . '\\provider', $provider); + } + + /** + * Set storage options in configuration for a given storage + * + * @param string $storage_name + * @param string $definition + * @param string $value + * + * @return void + */ + public function set_storage_definition(string $storage_name, string $definition, string $value): void + { + $this->config->set('storage\\' . $storage_name . '\\config\\' . $definition, $value); + } + + /** + * Copy a file from the current adapter to the new adapter + * + * @param $storage_name + * @param $file + * + * @return void + */ + public function copy_file_to_new_adapter($storage_name, $file): void + { + $current_adapter = $this->get_current_adapter($storage_name); + $new_adapter = $this->get_new_adapter($storage_name); + + $stream = $current_adapter->read_stream($file); + $new_adapter->write_stream($file, $stream); + + if (is_resource($stream)) + { + fclose($stream); + } + } + + + /** + * Updates a storage with the info provided in the form (that is stored in the state at this point) + * + * @param string $storage_name Storage name + */ + public function update_storage_config(string $storage_name) : void + { + // Remove old storage config + $this->delete_storage_options($storage_name); + + // Update provider + $new_provider = $this->state_helper->new_provider($storage_name); + $this->set_storage_provider($storage_name, $new_provider); + + // Set new storage config + $new_options = $this->get_provider_options($new_provider); + + foreach (array_keys($new_options) as $definition) + { + $new_definition_value = $this->state_helper->new_definition_value($storage_name, $definition); + $this->set_storage_definition($storage_name, $definition, $new_definition_value); + } + } + +} diff --git a/phpBB/phpbb/storage/provider/local.php b/phpBB/phpbb/storage/provider/local.php index 3bec0c5ce5..c6338e0c31 100644 --- a/phpBB/phpbb/storage/provider/local.php +++ b/phpBB/phpbb/storage/provider/local.php @@ -37,7 +37,10 @@ class local implements provider_interface public function get_options() { return [ - 'path' => ['type' => 'text'], + 'path' => [ + 'tag' => 'input', + 'type' => 'text', + ], ]; } diff --git a/phpBB/phpbb/storage/state_helper.php b/phpBB/phpbb/storage/state_helper.php new file mode 100644 index 0000000000..82523f0aa0 --- /dev/null +++ b/phpBB/phpbb/storage/state_helper.php @@ -0,0 +1,278 @@ + + * @license GNU General Public License, version 2 (GPL-2.0) + * + * For full copyright and license information, please see + * the docs/CREDITS.txt file. + * + */ + +namespace phpbb\storage; + +use phpbb\config\config; +use phpbb\config\db_text; +use phpbb\di\service_collection; +use phpbb\request\request; +use phpbb\storage\exception\action_in_progress_exception; +use phpbb\storage\exception\no_action_in_progress_exception; + +class state_helper +{ + /** @var config */ + protected $config; + + /** @var db_text $config_text */ + protected $config_text; + + /** @var service_collection */ + protected $provider_collection; + + /** + * @param config $config + * @param db_text $config_text + * @param service_collection $provider_collection + */ + public function __construct(config $config, db_text $config_text, service_collection $provider_collection) + { + $this->config = $config; + $this->config_text = $config_text; + $this->provider_collection = $provider_collection; + } + + /** + * Returns if there is an action in progress + * + * @return bool + */ + public function is_action_in_progress(): bool + { + return !empty(json_decode($this->config_text->get('storage_update_state'), true)); + } + + /** + * Get new provider for the specified storage + * + * @param string $storage_name + * + * @return string + */ + public function new_provider(string $storage_name): string + { + $state = $this->load_state(); + + return $state['storages'][$storage_name]['provider']; + } + + /** + * Get new definition value for the specified storage + * + * @param string $storage_name + * @param string $definition + * + * @return string + */ + public function new_definition_value(string $storage_name, string $definition): string + { + $state = $this->load_state(); + + return $state['storages'][$storage_name]['config'][$definition]; + } + + /** + * Get the update type + * + * @return update_type + */ + public function update_type(): update_type + { + $state = $this->load_state(); + + return update_type::from($state['update_type']); + } + + /** + * Get the current storage index + * + * @return int + */ + public function storage_index(): int + { + $state = $this->load_state(); + + return $state['storage_index']; + } + + /** + * Update the storage index + * + * @param int $storage_index + * + * @return void + */ + public function set_storage_index(int $storage_index): void + { + $state = $this->load_state(); + + $state['storage_index'] = $storage_index; + + $this->save_state($state); + } + + /** + * Get the current remove storage index + * + * @return int + */ + public function remove_storage_index(): int + { + $state = $this->load_state(); + + return $state['remove_storage_index']; + } + + /** + * Update the remove storage index + * + * @param int $storage_index + * + * @return void + */ + public function set_remove_storage_index(int $storage_index): void + { + $state = $this->load_state(); + + $state['remove_storage_index'] = $storage_index; + + $this->save_state($state); + } + + /** + * Get the file index + * + * @return int + */ + public function file_index(): int + { + $state = $this->load_state(); + + return $state['file_index']; + } + + /** + * Set the file index + * + * @param int $file_index + * @return void + */ + public function set_file_index(int $file_index): void + { + $state = $this->load_state(); + + $state['file_index'] = $file_index; + + $this->save_state($state); + } + + /** + * Get the storage names to be updated + * + * @return array + */ + public function storages(): array + { + $state = $this->load_state(); + + return array_keys($state['storages']); + } + + /** + * Start a indexing or delete process. + * + * @param update_type $update_type + * @param array $modified_storages + * @param request $request + * + * @throws action_in_progress_exception If there is an action in progress + * @throws \JsonException + */ + public function init(update_type $update_type, array $modified_storages, request $request): void + { + // Is not possible to start a new process when there is one already running + if ($this->is_action_in_progress()) + { + throw new action_in_progress_exception(); + } + + $state = [ + // Save the value of the checkbox, to remove all files from the + // old storage once they have been successfully moved + 'update_type' => $update_type->value, + 'storages' => [], + 'storage_index' => 0, + 'file_index' => 0, + 'remove_storage_index' => 0, + ]; + + // Save in the state the selected storages and their new configuration + foreach ($modified_storages as $storage_name) + { + $state['storages'][$storage_name] = []; + + $state['storages'][$storage_name]['provider'] = $request->variable([$storage_name, 'provider'], ''); + + $options = $this->provider_collection->get_by_class($request->variable([$storage_name, 'provider'], ''))->get_options(); + + foreach (array_keys($options) as $definition) + { + /** @psalm-suppress InvalidArrayOffset */ + $state['storages'][$storage_name]['config'][$definition] = $request->variable([$storage_name, $definition], ''); + } + } + + $this->save_state($state); + } + + /** + * Clear the state + * + * @throws \JsonException + */ + public function clear_state(): void + { + $this->save_state(); + } + + /** + * Load the state from the database + * + * @return array + * + * @throws no_action_in_progress_exception If there is no action in progress + */ + private function load_state(): array + { + // Is not possible to execute an action over state if is empty + if (!$this->is_action_in_progress()) + { + throw new no_action_in_progress_exception(); + } + + return json_decode($this->config_text->get('storage_update_state'), true) ?? []; + } + + /** + * Save the specified state in the database + * + * @param array $state + * + * @throws \JsonException + */ + private function save_state(array $state = []): void + { + $this->config_text->set('storage_update_state', json_encode($state, JSON_THROW_ON_ERROR)); + } +} diff --git a/phpBB/phpbb/storage/storage.php b/phpBB/phpbb/storage/storage.php index 8e4620ab23..a4c652f40a 100644 --- a/phpBB/phpbb/storage/storage.php +++ b/phpBB/phpbb/storage/storage.php @@ -15,7 +15,8 @@ namespace phpbb\storage; use phpbb\cache\driver\driver_interface as cache; use phpbb\db\driver\driver_interface as db; -use phpbb\storage\exception\exception; +use phpbb\storage\adapter\adapter_interface; +use phpbb\storage\exception\storage_exception; /** * Experimental @@ -23,7 +24,7 @@ use phpbb\storage\exception\exception; class storage { /** - * @var \phpbb\storage\adapter\adapter_interface + * @var adapter_interface */ protected $adapter; @@ -39,7 +40,7 @@ class storage protected $cache; /** - * @var \phpbb\storage\adapter_factory + * @var adapter_factory */ protected $factory; @@ -58,7 +59,7 @@ class storage * * @param db $db * @param cache $cache - * @param \phpbb\storage\adapter_factory $factory + * @param adapter_factory $factory * @param string $storage_name * @param string $storage_table */ @@ -84,7 +85,7 @@ class storage /** * Returns an adapter instance * - * @return \phpbb\storage\adapter\adapter_interface + * @return adapter_interface */ protected function get_adapter() { @@ -102,14 +103,14 @@ class storage * @param string $path The file to be written to. * @param string $content The data to write into the file. * - * @throws exception When the file already exists + * @throws storage_exception When the file already exists * When the file cannot be written */ public function put_contents($path, $content) { if ($this->exists($path)) { - throw new exception('STORAGE_FILE_EXISTS', $path); + throw new storage_exception('STORAGE_FILE_EXISTS', $path); } $this->get_adapter()->put_contents($path, $content); @@ -121,17 +122,17 @@ class storage * * @param string $path The file to read * - * @throws exception When the file doesn't exist - * When cannot read file contents + * @return string Returns file contents * - * @return string Returns file contents + * @throws storage_exception When the file doesn't exist + * When cannot read file contents * */ public function get_contents($path) { if (!$this->exists($path)) { - throw new exception('STORAGE_FILE_NO_EXIST', $path); + throw new storage_exception('STORAGE_FILE_NO_EXIST', $path); } return $this->get_adapter()->get_contents($path); @@ -155,14 +156,14 @@ class storage * * @param string $path file/directory to remove * - * @throws exception When removal fails + * @throws storage_exception When removal fails * When the file doesn't exist */ public function delete($path) { if (!$this->exists($path)) { - throw new exception('STORAGE_FILE_NO_EXIST', $path); + throw new storage_exception('STORAGE_FILE_NO_EXIST', $path); } $this->get_adapter()->delete($path); @@ -175,7 +176,7 @@ class storage * @param string $path_orig The original file/direcotry * @param string $path_dest The target file/directory * - * @throws exception When the file doesn't exist + * @throws storage_exception When the file doesn't exist * When target exists * When file/directory cannot be renamed */ @@ -183,12 +184,12 @@ class storage { if (!$this->exists($path_orig)) { - throw new exception('STORAGE_FILE_NO_EXIST', $path_orig); + throw new storage_exception('STORAGE_FILE_NO_EXIST', $path_orig); } if ($this->exists($path_dest)) { - throw new exception('STORAGE_FILE_EXISTS', $path_dest); + throw new storage_exception('STORAGE_FILE_EXISTS', $path_dest); } $this->get_adapter()->rename($path_orig, $path_dest); @@ -201,7 +202,7 @@ class storage * @param string $path_orig The original filename * @param string $path_dest The target filename * - * @throws exception When the file doesn't exist + * @throws storage_exception When the file doesn't exist * When target exists * When the file cannot be copied */ @@ -209,12 +210,12 @@ class storage { if (!$this->exists($path_orig)) { - throw new exception('STORAGE_FILE_NO_EXIST', $path_orig); + throw new storage_exception('STORAGE_FILE_NO_EXIST', $path_orig); } if ($this->exists($path_dest)) { - throw new exception('STORAGE_FILE_EXISTS', $path_dest); + throw new storage_exception('STORAGE_FILE_EXISTS', $path_dest); } $this->get_adapter()->copy($path_orig, $path_dest); @@ -226,16 +227,16 @@ class storage * * @param string $path File to read * - * @throws exception When the file doesn't exist + * @return resource Returns a file pointer + * @throws storage_exception When the file doesn't exist * When unable to open file * - * @return resource Returns a file pointer */ public function read_stream($path) { if (!$this->exists($path)) { - throw new exception('STORAGE_FILE_NO_EXIST', $path); + throw new storage_exception('STORAGE_FILE_NO_EXIST', $path); } $stream = null; @@ -262,19 +263,19 @@ class storage * @param string $path The target file * @param resource $resource The resource * - * @throws exception When the file exist + * @throws storage_exception When the file exist * When target file cannot be created */ public function write_stream($path, $resource) { if ($this->exists($path)) { - throw new exception('STORAGE_FILE_EXISTS', $path); + throw new storage_exception('STORAGE_FILE_EXISTS', $path); } if (!is_resource($resource)) { - throw new exception('STORAGE_INVALID_RESOURCE'); + throw new storage_exception('STORAGE_INVALID_RESOURCE'); } $adapter = $this->get_adapter(); @@ -301,7 +302,7 @@ class storage { if (!$this->get_adapter()->exists($path)) { - throw new exception('STORAGE_FILE_NO_EXIST', $path); + throw new storage_exception('STORAGE_FILE_NO_EXIST', $path); } $sql_ary = array( @@ -403,16 +404,16 @@ class storage * * @param string $path The file * - * @throws exception When the adapter doesn't implement the method + * @return \phpbb\storage\file_info Returns file_info object + * @throws storage_exception When the adapter doesn't implement the method * When the file doesn't exist * - * @return \phpbb\storage\file_info Returns file_info object */ public function file_info($path) { if (!$this->exists($path)) { - throw new exception('STORAGE_FILE_NO_EXIST', $path); + throw new storage_exception('STORAGE_FILE_NO_EXIST', $path); } return new file_info($this->get_adapter(), $path); @@ -484,9 +485,9 @@ class storage /** * Get space available in bytes * - * @throws exception When unable to retrieve available storage space + * @return float Returns available space + * @throws storage_exception When unable to retrieve available storage space * - * @return float Returns available space */ public function free_space() { diff --git a/phpBB/phpbb/storage/stream_interface.php b/phpBB/phpbb/storage/stream_interface.php index 9687a2d910..424ffcb95c 100644 --- a/phpBB/phpbb/storage/stream_interface.php +++ b/phpBB/phpbb/storage/stream_interface.php @@ -13,7 +13,7 @@ namespace phpbb\storage; -use phpbb\storage\exception\exception; +use phpbb\storage\exception\storage_exception; interface stream_interface { @@ -23,7 +23,7 @@ interface stream_interface * @param string $path File to read * * @return resource Returns a file pointer - * @throws exception When unable to open file + * @throws storage_exception When unable to open file */ public function read_stream(string $path); @@ -34,7 +34,7 @@ interface stream_interface * @param resource $resource The resource * * @return void - * @throws exception When target file exists + * @throws storage_exception When target file exists * When target file cannot be created */ public function write_stream(string $path, $resource): void; diff --git a/phpBB/phpbb/storage/update_type.php b/phpBB/phpbb/storage/update_type.php new file mode 100644 index 0000000000..32f25ad13d --- /dev/null +++ b/phpBB/phpbb/storage/update_type.php @@ -0,0 +1,21 @@ + + * @license GNU General Public License, version 2 (GPL-2.0) + * + * For full copyright and license information, please see + * the docs/CREDITS.txt file. + * + */ + +namespace phpbb\storage; + +enum update_type: int +{ + case CONFIG = 0; + case COPY = 1; + case MOVE = 2; +} diff --git a/tests/attachment/delete_test.php b/tests/attachment/delete_test.php index faf2f960e6..7acac28f33 100644 --- a/tests/attachment/delete_test.php +++ b/tests/attachment/delete_test.php @@ -102,7 +102,7 @@ class phpbb_attachment_delete_test extends \phpbb_database_test_case { $this->storage->expects($this->any()) ->method('delete') - ->willThrowException(new \phpbb\storage\exception\exception); + ->willThrowException(new \phpbb\storage\exception\storage_exception); } else { diff --git a/tests/functional/acp_storage_settings_test.php b/tests/functional/acp_storage_settings_test.php index b99e3e8fa9..f8c1728dcb 100644 --- a/tests/functional/acp_storage_settings_test.php +++ b/tests/functional/acp_storage_settings_test.php @@ -68,9 +68,9 @@ class phpbb_functional_acp_storage_settings_test extends phpbb_functional_test_c // Visit ACP Storage settings again - warning should be displayed $crawler = self::request('GET', 'adm/index.php?i=acp_storage&mode=settings&sid=' . $this->sid); $this->assertContainsLang('WARNING', $crawler->filter('div[class="errorbox"] > h3')->text()); - $this->assertStringContainsString($this->lang('STORAGE_PATH_NOT_EXISTS', $this->lang('STORAGE_ATTACHMENT_TITLE')), $crawler->filter('div[class="errorbox"] > p')->text()); - $this->assertStringContainsString($this->lang('STORAGE_PATH_NOT_EXISTS', $this->lang('STORAGE_AVATAR_TITLE')), $crawler->filter('div[class="errorbox"] > p')->text()); - $this->assertStringContainsString($this->lang('STORAGE_PATH_NOT_EXISTS', $this->lang('STORAGE_BACKUP_TITLE')), $crawler->filter('div[class="errorbox"] > p')->text()); + $this->assertStringContainsString($this->lang('STORAGE_PATH_NOT_EXISTS', $this->lang('STORAGE_ATTACHMENT_TITLE')), $crawler->filter('div[class="errorbox"]')->text()); + $this->assertStringContainsString($this->lang('STORAGE_PATH_NOT_EXISTS', $this->lang('STORAGE_AVATAR_TITLE')), $crawler->filter('div[class="errorbox"]')->text()); + $this->assertStringContainsString($this->lang('STORAGE_PATH_NOT_EXISTS', $this->lang('STORAGE_BACKUP_TITLE')), $crawler->filter('div[class="errorbox"]')->text()); // Restore default state $filesystem->chmod($phpbb_root_path . $attachments_storage_path, 777);