Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 224 |
|
0.00% |
0 / 9 |
CRAP | |
0.00% |
0 / 1 |
acp_search | |
0.00% |
0 / 222 |
|
0.00% |
0 / 9 |
3192 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
2 | |||
main | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
settings | |
0.00% |
0 / 81 |
|
0.00% |
0 / 1 |
506 | |||
index | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
72 | |||
index_overview | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
6 | |||
index_inprogress | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
2 | |||
index_action | |
0.00% |
0 / 49 |
|
0.00% |
0 / 1 |
210 | |||
display_progress_bar | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
12 | |||
get_post_index_progress | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | /** |
3 | * |
4 | * This file is part of the phpBB Forum Software package. |
5 | * |
6 | * @copyright (c) phpBB Limited <https://www.phpbb.com> |
7 | * @license GNU General Public License, version 2 (GPL-2.0) |
8 | * |
9 | * For full copyright and license information, please see |
10 | * the docs/CREDITS.txt file. |
11 | * |
12 | */ |
13 | |
14 | /** |
15 | * @ignore |
16 | */ |
17 | |
18 | use phpbb\config\config; |
19 | use phpbb\db\driver\driver_interface; |
20 | use phpbb\di\service_collection; |
21 | use phpbb\language\language; |
22 | use phpbb\log\log; |
23 | use phpbb\request\request; |
24 | use phpbb\search\backend\search_backend_interface; |
25 | use phpbb\search\search_backend_factory; |
26 | use phpbb\search\state_helper; |
27 | use phpbb\template\template; |
28 | use phpbb\user; |
29 | |
30 | if (!defined('IN_PHPBB')) |
31 | { |
32 | exit; |
33 | } |
34 | |
35 | class acp_search |
36 | { |
37 | public $u_action; |
38 | public $tpl_name; |
39 | public $page_title; |
40 | |
41 | /** @var config */ |
42 | protected $config; |
43 | |
44 | /** @var driver_interface */ |
45 | protected $db; |
46 | |
47 | /** @var language */ |
48 | protected $language; |
49 | |
50 | /** @var log */ |
51 | protected $log; |
52 | |
53 | /** @var request */ |
54 | protected $request; |
55 | |
56 | /** @var service_collection */ |
57 | protected $search_backend_collection; |
58 | |
59 | /** @var search_backend_factory */ |
60 | protected $search_backend_factory; |
61 | |
62 | /** @var state_helper */ |
63 | protected $search_state_helper; |
64 | |
65 | /** @var template */ |
66 | protected $template; |
67 | |
68 | /** @var user */ |
69 | protected $user; |
70 | |
71 | /** @var string */ |
72 | protected $phpbb_admin_path; |
73 | |
74 | /** @var string */ |
75 | protected $php_ex; |
76 | |
77 | public function __construct($p_master) |
78 | { |
79 | global $config, $db, $phpbb_container, $language, $phpbb_log, $request, $template, $user, $phpbb_admin_path, $phpEx; |
80 | |
81 | $this->config = $config; |
82 | $this->db = $db; |
83 | $this->language = $language; |
84 | $this->log = $phpbb_log; |
85 | $this->request = $request; |
86 | $this->search_backend_collection = $phpbb_container->get('search.backend_collection'); |
87 | $this->search_backend_factory = $phpbb_container->get('search.backend_factory'); |
88 | $this->search_state_helper = $phpbb_container->get('search.state_helper'); |
89 | $this->template = $template; |
90 | $this->user = $user; |
91 | $this->phpbb_admin_path = $phpbb_admin_path; |
92 | $this->php_ex = $phpEx; |
93 | } |
94 | |
95 | /** |
96 | * @param string $id |
97 | * @param string $mode |
98 | * @throws Exception |
99 | * @return void |
100 | */ |
101 | public function main(string $id, string $mode): void |
102 | { |
103 | $this->language->add_lang('acp/search'); |
104 | |
105 | switch ($mode) |
106 | { |
107 | case 'settings': |
108 | $this->settings($id, $mode); |
109 | break; |
110 | |
111 | case 'index': |
112 | $this->index($id, $mode); |
113 | break; |
114 | } |
115 | } |
116 | |
117 | /** |
118 | * Settings page |
119 | * |
120 | * @param string $id |
121 | * @param string $mode |
122 | */ |
123 | public function settings(string $id, string $mode): void |
124 | { |
125 | $submit = $this->request->is_set_post('submit'); |
126 | |
127 | if ($submit && !check_link_hash($this->request->variable('hash', ''), 'acp_search')) |
128 | { |
129 | trigger_error($this->language->lang('FORM_INVALID') . adm_back_link($this->u_action), E_USER_WARNING); |
130 | } |
131 | |
132 | $settings = [ |
133 | 'search_interval' => 'float', |
134 | 'search_anonymous_interval' => 'float', |
135 | 'load_search' => 'bool', |
136 | 'limit_search_load' => 'float', |
137 | 'min_search_author_chars' => 'integer', |
138 | 'max_num_search_keywords' => 'integer', |
139 | 'default_search_return_chars' => 'integer', |
140 | 'search_store_results' => 'integer', |
141 | ]; |
142 | |
143 | $search_options = ''; |
144 | |
145 | foreach ($this->search_backend_collection as $search) |
146 | { |
147 | // Only show available search backends |
148 | if ($search->is_available()) |
149 | { |
150 | $name = $search->get_name(); |
151 | $type = $search->get_type(); |
152 | |
153 | $selected = ($this->config['search_type'] === $type) ? ' selected="selected"' : ''; |
154 | $identifier = substr($type, strrpos($type, '\\') + 1); |
155 | $search_options .= "<option value=\"$type\"$selected data-toggle-setting=\"#search_{$identifier}_settings\">$name</option>"; |
156 | |
157 | $vars = $search->get_acp_options(); |
158 | |
159 | if (!$submit) |
160 | { |
161 | $this->template->assign_block_vars('backend', [ |
162 | 'NAME' => $name, |
163 | 'SETTINGS' => $vars['tpl'], |
164 | 'IDENTIFIER' => $identifier, |
165 | ]); |
166 | } |
167 | else if (is_array($vars['config'])) |
168 | { |
169 | $settings = array_merge($settings, $vars['config']); |
170 | } |
171 | } |
172 | } |
173 | |
174 | $cfg_array = (isset($_REQUEST['config'])) ? $this->request->variable('config', ['' => ''], true) : []; |
175 | $updated = $this->request->variable('updated', false); |
176 | |
177 | foreach ($settings as $config_name => $var_type) |
178 | { |
179 | if (!isset($cfg_array[$config_name])) |
180 | { |
181 | continue; |
182 | } |
183 | |
184 | // e.g. integer:4:12 (min 4, max 12) |
185 | $var_type = explode(':', $var_type); |
186 | |
187 | $config_value = $cfg_array[$config_name]; |
188 | settype($config_value, $var_type[0]); |
189 | |
190 | if (isset($var_type[1])) |
191 | { |
192 | $config_value = max($var_type[1], $config_value); |
193 | } |
194 | |
195 | if (isset($var_type[2])) |
196 | { |
197 | $config_value = min($var_type[2], $config_value); |
198 | } |
199 | |
200 | // only change config if anything was actually changed |
201 | if ($submit && ($this->config[$config_name] !== $config_value)) |
202 | { |
203 | $this->config->set($config_name, $config_value); |
204 | $updated = true; |
205 | } |
206 | } |
207 | |
208 | if ($submit) |
209 | { |
210 | $extra_message = ''; |
211 | if ($updated) |
212 | { |
213 | $this->log->add('admin', $this->user->data['user_id'], $this->user->ip, 'LOG_CONFIG_SEARCH'); |
214 | } |
215 | |
216 | if (isset($cfg_array['search_type']) && ($cfg_array['search_type'] !== $this->config['search_type'])) |
217 | { |
218 | $search = $this->search_backend_factory->get($cfg_array['search_type']); |
219 | if (confirm_box(true)) |
220 | { |
221 | // Initialize search backend, if $error is false means that everything is ok |
222 | if (!($error = $search->init())) |
223 | { |
224 | $this->config->set('search_type', $cfg_array['search_type']); |
225 | |
226 | if (!$updated) |
227 | { |
228 | $this->log->add('admin', $this->user->data['user_id'], $this->user->ip, 'LOG_CONFIG_SEARCH'); |
229 | } |
230 | $extra_message = '<br>' . $this->language->lang('SWITCHED_SEARCH_BACKEND') . '<br><a href="' . append_sid($this->phpbb_admin_path . "index." . $this->php_ex, 'i=search&mode=index') . '">» ' . $this->language->lang('GO_TO_SEARCH_INDEX') . '</a>'; |
231 | } |
232 | else |
233 | { |
234 | trigger_error($error . adm_back_link($this->u_action), E_USER_WARNING); |
235 | } |
236 | } |
237 | else |
238 | { |
239 | confirm_box(false, $this->language->lang('CONFIRM_SEARCH_BACKEND'), build_hidden_fields([ |
240 | 'i' => $id, |
241 | 'mode' => $mode, |
242 | 'submit' => true, |
243 | 'updated' => $updated, |
244 | 'config' => ['search_type' => $cfg_array['search_type']], |
245 | ])); |
246 | } |
247 | } |
248 | |
249 | trigger_error($this->language->lang('CONFIG_UPDATED') . $extra_message . adm_back_link($this->u_action)); |
250 | } |
251 | unset($cfg_array); |
252 | |
253 | $this->tpl_name = 'acp_search_settings'; |
254 | $this->page_title = 'ACP_SEARCH_SETTINGS'; |
255 | |
256 | $this->template->assign_vars([ |
257 | 'DEFAULT_SEARCH_RETURN_CHARS' => (int) $this->config['default_search_return_chars'], |
258 | 'LIMIT_SEARCH_LOAD' => (float) $this->config['limit_search_load'], |
259 | 'MIN_SEARCH_AUTHOR_CHARS' => (int) $this->config['min_search_author_chars'], |
260 | 'SEARCH_INTERVAL' => (float) $this->config['search_interval'], |
261 | 'SEARCH_GUEST_INTERVAL' => (float) $this->config['search_anonymous_interval'], |
262 | 'SEARCH_STORE_RESULTS' => (int) $this->config['search_store_results'], |
263 | 'MAX_NUM_SEARCH_KEYWORDS' => (int) $this->config['max_num_search_keywords'], |
264 | |
265 | 'S_SEARCH_TYPES' => $search_options, |
266 | 'S_YES_SEARCH' => (bool) $this->config['load_search'], |
267 | |
268 | 'U_ACTION' => $this->u_action . '&hash=' . generate_link_hash('acp_search'), |
269 | ]); |
270 | } |
271 | |
272 | /** |
273 | * Execute action depending on the action and state |
274 | * |
275 | * @param string $id |
276 | * @param string $mode |
277 | * @throws Exception |
278 | */ |
279 | public function index(string $id, string $mode): void |
280 | { |
281 | $action = $this->request->variable('action', ''); |
282 | |
283 | if ($action && !$this->request->is_set_post('cancel')) |
284 | { |
285 | switch ($action) |
286 | { |
287 | case 'create': |
288 | case 'delete': |
289 | $this->index_action($id, $mode, $action); |
290 | break; |
291 | |
292 | default: |
293 | trigger_error('NO_ACTION', E_USER_ERROR); |
294 | } |
295 | } |
296 | else |
297 | { |
298 | // If clicked to cancel the indexing progress (acp_search_index_inprogress form) |
299 | if ($this->request->is_set_post('cancel')) |
300 | { |
301 | $this->search_state_helper->clear_state(); |
302 | } |
303 | |
304 | if ($this->search_state_helper->is_action_in_progress()) |
305 | { |
306 | $this->index_inprogress($id, $mode); |
307 | } |
308 | else |
309 | { |
310 | $this->index_overview($id, $mode); |
311 | } |
312 | } |
313 | } |
314 | |
315 | /** |
316 | * @param string $id |
317 | * @param string $mode |
318 | * |
319 | * @throws Exception |
320 | */ |
321 | private function index_overview(string $id, string $mode): void |
322 | { |
323 | $this->tpl_name = 'acp_search_index'; |
324 | $this->page_title = 'ACP_SEARCH_INDEX'; |
325 | |
326 | /** @var search_backend_interface $search */ |
327 | foreach ($this->search_backend_collection as $search) |
328 | { |
329 | $this->template->assign_block_vars('backends', [ |
330 | 'NAME' => $search->get_name(), |
331 | 'TYPE' => $search->get_type(), |
332 | |
333 | 'S_ACTIVE' => $search->get_type() === $this->config['search_type'], |
334 | 'S_HIDDEN_FIELDS' => build_hidden_fields(['search_type' => $search->get_type()]), |
335 | 'S_INDEXED' => $search->index_created(), |
336 | 'S_STATS' => $search->index_stats(), |
337 | ]); |
338 | } |
339 | |
340 | $this->template->assign_vars([ |
341 | 'U_ACTION' => $this->u_action . '&hash=' . generate_link_hash('acp_search'), |
342 | 'UA_PROGRESS_BAR' => addslashes($this->u_action . '&action=progress_bar'), |
343 | ]); |
344 | } |
345 | |
346 | /** |
347 | * Form to continue or cancel indexing process |
348 | * |
349 | * @param string $id |
350 | * @param string $mode |
351 | */ |
352 | private function index_inprogress(string $id, string $mode): void |
353 | { |
354 | $this->tpl_name = 'acp_search_index_inprogress'; |
355 | $this->page_title = 'ACP_SEARCH_INDEX'; |
356 | |
357 | $action = $this->search_state_helper->action(); |
358 | $post_counter = $this->search_state_helper->counter(); |
359 | |
360 | $this->template->assign_vars([ |
361 | 'U_ACTION' => $this->u_action . '&action=' . $action . '&hash=' . generate_link_hash('acp_search'), |
362 | 'CONTINUE_PROGRESS' => $this->get_post_index_progress($post_counter), |
363 | 'S_ACTION' => $action, |
364 | ]); |
365 | } |
366 | |
367 | /** |
368 | * Progress that do the indexing/index removal, updating the page continuously until is finished |
369 | * |
370 | * @param string $id |
371 | * @param string $mode |
372 | * @param string $action |
373 | */ |
374 | private function index_action(string $id, string $mode, string $action): void |
375 | { |
376 | // For some this may be of help... |
377 | @ini_set('memory_limit', '128M'); |
378 | |
379 | if (!check_link_hash($this->request->variable('hash', ''), 'acp_search')) |
380 | { |
381 | trigger_error($this->language->lang('FORM_INVALID') . adm_back_link($this->u_action), E_USER_WARNING); |
382 | } |
383 | |
384 | // Entering here for the first time |
385 | if (!$this->search_state_helper->is_action_in_progress()) |
386 | { |
387 | if ($this->request->is_set_post('search_type', '')) |
388 | { |
389 | $this->search_state_helper->init($this->request->variable('search_type', ''), $action); |
390 | } |
391 | else |
392 | { |
393 | trigger_error($this->language->lang('FORM_INVALID') . adm_back_link($this->u_action), E_USER_WARNING); |
394 | } |
395 | } |
396 | |
397 | // Start displaying progress on first submit |
398 | if ($this->request->is_set_post('submit')) |
399 | { |
400 | $this->display_progress_bar($id, $mode); |
401 | return; |
402 | } |
403 | |
404 | // Execute create/delete |
405 | $type = $this->search_state_helper->type(); |
406 | $action = $this->search_state_helper->action(); |
407 | $post_counter = $this->search_state_helper->counter(); |
408 | |
409 | $search = $this->search_backend_factory->get($type); |
410 | |
411 | try |
412 | { |
413 | $status = ($action == 'create') ? $search->create_index($post_counter) : $search->delete_index($post_counter); |
414 | if ($status) // Status is not null, so action is in progress.... |
415 | { |
416 | $this->search_state_helper->update_counter($status['post_counter']); |
417 | |
418 | $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); |
419 | meta_refresh(1, $u_action); |
420 | |
421 | $message_progress = $this->language->lang(($action === 'create') ? 'INDEXING_IN_PROGRESS' : 'DELETING_INDEX_IN_PROGRESS'); |
422 | $message_progress_explain = $this->language->lang(($action == 'create') ? 'INDEXING_IN_PROGRESS_EXPLAIN' : 'DELETING_INDEX_IN_PROGRESS_EXPLAIN'); |
423 | $message_redirect = $this->language->lang( |
424 | ($action === 'create') ? 'SEARCH_INDEX_CREATE_REDIRECT' : 'SEARCH_INDEX_DELETE_REDIRECT', |
425 | (int) $status['row_count'], |
426 | $status['post_counter'] |
427 | ); |
428 | $message_redirect_rate = $this->language->lang( |
429 | ($action === 'create') ? 'SEARCH_INDEX_CREATE_REDIRECT_RATE' : 'SEARCH_INDEX_DELETE_REDIRECT_RATE', |
430 | $status['rows_per_second'] |
431 | ); |
432 | |
433 | $this->template->assign_vars([ |
434 | 'INDEXING_TITLE' => $message_progress, |
435 | 'INDEXING_EXPLAIN' => $message_progress_explain, |
436 | 'INDEXING_PROGRESS' => $message_redirect, |
437 | 'INDEXING_RATE' => $message_redirect_rate, |
438 | 'INDEXING_PROGRESS_BAR' => $this->get_post_index_progress($post_counter), |
439 | ]); |
440 | |
441 | $this->tpl_name = 'acp_search_index_progress'; |
442 | $this->page_title = 'ACP_SEARCH_INDEX'; |
443 | |
444 | return; |
445 | } |
446 | } |
447 | catch (Exception $e) |
448 | { |
449 | $this->search_state_helper->clear_state(); // Unexpected error, cancel action |
450 | trigger_error($e->getMessage() . adm_back_link($this->u_action), E_USER_WARNING); |
451 | } |
452 | |
453 | $search->tidy(); |
454 | |
455 | $this->search_state_helper->clear_state(); // finished operation, cancel action |
456 | |
457 | $log_operation = ($action == 'create') ? 'LOG_SEARCH_INDEX_CREATED' : 'LOG_SEARCH_INDEX_REMOVED'; |
458 | $this->log->add('admin', $this->user->data['user_id'], $this->user->ip, $log_operation, false, [$search->get_name()]); |
459 | |
460 | $message = $this->language->lang(($action == 'create') ? 'SEARCH_INDEX_CREATED' : 'SEARCH_INDEX_REMOVED'); |
461 | trigger_error($message . adm_back_link($this->u_action)); |
462 | } |
463 | |
464 | /** |
465 | * Display progress bar for search after first submit |
466 | * |
467 | * @param string $id ACP module id |
468 | * @param string $mode ACP module mode |
469 | */ |
470 | private function display_progress_bar(string $id, string $mode): void |
471 | { |
472 | $action = $this->search_state_helper->action(); |
473 | $post_counter = $this->search_state_helper->counter(); |
474 | |
475 | $message_progress = $this->language->lang(($action === 'create') ? 'INDEXING_IN_PROGRESS' : 'DELETING_INDEX_IN_PROGRESS'); |
476 | $message_progress_explain = $this->language->lang(($action == 'create') ? 'INDEXING_IN_PROGRESS_EXPLAIN' : 'DELETING_INDEX_IN_PROGRESS_EXPLAIN'); |
477 | |
478 | $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); |
479 | meta_refresh(1, $u_action); |
480 | |
481 | adm_page_header($this->language->lang($message_progress)); |
482 | |
483 | $this->template->set_filenames([ |
484 | 'body' => 'acp_search_index_progress.html' |
485 | ]); |
486 | |
487 | $this->template->assign_vars([ |
488 | 'INDEXING_TITLE' => $message_progress, |
489 | 'INDEXING_EXPLAIN' => $message_progress_explain, |
490 | 'INDEXING_PROGRESS_BAR' => $this->get_post_index_progress($post_counter), |
491 | ]); |
492 | |
493 | adm_page_footer(); |
494 | } |
495 | |
496 | /** |
497 | * Get progress stats of search index with HTML progress bar. |
498 | * |
499 | * @param int $post_counter Post ID of last post indexed. |
500 | * @return array Returns array with progress bar data. |
501 | */ |
502 | protected function get_post_index_progress(int $post_counter): array |
503 | { |
504 | $sql = 'SELECT COUNT(post_id) as done_count |
505 | FROM ' . POSTS_TABLE . ' |
506 | WHERE post_id <= ' . $post_counter; |
507 | $result = $this->db->sql_query($sql); |
508 | $done_count = (int) $this->db->sql_fetchfield('done_count'); |
509 | $this->db->sql_freeresult($result); |
510 | |
511 | $sql = 'SELECT COUNT(post_id) as remain_count |
512 | FROM ' . POSTS_TABLE . ' |
513 | WHERE post_id > ' . $post_counter; |
514 | $result = $this->db->sql_query($sql); |
515 | $remain_count = (int) $this->db->sql_fetchfield('remain_count'); |
516 | $this->db->sql_freeresult($result); |
517 | |
518 | $total_count = $done_count + $remain_count; |
519 | $percent = $total_count > 0 ? ($done_count / $total_count) * 100 : 100; |
520 | |
521 | return [ |
522 | 'VALUE' => $done_count, |
523 | 'TOTAL' => $total_count, |
524 | 'PERCENTAGE' => $percent, |
525 | 'REMAINING' => $remain_count, |
526 | ]; |
527 | } |
528 | } |