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