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_mysql.php

Zuletzt modifiziert: 09.10.2024, 12:52 - Dateigröße: 26.96 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  /**
 017  * Fulltext search for MySQL
 018  */
 019  class fulltext_mysql extends \phpbb\search\base
 020  {
 021      /**
 022       * Associative array holding index stats
 023       * @var array
 024       */
 025      protected $stats = array();
 026   
 027      /**
 028       * Holds the words entered by user, obtained by splitting the entered query on whitespace
 029       * @var array
 030       */
 031      protected $split_words = array();
 032   
 033      /**
 034       * Config object
 035       * @var \phpbb\config\config
 036       */
 037      protected $config;
 038   
 039      /**
 040       * Database connection
 041       * @var \phpbb\db\driver\driver_interface
 042       */
 043      protected $db;
 044   
 045      /**
 046       * User object
 047       * @var \phpbb\user
 048       */
 049      protected $user;
 050   
 051      /**
 052       * Associative array stores the min and max word length to be searched
 053       * @var array
 054       */
 055      protected $word_length = array();
 056   
 057      /**
 058       * Contains tidied search query.
 059       * Operators are prefixed in search query and common words excluded
 060       * @var string
 061       */
 062      protected $search_query;
 063   
 064      /**
 065       * Contains common words.
 066       * Common words are words with length less/more than min/max length
 067       * @var array
 068       */
 069      protected $common_words = array();
 070   
 071      /**
 072       * Constructor
 073       * Creates a new \phpbb\search\fulltext_mysql, which is used as a search backend
 074       *
 075       * @param string|bool $error Any error that occurs is passed on through this reference variable otherwise false
 076       * @param string $phpbb_root_path Relative path to phpBB root
 077       * @param string $phpEx PHP file extension
 078       * @param \phpbb\auth\auth $auth Auth object
 079       * @param \phpbb\config\config $config Config object
 080       * @param \phpbb\db\driver\driver_interface Database object
 081       * @param \phpbb\user $user User object
 082       */
 083      public function __construct(&$error, $phpbb_root_path, $phpEx, $auth, $config, $db, $user)
 084      {
 085          $this->config = $config;
 086          $this->db = $db;
 087          $this->user = $user;
 088   
 089          $this->word_length = array('min' => $this->config['fulltext_mysql_min_word_len'], 'max' => $this->config['fulltext_mysql_max_word_len']);
 090   
 091          /**
 092           * Load the UTF tools
 093           */
 094          if (!function_exists('utf8_strlen'))
 095          {
 096              include($phpbb_root_path . 'includes/utf/utf_tools.' . $phpEx);
 097          }
 098   
 099          $error = false;
 100      }
 101   
 102      /**
 103      * Returns the name of this search backend to be displayed to administrators
 104      *
 105      * @return string Name
 106      */
 107      public function get_name()
 108      {
 109          return 'MySQL Fulltext';
 110      }
 111   
 112      /**
 113       * Returns the search_query
 114       *
 115       * @return string search query
 116       */
 117      public function get_search_query()
 118      {
 119          return $this->search_query;
 120      }
 121   
 122      /**
 123       * Returns the common_words array
 124       *
 125       * @return array common words that are ignored by search backend
 126       */
 127      public function get_common_words()
 128      {
 129          return $this->common_words;
 130      }
 131   
 132      /**
 133       * Returns the word_length array
 134       *
 135       * @return array min and max word length for searching
 136       */
 137      public function get_word_length()
 138      {
 139          return $this->word_length;
 140      }
 141   
 142      /**
 143      * Checks for correct MySQL version and stores min/max word length in the config
 144      *
 145      * @return string|bool Language key of the error/incompatiblity occurred
 146      */
 147      public function init()
 148      {
 149          if ($this->db->get_sql_layer() != 'mysql4' && $this->db->get_sql_layer() != 'mysqli')
 150          {
 151              return $this->user->lang['FULLTEXT_MYSQL_INCOMPATIBLE_DATABASE'];
 152          }
 153   
 154          $result = $this->db->sql_query('SHOW TABLE STATUS LIKE \'' . POSTS_TABLE . '\'');
 155          $info = $this->db->sql_fetchrow($result);
 156          $this->db->sql_freeresult($result);
 157   
 158          $engine = '';
 159          if (isset($info['Engine']))
 160          {
 161              $engine = $info['Engine'];
 162          }
 163          else if (isset($info['Type']))
 164          {
 165              $engine = $info['Type'];
 166          }
 167   
 168          $fulltext_supported =
 169              $engine === 'MyISAM' ||
 170              // FULLTEXT is supported on InnoDB since MySQL 5.6.4 according to
 171              // http://dev.mysql.com/doc/refman/5.6/en/innodb-storage-engine.html
 172              $engine === 'InnoDB' &&
 173              phpbb_version_compare($this->db->sql_server_info(true), '5.6.4', '>=');
 174   
 175          if (!$fulltext_supported)
 176          {
 177              return $this->user->lang['FULLTEXT_MYSQL_NOT_SUPPORTED'];
 178          }
 179   
 180          $sql = 'SHOW VARIABLES
 181              LIKE \'ft\_%\'';
 182          $result = $this->db->sql_query($sql);
 183   
 184          $mysql_info = array();
 185          while ($row = $this->db->sql_fetchrow($result))
 186          {
 187              $mysql_info[$row['Variable_name']] = $row['Value'];
 188          }
 189          $this->db->sql_freeresult($result);
 190   
 191          set_config('fulltext_mysql_max_word_len', $mysql_info['ft_max_word_len']);
 192          set_config('fulltext_mysql_min_word_len', $mysql_info['ft_min_word_len']);
 193   
 194          return false;
 195      }
 196   
 197      /**
 198      * Splits keywords entered by a user into an array of words stored in $this->split_words
 199      * Stores the tidied search query in $this->search_query
 200      *
 201      * @param string &$keywords Contains the keyword as entered by the user
 202      * @param string $terms is either 'all' or 'any'
 203      * @return bool false if no valid keywords were found and otherwise true
 204      */
 205      public function split_keywords(&$keywords, $terms)
 206      {
 207          if ($terms == 'all')
 208          {
 209              $match        = array('#\sand\s#iu', '#\sor\s#iu', '#\snot\s#iu', '#(^|\s)\+#', '#(^|\s)-#', '#(^|\s)\|#');
 210              $replace    = array(' +', ' |', ' -', ' +', ' -', ' |');
 211   
 212              $keywords = preg_replace($match, $replace, $keywords);
 213          }
 214   
 215          // Filter out as above
 216          $split_keywords = preg_replace("#[\n\r\t]+#", ' ', trim(htmlspecialchars_decode($keywords)));
 217   
 218          // Split words
 219          $split_keywords = preg_replace('#([^\p{L}\p{N}\'*"()])#u', '$1$1', str_replace('\'\'', '\' \'', trim($split_keywords)));
 220          $matches = array();
 221          preg_match_all('#(?:[^\p{L}\p{N}*"()]|^)([+\-|]?(?:[\p{L}\p{N}*"()]+\'?)*[\p{L}\p{N}*"()])(?:[^\p{L}\p{N}*"()]|$)#u', $split_keywords, $matches);
 222          $this->split_words = $matches[1];
 223   
 224          // We limit the number of allowed keywords to minimize load on the database
 225          if ($this->config['max_num_search_keywords'] && sizeof($this->split_words) > $this->config['max_num_search_keywords'])
 226          {
 227              trigger_error($this->user->lang('MAX_NUM_SEARCH_KEYWORDS_REFINE', (int) $this->config['max_num_search_keywords'], sizeof($this->split_words)));
 228          }
 229   
 230          // to allow phrase search, we need to concatenate quoted words
 231          $tmp_split_words = array();
 232          $phrase = '';
 233          foreach ($this->split_words as $word)
 234          {
 235              if ($phrase)
 236              {
 237                  $phrase .= ' ' . $word;
 238                  if (strpos($word, '"') !== false && substr_count($word, '"') % 2 == 1)
 239                  {
 240                      $tmp_split_words[] = $phrase;
 241                      $phrase = '';
 242                  }
 243              }
 244              else if (strpos($word, '"') !== false && substr_count($word, '"') % 2 == 1)
 245              {
 246                  $phrase = $word;
 247              }
 248              else
 249              {
 250                  $tmp_split_words[] = $word;
 251              }
 252          }
 253          if ($phrase)
 254          {
 255              $tmp_split_words[] = $phrase;
 256          }
 257   
 258          $this->split_words = $tmp_split_words;
 259   
 260          unset($tmp_split_words);
 261          unset($phrase);
 262   
 263          foreach ($this->split_words as $i => $word)
 264          {
 265              $clean_word = preg_replace('#^[+\-|"]#', '', $word);
 266   
 267              // check word length
 268              $clean_len = utf8_strlen(str_replace('*', '', $clean_word));
 269              if (($clean_len < $this->config['fulltext_mysql_min_word_len']) || ($clean_len > $this->config['fulltext_mysql_max_word_len']))
 270              {
 271                  $this->common_words[] = $word;
 272                  unset($this->split_words[$i]);
 273              }
 274          }
 275   
 276          if ($terms == 'any')
 277          {
 278              $this->search_query = '';
 279              foreach ($this->split_words as $word)
 280              {
 281                  if ((strpos($word, '+') === 0) || (strpos($word, '-') === 0) || (strpos($word, '|') === 0))
 282                  {
 283                      $word = substr($word, 1);
 284                  }
 285                  $this->search_query .= $word . ' ';
 286              }
 287          }
 288          else
 289          {
 290              $this->search_query = '';
 291              foreach ($this->split_words as $word)
 292              {
 293                  if ((strpos($word, '+') === 0) || (strpos($word, '-') === 0))
 294                  {
 295                      $this->search_query .= $word . ' ';
 296                  }
 297                  else if (strpos($word, '|') === 0)
 298                  {
 299                      $this->search_query .= substr($word, 1) . ' ';
 300                  }
 301                  else
 302                  {
 303                      $this->search_query .= '+' . $word . ' ';
 304                  }
 305              }
 306          }
 307   
 308          $this->search_query = utf8_htmlspecialchars($this->search_query);
 309   
 310          if ($this->search_query)
 311          {
 312              $this->split_words = array_values($this->split_words);
 313              sort($this->split_words);
 314              return true;
 315          }
 316          return false;
 317      }
 318   
 319      /**
 320      * Turns text into an array of words
 321      * @param string $text contains post text/subject
 322      */
 323      public function split_message($text)
 324      {
 325          // Split words
 326          $text = preg_replace('#([^\p{L}\p{N}\'*])#u', '$1$1', str_replace('\'\'', '\' \'', trim($text)));
 327          $matches = array();
 328          preg_match_all('#(?:[^\p{L}\p{N}*]|^)([+\-|]?(?:[\p{L}\p{N}*]+\'?)*[\p{L}\p{N}*])(?:[^\p{L}\p{N}*]|$)#u', $text, $matches);
 329          $text = $matches[1];
 330   
 331          // remove too short or too long words
 332          $text = array_values($text);
 333          for ($i = 0, $n = sizeof($text); $i < $n; $i++)
 334          {
 335              $text[$i] = trim($text[$i]);
 336              if (utf8_strlen($text[$i]) < $this->config['fulltext_mysql_min_word_len'] || utf8_strlen($text[$i]) > $this->config['fulltext_mysql_max_word_len'])
 337              {
 338                  unset($text[$i]);
 339              }
 340          }
 341   
 342          return array_values($text);
 343      }
 344   
 345      /**
 346      * Performs a search on keywords depending on display specific params. You have to run split_keywords() first
 347      *
 348      * @param    string        $type                contains either posts or topics depending on what should be searched for
 349      * @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)
 350      * @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)
 351      * @param    array        $sort_by_sql        contains SQL code for the ORDER BY part of a query
 352      * @param    string        $sort_key            is the key of $sort_by_sql for the selected sorting
 353      * @param    string        $sort_dir            is either a or d representing ASC and DESC
 354      * @param    string        $sort_days            specifies the maximum amount of days a post may be old
 355      * @param    array        $ex_fid_ary            specifies an array of forum ids which should not be searched
 356      * @param    string        $post_visibility    specifies which types of posts the user can view in which forums
 357      * @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
 358      * @param    array        $author_ary            an array of author ids if the author should be ignored during the search the array is empty
 359      * @param    string        $author_name        specifies the author match, when ANONYMOUS is also a search-match
 360      * @param    array        &$id_ary            passed by reference, to be filled with ids for the page specified by $start and $per_page, should be ordered
 361      * @param    int            $start                indicates the first index of the page
 362      * @param    int            $per_page            number of ids each page is supposed to contain
 363      * @return    boolean|int                        total number of results
 364      */
 365      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)
 366      {
 367          // No keywords? No posts
 368          if (!$this->search_query)
 369          {
 370              return false;
 371          }
 372   
 373          // generate a search_key from all the options to identify the results
 374          $search_key = md5(implode('#', array(
 375              implode(', ', $this->split_words),
 376              $type,
 377              $fields,
 378              $terms,
 379              $sort_days,
 380              $sort_key,
 381              $topic_id,
 382              implode(',', $ex_fid_ary),
 383              $post_visibility,
 384              implode(',', $author_ary)
 385          )));
 386   
 387          if ($start < 0)
 388          {
 389              $start = 0;
 390          }
 391   
 392          // try reading the results from cache
 393          $result_count = 0;
 394          if ($this->obtain_ids($search_key, $result_count, $id_ary, $start, $per_page, $sort_dir) == SEARCH_RESULT_IN_CACHE)
 395          {
 396              return $result_count;
 397          }
 398   
 399          $id_ary = array();
 400   
 401          $join_topic = ($type == 'posts') ? false : true;
 402   
 403          // Build sql strings for sorting
 404          $sql_sort = $sort_by_sql[$sort_key] . (($sort_dir == 'a') ? ' ASC' : ' DESC');
 405          $sql_sort_table = $sql_sort_join = '';
 406   
 407          switch ($sql_sort[0])
 408          {
 409              case 'u':
 410                  $sql_sort_table    = USERS_TABLE . ' u, ';
 411                  $sql_sort_join    = ($type == 'posts') ? ' AND u.user_id = p.poster_id ' : ' AND u.user_id = t.topic_poster ';
 412              break;
 413   
 414              case 't':
 415                  $join_topic = true;
 416              break;
 417   
 418              case 'f':
 419                  $sql_sort_table    = FORUMS_TABLE . ' f, ';
 420                  $sql_sort_join    = ' AND f.forum_id = p.forum_id ';
 421              break;
 422          }
 423   
 424          // Build some display specific sql strings
 425          switch ($fields)
 426          {
 427              case 'titleonly':
 428                  $sql_match = 'p.post_subject';
 429                  $sql_match_where = ' AND p.post_id = t.topic_first_post_id';
 430                  $join_topic = true;
 431              break;
 432   
 433              case 'msgonly':
 434                  $sql_match = 'p.post_text';
 435                  $sql_match_where = '';
 436              break;
 437   
 438              case 'firstpost':
 439                  $sql_match = 'p.post_subject, p.post_text';
 440                  $sql_match_where = ' AND p.post_id = t.topic_first_post_id';
 441                  $join_topic = true;
 442              break;
 443   
 444              default:
 445                  $sql_match = 'p.post_subject, p.post_text';
 446                  $sql_match_where = '';
 447              break;
 448          }
 449   
 450          $sql_select            = (!$result_count) ? 'SQL_CALC_FOUND_ROWS ' : '';
 451          $sql_select            = ($type == 'posts') ? $sql_select . 'p.post_id' : 'DISTINCT ' . $sql_select . 't.topic_id';
 452          $sql_from            = ($join_topic) ? TOPICS_TABLE . ' t, ' : '';
 453          $field                = ($type == 'posts') ? 'post_id' : 'topic_id';
 454          if (sizeof($author_ary) && $author_name)
 455          {
 456              // first one matches post of registered users, second one guests and deleted users
 457              $sql_author = ' AND (' . $this->db->sql_in_set('p.poster_id', array_diff($author_ary, array(ANONYMOUS)), false, true) . ' OR p.post_username ' . $author_name . ')';
 458          }
 459          else if (sizeof($author_ary))
 460          {
 461              $sql_author = ' AND ' . $this->db->sql_in_set('p.poster_id', $author_ary);
 462          }
 463          else
 464          {
 465              $sql_author = '';
 466          }
 467   
 468          $sql_where_options = $sql_sort_join;
 469          $sql_where_options .= ($topic_id) ? ' AND p.topic_id = ' . $topic_id : '';
 470          $sql_where_options .= ($join_topic) ? ' AND t.topic_id = p.topic_id' : '';
 471          $sql_where_options .= (sizeof($ex_fid_ary)) ? ' AND ' . $this->db->sql_in_set('p.forum_id', $ex_fid_ary, true) : '';
 472          $sql_where_options .= ' AND ' . $post_visibility;
 473          $sql_where_options .= $sql_author;
 474          $sql_where_options .= ($sort_days) ? ' AND p.post_time >= ' . (time() - ($sort_days * 86400)) : '';
 475          $sql_where_options .= $sql_match_where;
 476   
 477          $sql = "SELECT $sql_select
 478              FROM $sql_from$sql_sort_table" . POSTS_TABLE . " p
 479              WHERE MATCH ($sql_match) AGAINST ('" . $this->db->sql_escape(htmlspecialchars_decode($this->search_query)) . "' IN BOOLEAN MODE)
 480                  $sql_where_options
 481              ORDER BY $sql_sort";
 482          $result = $this->db->sql_query_limit($sql, $this->config['search_block_size'], $start);
 483   
 484          while ($row = $this->db->sql_fetchrow($result))
 485          {
 486              $id_ary[] = (int) $row[$field];
 487          }
 488          $this->db->sql_freeresult($result);
 489   
 490          $id_ary = array_unique($id_ary);
 491   
 492          // if the total result count is not cached yet, retrieve it from the db
 493          if (!$result_count)
 494          {
 495              $sql_found_rows = 'SELECT FOUND_ROWS() as result_count';
 496              $result = $this->db->sql_query($sql_found_rows);
 497              $result_count = (int) $this->db->sql_fetchfield('result_count');
 498              $this->db->sql_freeresult($result);
 499   
 500              if (!$result_count)
 501              {
 502                  return false;
 503              }
 504          }
 505   
 506          if ($start >= $result_count)
 507          {
 508              $start = floor(($result_count - 1) / $per_page) * $per_page;
 509   
 510              $result = $this->db->sql_query_limit($sql, $this->config['search_block_size'], $start);
 511   
 512              while ($row = $this->db->sql_fetchrow($result))
 513              {
 514                  $id_ary[] = (int) $row[$field];
 515              }
 516              $this->db->sql_freeresult($result);
 517   
 518              $id_ary = array_unique($id_ary);
 519          }
 520   
 521          // store the ids, from start on then delete anything that isn't on the current page because we only need ids for one page
 522          $this->save_ids($search_key, implode(' ', $this->split_words), $author_ary, $result_count, $id_ary, $start, $sort_dir);
 523          $id_ary = array_slice($id_ary, 0, (int) $per_page);
 524   
 525          return $result_count;
 526      }
 527   
 528      /**
 529      * Performs a search on an author's posts without caring about message contents. Depends on display specific params
 530      *
 531      * @param    string        $type                contains either posts or topics depending on what should be searched for
 532      * @param    boolean        $firstpost_only        if true, only topic starting posts will be considered
 533      * @param    array        $sort_by_sql        contains SQL code for the ORDER BY part of a query
 534      * @param    string        $sort_key            is the key of $sort_by_sql for the selected sorting
 535      * @param    string        $sort_dir            is either a or d representing ASC and DESC
 536      * @param    string        $sort_days            specifies the maximum amount of days a post may be old
 537      * @param    array        $ex_fid_ary            specifies an array of forum ids which should not be searched
 538      * @param    string        $post_visibility    specifies which types of posts the user can view in which forums
 539      * @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
 540      * @param    array        $author_ary            an array of author ids
 541      * @param    string        $author_name        specifies the author match, when ANONYMOUS is also a search-match
 542      * @param    array        &$id_ary            passed by reference, to be filled with ids for the page specified by $start and $per_page, should be ordered
 543      * @param    int            $start                indicates the first index of the page
 544      * @param    int            $per_page            number of ids each page is supposed to contain
 545      * @return    boolean|int                        total number of results
 546      */
 547      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)
 548      {
 549          // No author? No posts
 550          if (!sizeof($author_ary))
 551          {
 552              return 0;
 553          }
 554   
 555          // generate a search_key from all the options to identify the results
 556          $search_key = md5(implode('#', array(
 557              '',
 558              $type,
 559              ($firstpost_only) ? 'firstpost' : '',
 560              '',
 561              '',
 562              $sort_days,
 563              $sort_key,
 564              $topic_id,
 565              implode(',', $ex_fid_ary),
 566              $post_visibility,
 567              implode(',', $author_ary),
 568              $author_name,
 569          )));
 570   
 571          if ($start < 0)
 572          {
 573              $start = 0;
 574          }
 575   
 576          // try reading the results from cache
 577          $result_count = 0;
 578          if ($this->obtain_ids($search_key, $result_count, $id_ary, $start, $per_page, $sort_dir) == SEARCH_RESULT_IN_CACHE)
 579          {
 580              return $result_count;
 581          }
 582   
 583          $id_ary = array();
 584   
 585          // Create some display specific sql strings
 586          if ($author_name)
 587          {
 588              // first one matches post of registered users, second one guests and deleted users
 589              $sql_author = '(' . $this->db->sql_in_set('p.poster_id', array_diff($author_ary, array(ANONYMOUS)), false, true) . ' OR p.post_username ' . $author_name . ')';
 590          }
 591          else
 592          {
 593              $sql_author = $this->db->sql_in_set('p.poster_id', $author_ary);
 594          }
 595          $sql_fora        = (sizeof($ex_fid_ary)) ? ' AND ' . $this->db->sql_in_set('p.forum_id', $ex_fid_ary, true) : '';
 596          $sql_topic_id    = ($topic_id) ? ' AND p.topic_id = ' . (int) $topic_id : '';
 597          $sql_time        = ($sort_days) ? ' AND p.post_time >= ' . (time() - ($sort_days * 86400)) : '';
 598          $sql_firstpost = ($firstpost_only) ? ' AND p.post_id = t.topic_first_post_id' : '';
 599   
 600          // Build sql strings for sorting
 601          $sql_sort = $sort_by_sql[$sort_key] . (($sort_dir == 'a') ? ' ASC' : ' DESC');
 602          $sql_sort_table = $sql_sort_join = '';
 603          switch ($sql_sort[0])
 604          {
 605              case 'u':
 606                  $sql_sort_table    = USERS_TABLE . ' u, ';
 607                  $sql_sort_join    = ($type == 'posts') ? ' AND u.user_id = p.poster_id ' : ' AND u.user_id = t.topic_poster ';
 608              break;
 609   
 610              case 't':
 611                  $sql_sort_table    = ($type == 'posts' && !$firstpost_only) ? TOPICS_TABLE . ' t, ' : '';
 612                  $sql_sort_join    = ($type == 'posts' && !$firstpost_only) ? ' AND t.topic_id = p.topic_id ' : '';
 613              break;
 614   
 615              case 'f':
 616                  $sql_sort_table    = FORUMS_TABLE . ' f, ';
 617                  $sql_sort_join    = ' AND f.forum_id = p.forum_id ';
 618              break;
 619          }
 620   
 621          $m_approve_fid_sql = ' AND ' . $post_visibility;
 622   
 623          // If the cache was completely empty count the results
 624          $calc_results = ($result_count) ? '' : 'SQL_CALC_FOUND_ROWS ';
 625   
 626          // Build the query for really selecting the post_ids
 627          if ($type == 'posts')
 628          {
 629              $sql = "SELECT {$calc_results}p.post_id
 630                  FROM " . $sql_sort_table . POSTS_TABLE . ' p' . (($firstpost_only) ? ', ' . TOPICS_TABLE . ' t ' : ' ') . "
 631                  WHERE $sql_author
 632                      $sql_topic_id
 633                      $sql_firstpost
 634                      $m_approve_fid_sql
 635                      $sql_fora
 636                      $sql_sort_join
 637                      $sql_time
 638                  ORDER BY $sql_sort";
 639              $field = 'post_id';
 640          }
 641          else
 642          {
 643              $sql = "SELECT {$calc_results}t.topic_id
 644                  FROM " . $sql_sort_table . TOPICS_TABLE . ' t, ' . POSTS_TABLE . " p
 645                  WHERE $sql_author
 646                      $sql_topic_id
 647                      $sql_firstpost
 648                      $m_approve_fid_sql
 649                      $sql_fora
 650                      AND t.topic_id = p.topic_id
 651                      $sql_sort_join
 652                      $sql_time
 653                  GROUP BY t.topic_id
 654                  ORDER BY $sql_sort";
 655              $field = 'topic_id';
 656          }
 657   
 658          // Only read one block of posts from the db and then cache it
 659          $result = $this->db->sql_query_limit($sql, $this->config['search_block_size'], $start);
 660   
 661          while ($row = $this->db->sql_fetchrow($result))
 662          {
 663              $id_ary[] = (int) $row[$field];
 664          }
 665          $this->db->sql_freeresult($result);
 666   
 667          // retrieve the total result count if needed
 668          if (!$result_count)
 669          {
 670              $sql_found_rows = 'SELECT FOUND_ROWS() as result_count';
 671              $result = $this->db->sql_query($sql_found_rows);
 672              $result_count = (int) $this->db->sql_fetchfield('result_count');
 673              $this->db->sql_freeresult($result);
 674   
 675              if (!$result_count)
 676              {
 677                  return false;
 678              }
 679          }
 680   
 681          if ($start >= $result_count)
 682          {
 683              $start = floor(($result_count - 1) / $per_page) * $per_page;
 684   
 685              $result = $this->db->sql_query_limit($sql, $this->config['search_block_size'], $start);
 686              while ($row = $this->db->sql_fetchrow($result))
 687              {
 688                  $id_ary[] = (int) $row[$field];
 689              }
 690              $this->db->sql_freeresult($result);
 691   
 692              $id_ary = array_unique($id_ary);
 693          }
 694   
 695          if (sizeof($id_ary))
 696          {
 697              $this->save_ids($search_key, '', $author_ary, $result_count, $id_ary, $start, $sort_dir);
 698              $id_ary = array_slice($id_ary, 0, $per_page);
 699   
 700              return $result_count;
 701          }
 702          return false;
 703      }
 704   
 705      /**
 706      * Destroys cached search results, that contained one of the new words in a post so the results won't be outdated
 707      *
 708      * @param    string        $mode contains the post mode: edit, post, reply, quote ...
 709      * @param    int            $post_id    contains the post id of the post to index
 710      * @param    string        $message    contains the post text of the post
 711      * @param    string        $subject    contains the subject of the post to index
 712      * @param    int            $poster_id    contains the user id of the poster
 713      * @param    int            $forum_id    contains the forum id of parent forum of the post
 714      */
 715      public function index($mode, $post_id, &$message, &$subject, $poster_id, $forum_id)
 716      {
 717          // Split old and new post/subject to obtain array of words
 718          $split_text = $this->split_message($message);
 719          $split_title = ($subject) ? $this->split_message($subject) : array();
 720   
 721          $words = array_unique(array_merge($split_text, $split_title));
 722   
 723          unset($split_text);
 724          unset($split_title);
 725   
 726          // destroy cached search results containing any of the words removed or added
 727          $this->destroy_cache($words, array($poster_id));
 728   
 729          unset($words);
 730      }
 731   
 732      /**
 733      * Destroy cached results, that might be outdated after deleting a post
 734      */
 735      public function index_remove($post_ids, $author_ids, $forum_ids)
 736      {
 737          $this->destroy_cache(array(), array_unique($author_ids));
 738      }
 739   
 740      /**
 741      * Destroy old cache entries
 742      */
 743      public function tidy()
 744      {
 745          // destroy too old cached search results
 746          $this->destroy_cache(array());
 747   
 748          set_config('search_last_gc', time(), true);
 749      }
 750   
 751      /**
 752      * Create fulltext index
 753      *
 754      * @return string|bool error string is returned incase of errors otherwise false
 755      */
 756      public function create_index($acp_module, $u_action)
 757      {
 758          // Make sure we can actually use MySQL with fulltext indexes
 759          if ($error = $this->init())
 760          {
 761              return $error;
 762          }
 763   
 764          if (empty($this->stats))
 765          {
 766              $this->get_stats();
 767          }
 768   
 769          $alter = array();
 770   
 771          if (!isset($this->stats['post_subject']))
 772          {
 773              if ($this->db->get_sql_layer() == 'mysqli' || version_compare($this->db->sql_server_info(true), '4.1.3', '>='))
 774              {
 775                  $alter[] = 'MODIFY post_subject varchar(255) COLLATE utf8_unicode_ci DEFAULT \'\' NOT NULL';
 776              }
 777              else
 778              {
 779                  $alter[] = 'MODIFY post_subject text NOT NULL';
 780              }
 781              $alter[] = 'ADD FULLTEXT (post_subject)';
 782          }
 783   
 784          if (!isset($this->stats['post_content']))
 785          {
 786              if ($this->db->get_sql_layer() == 'mysqli' || version_compare($this->db->sql_server_info(true), '4.1.3', '>='))
 787              {
 788                  $alter[] = 'MODIFY post_text mediumtext COLLATE utf8_unicode_ci NOT NULL';
 789              }
 790              else
 791              {
 792                  $alter[] = 'MODIFY post_text mediumtext NOT NULL';
 793              }
 794   
 795              $alter[] = 'ADD FULLTEXT post_content (post_text, post_subject)';
 796          }
 797   
 798          if (sizeof($alter))
 799          {
 800              $this->db->sql_query('ALTER TABLE ' . POSTS_TABLE . ' ' . implode(', ', $alter));
 801          }
 802   
 803          $this->db->sql_query('TRUNCATE TABLE ' . SEARCH_RESULTS_TABLE);
 804   
 805          return false;
 806      }
 807   
 808      /**
 809      * Drop fulltext index
 810      *
 811      * @return string|bool error string is returned incase of errors otherwise false
 812      */
 813      public function delete_index($acp_module, $u_action)
 814      {
 815          // Make sure we can actually use MySQL with fulltext indexes
 816          if ($error = $this->init())
 817          {
 818              return $error;
 819          }
 820   
 821          if (empty($this->stats))
 822          {
 823              $this->get_stats();
 824          }
 825   
 826          $alter = array();
 827   
 828          if (isset($this->stats['post_subject']))
 829          {
 830              $alter[] = 'DROP INDEX post_subject';
 831          }
 832   
 833          if (isset($this->stats['post_content']))
 834          {
 835              $alter[] = 'DROP INDEX post_content';
 836          }
 837   
 838          if (sizeof($alter))
 839          {
 840              $this->db->sql_query('ALTER TABLE ' . POSTS_TABLE . ' ' . implode(', ', $alter));
 841          }
 842   
 843          $this->db->sql_query('TRUNCATE TABLE ' . SEARCH_RESULTS_TABLE);
 844   
 845          return false;
 846      }
 847   
 848      /**
 849      * Returns true if both FULLTEXT indexes exist
 850      */
 851      public function index_created()
 852      {
 853          if (empty($this->stats))
 854          {
 855              $this->get_stats();
 856          }
 857   
 858          return isset($this->stats['post_subject']) && isset($this->stats['post_content']);
 859      }
 860   
 861      /**
 862      * Returns an associative array containing information about the indexes
 863      */
 864      public function index_stats()
 865      {
 866          if (empty($this->stats))
 867          {
 868              $this->get_stats();
 869          }
 870   
 871          return array(
 872              $this->user->lang['FULLTEXT_MYSQL_TOTAL_POSTS']            => ($this->index_created()) ? $this->stats['total_posts'] : 0,
 873          );
 874      }
 875   
 876      /**
 877       * Computes the stats and store them in the $this->stats associative array
 878       */
 879      protected function get_stats()
 880      {
 881          if (strpos($this->db->get_sql_layer(), 'mysql') === false)
 882          {
 883              $this->stats = array();
 884              return;
 885          }
 886   
 887          $sql = 'SHOW INDEX
 888              FROM ' . POSTS_TABLE;
 889          $result = $this->db->sql_query($sql);
 890   
 891          while ($row = $this->db->sql_fetchrow($result))
 892          {
 893              // deal with older MySQL versions which didn't use Index_type
 894              $index_type = (isset($row['Index_type'])) ? $row['Index_type'] : $row['Comment'];
 895   
 896              if ($index_type == 'FULLTEXT')
 897              {
 898                  if ($row['Key_name'] == 'post_subject')
 899                  {
 900                      $this->stats['post_subject'] = $row;
 901                  }
 902                  else if ($row['Key_name'] == 'post_content')
 903                  {
 904                      $this->stats['post_content'] = $row;
 905                  }
 906              }
 907          }
 908          $this->db->sql_freeresult($result);
 909   
 910          $this->stats['total_posts'] = empty($this->stats) ? 0 : $this->db->get_estimated_row_count(POSTS_TABLE);
 911      }
 912   
 913      /**
 914      * Display a note, that UTF-8 support is not available with certain versions of PHP
 915      *
 916      * @return associative array containing template and config variables
 917      */
 918      public function acp()
 919      {
 920          $tpl = '
 921          <dl>
 922              <dt><label>' . $this->user->lang['MIN_SEARCH_CHARS'] . $this->user->lang['COLON'] . '</label><br /><span>' . $this->user->lang['FULLTEXT_MYSQL_MIN_SEARCH_CHARS_EXPLAIN'] . '</span></dt>
 923              <dd>' . $this->config['fulltext_mysql_min_word_len'] . '</dd>
 924          </dl>
 925          <dl>
 926              <dt><label>' . $this->user->lang['MAX_SEARCH_CHARS'] . $this->user->lang['COLON'] . '</label><br /><span>' . $this->user->lang['FULLTEXT_MYSQL_MAX_SEARCH_CHARS_EXPLAIN'] . '</span></dt>
 927              <dd>' . $this->config['fulltext_mysql_max_word_len'] . '</dd>
 928          </dl>
 929          ';
 930   
 931          // These are fields required in the config table
 932          return array(
 933              'tpl'        => $tpl,
 934              'config'    => array()
 935          );
 936      }
 937  }
 938