Verzeichnisstruktur phpBB-3.1.0


Veröffentlicht
27.10.2014

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: 30.81 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
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       * User object
100       * @var \phpbb\user
101       */
102      protected $user;
103   
104      /**
105       * Stores the generated content of the sphinx config file
106       * @var string
107       */
108      protected $config_file_data = '';
109   
110      /**
111       * Contains tidied search query.
112       * Operators are prefixed in search query and common words excluded
113       * @var string
114       */
115      protected $search_query;
116   
117      /**
118       * Constructor
119       * Creates a new \phpbb\search\fulltext_postgres, which is used as a search backend
120       *
121       * @param string|bool $error Any error that occurs is passed on through this reference variable otherwise false
122       * @param string $phpbb_root_path Relative path to phpBB root
123       * @param string $phpEx PHP file extension
124       * @param \phpbb\auth\auth $auth Auth object
125       * @param \phpbb\config\config $config Config object
126       * @param \phpbb\db\driver\driver_interface Database object
127       * @param \phpbb\user $user User object
128       */
129      public function __construct(&$error, $phpbb_root_path, $phpEx, $auth, $config, $db, $user)
130      {
131          $this->phpbb_root_path = $phpbb_root_path;
132          $this->php_ext = $phpEx;
133          $this->config = $config;
134          $this->user = $user;
135          $this->db = $db;
136          $this->auth = $auth;
137   
138          // Initialize \phpbb\db\tools object
139          $this->db_tools = new \phpbb\db\tools($this->db);
140   
141          if(!$this->config['fulltext_sphinx_id'])
142          {
143              set_config('fulltext_sphinx_id', unique_id());
144          }
145          $this->id = $this->config['fulltext_sphinx_id'];
146          $this->indexes = 'index_phpbb_' . $this->id . '_delta;index_phpbb_' . $this->id . '_main';
147   
148          if (!class_exists('SphinxClient'))
149          {
150              require($this->phpbb_root_path . 'includes/sphinxapi.' . $this->php_ext);
151          }
152   
153          // Initialize sphinx client
154          $this->sphinx = new \SphinxClient();
155   
156          $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));
157   
158          $error = false;
159      }
160   
161      /**
162      * Returns the name of this search backend to be displayed to administrators
163      *
164      * @return string Name
165      */
166      public function get_name()
167      {
168          return 'Sphinx Fulltext';
169      }
170   
171      /**
172       * Returns the search_query
173       *
174       * @return string search query
175       */
176      public function get_search_query()
177      {
178          return $this->search_query;
179      }
180   
181      /**
182       * Returns false as there is no word_len array
183       *
184       * @return false
185       */
186      public function get_word_length()
187      {
188          return false;
189      }
190   
191      /**
192       * Returns an empty array as there are no common_words
193       *
194       * @return array common words that are ignored by search backend
195       */
196      public function get_common_words()
197      {
198          return array();
199      }
200   
201      /**
202      * Checks permissions and paths, if everything is correct it generates the config file
203      *
204      * @return string|bool Language key of the error/incompatiblity encountered, or false if successful
205      */
206      public function init()
207      {
208          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')
209          {
210              return $this->user->lang['FULLTEXT_SPHINX_WRONG_DATABASE'];
211          }
212   
213          // Move delta to main index each hour
214          set_config('search_gc', 3600);
215   
216          return false;
217      }
218   
219      /**
220       * Generates content of sphinx.conf
221       *
222       * @return bool True if sphinx.conf content is correctly generated, false otherwise
223       */
224      protected function config_generate()
225      {
226          // Check if Database is supported by Sphinx
227          if ($this->db->get_sql_layer() =='mysql' || $this->db->get_sql_layer() == 'mysql4' || $this->db->get_sql_layer() == 'mysqli')
228          {
229              $this->dbtype = 'mysql';
230          }
231          else if ($this->db->get_sql_layer() == 'postgres')
232          {
233              $this->dbtype = 'pgsql';
234          }
235          else
236          {
237              $this->config_file_data = $this->user->lang('FULLTEXT_SPHINX_WRONG_DATABASE');
238              return false;
239          }
240   
241          // Check if directory paths have been filled
242          if (!$this->config['fulltext_sphinx_data_path'])
243          {
244              $this->config_file_data = $this->user->lang('FULLTEXT_SPHINX_NO_CONFIG_DATA');
245              return false;
246          }
247   
248          include($this->phpbb_root_path . 'config.' . $this->php_ext);
249   
250          /* Now that we're sure everything was entered correctly,
251          generate a config for the index. We use a config value
252          fulltext_sphinx_id for this, as it should be unique. */
253          $config_object = new \phpbb\search\sphinx\config($this->config_file_data);
254          $config_data = array(
255              'source source_phpbb_' . $this->id . '_main' => array(
256                  array('type',                        $this->dbtype . ' # mysql or pgsql'),
257                  // This config value sql_host needs to be changed incase sphinx and sql are on different servers
258                  array('sql_host',                    $dbhost . ' # SQL server host sphinx connects to'),
259                  array('sql_user',                    '[dbuser]'),
260                  array('sql_pass',                    '[dbpassword]'),
261                  array('sql_db',                        $dbname),
262                  array('sql_port',                    $dbport . ' # optional, default is 3306 for mysql and 5432 for pgsql'),
263                  array('sql_query_pre',                'SET NAMES \'utf8\''),
264                  array('sql_query_pre',                'UPDATE ' . SPHINX_TABLE . ' SET max_doc_id = (SELECT MAX(post_id) FROM ' . POSTS_TABLE . ') WHERE counter_id = 1'),
265                  array('sql_query_range',            'SELECT MIN(post_id), MAX(post_id) FROM ' . POSTS_TABLE . ''),
266                  array('sql_range_step',                '5000'),
267                  array('sql_query',                    'SELECT
268                          p.post_id AS id,
269                          p.forum_id,
270                          p.topic_id,
271                          p.poster_id,
272                          p.post_visibility,
273                          CASE WHEN p.post_id = t.topic_first_post_id THEN 1 ELSE 0 END as topic_first_post,
274                          p.post_time,
275                          p.post_subject,
276                          p.post_subject as title,
277                          p.post_text as data,
278                          t.topic_last_post_time,
279                          0 as deleted
280                      FROM ' . POSTS_TABLE . ' p, ' . TOPICS_TABLE . ' t
281                      WHERE
282                          p.topic_id = t.topic_id
283                          AND p.post_id >= $start AND p.post_id <= $end'),
284                  array('sql_query_post',                ''),
285                  array('sql_query_post_index',        'UPDATE ' . SPHINX_TABLE . ' SET max_doc_id = $maxid WHERE counter_id = 1'),
286                  array('sql_query_info',                'SELECT * FROM ' . POSTS_TABLE . ' WHERE post_id = $id'),
287                  array('sql_attr_uint',                'forum_id'),
288                  array('sql_attr_uint',                'topic_id'),
289                  array('sql_attr_uint',                'poster_id'),
290                  array('sql_attr_uint',                'post_visibility'),
291                  array('sql_attr_bool',                'topic_first_post'),
292                  array('sql_attr_bool',                'deleted'),
293                  array('sql_attr_timestamp',            'post_time'),
294                  array('sql_attr_timestamp',            'topic_last_post_time'),
295                  array('sql_attr_string',            'post_subject'),
296              ),
297              'source source_phpbb_' . $this->id . '_delta : source_phpbb_' . $this->id . '_main' => array(
298                  array('sql_query_pre',                ''),
299                  array('sql_query_range',            ''),
300                  array('sql_range_step',                ''),
301                  array('sql_query',                    'SELECT
302                          p.post_id AS id,
303                          p.forum_id,
304                          p.topic_id,
305                          p.poster_id,
306                          p.post_visibility,
307                          CASE WHEN p.post_id = t.topic_first_post_id THEN 1 ELSE 0 END as topic_first_post,
308                          p.post_time,
309                          p.post_subject,
310                          p.post_subject as title,
311                          p.post_text as data,
312                          t.topic_last_post_time,
313                          0 as deleted
314                      FROM ' . POSTS_TABLE . ' p, ' . TOPICS_TABLE . ' t
315                      WHERE
316                          p.topic_id = t.topic_id
317                          AND p.post_id >=  ( SELECT max_doc_id FROM ' . SPHINX_TABLE . ' WHERE counter_id=1 )'),
318              ),
319              'index index_phpbb_' . $this->id . '_main' => array(
320                  array('path',                        $this->config['fulltext_sphinx_data_path'] . 'index_phpbb_' . $this->id . '_main'),
321                  array('source',                        'source_phpbb_' . $this->id . '_main'),
322                  array('docinfo',                    'extern'),
323                  array('morphology',                    'none'),
324                  array('stopwords',                    ''),
325                  array('min_word_len',                '2'),
326                  array('charset_type',                'utf-8'),
327                  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'),
328                  array('min_prefix_len',                '0'),
329                  array('min_infix_len',                '0'),
330              ),
331              'index index_phpbb_' . $this->id . '_delta : index_phpbb_' . $this->id . '_main' => array(
332                  array('path',                        $this->config['fulltext_sphinx_data_path'] . 'index_phpbb_' . $this->id . '_delta'),
333                  array('source',                        'source_phpbb_' . $this->id . '_delta'),
334              ),
335              'indexer' => array(
336                  array('mem_limit',                    $this->config['fulltext_sphinx_indexer_mem_limit'] . 'M'),
337              ),
338              'searchd' => array(
339                  array('compat_sphinxql_magics'    ,    '0'),
340                  array('listen'    ,                    ($this->config['fulltext_sphinx_host'] ? $this->config['fulltext_sphinx_host'] : 'localhost') . ':' . ($this->config['fulltext_sphinx_port'] ? $this->config['fulltext_sphinx_port'] : '9312')),
341                  array('log',                        $this->config['fulltext_sphinx_data_path'] . 'log/searchd.log'),
342                  array('query_log',                    $this->config['fulltext_sphinx_data_path'] . 'log/sphinx-query.log'),
343                  array('read_timeout',                '5'),
344                  array('max_children',                '30'),
345                  array('pid_file',                    $this->config['fulltext_sphinx_data_path'] . 'searchd.pid'),
346                  array('max_matches',                (string) SPHINX_MAX_MATCHES),
347                  array('binlog_path',                $this->config['fulltext_sphinx_data_path']),
348              ),
349          );
350   
351          $non_unique = array('sql_query_pre' => true, 'sql_attr_uint' => true, 'sql_attr_timestamp' => true, 'sql_attr_str2ordinal' => true, 'sql_attr_bool' => true);
352          $delete = array('sql_group_column' => true, 'sql_date_column' => true, 'sql_str2ordinal_column' => true);
353          foreach ($config_data as $section_name => $section_data)
354          {
355              $section = $config_object->get_section_by_name($section_name);
356              if (!$section)
357              {
358                  $section = $config_object->add_section($section_name);
359              }
360   
361              foreach ($delete as $key => $void)
362              {
363                  $section->delete_variables_by_name($key);
364              }
365   
366              foreach ($non_unique as $key => $void)
367              {
368                  $section->delete_variables_by_name($key);
369              }
370   
371              foreach ($section_data as $entry)
372              {
373                  $key = $entry[0];
374                  $value = $entry[1];
375   
376                  if (!isset($non_unique[$key]))
377                  {
378                      $variable = $section->get_variable_by_name($key);
379                      if (!$variable)
380                      {
381                          $variable = $section->create_variable($key, $value);
382                      }
383                      else
384                      {
385                          $variable->set_value($value);
386                      }
387                  }
388                  else
389                  {
390                      $variable = $section->create_variable($key, $value);
391                  }
392              }
393          }
394          $this->config_file_data = $config_object->get_data();
395   
396          return true;
397      }
398   
399      /**
400      * Splits keywords entered by a user into an array of words stored in $this->split_words
401      * Stores the tidied search query in $this->search_query
402      *
403      * @param string $keywords Contains the keyword as entered by the user
404      * @param string $terms is either 'all' or 'any'
405      * @return false if no valid keywords were found and otherwise true
406      */
407      public function split_keywords(&$keywords, $terms)
408      {
409          if ($terms == 'all')
410          {
411              $match        = array('#\sand\s#i', '#\sor\s#i', '#\snot\s#i', '#\+#', '#-#', '#\|#', '#@#');
412              $replace    = array(' & ', ' | ', '  - ', ' +', ' -', ' |', '');
413   
414              $replacements = 0;
415              $keywords = preg_replace($match, $replace, $keywords);
416              $this->sphinx->SetMatchMode(SPH_MATCH_EXTENDED);
417          }
418          else
419          {
420              $this->sphinx->SetMatchMode(SPH_MATCH_ANY);
421          }
422   
423          // Keep quotes and new lines
424          $keywords = str_replace(array('&quot;', "\n"), array('"', ' '), trim($keywords));
425   
426          if (strlen($keywords) > 0)
427          {
428              $this->search_query = str_replace('"', '&quot;', $keywords);
429              return true;
430          }
431   
432          return false;
433      }
434   
435      /**
436      * Performs a search on keywords depending on display specific params. You have to run split_keywords() first
437      *
438      * @param    string        $type                contains either posts or topics depending on what should be searched for
439      * @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)
440      * @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)
441      * @param    array        $sort_by_sql        contains SQL code for the ORDER BY part of a query
442      * @param    string        $sort_key            is the key of $sort_by_sql for the selected sorting
443      * @param    string        $sort_dir            is either a or d representing ASC and DESC
444      * @param    string        $sort_days            specifies the maximum amount of days a post may be old
445      * @param    array        $ex_fid_ary            specifies an array of forum ids which should not be searched
446      * @param    string        $post_visibility    specifies which types of posts the user can view in which forums
447      * @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
448      * @param    array        $author_ary            an array of author ids if the author should be ignored during the search the array is empty
449      * @param    string        $author_name        specifies the author match, when ANONYMOUS is also a search-match
450      * @param    array        &$id_ary            passed by reference, to be filled with ids for the page specified by $start and $per_page, should be ordered
451      * @param    int            $start                indicates the first index of the page
452      * @param    int            $per_page            number of ids each page is supposed to contain
453      * @return    boolean|int                        total number of results
454      */
455      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)
456      {
457          // No keywords? No posts.
458          if (!strlen($this->search_query) && !sizeof($author_ary))
459          {
460              return false;
461          }
462   
463          $id_ary = array();
464   
465          $join_topic = ($type != 'posts');
466   
467          // Sorting
468   
469          if ($type == 'topics')
470          {
471              switch ($sort_key)
472              {
473                  case 'a':
474                      $this->sphinx->SetGroupBy('topic_id', SPH_GROUPBY_ATTR, 'poster_id ' . (($sort_dir == 'a') ? 'ASC' : 'DESC'));
475                  break;
476   
477                  case 'f':
478                      $this->sphinx->SetGroupBy('topic_id', SPH_GROUPBY_ATTR, 'forum_id ' . (($sort_dir == 'a') ? 'ASC' : 'DESC'));
479                  break;
480   
481                  case 'i':
482   
483                  case 's':
484                      $this->sphinx->SetGroupBy('topic_id', SPH_GROUPBY_ATTR, 'post_subject ' . (($sort_dir == 'a') ? 'ASC' : 'DESC'));
485                  break;
486   
487                  case 't':
488   
489                  default:
490                      $this->sphinx->SetGroupBy('topic_id', SPH_GROUPBY_ATTR, 'topic_last_post_time ' . (($sort_dir == 'a') ? 'ASC' : 'DESC'));
491                  break;
492              }
493          }
494          else
495          {
496              switch ($sort_key)
497              {
498                  case 'a':
499                      $this->sphinx->SetSortMode(($sort_dir == 'a') ? SPH_SORT_ATTR_ASC : SPH_SORT_ATTR_DESC, 'poster_id');
500                  break;
501   
502                  case 'f':
503                      $this->sphinx->SetSortMode(($sort_dir == 'a') ? SPH_SORT_ATTR_ASC : SPH_SORT_ATTR_DESC, 'forum_id');
504                  break;
505   
506                  case 'i':
507   
508                  case 's':
509                      $this->sphinx->SetSortMode(($sort_dir == 'a') ? SPH_SORT_ATTR_ASC : SPH_SORT_ATTR_DESC, 'post_subject');
510                  break;
511   
512                  case 't':
513   
514                  default:
515                      $this->sphinx->SetSortMode(($sort_dir == 'a') ? SPH_SORT_ATTR_ASC : SPH_SORT_ATTR_DESC, 'post_time');
516                  break;
517              }
518          }
519   
520          // Most narrow filters first
521          if ($topic_id)
522          {
523              $this->sphinx->SetFilter('topic_id', array($topic_id));
524          }
525   
526          $search_query_prefix = '';
527   
528          switch ($fields)
529          {
530              case 'titleonly':
531                  // Only search the title
532                  if ($terms == 'all')
533                  {
534                      $search_query_prefix = '@title ';
535                  }
536                  // Weight for the title
537                  $this->sphinx->SetFieldWeights(array("title" => 5, "data" => 1));
538                  // 1 is first_post, 0 is not first post
539                  $this->sphinx->SetFilter('topic_first_post', array(1));
540              break;
541   
542              case 'msgonly':
543                  // Only search the body
544                  if ($terms == 'all')
545                  {
546                      $search_query_prefix = '@data ';
547                  }
548                  // Weight for the body
549                  $this->sphinx->SetFieldWeights(array("title" => 1, "data" => 5));
550              break;
551   
552              case 'firstpost':
553                  // More relative weight for the title, also search the body
554                  $this->sphinx->SetFieldWeights(array("title" => 5, "data" => 1));
555                  // 1 is first_post, 0 is not first post
556                  $this->sphinx->SetFilter('topic_first_post', array(1));
557              break;
558   
559              default:
560                  // More relative weight for the title, also search the body
561                  $this->sphinx->SetFieldWeights(array("title" => 5, "data" => 1));
562              break;
563          }
564   
565          if (sizeof($author_ary))
566          {
567              $this->sphinx->SetFilter('poster_id', $author_ary);
568          }
569   
570          // As this is not simply possible at the moment, we limit the result to approved posts.
571          // This will make it impossible for moderators to search unapproved and softdeleted posts,
572          // but at least it will also cause the same for normal users.
573          $this->sphinx->SetFilter('post_visibility', array(ITEM_APPROVED));
574   
575          if (sizeof($ex_fid_ary))
576          {
577              // All forums that a user is allowed to access
578              $fid_ary = array_unique(array_intersect(array_keys($this->auth->acl_getf('f_read', true)), array_keys($this->auth->acl_getf('f_search', true))));
579              // All forums that the user wants to and can search in
580              $search_forums = array_diff($fid_ary, $ex_fid_ary);
581   
582              if (sizeof($search_forums))
583              {
584                  $this->sphinx->SetFilter('forum_id', $search_forums);
585              }
586          }
587   
588          $this->sphinx->SetFilter('deleted', array(0));
589   
590          $this->sphinx->SetLimits($start, (int) $per_page, SPHINX_MAX_MATCHES);
591          $result = $this->sphinx->Query($search_query_prefix . str_replace('&quot;', '"', $this->search_query), $this->indexes);
592   
593          // Could be connection to localhost:9312 failed (errno=111,
594          // msg=Connection refused) during rotate, retry if so
595          $retries = SPHINX_CONNECT_RETRIES;
596          while (!$result && (strpos($this->sphinx->GetLastError(), "errno=111,") !== false) && $retries--)
597          {
598              usleep(SPHINX_CONNECT_WAIT_TIME);
599              $result = $this->sphinx->Query($search_query_prefix . str_replace('&quot;', '"', $this->search_query), $this->indexes);
600          }
601   
602          if ($this->sphinx->GetLastError())
603          {
604              add_log('critical', 'LOG_SPHINX_ERROR', $this->sphinx->GetLastError());
605              if ($this->auth->acl_get('a_'))
606              {
607                  trigger_error($this->user->lang('SPHINX_SEARCH_FAILED', $this->sphinx->GetLastError()));
608              }
609              else
610              {
611                  trigger_error($this->user->lang('SPHINX_SEARCH_FAILED_LOG'));
612              }
613          }
614   
615          $result_count = $result['total_found'];
616   
617          if ($result_count && $start >= $result_count)
618          {
619              $start = floor(($result_count - 1) / $per_page) * $per_page;
620   
621              $this->sphinx->SetLimits((int) $start, (int) $per_page, SPHINX_MAX_MATCHES);
622              $result = $this->sphinx->Query($search_query_prefix . str_replace('&quot;', '"', $this->search_query), $this->indexes);
623   
624              // Could be connection to localhost:9312 failed (errno=111,
625              // msg=Connection refused) during rotate, retry if so
626              $retries = SPHINX_CONNECT_RETRIES;
627              while (!$result && (strpos($this->sphinx->GetLastError(), "errno=111,") !== false) && $retries--)
628              {
629                  usleep(SPHINX_CONNECT_WAIT_TIME);
630                  $result = $this->sphinx->Query($search_query_prefix . str_replace('&quot;', '"', $this->search_query), $this->indexes);
631              }
632          }
633   
634          $id_ary = array();
635          if (isset($result['matches']))
636          {
637              if ($type == 'posts')
638              {
639                  $id_ary = array_keys($result['matches']);
640              }
641              else
642              {
643                  foreach ($result['matches'] as $key => $value)
644                  {
645                      $id_ary[] = $value['attrs']['topic_id'];
646                  }
647              }
648          }
649          else
650          {
651              return false;
652          }
653   
654          $id_ary = array_slice($id_ary, 0, (int) $per_page);
655   
656          return $result_count;
657      }
658   
659      /**
660      * Performs a search on an author's posts without caring about message contents. Depends on display specific params
661      *
662      * @param    string        $type                contains either posts or topics depending on what should be searched for
663      * @param    boolean        $firstpost_only        if true, only topic starting posts will be considered
664      * @param    array        $sort_by_sql        contains SQL code for the ORDER BY part of a query
665      * @param    string        $sort_key            is the key of $sort_by_sql for the selected sorting
666      * @param    string        $sort_dir            is either a or d representing ASC and DESC
667      * @param    string        $sort_days            specifies the maximum amount of days a post may be old
668      * @param    array        $ex_fid_ary            specifies an array of forum ids which should not be searched
669      * @param    string        $post_visibility    specifies which types of posts the user can view in which forums
670      * @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
671      * @param    array        $author_ary            an array of author ids
672      * @param    string        $author_name        specifies the author match, when ANONYMOUS is also a search-match
673      * @param    array        &$id_ary            passed by reference, to be filled with ids for the page specified by $start and $per_page, should be ordered
674      * @param    int            $start                indicates the first index of the page
675      * @param    int            $per_page            number of ids each page is supposed to contain
676      * @return    boolean|int                        total number of results
677      */
678      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)
679      {
680          $this->search_query = '';
681   
682          $this->sphinx->SetMatchMode(SPH_MATCH_FULLSCAN);
683          $fields = ($firstpost_only) ? 'firstpost' : 'all';
684          $terms = 'all';
685          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);
686      }
687   
688      /**
689       * Updates wordlist and wordmatch tables when a message is posted or changed
690       *
691       * @param    string    $mode    Contains the post mode: edit, post, reply, quote
692       * @param    int    $post_id    The id of the post which is modified/created
693       * @param    string    &$message    New or updated post content
694       * @param    string    &$subject    New or updated post subject
695       * @param    int    $poster_id    Post author's user id
696       * @param    int    $forum_id    The id of the forum in which the post is located
697       */
698      public function index($mode, $post_id, &$message, &$subject, $poster_id, $forum_id)
699      {
700          if ($mode == 'edit')
701          {
702              $this->sphinx->UpdateAttributes($this->indexes, array('forum_id', 'poster_id'), array((int) $post_id => array((int) $forum_id, (int) $poster_id)));
703          }
704          else if ($mode != 'post' && $post_id)
705          {
706              // Update topic_last_post_time for full topic
707              $sql_array = array(
708                  'SELECT'    => 'p1.post_id',
709                  'FROM'        => array(
710                      POSTS_TABLE    => 'p1',
711                  ),
712                  'LEFT_JOIN'    => array(array(
713                      'FROM'    => array(
714                          POSTS_TABLE    => 'p2'
715                      ),
716                      'ON'    => 'p1.topic_id = p2.topic_id',
717                  )),
718                  'WHERE' => 'p2.post_id = ' . ((int) $post_id),
719              );
720   
721              $sql = $this->db->sql_build_query('SELECT', $sql_array);
722              $result = $this->db->sql_query($sql);
723   
724              $post_updates = array();
725              $post_time = time();
726              while ($row = $this->db->sql_fetchrow($result))
727              {
728                  $post_updates[(int) $row['post_id']] = array($post_time);
729              }
730              $this->db->sql_freeresult($result);
731   
732              if (sizeof($post_updates))
733              {
734                  $this->sphinx->UpdateAttributes($this->indexes, array('topic_last_post_time'), $post_updates);
735              }
736          }
737      }
738   
739      /**
740      * Delete a post from the index after it was deleted
741      */
742      public function index_remove($post_ids, $author_ids, $forum_ids)
743      {
744          $values = array();
745          foreach ($post_ids as $post_id)
746          {
747              $values[$post_id] = array(1);
748          }
749   
750          $this->sphinx->UpdateAttributes($this->indexes, array('deleted'), $values);
751      }
752   
753      /**
754      * Nothing needs to be destroyed
755      */
756      public function tidy($create = false)
757      {
758          set_config('search_last_gc', time(), true);
759      }
760   
761      /**
762      * Create sphinx table
763      *
764      * @return string|bool error string is returned incase of errors otherwise false
765      */
766      public function create_index($acp_module, $u_action)
767      {
768          if (!$this->index_created())
769          {
770              $table_data = array(
771                  'COLUMNS'    => array(
772                      'counter_id'    => array('UINT', 0),
773                      'max_doc_id'    => array('UINT', 0),
774                  ),
775                  'PRIMARY_KEY'    => 'counter_id',
776              );
777              $this->db_tools->sql_create_table(SPHINX_TABLE, $table_data);
778   
779              $sql = 'TRUNCATE TABLE ' . SPHINX_TABLE;
780              $this->db->sql_query($sql);
781   
782              $data = array(
783                  'counter_id'    => '1',
784                  'max_doc_id'    => '0',
785              );
786              $sql = 'INSERT INTO ' . SPHINX_TABLE . ' ' . $this->db->sql_build_array('INSERT', $data);
787              $this->db->sql_query($sql);
788          }
789   
790          return false;
791      }
792   
793      /**
794      * Drop sphinx table
795      *
796      * @return string|bool error string is returned incase of errors otherwise false
797      */
798      public function delete_index($acp_module, $u_action)
799      {
800          if (!$this->index_created())
801          {
802              return false;
803          }
804   
805          $this->db_tools->sql_table_drop(SPHINX_TABLE);
806   
807          return false;
808      }
809   
810      /**
811      * Returns true if the sphinx table was created
812      *
813      * @return bool true if sphinx table was created
814      */
815      public function index_created($allow_new_files = true)
816      {
817          $created = false;
818   
819          if ($this->db_tools->sql_table_exists(SPHINX_TABLE))
820          {
821              $created = true;
822          }
823   
824          return $created;
825      }
826   
827      /**
828      * Returns an associative array containing information about the indexes
829      *
830      * @return string|bool Language string of error false otherwise
831      */
832      public function index_stats()
833      {
834          if (empty($this->stats))
835          {
836              $this->get_stats();
837          }
838   
839          return array(
840              $this->user->lang['FULLTEXT_SPHINX_MAIN_POSTS']            => ($this->index_created()) ? $this->stats['main_posts'] : 0,
841              $this->user->lang['FULLTEXT_SPHINX_DELTA_POSTS']            => ($this->index_created()) ? $this->stats['total_posts'] - $this->stats['main_posts'] : 0,
842              $this->user->lang['FULLTEXT_MYSQL_TOTAL_POSTS']            => ($this->index_created()) ? $this->stats['total_posts'] : 0,
843          );
844      }
845   
846      /**
847      * Collects stats that can be displayed on the index maintenance page
848      */
849      protected function get_stats()
850      {
851          if ($this->index_created())
852          {
853              $sql = 'SELECT COUNT(post_id) as total_posts
854                  FROM ' . POSTS_TABLE;
855              $result = $this->db->sql_query($sql);
856              $this->stats['total_posts'] = (int) $this->db->sql_fetchfield('total_posts');
857              $this->db->sql_freeresult($result);
858   
859              $sql = 'SELECT COUNT(p.post_id) as main_posts
860                  FROM ' . POSTS_TABLE . ' p, ' . SPHINX_TABLE . ' m
861                  WHERE p.post_id <= m.max_doc_id
862                      AND m.counter_id = 1';
863              $result = $this->db->sql_query($sql);
864              $this->stats['main_posts'] = (int) $this->db->sql_fetchfield('main_posts');
865              $this->db->sql_freeresult($result);
866          }
867      }
868   
869      /**
870      * Returns a list of options for the ACP to display
871      *
872      * @return associative array containing template and config variables
873      */
874      public function acp()
875      {
876          $config_vars = array(
877              'fulltext_sphinx_data_path' => 'string',
878              'fulltext_sphinx_host' => 'string',
879              'fulltext_sphinx_port' => 'string',
880              'fulltext_sphinx_indexer_mem_limit' => 'int',
881          );
882   
883          $tpl = '
884          <span class="error">' . $this->user->lang['FULLTEXT_SPHINX_CONFIGURE']. '</span>
885          <dl>
886              <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>
887              <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>
888          </dl>
889          <dl>
890              <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>
891              <dd><input id="fulltext_sphinx_host" type="text" size="40" maxlength="255" name="config[fulltext_sphinx_host]" value="' . $this->config['fulltext_sphinx_host'] . '" /></dd>
892          </dl>
893          <dl>
894              <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>
895              <dd><input id="fulltext_sphinx_port" type="number" size="4" maxlength="10" name="config[fulltext_sphinx_port]" value="' . $this->config['fulltext_sphinx_port'] . '" /></dd>
896          </dl>
897          <dl>
898              <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>
899              <dd><input id="fulltext_sphinx_indexer_mem_limit" type="number" size="4" maxlength="10" name="config[fulltext_sphinx_indexer_mem_limit]" value="' . $this->config['fulltext_sphinx_indexer_mem_limit'] . '" /> ' . $this->user->lang['MIB'] . '</dd>
900          </dl>
901          <dl>
902              <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>
903              <dd>' . (($this->config_generate()) ? '<textarea readonly="readonly" rows="6" id="sphinx_config_data">' . htmlspecialchars($this->config_file_data) . '</textarea>' : $this->config_file_data) . '</dd>
904          <dl>
905          ';
906   
907          // These are fields required in the config table
908          return array(
909              'tpl'        => $tpl,
910              'config'    => $config_vars
911          );
912      }
913  }
914