Verzeichnisstruktur phpBB-3.2.0


Veröffentlicht
06.01.2017

So funktioniert es


Auf das letzte Element klicken. Dies geht jeweils ein Schritt zurück

Auf das Icon klicken, dies öffnet das Verzeichnis. Nochmal klicken schließt das Verzeichnis.
Auf den Verzeichnisnamen klicken, dies zeigt nur das Verzeichnis mit Inhalt an

(Beispiel Datei-Icons)

Auf das Icon klicken um den Quellcode anzuzeigen

fulltext_sphinx.php

Zuletzt modifiziert: 09.10.2024, 12:52 - Dateigröße: 32.92 KiB


001  <?php
002  /**
003  *
004  * This file is part of the phpBB Forum Software package.
005  *
006  * @copyright (c) phpBB Limited <https://www.phpbb.com>
007  * @license GNU General Public License, version 2 (GPL-2.0)
008  *
009  * For full copyright and license information, please see
010  * the docs/CREDITS.txt file.
011  *
012  */
013   
014  namespace phpbb\search;
015   
016  define('SPHINX_MAX_MATCHES', 20000);
017  define('SPHINX_CONNECT_RETRIES', 3);
018  define('SPHINX_CONNECT_WAIT_TIME', 300);
019   
020  /**
021  * Fulltext search based on the sphinx search deamon
022  */
023  class fulltext_sphinx
024  {
025      /**
026       * Associative array holding index stats
027       * @var array
028       */
029      protected $stats = array();
030   
031      /**
032       * Holds the words entered by user, obtained by splitting the entered query on whitespace
033       * @var array
034       */
035      protected $split_words = array();
036   
037      /**
038       * Holds unique sphinx id
039       * @var string
040       */
041      protected $id;
042   
043      /**
044       * Stores the names of both main and delta sphinx indexes
045       * separated by a semicolon
046       * @var string
047       */
048      protected $indexes;
049   
050      /**
051       * Sphinx searchd client object
052       * @var SphinxClient
053       */
054      protected $sphinx;
055   
056      /**
057       * Relative path to board root
058       * @var string
059       */
060      protected $phpbb_root_path;
061   
062      /**
063       * PHP Extension
064       * @var string
065       */
066      protected $php_ext;
067   
068      /**
069       * Auth object
070       * @var \phpbb\auth\auth
071       */
072      protected $auth;
073   
074      /**
075       * Config object
076       * @var \phpbb\config\config
077       */
078      protected $config;
079   
080      /**
081       * Database connection
082       * @var \phpbb\db\driver\driver_interface
083       */
084      protected $db;
085   
086      /**
087       * Database Tools object
088       * @var \phpbb\db\tools\tools_interface
089       */
090      protected $db_tools;
091   
092      /**
093       * Stores the database type if supported by sphinx
094       * @var string
095       */
096      protected $dbtype;
097   
098      /**
099       * phpBB event dispatcher object
100       * @var \phpbb\event\dispatcher_interface
101       */
102      protected $phpbb_dispatcher;
103   
104      /**
105       * User object
106       * @var \phpbb\user
107       */
108      protected $user;
109   
110      /**
111       * Stores the generated content of the sphinx config file
112       * @var string
113       */
114      protected $config_file_data = '';
115   
116      /**
117       * Contains tidied search query.
118       * Operators are prefixed in search query and common words excluded
119       * @var string
120       */
121      protected $search_query;
122   
123      /**
124       * Constructor
125       * Creates a new \phpbb\search\fulltext_postgres, which is used as a search backend
126       *
127       * @param string|bool $error Any error that occurs is passed on through this reference variable otherwise false
128       * @param string $phpbb_root_path Relative path to phpBB root
129       * @param string $phpEx PHP file extension
130       * @param \phpbb\auth\auth $auth Auth object
131       * @param \phpbb\config\config $config Config object
132       * @param \phpbb\db\driver\driver_interface Database object
133       * @param \phpbb\user $user User object
134       * @param \phpbb\event\dispatcher_interface    $phpbb_dispatcher    Event dispatcher object
135       */
136      public function __construct(&$error, $phpbb_root_path, $phpEx, $auth, $config, $db, $user, $phpbb_dispatcher)
137      {
138          $this->phpbb_root_path = $phpbb_root_path;
139          $this->php_ext = $phpEx;
140          $this->config = $config;
141          $this->phpbb_dispatcher = $phpbb_dispatcher;
142          $this->user = $user;
143          $this->db = $db;
144          $this->auth = $auth;
145   
146          // Initialize \phpbb\db\tools\tools object
147          global $phpbb_container; // TODO inject into object
148          $this->db_tools = $phpbb_container->get('dbal.tools');
149   
150          if (!$this->config['fulltext_sphinx_id'])
151          {
152              $this->config->set('fulltext_sphinx_id', unique_id());
153          }
154          $this->id = $this->config['fulltext_sphinx_id'];
155          $this->indexes = 'index_phpbb_' . $this->id . '_delta;index_phpbb_' . $this->id . '_main';
156   
157          if (!class_exists('SphinxClient'))
158          {
159              require($this->phpbb_root_path . 'includes/sphinxapi.' . $this->php_ext);
160          }
161   
162          // Initialize sphinx client
163          $this->sphinx = new \SphinxClient();
164   
165          $this->sphinx->SetServer(($this->config['fulltext_sphinx_host'] ? $this->config['fulltext_sphinx_host'] : 'localhost'), ($this->config['fulltext_sphinx_port'] ? (int) $this->config['fulltext_sphinx_port'] : 9312));
166   
167          $error = false;
168      }
169   
170      /**
171      * Returns the name of this search backend to be displayed to administrators
172      *
173      * @return string Name
174      */
175      public function get_name()
176      {
177          return 'Sphinx Fulltext';
178      }
179   
180      /**
181       * Returns the search_query
182       *
183       * @return string search query
184       */
185      public function get_search_query()
186      {
187          return $this->search_query;
188      }
189   
190      /**
191       * Returns false as there is no word_len array
192       *
193       * @return false
194       */
195      public function get_word_length()
196      {
197          return false;
198      }
199   
200      /**
201       * Returns an empty array as there are no common_words
202       *
203       * @return array common words that are ignored by search backend
204       */
205      public function get_common_words()
206      {
207          return array();
208      }
209   
210      /**
211      * Checks permissions and paths, if everything is correct it generates the config file
212      *
213      * @return string|bool Language key of the error/incompatiblity encountered, or false if successful
214      */
215      public function init()
216      {
217          if ($this->db->get_sql_layer() != 'mysql' && $this->db->get_sql_layer() != 'mysql4' && $this->db->get_sql_layer() != 'mysqli' && $this->db->get_sql_layer() != 'postgres')
218          {
219              return $this->user->lang['FULLTEXT_SPHINX_WRONG_DATABASE'];
220          }
221   
222          // Move delta to main index each hour
223          $this->config->set('search_gc', 3600);
224   
225          return false;
226      }
227   
228      /**
229       * Generates content of sphinx.conf
230       *
231       * @return bool True if sphinx.conf content is correctly generated, false otherwise
232       */
233      protected function config_generate()
234      {
235          // Check if Database is supported by Sphinx
236          if ($this->db->get_sql_layer() =='mysql' || $this->db->get_sql_layer() == 'mysql4' || $this->db->get_sql_layer() == 'mysqli')
237          {
238              $this->dbtype = 'mysql';
239          }
240          else if ($this->db->get_sql_layer() == 'postgres')
241          {
242              $this->dbtype = 'pgsql';
243          }
244          else
245          {
246              $this->config_file_data = $this->user->lang('FULLTEXT_SPHINX_WRONG_DATABASE');
247              return false;
248          }
249   
250          // Check if directory paths have been filled
251          if (!$this->config['fulltext_sphinx_data_path'])
252          {
253              $this->config_file_data = $this->user->lang('FULLTEXT_SPHINX_NO_CONFIG_DATA');
254              return false;
255          }
256   
257          include($this->phpbb_root_path . 'config.' . $this->php_ext);
258   
259          /* Now that we're sure everything was entered correctly,
260          generate a config for the index. We use a config value
261          fulltext_sphinx_id for this, as it should be unique. */
262          $config_object = new \phpbb\search\sphinx\config($this->config_file_data);
263          $config_data = array(
264              'source source_phpbb_' . $this->id . '_main' => array(
265                  array('type',                        $this->dbtype . ' # mysql or pgsql'),
266                  // This config value sql_host needs to be changed incase sphinx and sql are on different servers
267                  array('sql_host',                    $dbhost . ' # SQL server host sphinx connects to'),
268                  array('sql_user',                    '[dbuser]'),
269                  array('sql_pass',                    '[dbpassword]'),
270                  array('sql_db',                        $dbname),
271                  array('sql_port',                    $dbport . ' # optional, default is 3306 for mysql and 5432 for pgsql'),
272                  array('sql_query_pre',                'SET NAMES \'utf8\''),
273                  array('sql_query_pre',                'UPDATE ' . SPHINX_TABLE . ' SET max_doc_id = (SELECT MAX(post_id) FROM ' . POSTS_TABLE . ') WHERE counter_id = 1'),
274                  array('sql_query_range',            'SELECT MIN(post_id), MAX(post_id) FROM ' . POSTS_TABLE . ''),
275                  array('sql_range_step',                '5000'),
276                  array('sql_query',                    'SELECT
277                          p.post_id AS id,
278                          p.forum_id,
279                          p.topic_id,
280                          p.poster_id,
281                          p.post_visibility,
282                          CASE WHEN p.post_id = t.topic_first_post_id THEN 1 ELSE 0 END as topic_first_post,
283                          p.post_time,
284                          p.post_subject,
285                          p.post_subject as title,
286                          p.post_text as data,
287                          t.topic_last_post_time,
288                          0 as deleted
289                      FROM ' . POSTS_TABLE . ' p, ' . TOPICS_TABLE . ' t
290                      WHERE
291                          p.topic_id = t.topic_id
292                          AND p.post_id >= $start AND p.post_id <= $end'),
293                  array('sql_query_post',                ''),
294                  array('sql_query_post_index',        'UPDATE ' . SPHINX_TABLE . ' SET max_doc_id = $maxid WHERE counter_id = 1'),
295                  array('sql_query_info',                'SELECT * FROM ' . POSTS_TABLE . ' WHERE post_id = $id'),
296                  array('sql_attr_uint',                'forum_id'),
297                  array('sql_attr_uint',                'topic_id'),
298                  array('sql_attr_uint',                'poster_id'),
299                  array('sql_attr_uint',                'post_visibility'),
300                  array('sql_attr_bool',                'topic_first_post'),
301                  array('sql_attr_bool',                'deleted'),
302                  array('sql_attr_timestamp',            'post_time'),
303                  array('sql_attr_timestamp',            'topic_last_post_time'),
304                  array('sql_attr_string',            'post_subject'),
305              ),
306              'source source_phpbb_' . $this->id . '_delta : source_phpbb_' . $this->id . '_main' => array(
307                  array('sql_query_pre',                ''),
308                  array('sql_query_range',            ''),
309                  array('sql_range_step',                ''),
310                  array('sql_query',                    'SELECT
311                          p.post_id AS id,
312                          p.forum_id,
313                          p.topic_id,
314                          p.poster_id,
315                          p.post_visibility,
316                          CASE WHEN p.post_id = t.topic_first_post_id THEN 1 ELSE 0 END as topic_first_post,
317                          p.post_time,
318                          p.post_subject,
319                          p.post_subject as title,
320                          p.post_text as data,
321                          t.topic_last_post_time,
322                          0 as deleted
323                      FROM ' . POSTS_TABLE . ' p, ' . TOPICS_TABLE . ' t
324                      WHERE
325                          p.topic_id = t.topic_id
326                          AND p.post_id >=  ( SELECT max_doc_id FROM ' . SPHINX_TABLE . ' WHERE counter_id=1 )'),
327              ),
328              'index index_phpbb_' . $this->id . '_main' => array(
329                  array('path',                        $this->config['fulltext_sphinx_data_path'] . 'index_phpbb_' . $this->id . '_main'),
330                  array('source',                        'source_phpbb_' . $this->id . '_main'),
331                  array('docinfo',                    'extern'),
332                  array('morphology',                    'none'),
333                  array('stopwords',                    ''),
334                  array('min_word_len',                '2'),
335                  array('charset_type',                'utf-8'),
336                  array('charset_table',                'U+FF10..U+FF19->0..9, 0..9, U+FF41..U+FF5A->a..z, U+FF21..U+FF3A->a..z, A..Z->a..z, a..z, U+0149, U+017F, U+0138, U+00DF, U+00FF, U+00C0..U+00D6->U+00E0..U+00F6, U+00E0..U+00F6, U+00D8..U+00DE->U+00F8..U+00FE, U+00F8..U+00FE, U+0100->U+0101, U+0101, U+0102->U+0103, U+0103, U+0104->U+0105, U+0105, U+0106->U+0107, U+0107, U+0108->U+0109, U+0109, U+010A->U+010B, U+010B, U+010C->U+010D, U+010D, U+010E->U+010F, U+010F, U+0110->U+0111, U+0111, U+0112->U+0113, U+0113, U+0114->U+0115, U+0115, U+0116->U+0117, U+0117, U+0118->U+0119, U+0119, U+011A->U+011B, U+011B, U+011C->U+011D, U+011D, U+011E->U+011F, U+011F, U+0130->U+0131, U+0131, U+0132->U+0133, U+0133, U+0134->U+0135, U+0135, U+0136->U+0137, U+0137, U+0139->U+013A, U+013A, U+013B->U+013C, U+013C, U+013D->U+013E, U+013E, U+013F->U+0140, U+0140, U+0141->U+0142, U+0142, U+0143->U+0144, U+0144, U+0145->U+0146, U+0146, U+0147->U+0148, U+0148, U+014A->U+014B, U+014B, U+014C->U+014D, U+014D, U+014E->U+014F, U+014F, U+0150->U+0151, U+0151, U+0152->U+0153, U+0153, U+0154->U+0155, U+0155, U+0156->U+0157, U+0157, U+0158->U+0159, U+0159, U+015A->U+015B, U+015B, U+015C->U+015D, U+015D, U+015E->U+015F, U+015F, U+0160->U+0161, U+0161, U+0162->U+0163, U+0163, U+0164->U+0165, U+0165, U+0166->U+0167, U+0167, U+0168->U+0169, U+0169, U+016A->U+016B, U+016B, U+016C->U+016D, U+016D, U+016E->U+016F, U+016F, U+0170->U+0171, U+0171, U+0172->U+0173, U+0173, U+0174->U+0175, U+0175, U+0176->U+0177, U+0177, U+0178->U+00FF, U+00FF, U+0179->U+017A, U+017A, U+017B->U+017C, U+017C, U+017D->U+017E, U+017E, U+0410..U+042F->U+0430..U+044F, U+0430..U+044F, U+4E00..U+9FFF'),
337                  array('min_prefix_len',                '0'),
338                  array('min_infix_len',                '0'),
339              ),
340              'index index_phpbb_' . $this->id . '_delta : index_phpbb_' . $this->id . '_main' => array(
341                  array('path',                        $this->config['fulltext_sphinx_data_path'] . 'index_phpbb_' . $this->id . '_delta'),
342                  array('source',                        'source_phpbb_' . $this->id . '_delta'),
343              ),
344              'indexer' => array(
345                  array('mem_limit',                    $this->config['fulltext_sphinx_indexer_mem_limit'] . 'M'),
346              ),
347              'searchd' => array(
348                  array('compat_sphinxql_magics'    ,    '0'),
349                  array('listen'    ,                    ($this->config['fulltext_sphinx_host'] ? $this->config['fulltext_sphinx_host'] : 'localhost') . ':' . ($this->config['fulltext_sphinx_port'] ? $this->config['fulltext_sphinx_port'] : '9312')),
350                  array('log',                        $this->config['fulltext_sphinx_data_path'] . 'log/searchd.log'),
351                  array('query_log',                    $this->config['fulltext_sphinx_data_path'] . 'log/sphinx-query.log'),
352                  array('read_timeout',                '5'),
353                  array('max_children',                '30'),
354                  array('pid_file',                    $this->config['fulltext_sphinx_data_path'] . 'searchd.pid'),
355                  array('max_matches',                (string) SPHINX_MAX_MATCHES),
356                  array('binlog_path',                $this->config['fulltext_sphinx_data_path']),
357              ),
358          );
359   
360          $non_unique = array('sql_query_pre' => true, 'sql_attr_uint' => true, 'sql_attr_timestamp' => true, 'sql_attr_str2ordinal' => true, 'sql_attr_bool' => true);
361          $delete = array('sql_group_column' => true, 'sql_date_column' => true, 'sql_str2ordinal_column' => true);
362   
363          /**
364          * Allow adding/changing the Sphinx configuration data
365          *
366          * @event core.search_sphinx_modify_config_data
367          * @var    array    config_data    Array with the Sphinx configuration data
368          * @var    array    non_unique    Array with the Sphinx non-unique variables to delete
369          * @var    array    delete        Array with the Sphinx variables to delete
370          * @since 3.1.7-RC1
371          */
372          $vars = array(
373              'config_data',
374              'non_unique',
375              'delete',
376          );
377          extract($this->phpbb_dispatcher->trigger_event('core.search_sphinx_modify_config_data', compact($vars)));
378   
379          foreach ($config_data as $section_name => $section_data)
380          {
381              $section = $config_object->get_section_by_name($section_name);
382              if (!$section)
383              {
384                  $section = $config_object->add_section($section_name);
385              }
386   
387              foreach ($delete as $key => $void)
388              {
389                  $section->delete_variables_by_name($key);
390              }
391   
392              foreach ($non_unique as $key => $void)
393              {
394                  $section->delete_variables_by_name($key);
395              }
396   
397              foreach ($section_data as $entry)
398              {
399                  $key = $entry[0];
400                  $value = $entry[1];
401   
402                  if (!isset($non_unique[$key]))
403                  {
404                      $variable = $section->get_variable_by_name($key);
405                      if (!$variable)
406                      {
407                          $section->create_variable($key, $value);
408                      }
409                      else
410                      {
411                          $variable->set_value($value);
412                      }
413                  }
414                  else
415                  {
416                      $section->create_variable($key, $value);
417                  }
418              }
419          }
420          $this->config_file_data = $config_object->get_data();
421   
422          return true;
423      }
424   
425      /**
426      * Splits keywords entered by a user into an array of words stored in $this->split_words
427      * Stores the tidied search query in $this->search_query
428      *
429      * @param string $keywords Contains the keyword as entered by the user
430      * @param string $terms is either 'all' or 'any'
431      * @return false if no valid keywords were found and otherwise true
432      */
433      public function split_keywords(&$keywords, $terms)
434      {
435          if ($terms == 'all')
436          {
437              $match        = array('#\sand\s#i', '#\sor\s#i', '#\snot\s#i', '#\+#', '#-#', '#\|#', '#@#');
438              $replace    = array(' & ', ' | ', '  - ', ' +', ' -', ' |', '');
439   
440              $keywords = preg_replace($match, $replace, $keywords);
441              $this->sphinx->SetMatchMode(SPH_MATCH_EXTENDED);
442          }
443          else
444          {
445              $this->sphinx->SetMatchMode(SPH_MATCH_ANY);
446          }
447   
448          // Keep quotes and new lines
449          $keywords = str_replace(array('&quot;', "\n"), array('"', ' '), trim($keywords));
450   
451          if (strlen($keywords) > 0)
452          {
453              $this->search_query = str_replace('"', '&quot;', $keywords);
454              return true;
455          }
456   
457          return false;
458      }
459   
460      /**
461      * Performs a search on keywords depending on display specific params. You have to run split_keywords() first
462      *
463      * @param    string        $type                contains either posts or topics depending on what should be searched for
464      * @param    string        $fields                contains either titleonly (topic titles should be searched), msgonly (only message bodies should be searched), firstpost (only subject and body of the first post should be searched) or all (all post bodies and subjects should be searched)
465      * @param    string        $terms                is either 'all' (use query as entered, words without prefix should default to "have to be in field") or 'any' (ignore search query parts and just return all posts that contain any of the specified words)
466      * @param    array        $sort_by_sql        contains SQL code for the ORDER BY part of a query
467      * @param    string        $sort_key            is the key of $sort_by_sql for the selected sorting
468      * @param    string        $sort_dir            is either a or d representing ASC and DESC
469      * @param    string        $sort_days            specifies the maximum amount of days a post may be old
470      * @param    array        $ex_fid_ary            specifies an array of forum ids which should not be searched
471      * @param    string        $post_visibility    specifies which types of posts the user can view in which forums
472      * @param    int            $topic_id            is set to 0 or a topic id, if it is not 0 then only posts in this topic should be searched
473      * @param    array        $author_ary            an array of author ids if the author should be ignored during the search the array is empty
474      * @param    string        $author_name        specifies the author match, when ANONYMOUS is also a search-match
475      * @param    array        &$id_ary            passed by reference, to be filled with ids for the page specified by $start and $per_page, should be ordered
476      * @param    int            $start                indicates the first index of the page
477      * @param    int            $per_page            number of ids each page is supposed to contain
478      * @return    boolean|int                        total number of results
479      */
480      public function keyword_search($type, $fields, $terms, $sort_by_sql, $sort_key, $sort_dir, $sort_days, $ex_fid_ary, $post_visibility, $topic_id, $author_ary, $author_name, &$id_ary, &$start, $per_page)
481      {
482          global $user, $phpbb_log;
483   
484          // No keywords? No posts.
485          if (!strlen($this->search_query) && !sizeof($author_ary))
486          {
487              return false;
488          }
489   
490          $id_ary = array();
491   
492          // Sorting
493   
494          if ($type == 'topics')
495          {
496              switch ($sort_key)
497              {
498                  case 'a':
499                      $this->sphinx->SetGroupBy('topic_id', SPH_GROUPBY_ATTR, 'poster_id ' . (($sort_dir == 'a') ? 'ASC' : 'DESC'));
500                  break;
501   
502                  case 'f':
503                      $this->sphinx->SetGroupBy('topic_id', SPH_GROUPBY_ATTR, 'forum_id ' . (($sort_dir == 'a') ? 'ASC' : 'DESC'));
504                  break;
505   
506                  case 'i':
507   
508                  case 's':
509                      $this->sphinx->SetGroupBy('topic_id', SPH_GROUPBY_ATTR, 'post_subject ' . (($sort_dir == 'a') ? 'ASC' : 'DESC'));
510                  break;
511   
512                  case 't':
513   
514                  default:
515                      $this->sphinx->SetGroupBy('topic_id', SPH_GROUPBY_ATTR, 'topic_last_post_time ' . (($sort_dir == 'a') ? 'ASC' : 'DESC'));
516                  break;
517              }
518          }
519          else
520          {
521              switch ($sort_key)
522              {
523                  case 'a':
524                      $this->sphinx->SetSortMode(($sort_dir == 'a') ? SPH_SORT_ATTR_ASC : SPH_SORT_ATTR_DESC, 'poster_id');
525                  break;
526   
527                  case 'f':
528                      $this->sphinx->SetSortMode(($sort_dir == 'a') ? SPH_SORT_ATTR_ASC : SPH_SORT_ATTR_DESC, 'forum_id');
529                  break;
530   
531                  case 'i':
532   
533                  case 's':
534                      $this->sphinx->SetSortMode(($sort_dir == 'a') ? SPH_SORT_ATTR_ASC : SPH_SORT_ATTR_DESC, 'post_subject');
535                  break;
536   
537                  case 't':
538   
539                  default:
540                      $this->sphinx->SetSortMode(($sort_dir == 'a') ? SPH_SORT_ATTR_ASC : SPH_SORT_ATTR_DESC, 'post_time');
541                  break;
542              }
543          }
544   
545          // Most narrow filters first
546          if ($topic_id)
547          {
548              $this->sphinx->SetFilter('topic_id', array($topic_id));
549          }
550   
551          /**
552          * Allow modifying the Sphinx search options
553          *
554          * @event core.search_sphinx_keywords_modify_options
555          * @var    string    type                Searching type ('posts', 'topics')
556          * @var    string    fields                Searching fields ('titleonly', 'msgonly', 'firstpost', 'all')
557          * @var    string    terms                Searching terms ('all', 'any')
558          * @var    int        sort_days            Time, in days, of the oldest possible post to list
559          * @var    string    sort_key            The sort type used from the possible sort types
560          * @var    int        topic_id            Limit the search to this topic_id only
561          * @var    array    ex_fid_ary            Which forums not to search on
562          * @var    string    post_visibility        Post visibility data
563          * @var    array    author_ary            Array of user_id containing the users to filter the results to
564          * @var    string    author_name            The username to search on
565          * @var    object    sphinx                The Sphinx searchd client object
566          * @since 3.1.7-RC1
567          */
568          $sphinx = $this->sphinx;
569          $vars = array(
570              'type',
571              'fields',
572              'terms',
573              'sort_days',
574              'sort_key',
575              'topic_id',
576              'ex_fid_ary',
577              'post_visibility',
578              'author_ary',
579              'author_name',
580              'sphinx',
581          );
582          extract($this->phpbb_dispatcher->trigger_event('core.search_sphinx_keywords_modify_options', compact($vars)));
583          $this->sphinx = $sphinx;
584          unset($sphinx);
585   
586          $search_query_prefix = '';
587   
588          switch ($fields)
589          {
590              case 'titleonly':
591                  // Only search the title
592                  if ($terms == 'all')
593                  {
594                      $search_query_prefix = '@title ';
595                  }
596                  // Weight for the title
597                  $this->sphinx->SetFieldWeights(array("title" => 5, "data" => 1));
598                  // 1 is first_post, 0 is not first post
599                  $this->sphinx->SetFilter('topic_first_post', array(1));
600              break;
601   
602              case 'msgonly':
603                  // Only search the body
604                  if ($terms == 'all')
605                  {
606                      $search_query_prefix = '@data ';
607                  }
608                  // Weight for the body
609                  $this->sphinx->SetFieldWeights(array("title" => 1, "data" => 5));
610              break;
611   
612              case 'firstpost':
613                  // More relative weight for the title, also search the body
614                  $this->sphinx->SetFieldWeights(array("title" => 5, "data" => 1));
615                  // 1 is first_post, 0 is not first post
616                  $this->sphinx->SetFilter('topic_first_post', array(1));
617              break;
618   
619              default:
620                  // More relative weight for the title, also search the body
621                  $this->sphinx->SetFieldWeights(array("title" => 5, "data" => 1));
622              break;
623          }
624   
625          if (sizeof($author_ary))
626          {
627              $this->sphinx->SetFilter('poster_id', $author_ary);
628          }
629   
630          // As this is not simply possible at the moment, we limit the result to approved posts.
631          // This will make it impossible for moderators to search unapproved and softdeleted posts,
632          // but at least it will also cause the same for normal users.
633          $this->sphinx->SetFilter('post_visibility', array(ITEM_APPROVED));
634   
635          if (sizeof($ex_fid_ary))
636          {
637              // All forums that a user is allowed to access
638              $fid_ary = array_unique(array_intersect(array_keys($this->auth->acl_getf('f_read', true)), array_keys($this->auth->acl_getf('f_search', true))));
639              // All forums that the user wants to and can search in
640              $search_forums = array_diff($fid_ary, $ex_fid_ary);
641   
642              if (sizeof($search_forums))
643              {
644                  $this->sphinx->SetFilter('forum_id', $search_forums);
645              }
646          }
647   
648          $this->sphinx->SetFilter('deleted', array(0));
649   
650          $this->sphinx->SetLimits($start, (int) $per_page, SPHINX_MAX_MATCHES);
651          $result = $this->sphinx->Query($search_query_prefix . str_replace('&quot;', '"', $this->search_query), $this->indexes);
652   
653          // Could be connection to localhost:9312 failed (errno=111,
654          // msg=Connection refused) during rotate, retry if so
655          $retries = SPHINX_CONNECT_RETRIES;
656          while (!$result && (strpos($this->sphinx->GetLastError(), "errno=111,") !== false) && $retries--)
657          {
658              usleep(SPHINX_CONNECT_WAIT_TIME);
659              $result = $this->sphinx->Query($search_query_prefix . str_replace('&quot;', '"', $this->search_query), $this->indexes);
660          }
661   
662          if ($this->sphinx->GetLastError())
663          {
664              $phpbb_log->add('critical', $user->data['user_id'], $user->ip, 'LOG_SPHINX_ERROR', false, array($this->sphinx->GetLastError()));
665              if ($this->auth->acl_get('a_'))
666              {
667                  trigger_error($this->user->lang('SPHINX_SEARCH_FAILED', $this->sphinx->GetLastError()));
668              }
669              else
670              {
671                  trigger_error($this->user->lang('SPHINX_SEARCH_FAILED_LOG'));
672              }
673          }
674   
675          $result_count = $result['total_found'];
676   
677          if ($result_count && $start >= $result_count)
678          {
679              $start = floor(($result_count - 1) / $per_page) * $per_page;
680   
681              $this->sphinx->SetLimits((int) $start, (int) $per_page, SPHINX_MAX_MATCHES);
682              $result = $this->sphinx->Query($search_query_prefix . str_replace('&quot;', '"', $this->search_query), $this->indexes);
683   
684              // Could be connection to localhost:9312 failed (errno=111,
685              // msg=Connection refused) during rotate, retry if so
686              $retries = SPHINX_CONNECT_RETRIES;
687              while (!$result && (strpos($this->sphinx->GetLastError(), "errno=111,") !== false) && $retries--)
688              {
689                  usleep(SPHINX_CONNECT_WAIT_TIME);
690                  $result = $this->sphinx->Query($search_query_prefix . str_replace('&quot;', '"', $this->search_query), $this->indexes);
691              }
692          }
693   
694          $id_ary = array();
695          if (isset($result['matches']))
696          {
697              if ($type == 'posts')
698              {
699                  $id_ary = array_keys($result['matches']);
700              }
701              else
702              {
703                  foreach ($result['matches'] as $key => $value)
704                  {
705                      $id_ary[] = $value['attrs']['topic_id'];
706                  }
707              }
708          }
709          else
710          {
711              return false;
712          }
713   
714          $id_ary = array_slice($id_ary, 0, (int) $per_page);
715   
716          return $result_count;
717      }
718   
719      /**
720      * Performs a search on an author's posts without caring about message contents. Depends on display specific params
721      *
722      * @param    string        $type                contains either posts or topics depending on what should be searched for
723      * @param    boolean        $firstpost_only        if true, only topic starting posts will be considered
724      * @param    array        $sort_by_sql        contains SQL code for the ORDER BY part of a query
725      * @param    string        $sort_key            is the key of $sort_by_sql for the selected sorting
726      * @param    string        $sort_dir            is either a or d representing ASC and DESC
727      * @param    string        $sort_days            specifies the maximum amount of days a post may be old
728      * @param    array        $ex_fid_ary            specifies an array of forum ids which should not be searched
729      * @param    string        $post_visibility    specifies which types of posts the user can view in which forums
730      * @param    int            $topic_id            is set to 0 or a topic id, if it is not 0 then only posts in this topic should be searched
731      * @param    array        $author_ary            an array of author ids
732      * @param    string        $author_name        specifies the author match, when ANONYMOUS is also a search-match
733      * @param    array        &$id_ary            passed by reference, to be filled with ids for the page specified by $start and $per_page, should be ordered
734      * @param    int            $start                indicates the first index of the page
735      * @param    int            $per_page            number of ids each page is supposed to contain
736      * @return    boolean|int                        total number of results
737      */
738      public function author_search($type, $firstpost_only, $sort_by_sql, $sort_key, $sort_dir, $sort_days, $ex_fid_ary, $post_visibility, $topic_id, $author_ary, $author_name, &$id_ary, $start, $per_page)
739      {
740          $this->search_query = '';
741   
742          $this->sphinx->SetMatchMode(SPH_MATCH_FULLSCAN);
743          $fields = ($firstpost_only) ? 'firstpost' : 'all';
744          $terms = 'all';
745          return $this->keyword_search($type, $fields, $terms, $sort_by_sql, $sort_key, $sort_dir, $sort_days, $ex_fid_ary, $post_visibility, $topic_id, $author_ary, $author_name, $id_ary, $start, $per_page);
746      }
747   
748      /**
749       * Updates wordlist and wordmatch tables when a message is posted or changed
750       *
751       * @param    string    $mode    Contains the post mode: edit, post, reply, quote
752       * @param    int    $post_id    The id of the post which is modified/created
753       * @param    string    &$message    New or updated post content
754       * @param    string    &$subject    New or updated post subject
755       * @param    int    $poster_id    Post author's user id
756       * @param    int    $forum_id    The id of the forum in which the post is located
757       */
758      public function index($mode, $post_id, &$message, &$subject, $poster_id, $forum_id)
759      {
760          if ($mode == 'edit')
761          {
762              $this->sphinx->UpdateAttributes($this->indexes, array('forum_id', 'poster_id'), array((int) $post_id => array((int) $forum_id, (int) $poster_id)));
763          }
764          else if ($mode != 'post' && $post_id)
765          {
766              // Update topic_last_post_time for full topic
767              $sql_array = array(
768                  'SELECT'    => 'p1.post_id',
769                  'FROM'        => array(
770                      POSTS_TABLE    => 'p1',
771                  ),
772                  'LEFT_JOIN'    => array(array(
773                      'FROM'    => array(
774                          POSTS_TABLE    => 'p2'
775                      ),
776                      'ON'    => 'p1.topic_id = p2.topic_id',
777                  )),
778                  'WHERE' => 'p2.post_id = ' . ((int) $post_id),
779              );
780   
781              $sql = $this->db->sql_build_query('SELECT', $sql_array);
782              $result = $this->db->sql_query($sql);
783   
784              $post_updates = array();
785              $post_time = time();
786              while ($row = $this->db->sql_fetchrow($result))
787              {
788                  $post_updates[(int) $row['post_id']] = array($post_time);
789              }
790              $this->db->sql_freeresult($result);
791   
792              if (sizeof($post_updates))
793              {
794                  $this->sphinx->UpdateAttributes($this->indexes, array('topic_last_post_time'), $post_updates);
795              }
796          }
797      }
798   
799      /**
800      * Delete a post from the index after it was deleted
801      */
802      public function index_remove($post_ids, $author_ids, $forum_ids)
803      {
804          $values = array();
805          foreach ($post_ids as $post_id)
806          {
807              $values[$post_id] = array(1);
808          }
809   
810          $this->sphinx->UpdateAttributes($this->indexes, array('deleted'), $values);
811      }
812   
813      /**
814      * Nothing needs to be destroyed
815      */
816      public function tidy($create = false)
817      {
818          $this->config->set('search_last_gc', time(), false);
819      }
820   
821      /**
822      * Create sphinx table
823      *
824      * @return string|bool error string is returned incase of errors otherwise false
825      */
826      public function create_index($acp_module, $u_action)
827      {
828          if (!$this->index_created())
829          {
830              $table_data = array(
831                  'COLUMNS'    => array(
832                      'counter_id'    => array('UINT', 0),
833                      'max_doc_id'    => array('UINT', 0),
834                  ),
835                  'PRIMARY_KEY'    => 'counter_id',
836              );
837              $this->db_tools->sql_create_table(SPHINX_TABLE, $table_data);
838   
839              $sql = 'TRUNCATE TABLE ' . SPHINX_TABLE;
840              $this->db->sql_query($sql);
841   
842              $data = array(
843                  'counter_id'    => '1',
844                  'max_doc_id'    => '0',
845              );
846              $sql = 'INSERT INTO ' . SPHINX_TABLE . ' ' . $this->db->sql_build_array('INSERT', $data);
847              $this->db->sql_query($sql);
848          }
849   
850          return false;
851      }
852   
853      /**
854      * Drop sphinx table
855      *
856      * @return string|bool error string is returned incase of errors otherwise false
857      */
858      public function delete_index($acp_module, $u_action)
859      {
860          if (!$this->index_created())
861          {
862              return false;
863          }
864   
865          $this->db_tools->sql_table_drop(SPHINX_TABLE);
866   
867          return false;
868      }
869   
870      /**
871      * Returns true if the sphinx table was created
872      *
873      * @return bool true if sphinx table was created
874      */
875      public function index_created($allow_new_files = true)
876      {
877          $created = false;
878   
879          if ($this->db_tools->sql_table_exists(SPHINX_TABLE))
880          {
881              $created = true;
882          }
883   
884          return $created;
885      }
886   
887      /**
888      * Returns an associative array containing information about the indexes
889      *
890      * @return string|bool Language string of error false otherwise
891      */
892      public function index_stats()
893      {
894          if (empty($this->stats))
895          {
896              $this->get_stats();
897          }
898   
899          return array(
900              $this->user->lang['FULLTEXT_SPHINX_MAIN_POSTS']            => ($this->index_created()) ? $this->stats['main_posts'] : 0,
901              $this->user->lang['FULLTEXT_SPHINX_DELTA_POSTS']            => ($this->index_created()) ? $this->stats['total_posts'] - $this->stats['main_posts'] : 0,
902              $this->user->lang['FULLTEXT_MYSQL_TOTAL_POSTS']            => ($this->index_created()) ? $this->stats['total_posts'] : 0,
903          );
904      }
905   
906      /**
907      * Collects stats that can be displayed on the index maintenance page
908      */
909      protected function get_stats()
910      {
911          if ($this->index_created())
912          {
913              $sql = 'SELECT COUNT(post_id) as total_posts
914                  FROM ' . POSTS_TABLE;
915              $result = $this->db->sql_query($sql);
916              $this->stats['total_posts'] = (int) $this->db->sql_fetchfield('total_posts');
917              $this->db->sql_freeresult($result);
918   
919              $sql = 'SELECT COUNT(p.post_id) as main_posts
920                  FROM ' . POSTS_TABLE . ' p, ' . SPHINX_TABLE . ' m
921                  WHERE p.post_id <= m.max_doc_id
922                      AND m.counter_id = 1';
923              $result = $this->db->sql_query($sql);
924              $this->stats['main_posts'] = (int) $this->db->sql_fetchfield('main_posts');
925              $this->db->sql_freeresult($result);
926          }
927      }
928   
929      /**
930      * Returns a list of options for the ACP to display
931      *
932      * @return associative array containing template and config variables
933      */
934      public function acp()
935      {
936          $config_vars = array(
937              'fulltext_sphinx_data_path' => 'string',
938              'fulltext_sphinx_host' => 'string',
939              'fulltext_sphinx_port' => 'string',
940              'fulltext_sphinx_indexer_mem_limit' => 'int',
941          );
942   
943          $tpl = '
944          <span class="error">' . $this->user->lang['FULLTEXT_SPHINX_CONFIGURE']. '</span>
945          <dl>
946              <dt><label for="fulltext_sphinx_data_path">' . $this->user->lang['FULLTEXT_SPHINX_DATA_PATH'] . $this->user->lang['COLON'] . '</label><br /><span>' . $this->user->lang['FULLTEXT_SPHINX_DATA_PATH_EXPLAIN'] . '</span></dt>
947              <dd><input id="fulltext_sphinx_data_path" type="text" size="40" maxlength="255" name="config[fulltext_sphinx_data_path]" value="' . $this->config['fulltext_sphinx_data_path'] . '" /></dd>
948          </dl>
949          <dl>
950              <dt><label for="fulltext_sphinx_host">' . $this->user->lang['FULLTEXT_SPHINX_HOST'] . $this->user->lang['COLON'] . '</label><br /><span>' . $this->user->lang['FULLTEXT_SPHINX_HOST_EXPLAIN'] . '</span></dt>
951              <dd><input id="fulltext_sphinx_host" type="text" size="40" maxlength="255" name="config[fulltext_sphinx_host]" value="' . $this->config['fulltext_sphinx_host'] . '" /></dd>
952          </dl>
953          <dl>
954              <dt><label for="fulltext_sphinx_port">' . $this->user->lang['FULLTEXT_SPHINX_PORT'] . $this->user->lang['COLON'] . '</label><br /><span>' . $this->user->lang['FULLTEXT_SPHINX_PORT_EXPLAIN'] . '</span></dt>
955              <dd><input id="fulltext_sphinx_port" type="number" min="0" max="9999999999" name="config[fulltext_sphinx_port]" value="' . $this->config['fulltext_sphinx_port'] . '" /></dd>
956          </dl>
957          <dl>
958              <dt><label for="fulltext_sphinx_indexer_mem_limit">' . $this->user->lang['FULLTEXT_SPHINX_INDEXER_MEM_LIMIT'] . $this->user->lang['COLON'] . '</label><br /><span>' . $this->user->lang['FULLTEXT_SPHINX_INDEXER_MEM_LIMIT_EXPLAIN'] . '</span></dt>
959              <dd><input id="fulltext_sphinx_indexer_mem_limit" type="number" min="0" max="9999999999" name="config[fulltext_sphinx_indexer_mem_limit]" value="' . $this->config['fulltext_sphinx_indexer_mem_limit'] . '" /> ' . $this->user->lang['MIB'] . '</dd>
960          </dl>
961          <dl>
962              <dt><label for="fulltext_sphinx_config_file">' . $this->user->lang['FULLTEXT_SPHINX_CONFIG_FILE'] . $this->user->lang['COLON'] . '</label><br /><span>' . $this->user->lang['FULLTEXT_SPHINX_CONFIG_FILE_EXPLAIN'] . '</span></dt>
963              <dd>' . (($this->config_generate()) ? '<textarea readonly="readonly" rows="6" id="sphinx_config_data">' . htmlspecialchars($this->config_file_data) . '</textarea>' : $this->config_file_data) . '</dd>
964          <dl>
965          ';
966   
967          // These are fields required in the config table
968          return array(
969              'tpl'        => $tpl,
970              'config'    => $config_vars
971          );
972      }
973  }
974