diff --git a/phpBB/config/default/container/services.yml b/phpBB/config/default/container/services.yml index 73260f7a92..aa997b8360 100644 --- a/phpBB/config/default/container/services.yml +++ b/phpBB/config/default/container/services.yml @@ -23,6 +23,7 @@ imports: - { resource: services_notification.yml } - { resource: services_password.yml } - { resource: services_php.yml } + - { resource: services_posting.yml } - { resource: services_profilefield.yml } - { resource: services_report.yml } - { resource: services_routing.yml } diff --git a/phpBB/config/default/container/services_console.yml b/phpBB/config/default/container/services_console.yml index 74cfade99d..f23d205b8f 100644 --- a/phpBB/config/default/container/services_console.yml +++ b/phpBB/config/default/container/services_console.yml @@ -246,6 +246,40 @@ services: tags: - { name: console.command } + console.command.searchindex.list_all: + class: phpbb\console\command\searchindex\list_all + arguments: + - '@config' + - '@language' + - '@search.backend_collection' + - '@user' + tags: + - { name: console.command } + + console.command.searchindex.create: + class: phpbb\console\command\searchindex\create + arguments: + - '@language' + - '@log' + - '@post.helper' + - '@search.backend_factory' + - '@search.state_helper' + - '@user' + tags: + - { name: console.command } + + console.command.searchindex.delete: + class: phpbb\console\command\searchindex\delete + arguments: + - '@language' + - '@log' + - '@post.helper' + - '@search.backend_factory' + - '@search.state_helper' + - '@user' + tags: + - { name: console.command } + console.command.thumbnail.delete: class: phpbb\console\command\thumbnail\delete arguments: diff --git a/phpBB/config/default/container/services_posting.yml b/phpBB/config/default/container/services_posting.yml new file mode 100644 index 0000000000..aa1a3e1959 --- /dev/null +++ b/phpBB/config/default/container/services_posting.yml @@ -0,0 +1,5 @@ +services: + post.helper: + class: phpbb\posting\post_helper + arguments: + - '@dbal.conn' diff --git a/phpBB/config/default/container/services_search.yml b/phpBB/config/default/container/services_search.yml index 1dba3732be..fceb254eaa 100644 --- a/phpBB/config/default/container/services_search.yml +++ b/phpBB/config/default/container/services_search.yml @@ -1,5 +1,11 @@ services: + search.state_helper: + class: phpbb\search\state_helper + arguments: + - '@config' + - '@search.backend_factory' + # Search backends search.fulltext.mysql: class: phpbb\search\backend\fulltext_mysql @@ -9,6 +15,7 @@ services: - '@dispatcher' - '@language' - '@user' + - '%tables.search_results%' - '%core.root_path%' - '%core.php_ext%' tags: @@ -22,6 +29,9 @@ services: - '@dispatcher' - '@language' - '@user' + - '%tables.search_results%' + - '%tables.search_wordlist%' + - '%tables.search_wordmatch%' - '%core.root_path%' - '%core.php_ext%' tags: @@ -35,6 +45,7 @@ services: - '@dispatcher' - '@language' - '@user' + - '%tables.search_results%' - '%core.root_path%' - '%core.php_ext%' tags: diff --git a/phpBB/includes/acp/acp_main.php b/phpBB/includes/acp/acp_main.php index b89c4e3779..71e9ed7a2e 100644 --- a/phpBB/includes/acp/acp_main.php +++ b/phpBB/includes/acp/acp_main.php @@ -652,16 +652,9 @@ class acp_main $search_backend_factory = $phpbb_container->get('search.backend_factory'); $search = $search_backend_factory->get_active(); } - catch (RuntimeException $e) + catch (\phpbb\search\exception\no_search_backend_found_exception $e) { - if (strpos($e->getMessage(), 'No service found') === 0) - { - trigger_error('NO_SUCH_SEARCH_MODULE'); - } - else - { - throw $e; - } + trigger_error('NO_SUCH_SEARCH_MODULE'); } if (!$search->index_created()) diff --git a/phpBB/includes/acp/acp_search.php b/phpBB/includes/acp/acp_search.php index e2cbea4956..f13b9c8dea 100644 --- a/phpBB/includes/acp/acp_search.php +++ b/phpBB/includes/acp/acp_search.php @@ -21,6 +21,7 @@ use phpbb\language\language; use phpbb\log\log; use phpbb\request\request; use phpbb\search\search_backend_factory; +use phpbb\search\state_helper; use phpbb\template\template; use phpbb\user; @@ -35,10 +36,6 @@ class acp_search public $tpl_name; public $page_title; - protected const STATE_SEARCH_TYPE = 0; - protected const STATE_ACTION = 1; - protected const STATE_POST_COUNTER = 2; - /** @var config */ protected $config; @@ -57,6 +54,9 @@ class acp_search /** @var search_backend_factory */ protected $search_backend_factory; + /** @var state_helper */ + protected $search_state_helper; + /** @var template */ protected $template; @@ -79,6 +79,7 @@ class acp_search $this->request = $request; $this->search_backend_collection = $phpbb_container->get('search.backend_collection'); $this->search_backend_factory = $phpbb_container->get('search.backend_factory'); + $this->search_state_helper = $phpbb_container->get('search.state_helper'); $this->template = $template; $this->user = $user; $this->phpbb_admin_path = $phpbb_admin_path; @@ -272,7 +273,6 @@ class acp_search public function index(string $id, string $mode): void { $action = $this->request->variable('action', ''); - $state = !empty($this->config['search_indexing_state']) ? explode(',', $this->config['search_indexing_state']) : []; if ($action && !$this->request->is_set_post('cancel')) { @@ -284,7 +284,7 @@ class acp_search case 'create': case 'delete': - $this->index_action($id, $mode, $action, $state); + $this->index_action($id, $mode, $action); break; default: @@ -296,13 +296,12 @@ class acp_search // If clicked to cancel the indexing progress (acp_search_index_inprogress form) if ($this->request->is_set_post('cancel')) { - $state = []; - $this->save_state($state); + $this->search_state_helper->clear_state(); } - if (!empty($state)) + if ($this->search_state_helper->is_action_in_progress()) { - $this->index_inprogress($id, $mode, $state[self::STATE_ACTION]); + $this->index_inprogress($id, $mode); } else { @@ -325,8 +324,8 @@ class acp_search foreach ($this->search_backend_collection as $search) { $this->template->assign_block_vars('backends', [ - 'NAME' => $search->get_name(), - 'TYPE' => $search->get_type(), + 'NAME' => $search->get_name(), + 'TYPE' => $search->get_type(), 'S_ACTIVE' => $search->get_type() === $this->config['search_type'], 'S_HIDDEN_FIELDS' => build_hidden_fields(['search_type' => $search->get_type()]), @@ -336,8 +335,8 @@ class acp_search } $this->template->assign_vars([ - 'U_ACTION' => $this->u_action . '&hash=' . generate_link_hash('acp_search'), - 'UA_PROGRESS_BAR' => addslashes($this->u_action . '&action=progress_bar'), + 'U_ACTION' => $this->u_action . '&hash=' . generate_link_hash('acp_search'), + 'UA_PROGRESS_BAR' => addslashes($this->u_action . '&action=progress_bar'), ]); } @@ -346,13 +345,14 @@ class acp_search * * @param string $id * @param string $mode - * @param string $action Action in progress: 'create' or 'delete' */ - private function index_inprogress(string $id, string $mode, string $action): void + private function index_inprogress(string $id, string $mode): void { $this->tpl_name = 'acp_search_index_inprogress'; $this->page_title = 'ACP_SEARCH_INDEX'; + $action = $this->search_state_helper->action(); + $this->template->assign_vars([ 'U_ACTION' => $this->u_action . '&action=' . $action . '&hash=' . generate_link_hash('acp_search'), 'UA_PROGRESS_BAR' => addslashes($this->u_action . '&action=progress_bar'), @@ -368,9 +368,8 @@ class acp_search * @param string $id * @param string $mode * @param string $action - * @param array $state */ - private function index_action(string $id, string $mode, string $action, array $state): void + private function index_action(string $id, string $mode, string $action): void { // For some this may be of help... @ini_set('memory_limit', '128M'); @@ -381,29 +380,23 @@ class acp_search } // Entering here for the first time - if (empty($state)) + if (!$this->search_state_helper->is_action_in_progress()) { if ($this->request->is_set_post('search_type', '')) { - $state = [ - self::STATE_SEARCH_TYPE => $this->request->variable('search_type', ''), - self::STATE_ACTION => $action, - self::STATE_POST_COUNTER => 0 - ]; + $this->search_state_helper->init($this->request->variable('search_type', ''), $action); } else { trigger_error($this->language->lang('FORM_INVALID') . adm_back_link($this->u_action), E_USER_WARNING); } - - $this->save_state($state); // Create new state in the database } - $type = $state[self::STATE_SEARCH_TYPE]; - $action = $state[self::STATE_ACTION]; - $post_counter = &$state[self::STATE_POST_COUNTER]; - // Execute create/delete + $type = $this->search_state_helper->type(); + $action = $this->search_state_helper->action(); + $post_counter = $this->search_state_helper->counter(); + $search = $this->search_backend_factory->get($type); try @@ -411,7 +404,7 @@ class acp_search $status = ($action == 'create') ? $search->create_index($post_counter) : $search->delete_index($post_counter); if ($status) // Status is not null, so action is in progress.... { - $this->save_state($state); // update $post_counter in $state in the database + $this->search_state_helper->update_counter($status['post_counter']); $u_action = append_sid($this->phpbb_admin_path . "index." . $this->php_ex, "i=$id&mode=$mode&action=$action&hash=" . generate_link_hash('acp_search'), false); meta_refresh(1, $u_action); @@ -423,13 +416,13 @@ class acp_search } catch (Exception $e) { - $this->save_state([]); // Unexpected error, cancel action + $this->search_state_helper->clear_state(); // Unexpected error, cancel action trigger_error($e->getMessage() . adm_back_link($this->u_action) . $this->close_popup_js(), E_USER_WARNING); } $search->tidy(); - $this->save_state([]); // finished operation, cancel action + $this->search_state_helper->clear_state(); // finished operation, cancel action $log_operation = ($action == 'create') ? 'LOG_SEARCH_INDEX_CREATED' : 'LOG_SEARCH_INDEX_REMOVED'; $this->log->add('admin', $this->user->data['user_id'], $this->user->ip, $log_operation, false, [$search->get_name()]); @@ -473,14 +466,4 @@ class acp_search "// ]]>\n" . "\n"; } - - /** - * @param array $state - */ - private function save_state(array $state = []): void - { - ksort($state); - - $this->config->set('search_indexing_state', implode(',', $state), true); - } } diff --git a/phpBB/includes/functions_admin.php b/phpBB/includes/functions_admin.php index fa4dac7e57..27f9292ac5 100644 --- a/phpBB/includes/functions_admin.php +++ b/phpBB/includes/functions_admin.php @@ -1092,16 +1092,9 @@ function delete_posts($where_type, $where_ids, $auto_sync = true, $posted_sync = $search_backend_factory = $phpbb_container->get('search.backend_factory'); $search = $search_backend_factory->get_active(); } - catch (RuntimeException $e) + catch (\phpbb\search\exception\no_search_backend_found_exception $e) { - if (strpos($e->getMessage(), 'No service found') === 0) - { - trigger_error('NO_SUCH_SEARCH_MODULE'); - } - else - { - throw $e; - } + trigger_error('NO_SUCH_SEARCH_MODULE'); } $search->index_remove($post_ids, $poster_ids, $forum_ids); diff --git a/phpBB/includes/functions_posting.php b/phpBB/includes/functions_posting.php index 770280195c..80f0e31e74 100644 --- a/phpBB/includes/functions_posting.php +++ b/phpBB/includes/functions_posting.php @@ -2356,16 +2356,9 @@ function submit_post($mode, $subject, $username, $topic_type, &$poll_ary, &$data $search_backend_factory = $phpbb_container->get('search.backend_factory'); $search = $search_backend_factory->get_active(); } - catch (RuntimeException $e) + catch (\phpbb\search\exception\no_search_backend_found_exception $e) { - if (strpos($e->getMessage(), 'No service found') === 0) - { - trigger_error('NO_SUCH_SEARCH_MODULE'); - } - else - { - throw $e; - } + trigger_error('NO_SUCH_SEARCH_MODULE'); } $search->index($mode, (int) $data_ary['post_id'], $data_ary['message'], $subject, $poster_id, (int) $data_ary['forum_id']); diff --git a/phpBB/includes/mcp/mcp_main.php b/phpBB/includes/mcp/mcp_main.php index b5ad7ffc8c..7dab25074f 100644 --- a/phpBB/includes/mcp/mcp_main.php +++ b/phpBB/includes/mcp/mcp_main.php @@ -1397,16 +1397,9 @@ function mcp_fork_topic($topic_ids) $search_backend_factory = $phpbb_container->get('search.backend_factory'); $search = $search_backend_factory->get_active(); } - catch (RuntimeException $e) + catch (\phpbb\search\exception\no_search_backend_found_exception $e) { - if (strpos($e->getMessage(), 'No service found') === 0) - { - trigger_error('NO_SUCH_SEARCH_MODULE'); - } - else - { - throw $e; - } + trigger_error('NO_SUCH_SEARCH_MODULE'); } $search_mode = 'post'; } diff --git a/phpBB/includes/mcp/mcp_post.php b/phpBB/includes/mcp/mcp_post.php index bb402b048d..8e7c0342e8 100644 --- a/phpBB/includes/mcp/mcp_post.php +++ b/phpBB/includes/mcp/mcp_post.php @@ -637,16 +637,9 @@ function change_poster(&$post_info, $userdata) $search_backend_factory = $phpbb_container->get('search.backend_factory'); $search = $search_backend_factory->get_active(); } - catch (RuntimeException $e) + catch (\phpbb\search\exception\no_search_backend_found_exception $e) { - if (strpos($e->getMessage(), 'No service found') === 0) - { - trigger_error('NO_SUCH_SEARCH_MODULE'); - } - else - { - throw $e; - } + trigger_error('NO_SUCH_SEARCH_MODULE'); } $search->index_remove([], [$post_info['user_id'], $userdata['user_id']], []); diff --git a/phpBB/includes/mcp/mcp_topic.php b/phpBB/includes/mcp/mcp_topic.php index 69c7800b81..77f6bea757 100644 --- a/phpBB/includes/mcp/mcp_topic.php +++ b/phpBB/includes/mcp/mcp_topic.php @@ -680,16 +680,9 @@ function split_topic($action, $topic_id, $to_forum_id, $subject) $search_backend_factory = $phpbb_container->get('search.backend_factory'); $search = $search_backend_factory->get_active(); } - catch (RuntimeException $e) + catch (\phpbb\search\exception\no_search_backend_found_exception $e) { - if (strpos($e->getMessage(), 'No service found') === 0) - { - trigger_error('NO_SUCH_SEARCH_MODULE'); - } - else - { - throw $e; - } + trigger_error('NO_SUCH_SEARCH_MODULE'); } $search->index('edit', (int) $first_post_data['post_id'], $first_post_data['post_text'], $subject, (int) $first_post_data['poster_id'], (int) $first_post_data['forum_id']); diff --git a/phpBB/language/en/acp/search.php b/phpBB/language/en/acp/search.php index a79c67fd8e..183ccf0d7b 100644 --- a/phpBB/language/en/acp/search.php +++ b/phpBB/language/en/acp/search.php @@ -91,6 +91,8 @@ $lang = array_merge($lang, array( 'GENERAL_SEARCH_SETTINGS' => 'General search settings', 'GO_TO_SEARCH_INDEX' => 'Go to search index page', + 'INVALID_ACTION' => 'Invalid action', + 'INDEX_STATS' => 'Index statistics', 'INDEXING_IN_PROGRESS' => 'Indexing in progress', 'INDEXING_IN_PROGRESS_EXPLAIN' => 'The search backend is currently indexing all posts on the board. This can take from a few minutes to a few hours depending on your board’s size.', diff --git a/phpBB/language/en/cli.php b/phpBB/language/en/cli.php index bdd76a32e5..80240b9cf2 100644 --- a/phpBB/language/en/cli.php +++ b/phpBB/language/en/cli.php @@ -83,6 +83,10 @@ $lang = array_merge($lang, array( 'CLI_DESCRIPTION_SET_ATOMIC_CONFIG' => 'Sets a configuration option’s value only if the old matches the current value', 'CLI_DESCRIPTION_SET_CONFIG' => 'Sets a configuration option’s value', + 'CLI_DESCRIPTION_SEARCHINDEX_DELETE' => 'Delete search index.', + 'CLI_DESCRIPTION_SEARCHINDEX_CREATE' => 'Create search index.', + 'CLI_DESCRIPTION_SEARCHINDEX_LIST' => 'List all search backends.', + 'CLI_DESCRIPTION_THUMBNAIL_DELETE' => 'Delete all existing thumbnails.', 'CLI_DESCRIPTION_THUMBNAIL_GENERATE' => 'Generate all missing thumbnails.', 'CLI_DESCRIPTION_THUMBNAIL_RECREATE' => 'Recreate all thumbnails.', @@ -142,6 +146,16 @@ $lang = array_merge($lang, array( 'CLI_REPARSER_REPARSE_REPARSING_START' => 'Reparsing %s...', 'CLI_REPARSER_REPARSE_SUCCESS' => 'Reparsing ended with success', + 'CLI_SEARCHINDEX_SEARCH_BACKEND_NAME' => 'Backend class', + 'CLI_SEARCHINDEX_BACKEND_NOT_FOUND' => 'Search module not found', + 'CLI_SEARCHINDEX_CREATE_SUCCESS' => 'Search index created successfully', + 'CLI_SEARCHINDEX_CREATE_FAILURE' => 'Error creating search index', + 'CLI_SEARCHINDEX_DELETE_SUCCESS' => 'Search index deleted successfully', + 'CLI_SEARCHINDEX_DELETE_FAILURE' => 'Error deleting search index', + 'CLI_SEARCHINDEX_ACTION_IN_PROGRESS' => 'There is an action currently in progress. CLI doesn’t support incomplete index/delete actions, please solve it from the ACP.', + 'CLI_SEARCHINDEX_ACTIVE_NOT_INDEXED' => 'Active search backend isn’t indexed', + 'CLI_SEARCHINDEX_BACKEND_NOT_AVAILABLE' => 'Search backend isn’t available.', + // In all the case %1$s is the logical name of the file and %2$s the real name on the filesystem // eg: big_image.png (2_a51529ae7932008cf8454a95af84cacd) generated. 'CLI_THUMBNAIL_DELETED' => '%1$s (%2$s) deleted.', diff --git a/phpBB/language/en/common.php b/phpBB/language/en/common.php index 6a10e8b846..8d71ca2952 100644 --- a/phpBB/language/en/common.php +++ b/phpBB/language/en/common.php @@ -215,6 +215,9 @@ $lang = array_merge($lang, array( 2 => 'Downloaded %d times', ), + 'DI_SERVICE_NOT_FOUND' => 'Service for class "%1$s" not found in collection.', + 'DI_MULTIPLE_SERVICE_DEFINITIONS' => 'More than one service definitions found for class "%1$s" in collection.', + 'EDIT_POST' => 'Edit post', 'ELLIPSIS' => '…', 'EMAIL' => 'Email', // Short form for EMAIL_ADDRESS @@ -715,6 +718,7 @@ $lang = array_merge($lang, array( 'SEARCH_UNANSWERED' => 'Unanswered topics', 'SEARCH_UNREAD' => 'Unread posts', 'SEARCH_USER_POSTS' => 'Search user’s posts', + 'SEARCH_BACKEND_NOT_FOUND' => 'No search backend could be found.', 'SECONDS' => 'Seconds', 'SEE_ALL' => 'See All', 'SELECT' => 'Select', diff --git a/phpBB/phpbb/console/command/reparser/list_all.php b/phpBB/phpbb/console/command/reparser/list_all.php index ae90c1a68f..83c301e00f 100644 --- a/phpBB/phpbb/console/command/reparser/list_all.php +++ b/phpBB/phpbb/console/command/reparser/list_all.php @@ -49,10 +49,8 @@ class list_all extends \phpbb\console\command\command */ protected function configure() { - $this - ->setName('reparser:list') - ->setDescription($this->user->lang('CLI_DESCRIPTION_REPARSER_LIST')) - ; + $this->setName('reparser:list') + ->setDescription($this->user->lang('CLI_DESCRIPTION_REPARSER_LIST')); } /** diff --git a/phpBB/phpbb/console/command/searchindex/create.php b/phpBB/phpbb/console/command/searchindex/create.php new file mode 100644 index 0000000000..2b368a475d --- /dev/null +++ b/phpBB/phpbb/console/command/searchindex/create.php @@ -0,0 +1,165 @@ + + * @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\console\command\searchindex; + +use phpbb\console\command\command; +use phpbb\language\language; +use phpbb\log\log; +use phpbb\posting\post_helper; +use phpbb\search\exception\no_search_backend_found_exception; +use phpbb\search\search_backend_factory; +use phpbb\search\state_helper; +use phpbb\user; +use Symfony\Component\Console\Command\Command as symfony_command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; + +class create extends command +{ + /** @var language */ + protected $language; + + /** @var log */ + protected $log; + + /** @var post_helper */ + protected $post_helper; + + /** @var search_backend_factory */ + protected $search_backend_factory; + + /** @var state_helper */ + protected $state_helper; + + /** + * Construct method + * + * @param language $language + * @param log $log + * @param post_helper $post_helper + * @param search_backend_factory $search_backend_factory + * @param state_helper $state_helper + * @param user $user + */ + public function __construct(language $language, log $log, post_helper $post_helper, search_backend_factory $search_backend_factory, state_helper $state_helper, user $user) + { + $this->language = $language; + $this->log = $log; + $this->post_helper = $post_helper; + $this->search_backend_factory = $search_backend_factory; + $this->state_helper = $state_helper; + + $this->language->add_lang(array('acp/common', 'acp/search')); + + parent::__construct($user); + } + + /** + * Sets the command name and description + * + * @return void + */ + protected function configure() + { + $this->setName('searchindex:create') + ->setDescription($this->language->lang('CLI_DESCRIPTION_SEARCHINDEX_CREATE')) + ->addArgument( + 'search-backend', + InputArgument::REQUIRED, + $this->language->lang('CLI_SEARCHINDEX_SEARCH_BACKEND_NAME') + ); + } + + /** + * Executes the command searchindex:create + * + * Create search index + * + * @param InputInterface $input The input stream used to get the options + * @param OutputInterface $output The output stream, used to print messages + * + * @return int 0 if all is well, 1 if any errors occurred + */ + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + + $io->section($this->language->lang('CLI_DESCRIPTION_SEARCHINDEX_CREATE')); + + $search_backend = $input->getArgument('search-backend'); + + try + { + $search = $this->search_backend_factory->get($search_backend); + $name = $search->get_name(); + } + catch (no_search_backend_found_exception $e) + { + $io->error($this->language->lang('CLI_SEARCHINDEX_BACKEND_NOT_FOUND', $search_backend)); + return symfony_command::FAILURE; + } + + if ($this->state_helper->is_action_in_progress()) + { + $io->error($this->language->lang('CLI_SEARCHINDEX_ACTION_IN_PROGRESS', $search_backend)); + return symfony_command::FAILURE; + } + + if (!$search->is_available()) + { + $io->error($this->language->lang('CLI_SEARCHINDEX_BACKEND_NOT_AVAILABLE', $search_backend)); + return symfony_command::FAILURE; + } + + try + { + $progress = $this->create_progress_bar($this->post_helper->get_max_post_id(), $io, $output, true); + $progress->setMessage(''); + $progress->start(); + + $this->state_helper->init($search->get_type(), 'create'); + + $counter = 0; + while (($status = $search->create_index($counter)) !== null) + { + $this->state_helper->update_counter($status['post_counter']); + + $progress->setProgress($status['post_counter']); + $progress->setMessage(round($status['rows_per_second'], 2) . ' rows/s'); + } + + $progress->finish(); + + $io->newLine(2); + } + catch (\Exception $e) + { + $this->state_helper->clear_state(); // Unexpected error, cancel action + $io->error($e->getMessage()); // Show also exception message like in acp + $io->error($this->language->lang('CLI_SEARCHINDEX_CREATE_FAILURE', $name)); + return symfony_command::FAILURE; + } + + $search->tidy(); + + $this->state_helper->clear_state(); + + $this->log->add('admin', ANONYMOUS, '', 'LOG_SEARCH_INDEX_CREATED', false, array($name)); + $io->success($this->language->lang('CLI_SEARCHINDEX_CREATE_SUCCESS', $name)); + + return symfony_command::SUCCESS; + } +} diff --git a/phpBB/phpbb/console/command/searchindex/delete.php b/phpBB/phpbb/console/command/searchindex/delete.php new file mode 100644 index 0000000000..b17622d68c --- /dev/null +++ b/phpBB/phpbb/console/command/searchindex/delete.php @@ -0,0 +1,165 @@ + + * @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\console\command\searchindex; + +use phpbb\console\command\command; +use phpbb\language\language; +use phpbb\log\log; +use phpbb\posting\post_helper; +use phpbb\search\exception\no_search_backend_found_exception; +use phpbb\search\search_backend_factory; +use phpbb\search\state_helper; +use phpbb\user; +use Symfony\Component\Console\Command\Command as symfony_command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; + +class delete extends command +{ + /** @var language */ + protected $language; + + /** @var log */ + protected $log; + + /** @var post_helper */ + protected $post_helper; + + /** @var search_backend_factory */ + protected $search_backend_factory; + + /** @var state_helper */ + protected $state_helper; + + /** + * Construct method + * + * @param language $language + * @param log $log + * @param post_helper $post_helper + * @param search_backend_factory $search_backend_factory + * @param state_helper $state_helper + * @param user $user + */ + public function __construct(language $language, log $log, post_helper $post_helper, search_backend_factory $search_backend_factory, state_helper $state_helper, user $user) + { + $this->language = $language; + $this->log = $log; + $this->post_helper = $post_helper; + $this->search_backend_factory = $search_backend_factory; + $this->state_helper = $state_helper; + + $this->language->add_lang(array('acp/common', 'acp/search')); + + parent::__construct($user); + } + + /** + * Sets the command name and description + * + * @return void + */ + protected function configure() + { + $this->setName('searchindex:delete') + ->setDescription($this->language->lang('CLI_DESCRIPTION_SEARCHINDEX_DELETE')) + ->addArgument( + 'search-backend', + InputArgument::REQUIRED, + $this->language->lang('CLI_SEARCHINDEX_SEARCH_BACKEND_NAME') + ); + } + + /** + * Executes the command searchindex:delete + * + * Delete search index + * + * @param InputInterface $input The input stream used to get the options + * @param OutputInterface $output The output stream, used to print messages + * + * @return int 0 if all is well, 1 if any errors occurred + */ + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + + $io->section($this->language->lang('CLI_DESCRIPTION_SEARCHINDEX_DELETE')); + + $search_backend = $input->getArgument('search-backend'); + + try + { + $search = $this->search_backend_factory->get($search_backend); + $name = $search->get_name(); + } + catch (no_search_backend_found_exception $e) + { + $io->error($this->language->lang('CLI_SEARCHINDEX_BACKEND_NOT_FOUND', $search_backend)); + return symfony_command::FAILURE; + } + + if ($this->state_helper->is_action_in_progress()) + { + $io->error($this->language->lang('CLI_SEARCHINDEX_ACTION_IN_PROGRESS', $search_backend)); + return symfony_command::FAILURE; + } + + if (!$search->is_available()) + { + $io->error($this->language->lang('CLI_SEARCHINDEX_BACKEND_NOT_AVAILABLE', $search_backend)); + return symfony_command::FAILURE; + } + + try + { + $progress = $this->create_progress_bar($this->post_helper->get_max_post_id(), $io, $output, true); + $progress->setMessage(''); + $progress->start(); + + $this->state_helper->init($search->get_type(), 'delete'); + + $counter = 0; + while (($status = $search->delete_index($counter)) !== null) + { + $this->state_helper->update_counter($status['post_counter']); + + $progress->setProgress($status['post_counter']); + $progress->setMessage(round($status['rows_per_second'], 2) . ' rows/s'); + } + + $progress->finish(); + + $io->newLine(2); + } + catch (\Exception $e) + { + $this->state_helper->clear_state(); // Unexpected error, cancel action + $io->error($e->getMessage()); // Show also exception message like in acp + $io->error($this->language->lang('CLI_SEARCHINDEX_DELETE_FAILURE', $name)); + return symfony_command::FAILURE; + } + + $search->tidy(); + + $this->state_helper->clear_state(); + + $this->log->add('admin', ANONYMOUS, '', 'LOG_SEARCH_INDEX_REMOVED', false, array($name)); + $io->success($this->language->lang('CLI_SEARCHINDEX_DELETE_SUCCESS', $name)); + + return symfony_command::SUCCESS; + } +} diff --git a/phpBB/phpbb/console/command/searchindex/list_all.php b/phpBB/phpbb/console/command/searchindex/list_all.php new file mode 100644 index 0000000000..e88949fd2a --- /dev/null +++ b/phpBB/phpbb/console/command/searchindex/list_all.php @@ -0,0 +1,96 @@ + + * @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\console\command\searchindex; + +use phpbb\config\config; +use phpbb\console\command\command; +use phpbb\di\service_collection; +use phpbb\language\language; +use phpbb\user; +use Symfony\Component\Console\Command\Command as symfony_command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; + +class list_all extends command +{ + /** @var config */ + protected $config; + + /** @var language */ + protected $language; + + /** @var service_collection */ + protected $search_backend_collection; + + /** + * Construct method + * + * @param config $config + * @param language $language + * @param service_collection $search_backend_collection + * @param user $user + */ + public function __construct(config $config, language $language, service_collection $search_backend_collection, user $user) + { + $this->config = $config; + $this->language = $language; + $this->search_backend_collection = $search_backend_collection; + + parent::__construct($user); + } + + /** + * Sets the command name and description + * + * @return void + */ + protected function configure() + { + $this->setName('searchindex:list') + ->setDescription($this->language->lang('CLI_DESCRIPTION_SEARCHINDEX_LIST')); + } + + /** + * Executes the command searchindex:list + * + * List all search backends. + * + * @param InputInterface $input The input stream used to get the options + * @param OutputInterface $output The output stream, used to print messages + * + * @return int 0 if all is well, 1 if any errors occurred + */ + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + + $search_backends = []; + foreach ($this->search_backend_collection as $search_backend) + { + $name = $search_backend->get_type(); + $active = ($name === $this->config['search_type']) ? '(' . $this->language->lang('ACTIVE') . ') ' : ''; + $search_backends[] = '' . $name . ' ' . $active . $search_backend->get_name(); + + if ($name === $this->config['search_type'] && !$search_backend->index_created()) + { + $io->error($this->language->lang('CLI_SEARCHINDEX_ACTIVE_NOT_INDEXED')); + } + } + + $io->listing($search_backends); + + return symfony_command::SUCCESS; + } +} diff --git a/phpBB/phpbb/cron/task/core/tidy_search.php b/phpBB/phpbb/cron/task/core/tidy_search.php index d3f5d41bc0..a29a403627 100644 --- a/phpBB/phpbb/cron/task/core/tidy_search.php +++ b/phpBB/phpbb/cron/task/core/tidy_search.php @@ -15,6 +15,7 @@ namespace phpbb\cron\task\core; use phpbb\config\config; use phpbb\cron\task\base; +use phpbb\di\exception\di_exception; use phpbb\search\backend\search_backend_interface; use phpbb\search\search_backend_factory; @@ -88,7 +89,7 @@ class tidy_search extends base $this->active_search = $this->search_backend_factory->get_active(); } } - catch (\RuntimeException $e) + catch (di_exception $e) { return false; } diff --git a/phpBB/phpbb/di/exception/di_exception.php b/phpBB/phpbb/di/exception/di_exception.php new file mode 100644 index 0000000000..b4f3984b39 --- /dev/null +++ b/phpBB/phpbb/di/exception/di_exception.php @@ -0,0 +1,20 @@ + + * @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\di\exception; + +use phpbb\exception\runtime_exception; + +class di_exception extends runtime_exception +{ +} diff --git a/phpBB/phpbb/di/exception/multiple_service_definitions_exception.php b/phpBB/phpbb/di/exception/multiple_service_definitions_exception.php new file mode 100644 index 0000000000..e29d47d26c --- /dev/null +++ b/phpBB/phpbb/di/exception/multiple_service_definitions_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\di\exception; + +class multiple_service_definitions_exception extends di_exception +{ +} diff --git a/phpBB/phpbb/di/exception/service_not_found_exception.php b/phpBB/phpbb/di/exception/service_not_found_exception.php new file mode 100644 index 0000000000..79c69c85f6 --- /dev/null +++ b/phpBB/phpbb/di/exception/service_not_found_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\di\exception; + +class service_not_found_exception extends di_exception +{ +} diff --git a/phpBB/phpbb/di/service_collection.php b/phpBB/phpbb/di/service_collection.php index 80fc011606..5c108bbb84 100644 --- a/phpBB/phpbb/di/service_collection.php +++ b/phpBB/phpbb/di/service_collection.php @@ -13,6 +13,8 @@ namespace phpbb\di; +use phpbb\di\exception\multiple_service_definitions_exception; +use phpbb\di\exception\service_not_found_exception; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -105,7 +107,7 @@ class service_collection extends \ArrayObject { if ($service_id !== null) { - throw new \RuntimeException('More than one service definitions found for class "'.$class.'" in collection.'); + throw new multiple_service_definitions_exception('DI_MULTIPLE_SERVICE_DEFINITIONS', [$class]); } $service_id = $id; @@ -114,7 +116,7 @@ class service_collection extends \ArrayObject if ($service_id === null) { - throw new \RuntimeException('No service found for class "'.$class.'" in collection.'); + throw new service_not_found_exception('DI_SERVICE_NOT_FOUND', [$class]); } return $this->offsetGet($service_id); diff --git a/phpBB/phpbb/install/module/install_data/task/create_search_index.php b/phpBB/phpbb/install/module/install_data/task/create_search_index.php index c8f77c105f..9f80e5e784 100644 --- a/phpBB/phpbb/install/module/install_data/task/create_search_index.php +++ b/phpBB/phpbb/install/module/install_data/task/create_search_index.php @@ -133,6 +133,9 @@ class create_search_index extends database_task $this->phpbb_dispatcher, $container->get('language'), $this->user, + SEARCH_RESULTS_TABLE, + SEARCH_WORDLIST_TABLE, + SEARCH_WORDMATCH_TABLE, $this->phpbb_root_path, $this->php_ext ); diff --git a/phpBB/phpbb/posting/post_helper.php b/phpBB/phpbb/posting/post_helper.php new file mode 100644 index 0000000000..2fbba9688b --- /dev/null +++ b/phpBB/phpbb/posting/post_helper.php @@ -0,0 +1,43 @@ + + * @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\posting; + +use phpbb\db\driver\driver_interface; + +class post_helper +{ + /** + * @var driver_interface + */ + protected $db; + + public function __construct(driver_interface $db) + { + $this->db = $db; + } + + /** + * Get last post id + */ + public function get_max_post_id(): int + { + $sql = 'SELECT MAX(post_id) as max_post_id + FROM '. POSTS_TABLE; + $result = $this->db->sql_query($sql); + $max_post_id = (int) $this->db->sql_fetchfield('max_post_id'); + $this->db->sql_freeresult($result); + + return $max_post_id; + } +} diff --git a/phpBB/phpbb/search/backend/base.php b/phpBB/phpbb/search/backend/base.php index 4003e102ef..e195e0f4c0 100644 --- a/phpBB/phpbb/search/backend/base.php +++ b/phpBB/phpbb/search/backend/base.php @@ -1,15 +1,15 @@ -* @license GNU General Public License, version 2 (GPL-2.0) -* -* For full copyright and license information, please see -* the docs/CREDITS.txt file. -* -*/ + * + * This file is part of the phpBB Forum Software package. + * + * @copyright (c) phpBB Limited + * @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\search\backend; @@ -19,9 +19,9 @@ use phpbb\db\driver\driver_interface; use phpbb\user; /** -* optional base class for search plugins providing simple caching based on ACM -* and functions to retrieve ignore_words and synonyms -*/ + * optional base class for search plugins providing simple caching based on ACM + * and functions to retrieve ignore_words and synonyms + */ abstract class base implements search_backend_interface { public const SEARCH_RESULT_NOT_IN_CACHE = 0; @@ -51,20 +51,27 @@ abstract class base implements search_backend_interface */ protected $user; + /** + * @var string + */ + protected $search_results_table; + /** * Constructor. * - * @param service $cache - * @param config $config - * @param driver_interface $db - * @param user $user + * @param service $cache + * @param config $config + * @param driver_interface $db + * @param user $user + * @param string $search_results_table */ - public function __construct(service $cache, config $config, driver_interface $db, user $user) + public function __construct(service $cache, config $config, driver_interface $db, user $user, string $search_results_table) { $this->cache = $cache; $this->config = $config; $this->db = $db; $this->user = $user; + $this->search_results_table = $search_results_table; } /** @@ -180,7 +187,7 @@ abstract class base implements search_backend_interface if (!empty($keywords) || count($author_ary)) { $sql = 'SELECT search_time - FROM ' . SEARCH_RESULTS_TABLE . ' + FROM ' . $this->search_results_table . ' WHERE search_key = \'' . $this->db->sql_escape($search_key) . '\''; $result = $this->db->sql_query($sql); @@ -193,7 +200,7 @@ abstract class base implements search_backend_interface 'search_authors' => ' ' . implode(' ', $author_ary) . ' ' ); - $sql = 'INSERT INTO ' . SEARCH_RESULTS_TABLE . ' ' . $this->db->sql_build_array('INSERT', $sql_ary); + $sql = 'INSERT INTO ' . $this->search_results_table . ' ' . $this->db->sql_build_array('INSERT', $sql_ary); $this->db->sql_query($sql); } $this->db->sql_freeresult($result); @@ -253,7 +260,7 @@ abstract class base implements search_backend_interface } $this->cache->put('_search_results_' . $search_key, $store, $this->config['search_store_results']); - $sql = 'UPDATE ' . SEARCH_RESULTS_TABLE . ' + $sql = 'UPDATE ' . $this->search_results_table . ' SET search_time = ' . time() . ' WHERE search_key = \'' . $this->db->sql_escape($search_key) . '\''; $this->db->sql_query($sql); @@ -280,7 +287,7 @@ abstract class base implements search_backend_interface } $sql = 'SELECT search_key - FROM ' . SEARCH_RESULTS_TABLE . " + FROM ' . $this->search_results_table . " WHERE search_keywords LIKE '%*%' $sql_where"; $result = $this->db->sql_query($sql); @@ -301,7 +308,7 @@ abstract class base implements search_backend_interface } $sql = 'SELECT search_key - FROM ' . SEARCH_RESULTS_TABLE . " + FROM ' . $this->search_results_table . " WHERE $sql_where"; $result = $this->db->sql_query($sql); @@ -313,7 +320,7 @@ abstract class base implements search_backend_interface } $sql = 'DELETE - FROM ' . SEARCH_RESULTS_TABLE . ' + FROM ' . $this->search_results_table . ' WHERE search_time < ' . (time() - (int) $this->config['search_store_results']); $this->db->sql_query($sql); } @@ -329,7 +336,7 @@ abstract class base implements search_backend_interface $starttime = microtime(true); $row_count = 0; - while (still_on_time() && $post_counter <= $max_post_id) + while (still_on_time() && $post_counter < $max_post_id) { $rows = $this->get_posts_batch_after($post_counter); @@ -346,7 +353,13 @@ abstract class base implements search_backend_interface $this->index('post', (int) $row['post_id'], $row['post_text'], $row['post_subject'], (int) $row['poster_id'], (int) $row['forum_id']); } $row_count++; - $post_counter = $row['post_id']; + $post_counter = (int) $row['post_id']; + } + + // With cli process only one batch each time to be able to track progress + if (PHP_SAPI === 'cli') + { + break; } } @@ -357,7 +370,7 @@ abstract class base implements search_backend_interface $this->tidy(); $this->config['num_posts'] = $num_posts; - if ($post_counter < $max_post_id) + if ($post_counter < $max_post_id) // If there are still post to index { $totaltime = microtime(true) - $starttime; $rows_per_second = $row_count / $totaltime; @@ -382,7 +395,8 @@ abstract class base implements search_backend_interface $starttime = microtime(true); $row_count = 0; - while (still_on_time() && $post_counter <= $max_post_id) + + while (still_on_time() && $post_counter < $max_post_id) { $rows = $this->get_posts_batch_after($post_counter); $ids = $posters = $forum_ids = array(); @@ -399,9 +413,15 @@ abstract class base implements search_backend_interface $this->index_remove($ids, $posters, $forum_ids); $post_counter = $ids[count($ids) - 1]; } + + // With cli process only one batch each time to be able to track progress + if (PHP_SAPI === 'cli') + { + break; + } } - if ($post_counter < $max_post_id) + if ($post_counter < $max_post_id) // If there are still post delete from index { $totaltime = microtime(true) - $starttime; $rows_per_second = $row_count / $totaltime; diff --git a/phpBB/phpbb/search/backend/fulltext_mysql.php b/phpBB/phpbb/search/backend/fulltext_mysql.php index 3e47cb4668..7635b503fa 100644 --- a/phpBB/phpbb/search/backend/fulltext_mysql.php +++ b/phpBB/phpbb/search/backend/fulltext_mysql.php @@ -17,8 +17,8 @@ use phpbb\config\config; use phpbb\db\driver\driver_interface; use phpbb\event\dispatcher_interface; use phpbb\language\language; +use phpbb\search\exception\search_exception; use phpbb\user; -use RuntimeException; /** * Fulltext search for MySQL @@ -72,19 +72,20 @@ class fulltext_mysql extends base implements search_backend_interface * Constructor * Creates a new \phpbb\search\backend\fulltext_mysql, which is used as a search backend * - * @param config $config Config object - * @param driver_interface $db Database object - * @param dispatcher_interface $phpbb_dispatcher Event dispatcher object - * @param language $language - * @param user $user User object - * @param string $phpbb_root_path Relative path to phpBB root - * @param string $phpEx PHP file extension + * @param config $config Config object + * @param driver_interface $db Database object + * @param dispatcher_interface $phpbb_dispatcher Event dispatcher object + * @param language $language + * @param user $user User object + * @param string $search_results_table + * @param string $phpbb_root_path Relative path to phpBB root + * @param string $phpEx PHP file extension */ - public function __construct(config $config, driver_interface $db, dispatcher_interface $phpbb_dispatcher, language $language, user $user, string $phpbb_root_path, string $phpEx) + public function __construct(config $config, driver_interface $db, dispatcher_interface $phpbb_dispatcher, language $language, user $user, string $search_results_table, string $phpbb_root_path, string $phpEx) { global $cache; - parent::__construct($cache, $config, $db, $user); + parent::__construct($cache, $config, $db, $user, $search_results_table); $this->phpbb_dispatcher = $phpbb_dispatcher; $this->language = $language; @@ -916,7 +917,7 @@ class fulltext_mysql extends base implements search_backend_interface // Make sure we can actually use MySQL with fulltext indexes if ($error = $this->init()) { - throw new RuntimeException($error); + throw new search_exception($error); } if (empty($this->stats)) @@ -975,7 +976,7 @@ class fulltext_mysql extends base implements search_backend_interface $this->db->sql_query($sql_query); } - $this->db->sql_query('TRUNCATE TABLE ' . SEARCH_RESULTS_TABLE); + $this->db->sql_query('TRUNCATE TABLE ' . $this->search_results_table); return null; } @@ -988,7 +989,7 @@ class fulltext_mysql extends base implements search_backend_interface // Make sure we can actually use MySQL with fulltext indexes if ($error = $this->init()) { - throw new RuntimeException($error); + throw new search_exception($error); } if (empty($this->stats)) @@ -1041,7 +1042,7 @@ class fulltext_mysql extends base implements search_backend_interface $this->db->sql_query($sql_query); } - $this->db->sql_query('TRUNCATE TABLE ' . SEARCH_RESULTS_TABLE); + $this->db->sql_query('TRUNCATE TABLE ' . $this->search_results_table); return null; } diff --git a/phpBB/phpbb/search/backend/fulltext_native.php b/phpBB/phpbb/search/backend/fulltext_native.php index a5c9bb8202..aefc73b8cd 100644 --- a/phpBB/phpbb/search/backend/fulltext_native.php +++ b/phpBB/phpbb/search/backend/fulltext_native.php @@ -93,30 +93,42 @@ class fulltext_native extends base implements search_backend_interface */ protected $phpbb_dispatcher; - /** - * @var language - */ + /** @var language */ protected $language; + /** @var string */ + protected $search_wordlist_table; + + /** @var string */ + protected $search_wordmatch_table; + /** * Initialises the fulltext_native search backend with min/max word length * - * @param config $config Config object - * @param driver_interface $db Database object - * @param dispatcher_interface $phpbb_dispatcher Event dispatcher object - * @param language $language - * @param user $user User object - * @param string $phpbb_root_path phpBB root path - * @param string $phpEx PHP file extension + * @param config $config Config object + * @param driver_interface $db Database object + * @param dispatcher_interface $phpbb_dispatcher Event dispatcher object + * @param language $language + * @param user $user User object + * @param string $search_results_table + * @param string $search_wordlist_table + * @param string $search_wordmatch_table + * @param string $phpbb_root_path phpBB root path + * @param string $phpEx PHP file extension */ - public function __construct(config $config, driver_interface $db, dispatcher_interface $phpbb_dispatcher, language $language, user $user, string $phpbb_root_path, string $phpEx) + public function __construct(config $config, driver_interface $db, dispatcher_interface $phpbb_dispatcher, + language $language, user $user, string $search_results_table, string $search_wordlist_table, + string $search_wordmatch_table, string $phpbb_root_path, string $phpEx) { global $cache; - parent::__construct($cache, $config, $db, $user); + parent::__construct($cache, $config, $db, $user, $search_results_table); $this->phpbb_dispatcher = $phpbb_dispatcher; $this->language = $language; + $this->search_wordlist_table = $search_wordlist_table; + $this->search_wordmatch_table = $search_wordmatch_table; + $this->phpbb_root_path = $phpbb_root_path; $this->php_ext = $phpEx; @@ -335,7 +347,7 @@ class fulltext_native extends base implements search_backend_interface if (count($exact_words)) { $sql = 'SELECT word_id, word_text, word_common - FROM ' . SEARCH_WORDLIST_TABLE . ' + FROM ' . $this->search_wordlist_table . ' WHERE ' . $this->db->sql_in_set('word_text', $exact_words) . ' ORDER BY word_count ASC'; $result = $this->db->sql_query($sql); @@ -607,8 +619,8 @@ class fulltext_native extends base implements search_backend_interface $sql_array = array( 'SELECT' => ($type == 'posts') ? 'DISTINCT p.post_id' : 'DISTINCT p.topic_id', 'FROM' => array( - SEARCH_WORDMATCH_TABLE => array(), - SEARCH_WORDLIST_TABLE => array(), + $this->search_wordmatch_table => array(), + $this->search_wordlist_table => array(), ), 'LEFT_JOIN' => array(array( 'FROM' => array(POSTS_TABLE => 'p'), @@ -660,7 +672,7 @@ class fulltext_native extends base implements search_backend_interface if (is_string($id)) { $sql_array['LEFT_JOIN'][] = array( - 'FROM' => array(SEARCH_WORDLIST_TABLE => 'w' . $w_num), + 'FROM' => array($this->search_wordlist_table => 'w' . $w_num), 'ON' => "w$w_num.word_text LIKE $id" ); $word_ids[] = "w$w_num.word_id"; @@ -680,7 +692,7 @@ class fulltext_native extends base implements search_backend_interface } else if (is_string($subquery)) { - $sql_array['FROM'][SEARCH_WORDLIST_TABLE][] = 'w' . $w_num; + $sql_array['FROM'][$this->search_wordlist_table][] = 'w' . $w_num; $sql_where[] = "w$w_num.word_text LIKE $subquery"; $sql_where[] = "m$m_num.word_id = w$w_num.word_id"; @@ -693,7 +705,7 @@ class fulltext_native extends base implements search_backend_interface $sql_where[] = "m$m_num.word_id = $subquery"; } - $sql_array['FROM'][SEARCH_WORDMATCH_TABLE][] = 'm' . $m_num; + $sql_array['FROM'][$this->search_wordmatch_table][] = 'm' . $m_num; if ($title_match) { @@ -712,7 +724,7 @@ class fulltext_native extends base implements search_backend_interface if (is_string($subquery)) { $sql_array['LEFT_JOIN'][] = array( - 'FROM' => array(SEARCH_WORDLIST_TABLE => 'w' . $w_num), + 'FROM' => array($this->search_wordlist_table => 'w' . $w_num), 'ON' => "w$w_num.word_text LIKE $subquery" ); @@ -726,7 +738,7 @@ class fulltext_native extends base implements search_backend_interface if (count($this->must_not_contain_ids)) { $sql_array['LEFT_JOIN'][] = array( - 'FROM' => array(SEARCH_WORDMATCH_TABLE => 'm' . $m_num), + 'FROM' => array($this->search_wordmatch_table => 'm' . $m_num), 'ON' => $this->db->sql_in_set("m$m_num.word_id", $this->must_not_contain_ids) . (($title_match) ? " AND m$m_num.$title_match" : '') . " AND m$m_num.post_id = m0.post_id" ); @@ -742,7 +754,7 @@ class fulltext_native extends base implements search_backend_interface if (is_string($id)) { $sql_array['LEFT_JOIN'][] = array( - 'FROM' => array(SEARCH_WORDLIST_TABLE => 'w' . $w_num), + 'FROM' => array($this->search_wordlist_table => 'w' . $w_num), 'ON' => "w$w_num.word_text LIKE $id" ); $id = "w$w_num.word_id"; @@ -752,7 +764,7 @@ class fulltext_native extends base implements search_backend_interface } $sql_array['LEFT_JOIN'][] = array( - 'FROM' => array(SEARCH_WORDMATCH_TABLE => 'm' . $m_num), + 'FROM' => array($this->search_wordmatch_table => 'm' . $m_num), 'ON' => "m$m_num.word_id = $id AND m$m_num.post_id = m0.post_id" . (($title_match) ? " AND m$m_num.$title_match" : '') ); $is_null_joins[] = "m$m_num.word_id IS NULL"; @@ -1310,7 +1322,7 @@ class fulltext_native extends base implements search_backend_interface $words['del']['title'] = array(); $sql = 'SELECT w.word_id, w.word_text, m.title_match - FROM ' . SEARCH_WORDLIST_TABLE . ' w, ' . SEARCH_WORDMATCH_TABLE . " m + FROM ' . $this->search_wordlist_table . ' w, ' . $this->search_wordmatch_table . " m WHERE m.post_id = $post_id AND w.word_id = m.word_id"; $result = $this->db->sql_query($sql); @@ -1379,7 +1391,7 @@ class fulltext_native extends base implements search_backend_interface if (count($unique_add_words)) { $sql = 'SELECT word_id, word_text - FROM ' . SEARCH_WORDLIST_TABLE . ' + FROM ' . $this->search_wordlist_table . ' WHERE ' . $this->db->sql_in_set('word_text', $unique_add_words); $result = $this->db->sql_query($sql); @@ -1401,7 +1413,7 @@ class fulltext_native extends base implements search_backend_interface $sql_ary[] = array('word_text' => (string) $word, 'word_count' => 0); } $this->db->sql_return_on_error(true); - $this->db->sql_multi_insert(SEARCH_WORDLIST_TABLE, $sql_ary); + $this->db->sql_multi_insert($this->search_wordlist_table, $sql_ary); $this->db->sql_return_on_error(false); } unset($new_words, $sql_ary); @@ -1424,13 +1436,13 @@ class fulltext_native extends base implements search_backend_interface $sql_in[] = $cur_words[$word_in][$word]; } - $sql = 'DELETE FROM ' . SEARCH_WORDMATCH_TABLE . ' + $sql = 'DELETE FROM ' . $this->search_wordmatch_table . ' WHERE ' . $this->db->sql_in_set('word_id', $sql_in) . ' AND post_id = ' . intval($post_id) . " AND title_match = $title_match"; $this->db->sql_query($sql); - $sql = 'UPDATE ' . SEARCH_WORDLIST_TABLE . ' + $sql = 'UPDATE ' . $this->search_wordlist_table . ' SET word_count = word_count - 1 WHERE ' . $this->db->sql_in_set('word_id', $sql_in) . ' AND word_count > 0'; @@ -1447,13 +1459,13 @@ class fulltext_native extends base implements search_backend_interface if (count($word_ary)) { - $sql = 'INSERT INTO ' . SEARCH_WORDMATCH_TABLE . ' (post_id, word_id, title_match) + $sql = 'INSERT INTO ' . $this->search_wordmatch_table . ' (post_id, word_id, title_match) SELECT ' . (int) $post_id . ', word_id, ' . (int) $title_match . ' - FROM ' . SEARCH_WORDLIST_TABLE . ' + FROM ' . $this->search_wordlist_table . ' WHERE ' . $this->db->sql_in_set('word_text', $word_ary); $this->db->sql_query($sql); - $sql = 'UPDATE ' . SEARCH_WORDLIST_TABLE . ' + $sql = 'UPDATE ' . $this->search_wordlist_table . ' SET word_count = word_count + 1 WHERE ' . $this->db->sql_in_set('word_text', $word_ary); $this->db->sql_query($sql); @@ -1479,7 +1491,7 @@ class fulltext_native extends base implements search_backend_interface if (count($post_ids)) { $sql = 'SELECT w.word_id, w.word_text, m.title_match - FROM ' . SEARCH_WORDMATCH_TABLE . ' m, ' . SEARCH_WORDLIST_TABLE . ' w + FROM ' . $this->search_wordmatch_table . ' m, ' . $this->search_wordlist_table . ' w WHERE ' . $this->db->sql_in_set('m.post_id', $post_ids) . ' AND w.word_id = m.word_id'; $result = $this->db->sql_query($sql); @@ -1501,7 +1513,7 @@ class fulltext_native extends base implements search_backend_interface if (count($title_word_ids)) { - $sql = 'UPDATE ' . SEARCH_WORDLIST_TABLE . ' + $sql = 'UPDATE ' . $this->search_wordlist_table . ' SET word_count = word_count - 1 WHERE ' . $this->db->sql_in_set('word_id', $title_word_ids) . ' AND word_count > 0'; @@ -1510,7 +1522,7 @@ class fulltext_native extends base implements search_backend_interface if (count($message_word_ids)) { - $sql = 'UPDATE ' . SEARCH_WORDLIST_TABLE . ' + $sql = 'UPDATE ' . $this->search_wordlist_table . ' SET word_count = word_count - 1 WHERE ' . $this->db->sql_in_set('word_id', $message_word_ids) . ' AND word_count > 0'; @@ -1520,7 +1532,7 @@ class fulltext_native extends base implements search_backend_interface unset($title_word_ids); unset($message_word_ids); - $sql = 'DELETE FROM ' . SEARCH_WORDMATCH_TABLE . ' + $sql = 'DELETE FROM ' . $this->search_wordmatch_table . ' WHERE ' . $this->db->sql_in_set('post_id', $post_ids); $this->db->sql_query($sql); } @@ -1549,7 +1561,7 @@ class fulltext_native extends base implements search_backend_interface $common_threshold = ((double) $this->config['fulltext_native_common_thres']) / 100.0; // First, get the IDs of common words $sql = 'SELECT word_id, word_text - FROM ' . SEARCH_WORDLIST_TABLE . ' + FROM ' . $this->search_wordlist_table . ' WHERE word_count > ' . floor($this->config['num_posts'] * $common_threshold) . ' OR word_common = 1'; $result = $this->db->sql_query($sql); @@ -1565,7 +1577,7 @@ class fulltext_native extends base implements search_backend_interface if (count($sql_in)) { // Flag the words - $sql = 'UPDATE ' . SEARCH_WORDLIST_TABLE . ' + $sql = 'UPDATE ' . $this->search_wordlist_table . ' SET word_common = 1 WHERE ' . $this->db->sql_in_set('word_id', $sql_in); $this->db->sql_query($sql); @@ -1575,7 +1587,7 @@ class fulltext_native extends base implements search_backend_interface $this->config->set('search_last_gc', time(), false); // Delete the matches - $sql = 'DELETE FROM ' . SEARCH_WORDMATCH_TABLE . ' + $sql = 'DELETE FROM ' . $this->search_wordmatch_table . ' WHERE ' . $this->db->sql_in_set('word_id', $sql_in); $this->db->sql_query($sql); } @@ -1603,15 +1615,15 @@ class fulltext_native extends base implements search_backend_interface switch ($this->db->get_sql_layer()) { case 'sqlite3': - $sql_queries[] = 'DELETE FROM ' . SEARCH_WORDLIST_TABLE; - $sql_queries[] = 'DELETE FROM ' . SEARCH_WORDMATCH_TABLE; - $sql_queries[] = 'DELETE FROM ' . SEARCH_RESULTS_TABLE; + $sql_queries[] = 'DELETE FROM ' . $this->search_wordlist_table; + $sql_queries[] = 'DELETE FROM ' . $this->search_wordmatch_table; + $sql_queries[] = 'DELETE FROM ' . $this->search_results_table; break; default: - $sql_queries[] = 'TRUNCATE TABLE ' . SEARCH_WORDLIST_TABLE; - $sql_queries[] = 'TRUNCATE TABLE ' . SEARCH_WORDMATCH_TABLE; - $sql_queries[] = 'TRUNCATE TABLE ' . SEARCH_RESULTS_TABLE; + $sql_queries[] = 'TRUNCATE TABLE ' . $this->search_wordlist_table; + $sql_queries[] = 'TRUNCATE TABLE ' . $this->search_wordmatch_table; + $sql_queries[] = 'TRUNCATE TABLE ' . $this->search_results_table; break; } @@ -1672,8 +1684,8 @@ class fulltext_native extends base implements search_backend_interface */ protected function get_stats() { - $this->stats['total_words'] = $this->db->get_estimated_row_count(SEARCH_WORDLIST_TABLE); - $this->stats['total_matches'] = $this->db->get_estimated_row_count(SEARCH_WORDMATCH_TABLE); + $this->stats['total_words'] = $this->db->get_estimated_row_count($this->search_wordlist_table); + $this->stats['total_matches'] = $this->db->get_estimated_row_count($this->search_wordmatch_table); } /** diff --git a/phpBB/phpbb/search/backend/fulltext_postgres.php b/phpBB/phpbb/search/backend/fulltext_postgres.php index bd2c24224c..efd6ecc148 100644 --- a/phpBB/phpbb/search/backend/fulltext_postgres.php +++ b/phpBB/phpbb/search/backend/fulltext_postgres.php @@ -17,8 +17,8 @@ use phpbb\config\config; use phpbb\db\driver\driver_interface; use phpbb\event\dispatcher_interface; use phpbb\language\language; +use phpbb\search\exception\search_exception; use phpbb\user; -use RuntimeException; /** * Fulltext search for PostgreSQL @@ -84,19 +84,20 @@ class fulltext_postgres extends base implements search_backend_interface * Constructor * Creates a new \phpbb\search\backend\fulltext_postgres, which is used as a search backend * - * @param config $config Config object - * @param driver_interface $db Database object - * @param dispatcher_interface $phpbb_dispatcher Event dispatcher object - * @param language $language - * @param user $user User object - * @param string $phpbb_root_path Relative path to phpBB root - * @param string $phpEx PHP file extension + * @param config $config Config object + * @param driver_interface $db Database object + * @param dispatcher_interface $phpbb_dispatcher Event dispatcher object + * @param language $language + * @param user $user User object + * @param string $search_results_table + * @param string $phpbb_root_path Relative path to phpBB root + * @param string $phpEx PHP file extension */ - public function __construct(config $config, driver_interface $db, dispatcher_interface $phpbb_dispatcher, language $language, user $user, string $phpbb_root_path, string $phpEx) + public function __construct(config $config, driver_interface $db, dispatcher_interface $phpbb_dispatcher, language $language, user $user, string $search_results_table, string $phpbb_root_path, string $phpEx) { global $cache; - parent::__construct($cache, $config, $db, $user); + parent::__construct($cache, $config, $db, $user, $search_results_table); $this->phpbb_dispatcher = $phpbb_dispatcher; $this->language = $language; @@ -871,7 +872,7 @@ class fulltext_postgres extends base implements search_backend_interface // Make sure we can actually use PostgreSQL with fulltext indexes if ($error = $this->init()) { - throw new RuntimeException($error); + throw new search_exception($error); } if (empty($this->stats)) @@ -917,7 +918,7 @@ class fulltext_postgres extends base implements search_backend_interface $this->db->sql_query($sql_query); } - $this->db->sql_query('TRUNCATE TABLE ' . SEARCH_RESULTS_TABLE); + $this->db->sql_query('TRUNCATE TABLE ' . $this->search_results_table); return null; } @@ -930,7 +931,7 @@ class fulltext_postgres extends base implements search_backend_interface // Make sure we can actually use PostgreSQL with fulltext indexes if ($error = $this->init()) { - throw new RuntimeException($error); + throw new search_exception($error); } if (empty($this->stats)) @@ -976,7 +977,7 @@ class fulltext_postgres extends base implements search_backend_interface $this->db->sql_query($sql_query); } - $this->db->sql_query('TRUNCATE TABLE ' . SEARCH_RESULTS_TABLE); + $this->db->sql_query('TRUNCATE TABLE ' . $this->search_results_table); return null; } diff --git a/phpBB/phpbb/search/exception/action_in_progress_exception.php b/phpBB/phpbb/search/exception/action_in_progress_exception.php new file mode 100644 index 0000000000..9187f2bff6 --- /dev/null +++ b/phpBB/phpbb/search/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\search\exception; + +class action_in_progress_exception extends search_exception +{ +} diff --git a/phpBB/phpbb/search/exception/no_action_in_progress_exception.php b/phpBB/phpbb/search/exception/no_action_in_progress_exception.php new file mode 100644 index 0000000000..2f8a9e28c5 --- /dev/null +++ b/phpBB/phpbb/search/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\search\exception; + +class no_action_in_progress_exception extends search_exception +{ +} diff --git a/phpBB/phpbb/search/exception/no_search_backend_found_exception.php b/phpBB/phpbb/search/exception/no_search_backend_found_exception.php new file mode 100644 index 0000000000..752db7ce17 --- /dev/null +++ b/phpBB/phpbb/search/exception/no_search_backend_found_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\search\exception; + +class no_search_backend_found_exception extends search_exception +{ +} diff --git a/phpBB/phpbb/search/exception/search_exception.php b/phpBB/phpbb/search/exception/search_exception.php new file mode 100644 index 0000000000..c1b904d5f4 --- /dev/null +++ b/phpBB/phpbb/search/exception/search_exception.php @@ -0,0 +1,20 @@ + + * @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\search\exception; + +use phpbb\exception\runtime_exception; + +class search_exception extends runtime_exception +{ +} diff --git a/phpBB/phpbb/search/search_backend_factory.php b/phpBB/phpbb/search/search_backend_factory.php index eac31885ed..019d3f6804 100644 --- a/phpBB/phpbb/search/search_backend_factory.php +++ b/phpBB/phpbb/search/search_backend_factory.php @@ -14,8 +14,10 @@ namespace phpbb\search; use phpbb\config\config; +use phpbb\di\exception\service_not_found_exception; use phpbb\di\service_collection; use phpbb\search\backend\search_backend_interface; +use phpbb\search\exception\no_search_backend_found_exception; class search_backend_factory { @@ -46,16 +48,29 @@ class search_backend_factory * * @param string $class * + * @throws no_search_backend_found_exception + * * @return search_backend_interface */ public function get(string $class): search_backend_interface { - return $this->search_backends->get_by_class($class); + try + { + $search = $this->search_backends->get_by_class($class); + } + catch (service_not_found_exception $e) + { + throw new no_search_backend_found_exception('SEARCH_BACKEND_NOT_FOUND', [], $e); + } + + return $search; } /** * Obtains active search backend * + * @throws no_search_backend_found_exception + * * @return search_backend_interface */ public function get_active(): search_backend_interface diff --git a/phpBB/phpbb/search/state_helper.php b/phpBB/phpbb/search/state_helper.php new file mode 100644 index 0000000000..b024c3dfe4 --- /dev/null +++ b/phpBB/phpbb/search/state_helper.php @@ -0,0 +1,181 @@ + + * @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\search; + +use phpbb\config\config; +use phpbb\search\exception\action_in_progress_exception; +use phpbb\search\exception\no_action_in_progress_exception; +use phpbb\search\exception\no_search_backend_found_exception; +use phpbb\search\exception\search_exception; + +class state_helper +{ + protected const STATE_SEARCH_TYPE = 0; + protected const STATE_ACTION = 1; + protected const STATE_POST_COUNTER = 2; + + /** @var config */ + protected $config; + + /** @var search_backend_factory */ + protected $search_backend_factory; + + /** + * Constructor. + * + * @param config $config + * @param search_backend_factory $search_backend_factory + */ + public function __construct(config $config, search_backend_factory $search_backend_factory) + { + $this->config = $config; + $this->search_backend_factory = $search_backend_factory; + } + + /** + * Returns whether there is an action in progress + * + * @return bool + */ + public function is_action_in_progress(): bool + { + return !empty($this->config['search_indexing_state']); + } + + /** + * @return string The class name of the search backend + * + * @throws no_action_in_progress_exception If there is no action in progress + */ + public function type(): string + { + $state = $this->load_state(); + + return $state[self::STATE_SEARCH_TYPE]; + } + + /** + * @return string The action that is being executed, can be 'create' or 'delete' + * + * @throws no_action_in_progress_exception If there is no action in progress + */ + public function action(): string + { + $state = $this->load_state(); + + return $state[self::STATE_ACTION]; + } + + /** + * @return int The post counter + * + * @throws no_action_in_progress_exception If there is no action in progress + */ + public function counter(): int + { + $state = $this->load_state(); + + return $state[self::STATE_POST_COUNTER]; + } + + /** + * Start a indexing or delete process. + * + * @param string $search_type + * @param string $action + * + * @throws action_in_progress_exception If there is an action in progress + * @throws no_search_backend_found_exception If search backend don't exist + * @throws search_exception If action isn't valid + */ + public function init(string $search_type, string $action): void + { + // It's 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(); + } + + // Make sure the search type exists (if not, throw an exception) + $this->search_backend_factory->get($search_type); + + // Make sure the action is correct (just in case) + if (!in_array($action, ['create', 'delete'])) + { + throw new search_exception('INVALID_ACTION'); + } + + $state = [ + self::STATE_SEARCH_TYPE => $search_type, + self::STATE_ACTION => $action, + self::STATE_POST_COUNTER => 0 + ]; + + $this->save_state($state); + } + + /** + * Set the post counter + * + * @param int $counter + * + * @throws no_action_in_progress_exception If there is no action in progress + */ + public function update_counter(int $counter): void + { + $state = $this->load_state(); + + $state[self::STATE_POST_COUNTER] = $counter; + + $this->save_state($state); + } + + /** + * Clear the state + */ + 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 it's empty + if (!$this->is_action_in_progress()) + { + throw new no_action_in_progress_exception(); + } + + return explode(',', $this->config['search_indexing_state']); + } + + /** + * Save the specified state in the database + * + * @param array $state + */ + private function save_state(array $state = []): void + { + ksort($state); + + $this->config->set('search_indexing_state', implode(',', $state)); + } +} diff --git a/phpBB/search.php b/phpBB/search.php index f786964620..68325bda7f 100644 --- a/phpBB/search.php +++ b/phpBB/search.php @@ -299,16 +299,9 @@ if ($keywords || $author || $author_id || $search_id || $submit) $search_backend_factory = $phpbb_container->get('search.backend_factory'); $search = $search_backend_factory->get_active(); } - catch (RuntimeException $e) + catch (\phpbb\search\exception\no_search_backend_found_exception $e) { - if (strpos($e->getMessage(), 'No service found') === 0) - { - trigger_error('NO_SUCH_SEARCH_MODULE'); - } - else - { - throw $e; - } + trigger_error('NO_SUCH_SEARCH_MODULE'); } // let the search module split up the keywords diff --git a/tests/console/searchindex/create_test.php b/tests/console/searchindex/create_test.php new file mode 100644 index 0000000000..8d4958442c --- /dev/null +++ b/tests/console/searchindex/create_test.php @@ -0,0 +1,92 @@ + + * @license GNU General Public License, version 2 (GPL-2.0) + * + * For full copyright and license information, please see + * the docs/CREDITS.txt file. + * + */ + +use phpbb\console\command\searchindex\create; +use Symfony\Component\Console\Application; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Tester\CommandTester; + +require_once __DIR__ . '/phpbb_console_searchindex_base.php'; +require_once __DIR__ . '/../../mock/search_backend_mock.php'; + +class phpbb_console_searchindex_create_test extends phpbb_console_searchindex_base +{ + public function get_command_tester() + { + $application = new Application(); + $application->add(new create( + $this->language, + $this->log, + $this->post_helper, + $this->search_backend_factory, + $this->state_helper, + $this->user + )); + + $command = $application->find('searchindex:create'); + + return new CommandTester($command); + } + + public function test_create() + { + $command_tester = $this->get_command_tester(); + + $command_tester->execute([ + 'search-backend' => 'search_backend_mock', + ]); + + $this->assertEquals(Command::SUCCESS, $command_tester->getStatusCode()); + $this->assertStringContainsString('CLI_SEARCHINDEX_CREATE_SUCCESS', $command_tester->getDisplay()); + } + + public function test_create_when_search_backend_dont_exist() + { + $command_tester = $this->get_command_tester(); + + $command_tester->execute([ + 'search-backend' => 'missing', + ]); + + $this->assertEquals(Command::FAILURE, $command_tester->getStatusCode()); + $this->assertStringContainsString('CLI_SEARCHINDEX_BACKEND_NOT_FOUND', $command_tester->getDisplay()); + } + + public function test_create_when_action_in_progress() + { + $this->config['search_indexing_state'] = ['not', 'empty']; + + $command_tester = $this->get_command_tester(); + + $command_tester->execute([ + 'search-backend' => 'search_backend_mock', + ]); + + $this->assertEquals(Command::FAILURE, $command_tester->getStatusCode()); + $this->assertStringContainsString('CLI_SEARCHINDEX_ACTION_IN_PROGRESS', $command_tester->getDisplay()); + + $this->config['search_indexing_state'] = []; + } + + public function test_create_when_search_backend_not_available() + { + $command_tester = $this->get_command_tester(); + + $command_tester->execute([ + 'search-backend' => 'search_backend_mock_not_available', + ]); + + $this->assertEquals(Command::FAILURE, $command_tester->getStatusCode()); + $this->assertStringContainsString('CLI_SEARCHINDEX_BACKEND_NOT_AVAILABLE', $command_tester->getDisplay()); + } +} diff --git a/tests/console/searchindex/delete_test.php b/tests/console/searchindex/delete_test.php new file mode 100644 index 0000000000..ac7762effd --- /dev/null +++ b/tests/console/searchindex/delete_test.php @@ -0,0 +1,92 @@ + + * @license GNU General Public License, version 2 (GPL-2.0) + * + * For full copyright and license information, please see + * the docs/CREDITS.txt file. + * + */ + +use phpbb\console\command\searchindex\delete; +use Symfony\Component\Console\Application; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Tester\CommandTester; + +require_once __DIR__ . '/phpbb_console_searchindex_base.php'; +require_once __DIR__ . '/../../mock/search_backend_mock.php'; + +class phpbb_console_searchindex_delete_test extends phpbb_console_searchindex_base +{ + public function get_command_tester() + { + $application = new Application(); + $application->add(new delete( + $this->language, + $this->log, + $this->post_helper, + $this->search_backend_factory, + $this->state_helper, + $this->user + )); + + $command = $application->find('searchindex:delete'); + + return new CommandTester($command); + } + + public function test_delete() + { + $command_tester = $this->get_command_tester(); + + $command_tester->execute([ + 'search-backend' => 'search_backend_mock', + ]); + + $this->assertEquals(Command::SUCCESS, $command_tester->getStatusCode()); + $this->assertStringContainsString('CLI_SEARCHINDEX_DELETE_SUCCESS', $command_tester->getDisplay()); + } + + public function test_delete_when_search_backend_dont_exist() + { + $command_tester = $this->get_command_tester(); + + $command_tester->execute([ + 'search-backend' => 'missing', + ]); + + $this->assertEquals(Command::FAILURE, $command_tester->getStatusCode()); + $this->assertStringContainsString('CLI_SEARCHINDEX_BACKEND_NOT_FOUND', $command_tester->getDisplay()); + } + + public function test_delete_when_action_in_progress() + { + $this->config['search_indexing_state'] = ['not', 'empty']; + + $command_tester = $this->get_command_tester(); + + $command_tester->execute([ + 'search-backend' => 'search_backend_mock', + ]); + + $this->assertEquals(Command::FAILURE, $command_tester->getStatusCode()); + $this->assertStringContainsString('CLI_SEARCHINDEX_ACTION_IN_PROGRESS', $command_tester->getDisplay()); + + $this->config['search_indexing_state'] = []; + } + + public function test_delete_when_search_backend_not_available() + { + $command_tester = $this->get_command_tester(); + + $command_tester->execute([ + 'search-backend' => 'search_backend_mock_not_available', + ]); + + $this->assertEquals(Command::FAILURE, $command_tester->getStatusCode()); + $this->assertStringContainsString('CLI_SEARCHINDEX_BACKEND_NOT_AVAILABLE', $command_tester->getDisplay()); + } +} diff --git a/tests/console/searchindex/list_all_test.php b/tests/console/searchindex/list_all_test.php new file mode 100644 index 0000000000..7968032644 --- /dev/null +++ b/tests/console/searchindex/list_all_test.php @@ -0,0 +1,49 @@ + + * @license GNU General Public License, version 2 (GPL-2.0) + * + * For full copyright and license information, please see + * the docs/CREDITS.txt file. + * + */ + +use phpbb\console\command\searchindex\list_all; +use Symfony\Component\Console\Application; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Tester\CommandTester; + +require_once __DIR__ . '/phpbb_console_searchindex_base.php'; +require_once __DIR__ . '/../../mock/search_backend_mock.php'; + +class phpbb_console_searchindex_list_all_test extends phpbb_console_searchindex_base +{ + public function get_command_tester() + { + $application = new Application(); + $application->add(new list_all( + $this->config, + $this->language, + $this->search_backend_collection, + $this->user + )); + + $command = $application->find('searchindex:list'); + + return new CommandTester($command); + } + + public function test_list() + { + $command_tester = $this->get_command_tester(); + + $command_tester->execute([]); + + $this->assertEquals(Command::SUCCESS, $command_tester->getStatusCode()); + $this->assertStringContainsString('Mock search backend', $command_tester->getDisplay()); + $this->assertStringContainsString('ACTIVE', $command_tester->getDisplay()); + } +} diff --git a/tests/console/searchindex/phpbb_console_searchindex_base.php b/tests/console/searchindex/phpbb_console_searchindex_base.php new file mode 100644 index 0000000000..209d12281a --- /dev/null +++ b/tests/console/searchindex/phpbb_console_searchindex_base.php @@ -0,0 +1,93 @@ + + * @license GNU General Public License, version 2 (GPL-2.0) + * + * For full copyright and license information, please see + * the docs/CREDITS.txt file. + * + */ + +use phpbb\config\config; +use phpbb\di\service_collection; +use phpbb\language\language; +use phpbb\log\log; +use phpbb\posting\post_helper; +use phpbb\search\search_backend_factory; +use phpbb\search\state_helper; +use phpbb\user; + +require_once __DIR__ . '/../../mock/search_backend_mock.php'; +require_once __DIR__ . '/../../mock/search_backend_mock_not_available.php'; + +class phpbb_console_searchindex_base extends phpbb_test_case +{ + /** @var config */ + protected $config; + + /** @var language */ + protected $language; + + /** @var log */ + protected $log; + + /** @var post_helper */ + protected $post_helper; + + /** @var user */ + protected $user; + + /** @var search_backend_factory */ + protected $search_backend_factory; + + /** @var state_helper */ + protected $state_helper; + + /** @var service_collection */ + protected $search_backend_collection; + + protected function setUp(): void + { + global $phpbb_root_path, $phpEx; + + $this->config = new \phpbb\config\config([ + 'search_indexing_state' => [], + 'search_type' => 'search_backend_mock' + ]); + + $lang_loader = new \phpbb\language\language_file_loader($phpbb_root_path, $phpEx); + $this->language = new \phpbb\language\language($lang_loader); + + $this->log = $this->createMock('\phpbb\log\log'); + + $this->post_helper = $this->createMock('\phpbb\posting\post_helper'); + $this->post_helper + ->method('get_max_post_id') + ->willReturn(1000); + + $this->user = $this->createMock('\phpbb\user'); + + $phpbb_container = new phpbb_mock_container_builder(); + $this->search_backend_collection = new \phpbb\di\service_collection($phpbb_container); + + $search_backend_mock = new search_backend_mock(); + $this->search_backend_collection->add('search_backend_mock'); + $this->search_backend_collection->add_service_class('search_backend_mock', 'search_backend_mock'); + $phpbb_container->set('search_backend_mock', $search_backend_mock); + + $search_backend_mock_not_available = new search_backend_mock_not_available(); + $this->search_backend_collection->add('search_backend_mock_not_available'); + $this->search_backend_collection->add_service_class('search_backend_mock_not_available', 'search_backend_mock_not_available'); + $phpbb_container->set('search_backend_mock_not_available', $search_backend_mock_not_available); + + $this->search_backend_factory = new search_backend_factory($this->config, $this->search_backend_collection); + + $this->state_helper = new state_helper($this->config, $this->search_backend_factory); + + parent::setUp(); + } +} + diff --git a/tests/di/service_collection_test.php b/tests/di/service_collection_test.php index fd0e13e48b..7fc3e94e41 100644 --- a/tests/di/service_collection_test.php +++ b/tests/di/service_collection_test.php @@ -57,7 +57,7 @@ class phpbb_service_collection_test extends \phpbb_test_case public function test_get_by_class_many_services_exception() { $this->expectException('RuntimeException'); - $this->expectExceptionMessage('More than one service definitions found for class "bar_class" in collection.'); + $this->expectExceptionMessage('DI_MULTIPLE_SERVICE_DEFINITIONS'); $this->service_collection->get_by_class('bar_class'); } @@ -65,7 +65,7 @@ class phpbb_service_collection_test extends \phpbb_test_case public function test_get_by_class_no_service_exception() { $this->expectException('RuntimeException'); - $this->expectExceptionMessage('No service found for class "baz_class" in collection.'); + $this->expectExceptionMessage('DI_SERVICE_NOT_FOUND'); $this->service_collection->get_by_class('baz_class'); } diff --git a/tests/mock/search_backend_mock.php b/tests/mock/search_backend_mock.php new file mode 100644 index 0000000000..84dd26385e --- /dev/null +++ b/tests/mock/search_backend_mock.php @@ -0,0 +1,117 @@ + + * @license GNU General Public License, version 2 (GPL-2.0) + * + * For full copyright and license information, please see + * the docs/CREDITS.txt file. + * + */ + +use phpbb\search\backend\search_backend_interface; + +class search_backend_mock implements search_backend_interface +{ + public $index_created = false; + + public function get_name(): string + { + return 'Mock search backend'; + } + + public function is_available(): bool + { + return true; + } + + public function init() + { + return false; + } + + public function get_search_query(): string + { + return ''; + } + + public function get_common_words(): array + { + return []; + } + + public function get_word_length() + { + return false; + } + + public function split_keywords(string &$keywords, string $terms): bool + { + return false; + } + + public function keyword_search(string $type, string $fields, string $terms, array $sort_by_sql, string $sort_key, string $sort_dir, string $sort_days, array $ex_fid_ary, string $post_visibility, int $topic_id, array $author_ary, string $author_name, array &$id_ary, int &$start, int $per_page) + { + return 0; + } + + public function author_search(string $type, bool $firstpost_only, array $sort_by_sql, string $sort_key, string $sort_dir, string $sort_days, array $ex_fid_ary, string $post_visibility, int $topic_id, array $author_ary, string $author_name, array &$id_ary, int &$start, int $per_page) + { + return 0; + } + + public function supports_phrase_search(): bool + { + return false; + } + + public function index(string $mode, int $post_id, string &$message, string &$subject, int $poster_id, int $forum_id) + { + // Nothing + } + + public function index_remove(array $post_ids, array $author_ids, array $forum_ids): void + { + // Nothing + } + + public function tidy(): void + { + // Nothing + } + + public function create_index(int &$post_counter = 0): ?array + { + $this->index_created = true; + return null; + } + + public function delete_index(int &$post_counter = 0): ?array + { + $this->index_created = true; + return null; + } + + public function index_created(): bool + { + return $this->index_created; + } + + public function index_stats() + { + return []; + } + + public function get_acp_options(): array + { + return []; + } + + public function get_type(): string + { + return static::class; + } +} + diff --git a/tests/mock/search_backend_mock_not_available.php b/tests/mock/search_backend_mock_not_available.php new file mode 100644 index 0000000000..5b299d334f --- /dev/null +++ b/tests/mock/search_backend_mock_not_available.php @@ -0,0 +1,25 @@ + + * @license GNU General Public License, version 2 (GPL-2.0) + * + * For full copyright and license information, please see + * the docs/CREDITS.txt file. + * + */ + +class search_backend_mock_not_available extends search_backend_mock +{ + public function get_name(): string + { + return 'Mock unavailable search backend'; + } + + public function is_available(): bool + { + return false; + } +} diff --git a/tests/search/mysql_test.php b/tests/search/mysql_test.php index 1e822b92af..fe7326bb3e 100644 --- a/tests/search/mysql_test.php +++ b/tests/search/mysql_test.php @@ -40,6 +40,6 @@ class phpbb_search_mysql_test extends phpbb_search_common_test_case $this->db = $this->new_dbal(); $phpbb_dispatcher = new phpbb_mock_event_dispatcher(); $class = self::get_search_wrapper('\phpbb\search\backend\fulltext_mysql'); - $this->search = new $class($config, $this->db, $phpbb_dispatcher, $language, $user, $phpbb_root_path, $phpEx); + $this->search = new $class($config, $this->db, $phpbb_dispatcher, $language, $user, SEARCH_RESULTS_TABLE, $phpbb_root_path, $phpEx); } } diff --git a/tests/search/native_test.php b/tests/search/native_test.php index 6d8a03f4aa..90702655d0 100644 --- a/tests/search/native_test.php +++ b/tests/search/native_test.php @@ -38,7 +38,7 @@ class phpbb_search_native_test extends phpbb_search_test_case $class = self::get_search_wrapper('\phpbb\search\backend\fulltext_native'); $config['fulltext_native_min_chars'] = 2; $config['fulltext_native_max_chars'] = 14; - $this->search = new $class($config, $this->db, $phpbb_dispatcher, $language, $user, $phpbb_root_path, $phpEx); + $this->search = new $class($config, $this->db, $phpbb_dispatcher, $language, $user, SEARCH_RESULTS_TABLE, SEARCH_WORDLIST_TABLE, SEARCH_WORDMATCH_TABLE, $phpbb_root_path, $phpEx); } public function keywords() diff --git a/tests/search/postgres_test.php b/tests/search/postgres_test.php index 545ffafd50..18c5a46f81 100644 --- a/tests/search/postgres_test.php +++ b/tests/search/postgres_test.php @@ -40,6 +40,6 @@ class phpbb_search_postgres_test extends phpbb_search_common_test_case $this->db = $this->new_dbal(); $phpbb_dispatcher = new phpbb_mock_event_dispatcher(); $class = self::get_search_wrapper('\phpbb\search\backend\fulltext_postgres'); - $this->search = new $class($config, $this->db, $phpbb_dispatcher, $language, $user, $phpbb_root_path, $phpEx); + $this->search = new $class($config, $this->db, $phpbb_dispatcher, $language, $user, SEARCH_RESULTS_TABLE, $phpbb_root_path, $phpEx); } }