Merge pull request #5439 from rubencm/ticket/12683

[ticket/12683] Add a CLI command to generate the search index
This commit is contained in:
Marc Alexander 2022-12-11 22:03:19 +01:00 committed by GitHub
commit 995717f4a0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
46 changed files with 1527 additions and 221 deletions

View file

@ -23,6 +23,7 @@ imports:
- { resource: services_notification.yml } - { resource: services_notification.yml }
- { resource: services_password.yml } - { resource: services_password.yml }
- { resource: services_php.yml } - { resource: services_php.yml }
- { resource: services_posting.yml }
- { resource: services_profilefield.yml } - { resource: services_profilefield.yml }
- { resource: services_report.yml } - { resource: services_report.yml }
- { resource: services_routing.yml } - { resource: services_routing.yml }

View file

@ -246,6 +246,40 @@ services:
tags: tags:
- { name: console.command } - { 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: console.command.thumbnail.delete:
class: phpbb\console\command\thumbnail\delete class: phpbb\console\command\thumbnail\delete
arguments: arguments:

View file

@ -0,0 +1,5 @@
services:
post.helper:
class: phpbb\posting\post_helper
arguments:
- '@dbal.conn'

View file

@ -1,5 +1,11 @@
services: services:
search.state_helper:
class: phpbb\search\state_helper
arguments:
- '@config'
- '@search.backend_factory'
# Search backends # Search backends
search.fulltext.mysql: search.fulltext.mysql:
class: phpbb\search\backend\fulltext_mysql class: phpbb\search\backend\fulltext_mysql
@ -9,6 +15,7 @@ services:
- '@dispatcher' - '@dispatcher'
- '@language' - '@language'
- '@user' - '@user'
- '%tables.search_results%'
- '%core.root_path%' - '%core.root_path%'
- '%core.php_ext%' - '%core.php_ext%'
tags: tags:
@ -22,6 +29,9 @@ services:
- '@dispatcher' - '@dispatcher'
- '@language' - '@language'
- '@user' - '@user'
- '%tables.search_results%'
- '%tables.search_wordlist%'
- '%tables.search_wordmatch%'
- '%core.root_path%' - '%core.root_path%'
- '%core.php_ext%' - '%core.php_ext%'
tags: tags:
@ -35,6 +45,7 @@ services:
- '@dispatcher' - '@dispatcher'
- '@language' - '@language'
- '@user' - '@user'
- '%tables.search_results%'
- '%core.root_path%' - '%core.root_path%'
- '%core.php_ext%' - '%core.php_ext%'
tags: tags:

View file

@ -652,16 +652,9 @@ class acp_main
$search_backend_factory = $phpbb_container->get('search.backend_factory'); $search_backend_factory = $phpbb_container->get('search.backend_factory');
$search = $search_backend_factory->get_active(); $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');
{
trigger_error('NO_SUCH_SEARCH_MODULE');
}
else
{
throw $e;
}
} }
if (!$search->index_created()) if (!$search->index_created())

View file

@ -21,6 +21,7 @@ use phpbb\language\language;
use phpbb\log\log; use phpbb\log\log;
use phpbb\request\request; use phpbb\request\request;
use phpbb\search\search_backend_factory; use phpbb\search\search_backend_factory;
use phpbb\search\state_helper;
use phpbb\template\template; use phpbb\template\template;
use phpbb\user; use phpbb\user;
@ -35,10 +36,6 @@ class acp_search
public $tpl_name; public $tpl_name;
public $page_title; public $page_title;
protected const STATE_SEARCH_TYPE = 0;
protected const STATE_ACTION = 1;
protected const STATE_POST_COUNTER = 2;
/** @var config */ /** @var config */
protected $config; protected $config;
@ -57,6 +54,9 @@ class acp_search
/** @var search_backend_factory */ /** @var search_backend_factory */
protected $search_backend_factory; protected $search_backend_factory;
/** @var state_helper */
protected $search_state_helper;
/** @var template */ /** @var template */
protected $template; protected $template;
@ -79,6 +79,7 @@ class acp_search
$this->request = $request; $this->request = $request;
$this->search_backend_collection = $phpbb_container->get('search.backend_collection'); $this->search_backend_collection = $phpbb_container->get('search.backend_collection');
$this->search_backend_factory = $phpbb_container->get('search.backend_factory'); $this->search_backend_factory = $phpbb_container->get('search.backend_factory');
$this->search_state_helper = $phpbb_container->get('search.state_helper');
$this->template = $template; $this->template = $template;
$this->user = $user; $this->user = $user;
$this->phpbb_admin_path = $phpbb_admin_path; $this->phpbb_admin_path = $phpbb_admin_path;
@ -272,7 +273,6 @@ class acp_search
public function index(string $id, string $mode): void public function index(string $id, string $mode): void
{ {
$action = $this->request->variable('action', ''); $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')) if ($action && !$this->request->is_set_post('cancel'))
{ {
@ -284,7 +284,7 @@ class acp_search
case 'create': case 'create':
case 'delete': case 'delete':
$this->index_action($id, $mode, $action, $state); $this->index_action($id, $mode, $action);
break; break;
default: default:
@ -296,13 +296,12 @@ class acp_search
// If clicked to cancel the indexing progress (acp_search_index_inprogress form) // If clicked to cancel the indexing progress (acp_search_index_inprogress form)
if ($this->request->is_set_post('cancel')) if ($this->request->is_set_post('cancel'))
{ {
$state = []; $this->search_state_helper->clear_state();
$this->save_state($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 else
{ {
@ -325,8 +324,8 @@ class acp_search
foreach ($this->search_backend_collection as $search) foreach ($this->search_backend_collection as $search)
{ {
$this->template->assign_block_vars('backends', [ $this->template->assign_block_vars('backends', [
'NAME' => $search->get_name(), 'NAME' => $search->get_name(),
'TYPE' => $search->get_type(), 'TYPE' => $search->get_type(),
'S_ACTIVE' => $search->get_type() === $this->config['search_type'], 'S_ACTIVE' => $search->get_type() === $this->config['search_type'],
'S_HIDDEN_FIELDS' => build_hidden_fields(['search_type' => $search->get_type()]), 'S_HIDDEN_FIELDS' => build_hidden_fields(['search_type' => $search->get_type()]),
@ -336,8 +335,8 @@ class acp_search
} }
$this->template->assign_vars([ $this->template->assign_vars([
'U_ACTION' => $this->u_action . '&hash=' . generate_link_hash('acp_search'), 'U_ACTION' => $this->u_action . '&hash=' . generate_link_hash('acp_search'),
'UA_PROGRESS_BAR' => addslashes($this->u_action . '&action=progress_bar'), 'UA_PROGRESS_BAR' => addslashes($this->u_action . '&action=progress_bar'),
]); ]);
} }
@ -346,13 +345,14 @@ class acp_search
* *
* @param string $id * @param string $id
* @param string $mode * @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->tpl_name = 'acp_search_index_inprogress';
$this->page_title = 'ACP_SEARCH_INDEX'; $this->page_title = 'ACP_SEARCH_INDEX';
$action = $this->search_state_helper->action();
$this->template->assign_vars([ $this->template->assign_vars([
'U_ACTION' => $this->u_action . '&action=' . $action . '&hash=' . generate_link_hash('acp_search'), 'U_ACTION' => $this->u_action . '&action=' . $action . '&hash=' . generate_link_hash('acp_search'),
'UA_PROGRESS_BAR' => addslashes($this->u_action . '&action=progress_bar'), 'UA_PROGRESS_BAR' => addslashes($this->u_action . '&action=progress_bar'),
@ -368,9 +368,8 @@ class acp_search
* @param string $id * @param string $id
* @param string $mode * @param string $mode
* @param string $action * @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... // For some this may be of help...
@ini_set('memory_limit', '128M'); @ini_set('memory_limit', '128M');
@ -381,29 +380,23 @@ class acp_search
} }
// Entering here for the first time // 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', '')) if ($this->request->is_set_post('search_type', ''))
{ {
$state = [ $this->search_state_helper->init($this->request->variable('search_type', ''), $action);
self::STATE_SEARCH_TYPE => $this->request->variable('search_type', ''),
self::STATE_ACTION => $action,
self::STATE_POST_COUNTER => 0
];
} }
else else
{ {
trigger_error($this->language->lang('FORM_INVALID') . adm_back_link($this->u_action), E_USER_WARNING); 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 // 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); $search = $this->search_backend_factory->get($type);
try try
@ -411,7 +404,7 @@ class acp_search
$status = ($action == 'create') ? $search->create_index($post_counter) : $search->delete_index($post_counter); $status = ($action == 'create') ? $search->create_index($post_counter) : $search->delete_index($post_counter);
if ($status) // Status is not null, so action is in progress.... 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); $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); meta_refresh(1, $u_action);
@ -423,13 +416,13 @@ class acp_search
} }
catch (Exception $e) 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); trigger_error($e->getMessage() . adm_back_link($this->u_action) . $this->close_popup_js(), E_USER_WARNING);
} }
$search->tidy(); $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'; $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()]); $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" .
"</script>\n"; "</script>\n";
} }
/**
* @param array $state
*/
private function save_state(array $state = []): void
{
ksort($state);
$this->config->set('search_indexing_state', implode(',', $state), true);
}
} }

View file

@ -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_backend_factory = $phpbb_container->get('search.backend_factory');
$search = $search_backend_factory->get_active(); $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');
{
trigger_error('NO_SUCH_SEARCH_MODULE');
}
else
{
throw $e;
}
} }
$search->index_remove($post_ids, $poster_ids, $forum_ids); $search->index_remove($post_ids, $poster_ids, $forum_ids);

View file

@ -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_backend_factory = $phpbb_container->get('search.backend_factory');
$search = $search_backend_factory->get_active(); $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');
{
trigger_error('NO_SUCH_SEARCH_MODULE');
}
else
{
throw $e;
}
} }
$search->index($mode, (int) $data_ary['post_id'], $data_ary['message'], $subject, $poster_id, (int) $data_ary['forum_id']); $search->index($mode, (int) $data_ary['post_id'], $data_ary['message'], $subject, $poster_id, (int) $data_ary['forum_id']);

View file

@ -1397,16 +1397,9 @@ function mcp_fork_topic($topic_ids)
$search_backend_factory = $phpbb_container->get('search.backend_factory'); $search_backend_factory = $phpbb_container->get('search.backend_factory');
$search = $search_backend_factory->get_active(); $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');
{
trigger_error('NO_SUCH_SEARCH_MODULE');
}
else
{
throw $e;
}
} }
$search_mode = 'post'; $search_mode = 'post';
} }

View file

@ -637,16 +637,9 @@ function change_poster(&$post_info, $userdata)
$search_backend_factory = $phpbb_container->get('search.backend_factory'); $search_backend_factory = $phpbb_container->get('search.backend_factory');
$search = $search_backend_factory->get_active(); $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');
{
trigger_error('NO_SUCH_SEARCH_MODULE');
}
else
{
throw $e;
}
} }
$search->index_remove([], [$post_info['user_id'], $userdata['user_id']], []); $search->index_remove([], [$post_info['user_id'], $userdata['user_id']], []);

View file

@ -680,16 +680,9 @@ function split_topic($action, $topic_id, $to_forum_id, $subject)
$search_backend_factory = $phpbb_container->get('search.backend_factory'); $search_backend_factory = $phpbb_container->get('search.backend_factory');
$search = $search_backend_factory->get_active(); $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');
{
trigger_error('NO_SUCH_SEARCH_MODULE');
}
else
{
throw $e;
}
} }
$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']); $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']);

View file

@ -91,6 +91,8 @@ $lang = array_merge($lang, array(
'GENERAL_SEARCH_SETTINGS' => 'General search settings', 'GENERAL_SEARCH_SETTINGS' => 'General search settings',
'GO_TO_SEARCH_INDEX' => 'Go to search index page', 'GO_TO_SEARCH_INDEX' => 'Go to search index page',
'INVALID_ACTION' => 'Invalid action',
'INDEX_STATS' => 'Index statistics', 'INDEX_STATS' => 'Index statistics',
'INDEXING_IN_PROGRESS' => 'Indexing in progress', '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 boards size.', '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 boards size.',

View file

@ -83,6 +83,10 @@ $lang = array_merge($lang, array(
'CLI_DESCRIPTION_SET_ATOMIC_CONFIG' => 'Sets a configuration options value only if the old matches the current value', 'CLI_DESCRIPTION_SET_ATOMIC_CONFIG' => 'Sets a configuration options value only if the old matches the current value',
'CLI_DESCRIPTION_SET_CONFIG' => 'Sets a configuration options value', 'CLI_DESCRIPTION_SET_CONFIG' => 'Sets a configuration options 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_DELETE' => 'Delete all existing thumbnails.',
'CLI_DESCRIPTION_THUMBNAIL_GENERATE' => 'Generate all missing thumbnails.', 'CLI_DESCRIPTION_THUMBNAIL_GENERATE' => 'Generate all missing thumbnails.',
'CLI_DESCRIPTION_THUMBNAIL_RECREATE' => 'Recreate all 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_REPARSING_START' => 'Reparsing %s...',
'CLI_REPARSER_REPARSE_SUCCESS' => 'Reparsing ended with success', '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 doesnt support incomplete index/delete actions, please solve it from the ACP.',
'CLI_SEARCHINDEX_ACTIVE_NOT_INDEXED' => 'Active search backend isnt indexed',
'CLI_SEARCHINDEX_BACKEND_NOT_AVAILABLE' => 'Search backend isnt available.',
// In all the case %1$s is the logical name of the file and %2$s the real name on the filesystem // 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. // eg: big_image.png (2_a51529ae7932008cf8454a95af84cacd) generated.
'CLI_THUMBNAIL_DELETED' => '%1$s (%2$s) deleted.', 'CLI_THUMBNAIL_DELETED' => '%1$s (%2$s) deleted.',

View file

@ -215,6 +215,9 @@ $lang = array_merge($lang, array(
2 => 'Downloaded %d times', 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', 'EDIT_POST' => 'Edit post',
'ELLIPSIS' => '…', 'ELLIPSIS' => '…',
'EMAIL' => 'Email', // Short form for EMAIL_ADDRESS 'EMAIL' => 'Email', // Short form for EMAIL_ADDRESS
@ -715,6 +718,7 @@ $lang = array_merge($lang, array(
'SEARCH_UNANSWERED' => 'Unanswered topics', 'SEARCH_UNANSWERED' => 'Unanswered topics',
'SEARCH_UNREAD' => 'Unread posts', 'SEARCH_UNREAD' => 'Unread posts',
'SEARCH_USER_POSTS' => 'Search users posts', 'SEARCH_USER_POSTS' => 'Search users posts',
'SEARCH_BACKEND_NOT_FOUND' => 'No search backend could be found.',
'SECONDS' => 'Seconds', 'SECONDS' => 'Seconds',
'SEE_ALL' => 'See All', 'SEE_ALL' => 'See All',
'SELECT' => 'Select', 'SELECT' => 'Select',

View file

@ -49,10 +49,8 @@ class list_all extends \phpbb\console\command\command
*/ */
protected function configure() protected function configure()
{ {
$this $this->setName('reparser:list')
->setName('reparser:list') ->setDescription($this->user->lang('CLI_DESCRIPTION_REPARSER_LIST'));
->setDescription($this->user->lang('CLI_DESCRIPTION_REPARSER_LIST'))
;
} }
/** /**

View file

@ -0,0 +1,165 @@
<?php
/**
*
* This file is part of the phpBB Forum Software package.
*
* @copyright (c) phpBB Limited <https://www.phpbb.com>
* @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;
}
}

View file

@ -0,0 +1,165 @@
<?php
/**
*
* This file is part of the phpBB Forum Software package.
*
* @copyright (c) phpBB Limited <https://www.phpbb.com>
* @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;
}
}

View file

@ -0,0 +1,96 @@
<?php
/**
*
* This file is part of the phpBB Forum Software package.
*
* @copyright (c) phpBB Limited <https://www.phpbb.com>
* @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']) ? '(<comment>' . $this->language->lang('ACTIVE') . '</comment>) ' : '';
$search_backends[] = '<info>' . $name . '</info> ' . $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;
}
}

View file

@ -15,6 +15,7 @@ namespace phpbb\cron\task\core;
use phpbb\config\config; use phpbb\config\config;
use phpbb\cron\task\base; use phpbb\cron\task\base;
use phpbb\di\exception\di_exception;
use phpbb\search\backend\search_backend_interface; use phpbb\search\backend\search_backend_interface;
use phpbb\search\search_backend_factory; use phpbb\search\search_backend_factory;
@ -88,7 +89,7 @@ class tidy_search extends base
$this->active_search = $this->search_backend_factory->get_active(); $this->active_search = $this->search_backend_factory->get_active();
} }
} }
catch (\RuntimeException $e) catch (di_exception $e)
{ {
return false; return false;
} }

View file

@ -0,0 +1,20 @@
<?php
/**
*
* This file is part of the phpBB Forum Software package.
*
* @copyright (c) phpBB Limited <https://www.phpbb.com>
* @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
{
}

View file

@ -0,0 +1,18 @@
<?php
/**
*
* This file is part of the phpBB Forum Software package.
*
* @copyright (c) phpBB Limited <https://www.phpbb.com>
* @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
{
}

View file

@ -0,0 +1,18 @@
<?php
/**
*
* This file is part of the phpBB Forum Software package.
*
* @copyright (c) phpBB Limited <https://www.phpbb.com>
* @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
{
}

View file

@ -13,6 +13,8 @@
namespace phpbb\di; namespace phpbb\di;
use phpbb\di\exception\multiple_service_definitions_exception;
use phpbb\di\exception\service_not_found_exception;
use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\ContainerInterface;
/** /**
@ -105,7 +107,7 @@ class service_collection extends \ArrayObject
{ {
if ($service_id !== null) 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; $service_id = $id;
@ -114,7 +116,7 @@ class service_collection extends \ArrayObject
if ($service_id === null) 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); return $this->offsetGet($service_id);

View file

@ -133,6 +133,9 @@ class create_search_index extends database_task
$this->phpbb_dispatcher, $this->phpbb_dispatcher,
$container->get('language'), $container->get('language'),
$this->user, $this->user,
SEARCH_RESULTS_TABLE,
SEARCH_WORDLIST_TABLE,
SEARCH_WORDMATCH_TABLE,
$this->phpbb_root_path, $this->phpbb_root_path,
$this->php_ext $this->php_ext
); );

View file

@ -0,0 +1,43 @@
<?php
/**
*
* This file is part of the phpBB Forum Software package.
*
* @copyright (c) phpBB Limited <https://www.phpbb.com>
* @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;
}
}

View file

@ -1,15 +1,15 @@
<?php <?php
/** /**
* *
* This file is part of the phpBB Forum Software package. * This file is part of the phpBB Forum Software package.
* *
* @copyright (c) phpBB Limited <https://www.phpbb.com> * @copyright (c) phpBB Limited <https://www.phpbb.com>
* @license GNU General Public License, version 2 (GPL-2.0) * @license GNU General Public License, version 2 (GPL-2.0)
* *
* For full copyright and license information, please see * For full copyright and license information, please see
* the docs/CREDITS.txt file. * the docs/CREDITS.txt file.
* *
*/ */
namespace phpbb\search\backend; namespace phpbb\search\backend;
@ -19,9 +19,9 @@ use phpbb\db\driver\driver_interface;
use phpbb\user; use phpbb\user;
/** /**
* optional base class for search plugins providing simple caching based on ACM * optional base class for search plugins providing simple caching based on ACM
* and functions to retrieve ignore_words and synonyms * and functions to retrieve ignore_words and synonyms
*/ */
abstract class base implements search_backend_interface abstract class base implements search_backend_interface
{ {
public const SEARCH_RESULT_NOT_IN_CACHE = 0; public const SEARCH_RESULT_NOT_IN_CACHE = 0;
@ -51,20 +51,27 @@ abstract class base implements search_backend_interface
*/ */
protected $user; protected $user;
/**
* @var string
*/
protected $search_results_table;
/** /**
* Constructor. * Constructor.
* *
* @param service $cache * @param service $cache
* @param config $config * @param config $config
* @param driver_interface $db * @param driver_interface $db
* @param user $user * @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->cache = $cache;
$this->config = $config; $this->config = $config;
$this->db = $db; $this->db = $db;
$this->user = $user; $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)) if (!empty($keywords) || count($author_ary))
{ {
$sql = 'SELECT search_time $sql = 'SELECT search_time
FROM ' . SEARCH_RESULTS_TABLE . ' FROM ' . $this->search_results_table . '
WHERE search_key = \'' . $this->db->sql_escape($search_key) . '\''; WHERE search_key = \'' . $this->db->sql_escape($search_key) . '\'';
$result = $this->db->sql_query($sql); $result = $this->db->sql_query($sql);
@ -193,7 +200,7 @@ abstract class base implements search_backend_interface
'search_authors' => ' ' . implode(' ', $author_ary) . ' ' '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_query($sql);
} }
$this->db->sql_freeresult($result); $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']); $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() . ' SET search_time = ' . time() . '
WHERE search_key = \'' . $this->db->sql_escape($search_key) . '\''; WHERE search_key = \'' . $this->db->sql_escape($search_key) . '\'';
$this->db->sql_query($sql); $this->db->sql_query($sql);
@ -280,7 +287,7 @@ abstract class base implements search_backend_interface
} }
$sql = 'SELECT search_key $sql = 'SELECT search_key
FROM ' . SEARCH_RESULTS_TABLE . " FROM ' . $this->search_results_table . "
WHERE search_keywords LIKE '%*%' $sql_where"; WHERE search_keywords LIKE '%*%' $sql_where";
$result = $this->db->sql_query($sql); $result = $this->db->sql_query($sql);
@ -301,7 +308,7 @@ abstract class base implements search_backend_interface
} }
$sql = 'SELECT search_key $sql = 'SELECT search_key
FROM ' . SEARCH_RESULTS_TABLE . " FROM ' . $this->search_results_table . "
WHERE $sql_where"; WHERE $sql_where";
$result = $this->db->sql_query($sql); $result = $this->db->sql_query($sql);
@ -313,7 +320,7 @@ abstract class base implements search_backend_interface
} }
$sql = 'DELETE $sql = 'DELETE
FROM ' . SEARCH_RESULTS_TABLE . ' FROM ' . $this->search_results_table . '
WHERE search_time < ' . (time() - (int) $this->config['search_store_results']); WHERE search_time < ' . (time() - (int) $this->config['search_store_results']);
$this->db->sql_query($sql); $this->db->sql_query($sql);
} }
@ -329,7 +336,7 @@ abstract class base implements search_backend_interface
$starttime = microtime(true); $starttime = microtime(true);
$row_count = 0; $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); $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']); $this->index('post', (int) $row['post_id'], $row['post_text'], $row['post_subject'], (int) $row['poster_id'], (int) $row['forum_id']);
} }
$row_count++; $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->tidy();
$this->config['num_posts'] = $num_posts; $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; $totaltime = microtime(true) - $starttime;
$rows_per_second = $row_count / $totaltime; $rows_per_second = $row_count / $totaltime;
@ -382,7 +395,8 @@ abstract class base implements search_backend_interface
$starttime = microtime(true); $starttime = microtime(true);
$row_count = 0; $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); $rows = $this->get_posts_batch_after($post_counter);
$ids = $posters = $forum_ids = array(); $ids = $posters = $forum_ids = array();
@ -399,9 +413,15 @@ abstract class base implements search_backend_interface
$this->index_remove($ids, $posters, $forum_ids); $this->index_remove($ids, $posters, $forum_ids);
$post_counter = $ids[count($ids) - 1]; $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; $totaltime = microtime(true) - $starttime;
$rows_per_second = $row_count / $totaltime; $rows_per_second = $row_count / $totaltime;

View file

@ -17,8 +17,8 @@ use phpbb\config\config;
use phpbb\db\driver\driver_interface; use phpbb\db\driver\driver_interface;
use phpbb\event\dispatcher_interface; use phpbb\event\dispatcher_interface;
use phpbb\language\language; use phpbb\language\language;
use phpbb\search\exception\search_exception;
use phpbb\user; use phpbb\user;
use RuntimeException;
/** /**
* Fulltext search for MySQL * Fulltext search for MySQL
@ -72,19 +72,20 @@ class fulltext_mysql extends base implements search_backend_interface
* Constructor * Constructor
* Creates a new \phpbb\search\backend\fulltext_mysql, which is used as a search backend * Creates a new \phpbb\search\backend\fulltext_mysql, which is used as a search backend
* *
* @param config $config Config object * @param config $config Config object
* @param driver_interface $db Database object * @param driver_interface $db Database object
* @param dispatcher_interface $phpbb_dispatcher Event dispatcher object * @param dispatcher_interface $phpbb_dispatcher Event dispatcher object
* @param language $language * @param language $language
* @param user $user User object * @param user $user User object
* @param string $phpbb_root_path Relative path to phpBB root * @param string $search_results_table
* @param string $phpEx PHP file extension * @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; global $cache;
parent::__construct($cache, $config, $db, $user); parent::__construct($cache, $config, $db, $user, $search_results_table);
$this->phpbb_dispatcher = $phpbb_dispatcher; $this->phpbb_dispatcher = $phpbb_dispatcher;
$this->language = $language; $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 // Make sure we can actually use MySQL with fulltext indexes
if ($error = $this->init()) if ($error = $this->init())
{ {
throw new RuntimeException($error); throw new search_exception($error);
} }
if (empty($this->stats)) 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($sql_query);
} }
$this->db->sql_query('TRUNCATE TABLE ' . SEARCH_RESULTS_TABLE); $this->db->sql_query('TRUNCATE TABLE ' . $this->search_results_table);
return null; 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 // Make sure we can actually use MySQL with fulltext indexes
if ($error = $this->init()) if ($error = $this->init())
{ {
throw new RuntimeException($error); throw new search_exception($error);
} }
if (empty($this->stats)) 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($sql_query);
} }
$this->db->sql_query('TRUNCATE TABLE ' . SEARCH_RESULTS_TABLE); $this->db->sql_query('TRUNCATE TABLE ' . $this->search_results_table);
return null; return null;
} }

View file

@ -93,30 +93,42 @@ class fulltext_native extends base implements search_backend_interface
*/ */
protected $phpbb_dispatcher; protected $phpbb_dispatcher;
/** /** @var language */
* @var language
*/
protected $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 * Initialises the fulltext_native search backend with min/max word length
* *
* @param config $config Config object * @param config $config Config object
* @param driver_interface $db Database object * @param driver_interface $db Database object
* @param dispatcher_interface $phpbb_dispatcher Event dispatcher object * @param dispatcher_interface $phpbb_dispatcher Event dispatcher object
* @param language $language * @param language $language
* @param user $user User object * @param user $user User object
* @param string $phpbb_root_path phpBB root path * @param string $search_results_table
* @param string $phpEx PHP file extension * @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; global $cache;
parent::__construct($cache, $config, $db, $user); parent::__construct($cache, $config, $db, $user, $search_results_table);
$this->phpbb_dispatcher = $phpbb_dispatcher; $this->phpbb_dispatcher = $phpbb_dispatcher;
$this->language = $language; $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->phpbb_root_path = $phpbb_root_path;
$this->php_ext = $phpEx; $this->php_ext = $phpEx;
@ -335,7 +347,7 @@ class fulltext_native extends base implements search_backend_interface
if (count($exact_words)) if (count($exact_words))
{ {
$sql = 'SELECT word_id, word_text, word_common $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) . ' WHERE ' . $this->db->sql_in_set('word_text', $exact_words) . '
ORDER BY word_count ASC'; ORDER BY word_count ASC';
$result = $this->db->sql_query($sql); $result = $this->db->sql_query($sql);
@ -607,8 +619,8 @@ class fulltext_native extends base implements search_backend_interface
$sql_array = array( $sql_array = array(
'SELECT' => ($type == 'posts') ? 'DISTINCT p.post_id' : 'DISTINCT p.topic_id', 'SELECT' => ($type == 'posts') ? 'DISTINCT p.post_id' : 'DISTINCT p.topic_id',
'FROM' => array( 'FROM' => array(
SEARCH_WORDMATCH_TABLE => array(), $this->search_wordmatch_table => array(),
SEARCH_WORDLIST_TABLE => array(), $this->search_wordlist_table => array(),
), ),
'LEFT_JOIN' => array(array( 'LEFT_JOIN' => array(array(
'FROM' => array(POSTS_TABLE => 'p'), 'FROM' => array(POSTS_TABLE => 'p'),
@ -660,7 +672,7 @@ class fulltext_native extends base implements search_backend_interface
if (is_string($id)) if (is_string($id))
{ {
$sql_array['LEFT_JOIN'][] = array( $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" 'ON' => "w$w_num.word_text LIKE $id"
); );
$word_ids[] = "w$w_num.word_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)) 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[] = "w$w_num.word_text LIKE $subquery";
$sql_where[] = "m$m_num.word_id = w$w_num.word_id"; $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_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) if ($title_match)
{ {
@ -712,7 +724,7 @@ class fulltext_native extends base implements search_backend_interface
if (is_string($subquery)) if (is_string($subquery))
{ {
$sql_array['LEFT_JOIN'][] = array( $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" '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)) if (count($this->must_not_contain_ids))
{ {
$sql_array['LEFT_JOIN'][] = array( $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" '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)) if (is_string($id))
{ {
$sql_array['LEFT_JOIN'][] = array( $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" 'ON' => "w$w_num.word_text LIKE $id"
); );
$id = "w$w_num.word_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( $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" : '') '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"; $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(); $words['del']['title'] = array();
$sql = 'SELECT w.word_id, w.word_text, m.title_match $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 WHERE m.post_id = $post_id
AND w.word_id = m.word_id"; AND w.word_id = m.word_id";
$result = $this->db->sql_query($sql); $result = $this->db->sql_query($sql);
@ -1379,7 +1391,7 @@ class fulltext_native extends base implements search_backend_interface
if (count($unique_add_words)) if (count($unique_add_words))
{ {
$sql = 'SELECT word_id, word_text $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); WHERE ' . $this->db->sql_in_set('word_text', $unique_add_words);
$result = $this->db->sql_query($sql); $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); $sql_ary[] = array('word_text' => (string) $word, 'word_count' => 0);
} }
$this->db->sql_return_on_error(true); $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); $this->db->sql_return_on_error(false);
} }
unset($new_words, $sql_ary); 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_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) . ' WHERE ' . $this->db->sql_in_set('word_id', $sql_in) . '
AND post_id = ' . intval($post_id) . " AND post_id = ' . intval($post_id) . "
AND title_match = $title_match"; AND title_match = $title_match";
$this->db->sql_query($sql); $this->db->sql_query($sql);
$sql = 'UPDATE ' . SEARCH_WORDLIST_TABLE . ' $sql = 'UPDATE ' . $this->search_wordlist_table . '
SET word_count = word_count - 1 SET word_count = word_count - 1
WHERE ' . $this->db->sql_in_set('word_id', $sql_in) . ' WHERE ' . $this->db->sql_in_set('word_id', $sql_in) . '
AND word_count > 0'; AND word_count > 0';
@ -1447,13 +1459,13 @@ class fulltext_native extends base implements search_backend_interface
if (count($word_ary)) 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 . ' 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); WHERE ' . $this->db->sql_in_set('word_text', $word_ary);
$this->db->sql_query($sql); $this->db->sql_query($sql);
$sql = 'UPDATE ' . SEARCH_WORDLIST_TABLE . ' $sql = 'UPDATE ' . $this->search_wordlist_table . '
SET word_count = word_count + 1 SET word_count = word_count + 1
WHERE ' . $this->db->sql_in_set('word_text', $word_ary); WHERE ' . $this->db->sql_in_set('word_text', $word_ary);
$this->db->sql_query($sql); $this->db->sql_query($sql);
@ -1479,7 +1491,7 @@ class fulltext_native extends base implements search_backend_interface
if (count($post_ids)) if (count($post_ids))
{ {
$sql = 'SELECT w.word_id, w.word_text, m.title_match $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) . ' WHERE ' . $this->db->sql_in_set('m.post_id', $post_ids) . '
AND w.word_id = m.word_id'; AND w.word_id = m.word_id';
$result = $this->db->sql_query($sql); $result = $this->db->sql_query($sql);
@ -1501,7 +1513,7 @@ class fulltext_native extends base implements search_backend_interface
if (count($title_word_ids)) if (count($title_word_ids))
{ {
$sql = 'UPDATE ' . SEARCH_WORDLIST_TABLE . ' $sql = 'UPDATE ' . $this->search_wordlist_table . '
SET word_count = word_count - 1 SET word_count = word_count - 1
WHERE ' . $this->db->sql_in_set('word_id', $title_word_ids) . ' WHERE ' . $this->db->sql_in_set('word_id', $title_word_ids) . '
AND word_count > 0'; AND word_count > 0';
@ -1510,7 +1522,7 @@ class fulltext_native extends base implements search_backend_interface
if (count($message_word_ids)) if (count($message_word_ids))
{ {
$sql = 'UPDATE ' . SEARCH_WORDLIST_TABLE . ' $sql = 'UPDATE ' . $this->search_wordlist_table . '
SET word_count = word_count - 1 SET word_count = word_count - 1
WHERE ' . $this->db->sql_in_set('word_id', $message_word_ids) . ' WHERE ' . $this->db->sql_in_set('word_id', $message_word_ids) . '
AND word_count > 0'; AND word_count > 0';
@ -1520,7 +1532,7 @@ class fulltext_native extends base implements search_backend_interface
unset($title_word_ids); unset($title_word_ids);
unset($message_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); WHERE ' . $this->db->sql_in_set('post_id', $post_ids);
$this->db->sql_query($sql); $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; $common_threshold = ((double) $this->config['fulltext_native_common_thres']) / 100.0;
// First, get the IDs of common words // First, get the IDs of common words
$sql = 'SELECT word_id, word_text $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) . ' WHERE word_count > ' . floor($this->config['num_posts'] * $common_threshold) . '
OR word_common = 1'; OR word_common = 1';
$result = $this->db->sql_query($sql); $result = $this->db->sql_query($sql);
@ -1565,7 +1577,7 @@ class fulltext_native extends base implements search_backend_interface
if (count($sql_in)) if (count($sql_in))
{ {
// Flag the words // Flag the words
$sql = 'UPDATE ' . SEARCH_WORDLIST_TABLE . ' $sql = 'UPDATE ' . $this->search_wordlist_table . '
SET word_common = 1 SET word_common = 1
WHERE ' . $this->db->sql_in_set('word_id', $sql_in); WHERE ' . $this->db->sql_in_set('word_id', $sql_in);
$this->db->sql_query($sql); $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); $this->config->set('search_last_gc', time(), false);
// Delete the matches // 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); WHERE ' . $this->db->sql_in_set('word_id', $sql_in);
$this->db->sql_query($sql); $this->db->sql_query($sql);
} }
@ -1603,15 +1615,15 @@ class fulltext_native extends base implements search_backend_interface
switch ($this->db->get_sql_layer()) switch ($this->db->get_sql_layer())
{ {
case 'sqlite3': case 'sqlite3':
$sql_queries[] = 'DELETE FROM ' . SEARCH_WORDLIST_TABLE; $sql_queries[] = 'DELETE FROM ' . $this->search_wordlist_table;
$sql_queries[] = 'DELETE FROM ' . SEARCH_WORDMATCH_TABLE; $sql_queries[] = 'DELETE FROM ' . $this->search_wordmatch_table;
$sql_queries[] = 'DELETE FROM ' . SEARCH_RESULTS_TABLE; $sql_queries[] = 'DELETE FROM ' . $this->search_results_table;
break; break;
default: default:
$sql_queries[] = 'TRUNCATE TABLE ' . SEARCH_WORDLIST_TABLE; $sql_queries[] = 'TRUNCATE TABLE ' . $this->search_wordlist_table;
$sql_queries[] = 'TRUNCATE TABLE ' . SEARCH_WORDMATCH_TABLE; $sql_queries[] = 'TRUNCATE TABLE ' . $this->search_wordmatch_table;
$sql_queries[] = 'TRUNCATE TABLE ' . SEARCH_RESULTS_TABLE; $sql_queries[] = 'TRUNCATE TABLE ' . $this->search_results_table;
break; break;
} }
@ -1672,8 +1684,8 @@ class fulltext_native extends base implements search_backend_interface
*/ */
protected function get_stats() protected function get_stats()
{ {
$this->stats['total_words'] = $this->db->get_estimated_row_count(SEARCH_WORDLIST_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(SEARCH_WORDMATCH_TABLE); $this->stats['total_matches'] = $this->db->get_estimated_row_count($this->search_wordmatch_table);
} }
/** /**

View file

@ -17,8 +17,8 @@ use phpbb\config\config;
use phpbb\db\driver\driver_interface; use phpbb\db\driver\driver_interface;
use phpbb\event\dispatcher_interface; use phpbb\event\dispatcher_interface;
use phpbb\language\language; use phpbb\language\language;
use phpbb\search\exception\search_exception;
use phpbb\user; use phpbb\user;
use RuntimeException;
/** /**
* Fulltext search for PostgreSQL * Fulltext search for PostgreSQL
@ -84,19 +84,20 @@ class fulltext_postgres extends base implements search_backend_interface
* Constructor * Constructor
* Creates a new \phpbb\search\backend\fulltext_postgres, which is used as a search backend * Creates a new \phpbb\search\backend\fulltext_postgres, which is used as a search backend
* *
* @param config $config Config object * @param config $config Config object
* @param driver_interface $db Database object * @param driver_interface $db Database object
* @param dispatcher_interface $phpbb_dispatcher Event dispatcher object * @param dispatcher_interface $phpbb_dispatcher Event dispatcher object
* @param language $language * @param language $language
* @param user $user User object * @param user $user User object
* @param string $phpbb_root_path Relative path to phpBB root * @param string $search_results_table
* @param string $phpEx PHP file extension * @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; global $cache;
parent::__construct($cache, $config, $db, $user); parent::__construct($cache, $config, $db, $user, $search_results_table);
$this->phpbb_dispatcher = $phpbb_dispatcher; $this->phpbb_dispatcher = $phpbb_dispatcher;
$this->language = $language; $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 // Make sure we can actually use PostgreSQL with fulltext indexes
if ($error = $this->init()) if ($error = $this->init())
{ {
throw new RuntimeException($error); throw new search_exception($error);
} }
if (empty($this->stats)) 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($sql_query);
} }
$this->db->sql_query('TRUNCATE TABLE ' . SEARCH_RESULTS_TABLE); $this->db->sql_query('TRUNCATE TABLE ' . $this->search_results_table);
return null; 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 // Make sure we can actually use PostgreSQL with fulltext indexes
if ($error = $this->init()) if ($error = $this->init())
{ {
throw new RuntimeException($error); throw new search_exception($error);
} }
if (empty($this->stats)) 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($sql_query);
} }
$this->db->sql_query('TRUNCATE TABLE ' . SEARCH_RESULTS_TABLE); $this->db->sql_query('TRUNCATE TABLE ' . $this->search_results_table);
return null; return null;
} }

View file

@ -0,0 +1,18 @@
<?php
/**
*
* This file is part of the phpBB Forum Software package.
*
* @copyright (c) phpBB Limited <https://www.phpbb.com>
* @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
{
}

View file

@ -0,0 +1,18 @@
<?php
/**
*
* This file is part of the phpBB Forum Software package.
*
* @copyright (c) phpBB Limited <https://www.phpbb.com>
* @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
{
}

View file

@ -0,0 +1,18 @@
<?php
/**
*
* This file is part of the phpBB Forum Software package.
*
* @copyright (c) phpBB Limited <https://www.phpbb.com>
* @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
{
}

View file

@ -0,0 +1,20 @@
<?php
/**
*
* This file is part of the phpBB Forum Software package.
*
* @copyright (c) phpBB Limited <https://www.phpbb.com>
* @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
{
}

View file

@ -14,8 +14,10 @@
namespace phpbb\search; namespace phpbb\search;
use phpbb\config\config; use phpbb\config\config;
use phpbb\di\exception\service_not_found_exception;
use phpbb\di\service_collection; use phpbb\di\service_collection;
use phpbb\search\backend\search_backend_interface; use phpbb\search\backend\search_backend_interface;
use phpbb\search\exception\no_search_backend_found_exception;
class search_backend_factory class search_backend_factory
{ {
@ -46,16 +48,29 @@ class search_backend_factory
* *
* @param string $class * @param string $class
* *
* @throws no_search_backend_found_exception
*
* @return search_backend_interface * @return search_backend_interface
*/ */
public function get(string $class): 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 * Obtains active search backend
* *
* @throws no_search_backend_found_exception
*
* @return search_backend_interface * @return search_backend_interface
*/ */
public function get_active(): search_backend_interface public function get_active(): search_backend_interface

View file

@ -0,0 +1,181 @@
<?php
/**
*
* This file is part of the phpBB Forum Software package.
*
* @copyright (c) phpBB Limited <https://www.phpbb.com>
* @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));
}
}

View file

@ -299,16 +299,9 @@ if ($keywords || $author || $author_id || $search_id || $submit)
$search_backend_factory = $phpbb_container->get('search.backend_factory'); $search_backend_factory = $phpbb_container->get('search.backend_factory');
$search = $search_backend_factory->get_active(); $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');
{
trigger_error('NO_SUCH_SEARCH_MODULE');
}
else
{
throw $e;
}
} }
// let the search module split up the keywords // let the search module split up the keywords

View file

@ -0,0 +1,92 @@
<?php
/**
*
* This file is part of the phpBB Forum Software package.
*
* @copyright (c) phpBB Limited <https://www.phpbb.com>
* @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());
}
}

View file

@ -0,0 +1,92 @@
<?php
/**
*
* This file is part of the phpBB Forum Software package.
*
* @copyright (c) phpBB Limited <https://www.phpbb.com>
* @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());
}
}

View file

@ -0,0 +1,49 @@
<?php
/**
*
* This file is part of the phpBB Forum Software package.
*
* @copyright (c) phpBB Limited <https://www.phpbb.com>
* @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());
}
}

View file

@ -0,0 +1,93 @@
<?php
/**
*
* This file is part of the phpBB Forum Software package.
*
* @copyright (c) phpBB Limited <https://www.phpbb.com>
* @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();
}
}

View file

@ -57,7 +57,7 @@ class phpbb_service_collection_test extends \phpbb_test_case
public function test_get_by_class_many_services_exception() public function test_get_by_class_many_services_exception()
{ {
$this->expectException('RuntimeException'); $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'); $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() public function test_get_by_class_no_service_exception()
{ {
$this->expectException('RuntimeException'); $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'); $this->service_collection->get_by_class('baz_class');
} }

View file

@ -0,0 +1,117 @@
<?php
/**
*
* This file is part of the phpBB Forum Software package.
*
* @copyright (c) phpBB Limited <https://www.phpbb.com>
* @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;
}
}

View file

@ -0,0 +1,25 @@
<?php
/**
*
* This file is part of the phpBB Forum Software package.
*
* @copyright (c) phpBB Limited <https://www.phpbb.com>
* @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;
}
}

View file

@ -40,6 +40,6 @@ class phpbb_search_mysql_test extends phpbb_search_common_test_case
$this->db = $this->new_dbal(); $this->db = $this->new_dbal();
$phpbb_dispatcher = new phpbb_mock_event_dispatcher(); $phpbb_dispatcher = new phpbb_mock_event_dispatcher();
$class = self::get_search_wrapper('\phpbb\search\backend\fulltext_mysql'); $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);
} }
} }

View file

@ -38,7 +38,7 @@ class phpbb_search_native_test extends phpbb_search_test_case
$class = self::get_search_wrapper('\phpbb\search\backend\fulltext_native'); $class = self::get_search_wrapper('\phpbb\search\backend\fulltext_native');
$config['fulltext_native_min_chars'] = 2; $config['fulltext_native_min_chars'] = 2;
$config['fulltext_native_max_chars'] = 14; $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() public function keywords()

View file

@ -40,6 +40,6 @@ class phpbb_search_postgres_test extends phpbb_search_common_test_case
$this->db = $this->new_dbal(); $this->db = $this->new_dbal();
$phpbb_dispatcher = new phpbb_mock_event_dispatcher(); $phpbb_dispatcher = new phpbb_mock_event_dispatcher();
$class = self::get_search_wrapper('\phpbb\search\backend\fulltext_postgres'); $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);
} }
} }