Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 224
0.00% covered (danger)
0.00%
0 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
acp_search
0.00% covered (danger)
0.00%
0 / 222
0.00% covered (danger)
0.00%
0 / 9
3192
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
2
 main
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 settings
0.00% covered (danger)
0.00%
0 / 81
0.00% covered (danger)
0.00%
0 / 1
506
 index
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
72
 index_overview
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
6
 index_inprogress
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 index_action
0.00% covered (danger)
0.00%
0 / 49
0.00% covered (danger)
0.00%
0 / 1
210
 display_progress_bar
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
12
 get_post_index_progress
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
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
18use phpbb\config\config;
19use phpbb\db\driver\driver_interface;
20use phpbb\di\service_collection;
21use phpbb\language\language;
22use phpbb\log\log;
23use phpbb\request\request;
24use phpbb\search\backend\search_backend_interface;
25use phpbb\search\search_backend_factory;
26use phpbb\search\state_helper;
27use phpbb\template\template;
28use phpbb\user;
29
30if (!defined('IN_PHPBB'))
31{
32    exit;
33}
34
35class 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&amp;mode=index') . '">&raquo; ' . $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 . '&amp;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 . '&amp;hash=' . generate_link_hash('acp_search'),
342            'UA_PROGRESS_BAR'    => addslashes($this->u_action . '&amp;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 . '&amp;action=' . $action . '&amp;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}