[ Index ]

PHP Cross Reference of phpBB-3.1.12-deutsch

title

Body

[close]

/phpbb/search/ -> fulltext_mysql.php (source)

   1  <?php
   2  /**
   3  *
   4  * This file is part of the phpBB Forum Software package.
   5  *
   6  * @copyright (c) phpBB Limited <https://www.phpbb.com>
   7  * @license GNU General Public License, version 2 (GPL-2.0)
   8  *
   9  * For full copyright and license information, please see
  10  * the docs/CREDITS.txt file.
  11  *
  12  */
  13  
  14  namespace phpbb\search;
  15  
  16  /**
  17  * Fulltext search for MySQL
  18  */
  19  class fulltext_mysql extends \phpbb\search\base
  20  {
  21      /**
  22       * Associative array holding index stats
  23       * @var array
  24       */
  25      protected $stats = array();
  26  
  27      /**
  28       * Holds the words entered by user, obtained by splitting the entered query on whitespace
  29       * @var array
  30       */
  31      protected $split_words = array();
  32  
  33      /**
  34       * Config object
  35       * @var \phpbb\config\config
  36       */
  37      protected $config;
  38  
  39      /**
  40       * Database connection
  41       * @var \phpbb\db\driver\driver_interface
  42       */
  43      protected $db;
  44  
  45      /**
  46       * phpBB event dispatcher object
  47       * @var \phpbb\event\dispatcher_interface
  48       */
  49      protected $phpbb_dispatcher;
  50  
  51      /**
  52       * User object
  53       * @var \phpbb\user
  54       */
  55      protected $user;
  56  
  57      /**
  58       * Associative array stores the min and max word length to be searched
  59       * @var array
  60       */
  61      protected $word_length = array();
  62  
  63      /**
  64       * Contains tidied search query.
  65       * Operators are prefixed in search query and common words excluded
  66       * @var string
  67       */
  68      protected $search_query;
  69  
  70      /**
  71       * Contains common words.
  72       * Common words are words with length less/more than min/max length
  73       * @var array
  74       */
  75      protected $common_words = array();
  76  
  77      /**
  78       * Constructor
  79       * Creates a new \phpbb\search\fulltext_mysql, which is used as a search backend
  80       *
  81       * @param string|bool $error Any error that occurs is passed on through this reference variable otherwise false
  82       * @param string $phpbb_root_path Relative path to phpBB root
  83       * @param string $phpEx PHP file extension
  84       * @param \phpbb\auth\auth $auth Auth object
  85       * @param \phpbb\config\config $config Config object
  86       * @param \phpbb\db\driver\driver_interface Database object
  87       * @param \phpbb\user $user User object
  88       * @param \phpbb\event\dispatcher_interface    $phpbb_dispatcher    Event dispatcher object
  89       */
  90  	public function __construct(&$error, $phpbb_root_path, $phpEx, $auth, $config, $db, $user, $phpbb_dispatcher)
  91      {
  92          $this->config = $config;
  93          $this->db = $db;
  94          $this->phpbb_dispatcher = $phpbb_dispatcher;
  95          $this->user = $user;
  96  
  97          $this->word_length = array('min' => $this->config['fulltext_mysql_min_word_len'], 'max' => $this->config['fulltext_mysql_max_word_len']);
  98  
  99          /**
 100           * Load the UTF tools
 101           */
 102          if (!function_exists('utf8_strlen'))
 103          {
 104              include($phpbb_root_path . 'includes/utf/utf_tools.' . $phpEx);
 105          }
 106  
 107          $error = false;
 108      }
 109  
 110      /**
 111      * Returns the name of this search backend to be displayed to administrators
 112      *
 113      * @return string Name
 114      */
 115  	public function get_name()
 116      {
 117          return 'MySQL Fulltext';
 118      }
 119  
 120      /**
 121       * Returns the search_query
 122       *
 123       * @return string search query
 124       */
 125  	public function get_search_query()
 126      {
 127          return $this->search_query;
 128      }
 129  
 130      /**
 131       * Returns the common_words array
 132       *
 133       * @return array common words that are ignored by search backend
 134       */
 135  	public function get_common_words()
 136      {
 137          return $this->common_words;
 138      }
 139  
 140      /**
 141       * Returns the word_length array
 142       *
 143       * @return array min and max word length for searching
 144       */
 145  	public function get_word_length()
 146      {
 147          return $this->word_length;
 148      }
 149  
 150      /**
 151      * Checks for correct MySQL version and stores min/max word length in the config
 152      *
 153      * @return string|bool Language key of the error/incompatiblity occurred
 154      */
 155  	public function init()
 156      {
 157          if ($this->db->get_sql_layer() != 'mysql4' && $this->db->get_sql_layer() != 'mysqli')
 158          {
 159              return $this->user->lang['FULLTEXT_MYSQL_INCOMPATIBLE_DATABASE'];
 160          }
 161  
 162          $result = $this->db->sql_query('SHOW TABLE STATUS LIKE \'' . POSTS_TABLE . '\'');
 163          $info = $this->db->sql_fetchrow($result);
 164          $this->db->sql_freeresult($result);
 165  
 166          $engine = '';
 167          if (isset($info['Engine']))
 168          {
 169              $engine = $info['Engine'];
 170          }
 171          else if (isset($info['Type']))
 172          {
 173              $engine = $info['Type'];
 174          }
 175  
 176          $fulltext_supported =
 177              $engine === 'MyISAM' ||
 178              // FULLTEXT is supported on InnoDB since MySQL 5.6.4 according to
 179              // http://dev.mysql.com/doc/refman/5.6/en/innodb-storage-engine.html
 180              // We also require https://bugs.mysql.com/bug.php?id=67004 to be
 181              // fixed for proper overall operation. Hence we require 5.6.8.
 182              $engine === 'InnoDB' &&
 183              phpbb_version_compare($this->db->sql_server_info(true), '5.6.8', '>=');
 184  
 185          if (!$fulltext_supported)
 186          {
 187              return $this->user->lang['FULLTEXT_MYSQL_NOT_SUPPORTED'];
 188          }
 189  
 190          $sql = 'SHOW VARIABLES
 191              LIKE \'ft\_%\'';
 192          $result = $this->db->sql_query($sql);
 193  
 194          $mysql_info = array();
 195          while ($row = $this->db->sql_fetchrow($result))
 196          {
 197              $mysql_info[$row['Variable_name']] = $row['Value'];
 198          }
 199          $this->db->sql_freeresult($result);
 200  
 201          set_config('fulltext_mysql_max_word_len', $mysql_info['ft_max_word_len']);
 202          set_config('fulltext_mysql_min_word_len', $mysql_info['ft_min_word_len']);
 203  
 204          return false;
 205      }
 206  
 207      /**
 208      * Splits keywords entered by a user into an array of words stored in $this->split_words
 209      * Stores the tidied search query in $this->search_query
 210      *
 211      * @param string &$keywords Contains the keyword as entered by the user
 212      * @param string $terms is either 'all' or 'any'
 213      * @return bool false if no valid keywords were found and otherwise true
 214      */
 215  	public function split_keywords(&$keywords, $terms)
 216      {
 217          if ($terms == 'all')
 218          {
 219              $match        = array('#\sand\s#iu', '#\sor\s#iu', '#\snot\s#iu', '#(^|\s)\+#', '#(^|\s)-#', '#(^|\s)\|#');
 220              $replace    = array(' +', ' |', ' -', ' +', ' -', ' |');
 221  
 222              $keywords = preg_replace($match, $replace, $keywords);
 223          }
 224  
 225          // Filter out as above
 226          $split_keywords = preg_replace("#[\n\r\t]+#", ' ', trim(htmlspecialchars_decode($keywords)));
 227  
 228          // Split words
 229          $split_keywords = preg_replace('#([^\p{L}\p{N}\'*"()])#u', '$1$1', str_replace('\'\'', '\' \'', trim($split_keywords)));
 230          $matches = array();
 231          preg_match_all('#(?:[^\p{L}\p{N}*"()]|^)([+\-|]?(?:[\p{L}\p{N}*"()]+\'?)*[\p{L}\p{N}*"()])(?:[^\p{L}\p{N}*"()]|$)#u', $split_keywords, $matches);
 232          $this->split_words = $matches[1];
 233  
 234          // We limit the number of allowed keywords to minimize load on the database
 235          if ($this->config['max_num_search_keywords'] && sizeof($this->split_words) > $this->config['max_num_search_keywords'])
 236          {
 237              trigger_error($this->user->lang('MAX_NUM_SEARCH_KEYWORDS_REFINE', (int) $this->config['max_num_search_keywords'], sizeof($this->split_words)));
 238          }
 239  
 240          // to allow phrase search, we need to concatenate quoted words
 241          $tmp_split_words = array();
 242          $phrase = '';
 243          foreach ($this->split_words as $word)
 244          {
 245              if ($phrase)
 246              {
 247                  $phrase .= ' ' . $word;
 248                  if (strpos($word, '"') !== false && substr_count($word, '"') % 2 == 1)
 249                  {
 250                      $tmp_split_words[] = $phrase;
 251                      $phrase = '';
 252                  }
 253              }
 254              else if (strpos($word, '"') !== false && substr_count($word, '"') % 2 == 1)
 255              {
 256                  $phrase = $word;
 257              }
 258              else
 259              {
 260                  $tmp_split_words[] = $word;
 261              }
 262          }
 263          if ($phrase)
 264          {
 265              $tmp_split_words[] = $phrase;
 266          }
 267  
 268          $this->split_words = $tmp_split_words;
 269  
 270          unset($tmp_split_words);
 271          unset($phrase);
 272  
 273          foreach ($this->split_words as $i => $word)
 274          {
 275              // Check for not allowed search queries for InnoDB.
 276              // We assume similar restrictions for MyISAM, which is usually even
 277              // slower but not as restrictive as InnoDB.
 278              // InnoDB full-text search does not support the use of a leading
 279              // plus sign with wildcard ('+*'), a plus and minus sign
 280              // combination ('+-'), or leading a plus and minus sign combination.
 281              // InnoDB full-text search only supports leading plus or minus signs.
 282              // For example, InnoDB supports '+apple' but does not support 'apple+'.
 283              // Specifying a trailing plus or minus sign causes InnoDB to report
 284              // a syntax error. InnoDB full-text search does not support the use
 285              // of multiple operators on a single search word, as in this example:
 286              // '++apple'. Use of multiple operators on a single search word
 287              // returns a syntax error to standard out.
 288              // Also, ensure that the wildcard character is only used at the
 289              // end of the line as it's intended by MySQL.
 290              if (preg_match('#^(\+[+-]|\+\*|.+[+-]$|.+\*(?!$))#', $word))
 291              {
 292                  unset($this->split_words[$i]);
 293                  continue;
 294              }
 295  
 296              $clean_word = preg_replace('#^[+\-|"]#', '', $word);
 297  
 298              // check word length
 299              $clean_len = utf8_strlen(str_replace('*', '', $clean_word));
 300              if (($clean_len < $this->config['fulltext_mysql_min_word_len']) || ($clean_len > $this->config['fulltext_mysql_max_word_len']))
 301              {
 302                  $this->common_words[] = $word;
 303                  unset($this->split_words[$i]);
 304              }
 305          }
 306  
 307          if ($terms == 'any')
 308          {
 309              $this->search_query = '';
 310              foreach ($this->split_words as $word)
 311              {
 312                  if ((strpos($word, '+') === 0) || (strpos($word, '-') === 0) || (strpos($word, '|') === 0))
 313                  {
 314                      $word = substr($word, 1);
 315                  }
 316                  $this->search_query .= $word . ' ';
 317              }
 318          }
 319          else
 320          {
 321              $this->search_query = '';
 322              foreach ($this->split_words as $word)
 323              {
 324                  if ((strpos($word, '+') === 0) || (strpos($word, '-') === 0))
 325                  {
 326                      $this->search_query .= $word . ' ';
 327                  }
 328                  else if (strpos($word, '|') === 0)
 329                  {
 330                      $this->search_query .= substr($word, 1) . ' ';
 331                  }
 332                  else
 333                  {
 334                      $this->search_query .= '+' . $word . ' ';
 335                  }
 336              }
 337          }
 338  
 339          $this->search_query = utf8_htmlspecialchars($this->search_query);
 340  
 341          if ($this->search_query)
 342          {
 343              $this->split_words = array_values($this->split_words);
 344              sort($this->split_words);
 345              return true;
 346          }
 347          return false;
 348      }
 349  
 350      /**
 351      * Turns text into an array of words
 352      * @param string $text contains post text/subject
 353      */
 354  	public function split_message($text)
 355      {
 356          // Split words
 357          $text = preg_replace('#([^\p{L}\p{N}\'*])#u', '$1$1', str_replace('\'\'', '\' \'', trim($text)));
 358          $matches = array();
 359          preg_match_all('#(?:[^\p{L}\p{N}*]|^)([+\-|]?(?:[\p{L}\p{N}*]+\'?)*[\p{L}\p{N}*])(?:[^\p{L}\p{N}*]|$)#u', $text, $matches);
 360          $text = $matches[1];
 361  
 362          // remove too short or too long words
 363          $text = array_values($text);
 364          for ($i = 0, $n = sizeof($text); $i < $n; $i++)
 365          {
 366              $text[$i] = trim($text[$i]);
 367              if (utf8_strlen($text[$i]) < $this->config['fulltext_mysql_min_word_len'] || utf8_strlen($text[$i]) > $this->config['fulltext_mysql_max_word_len'])
 368              {
 369                  unset($text[$i]);
 370              }
 371          }
 372  
 373          return array_values($text);
 374      }
 375  
 376      /**
 377      * Performs a search on keywords depending on display specific params. You have to run split_keywords() first
 378      *
 379      * @param    string        $type                contains either posts or topics depending on what should be searched for
 380      * @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)
 381      * @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)
 382      * @param    array        $sort_by_sql        contains SQL code for the ORDER BY part of a query
 383      * @param    string        $sort_key            is the key of $sort_by_sql for the selected sorting
 384      * @param    string        $sort_dir            is either a or d representing ASC and DESC
 385      * @param    string        $sort_days            specifies the maximum amount of days a post may be old
 386      * @param    array        $ex_fid_ary            specifies an array of forum ids which should not be searched
 387      * @param    string        $post_visibility    specifies which types of posts the user can view in which forums
 388      * @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
 389      * @param    array        $author_ary            an array of author ids if the author should be ignored during the search the array is empty
 390      * @param    string        $author_name        specifies the author match, when ANONYMOUS is also a search-match
 391      * @param    array        &$id_ary            passed by reference, to be filled with ids for the page specified by $start and $per_page, should be ordered
 392      * @param    int            $start                indicates the first index of the page
 393      * @param    int            $per_page            number of ids each page is supposed to contain
 394      * @return    boolean|int                        total number of results
 395      */
 396  	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)
 397      {
 398          // No keywords? No posts
 399          if (!$this->search_query)
 400          {
 401              return false;
 402          }
 403  
 404          // generate a search_key from all the options to identify the results
 405          $search_key_array = array(
 406              implode(', ', $this->split_words),
 407              $type,
 408              $fields,
 409              $terms,
 410              $sort_days,
 411              $sort_key,
 412              $topic_id,
 413              implode(',', $ex_fid_ary),
 414              $post_visibility,
 415              implode(',', $author_ary)
 416          );
 417  
 418          /**
 419          * Allow changing the search_key for cached results
 420          *
 421          * @event core.search_mysql_by_keyword_modify_search_key
 422          * @var    array    search_key_array    Array with search parameters to generate the search_key
 423          * @var    string    type                Searching type ('posts', 'topics')
 424          * @var    string    fields                Searching fields ('titleonly', 'msgonly', 'firstpost', 'all')
 425          * @var    string    terms                Searching terms ('all', 'any')
 426          * @var    int        sort_days            Time, in days, of the oldest possible post to list
 427          * @var    string    sort_key            The sort type used from the possible sort types
 428          * @var    int        topic_id            Limit the search to this topic_id only
 429          * @var    array    ex_fid_ary            Which forums not to search on
 430          * @var    string    post_visibility        Post visibility data
 431          * @var    array    author_ary            Array of user_id containing the users to filter the results to
 432          * @since 3.1.7-RC1
 433          */
 434          $vars = array(
 435              'search_key_array',
 436              'type',
 437              'fields',
 438              'terms',
 439              'sort_days',
 440              'sort_key',
 441              'topic_id',
 442              'ex_fid_ary',
 443              'post_visibility',
 444              'author_ary',
 445          );
 446          extract($this->phpbb_dispatcher->trigger_event('core.search_mysql_by_keyword_modify_search_key', compact($vars)));
 447  
 448          $search_key = md5(implode('#', $search_key_array));
 449  
 450          if ($start < 0)
 451          {
 452              $start = 0;
 453          }
 454  
 455          // try reading the results from cache
 456          $result_count = 0;
 457          if ($this->obtain_ids($search_key, $result_count, $id_ary, $start, $per_page, $sort_dir) == SEARCH_RESULT_IN_CACHE)
 458          {
 459              return $result_count;
 460          }
 461  
 462          $id_ary = array();
 463  
 464          $join_topic = ($type == 'posts') ? false : true;
 465  
 466          // Build sql strings for sorting
 467          $sql_sort = $sort_by_sql[$sort_key] . (($sort_dir == 'a') ? ' ASC' : ' DESC');
 468          $sql_sort_table = $sql_sort_join = '';
 469  
 470          switch ($sql_sort[0])
 471          {
 472              case 'u':
 473                  $sql_sort_table    = USERS_TABLE . ' u, ';
 474                  $sql_sort_join    = ($type == 'posts') ? ' AND u.user_id = p.poster_id ' : ' AND u.user_id = t.topic_poster ';
 475              break;
 476  
 477              case 't':
 478                  $join_topic = true;
 479              break;
 480  
 481              case 'f':
 482                  $sql_sort_table    = FORUMS_TABLE . ' f, ';
 483                  $sql_sort_join    = ' AND f.forum_id = p.forum_id ';
 484              break;
 485          }
 486  
 487          // Build some display specific sql strings
 488          switch ($fields)
 489          {
 490              case 'titleonly':
 491                  $sql_match = 'p.post_subject';
 492                  $sql_match_where = ' AND p.post_id = t.topic_first_post_id';
 493                  $join_topic = true;
 494              break;
 495  
 496              case 'msgonly':
 497                  $sql_match = 'p.post_text';
 498                  $sql_match_where = '';
 499              break;
 500  
 501              case 'firstpost':
 502                  $sql_match = 'p.post_subject, p.post_text';
 503                  $sql_match_where = ' AND p.post_id = t.topic_first_post_id';
 504                  $join_topic = true;
 505              break;
 506  
 507              default:
 508                  $sql_match = 'p.post_subject, p.post_text';
 509                  $sql_match_where = '';
 510              break;
 511          }
 512  
 513          $search_query = $this->search_query;
 514  
 515          /**
 516          * Allow changing the query used to search for posts using fulltext_mysql
 517          *
 518          * @event core.search_mysql_keywords_main_query_before
 519          * @var    string    search_query        The parsed keywords used for this search
 520          * @var    int        result_count        The previous result count for the format of the query.
 521          *                                    Set to 0 to force a re-count
 522          * @var    bool    join_topic            Weather or not TOPICS_TABLE should be CROSS JOIN'ED
 523          * @var    array    author_ary            Array of user_id containing the users to filter the results to
 524          * @var    string    author_name            An extra username to search on (!empty(author_ary) must be true, to be relevant)
 525          * @var    array    ex_fid_ary            Which forums not to search on
 526          * @var    int        topic_id            Limit the search to this topic_id only
 527          * @var    string    sql_sort_table        Extra tables to include in the SQL query.
 528          *                                    Used in conjunction with sql_sort_join
 529          * @var    string    sql_sort_join        SQL conditions to join all the tables used together.
 530          *                                    Used in conjunction with sql_sort_table
 531          * @var    int        sort_days            Time, in days, of the oldest possible post to list
 532          * @var    string    sql_match            Which columns to do the search on.
 533          * @var    string    sql_match_where        Extra conditions to use to properly filter the matching process
 534          * @var    string    sort_by_sql            The possible predefined sort types
 535          * @var    string    sort_key            The sort type used from the possible sort types
 536          * @var    string    sort_dir            "a" for ASC or "d" dor DESC for the sort order used
 537          * @var    string    sql_sort            The result SQL when processing sort_by_sql + sort_key + sort_dir
 538          * @var    int        start                How many posts to skip in the search results (used for pagination)
 539          * @since 3.1.5-RC1
 540          */
 541          $vars = array(
 542              'search_query',
 543              'result_count',
 544              'join_topic',
 545              'author_ary',
 546              'author_name',
 547              'ex_fid_ary',
 548              'topic_id',
 549              'sql_sort_table',
 550              'sql_sort_join',
 551              'sort_days',
 552              'sql_match',
 553              'sql_match_where',
 554              'sort_by_sql',
 555              'sort_key',
 556              'sort_dir',
 557              'sql_sort',
 558              'start',
 559          );
 560          extract($this->phpbb_dispatcher->trigger_event('core.search_mysql_keywords_main_query_before', compact($vars)));
 561  
 562          $sql_select            = (!$result_count) ? 'SQL_CALC_FOUND_ROWS ' : '';
 563          $sql_select            = ($type == 'posts') ? $sql_select . 'p.post_id' : 'DISTINCT ' . $sql_select . 't.topic_id';
 564          $sql_from            = ($join_topic) ? TOPICS_TABLE . ' t, ' : '';
 565          $field                = ($type == 'posts') ? 'post_id' : 'topic_id';
 566          if (sizeof($author_ary) && $author_name)
 567          {
 568              // first one matches post of registered users, second one guests and deleted users
 569              $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 . ')';
 570          }
 571          else if (sizeof($author_ary))
 572          {
 573              $sql_author = ' AND ' . $this->db->sql_in_set('p.poster_id', $author_ary);
 574          }
 575          else
 576          {
 577              $sql_author = '';
 578          }
 579  
 580          $sql_where_options = $sql_sort_join;
 581          $sql_where_options .= ($topic_id) ? ' AND p.topic_id = ' . $topic_id : '';
 582          $sql_where_options .= ($join_topic) ? ' AND t.topic_id = p.topic_id' : '';
 583          $sql_where_options .= (sizeof($ex_fid_ary)) ? ' AND ' . $this->db->sql_in_set('p.forum_id', $ex_fid_ary, true) : '';
 584          $sql_where_options .= ' AND ' . $post_visibility;
 585          $sql_where_options .= $sql_author;
 586          $sql_where_options .= ($sort_days) ? ' AND p.post_time >= ' . (time() - ($sort_days * 86400)) : '';
 587          $sql_where_options .= $sql_match_where;
 588  
 589          $sql = "SELECT $sql_select
 590              FROM $sql_from$sql_sort_table" . POSTS_TABLE . " p
 591              WHERE MATCH ($sql_match) AGAINST ('" . $this->db->sql_escape(htmlspecialchars_decode($this->search_query)) . "' IN BOOLEAN MODE)
 592                  $sql_where_options
 593              ORDER BY $sql_sort";
 594          $result = $this->db->sql_query_limit($sql, $this->config['search_block_size'], $start);
 595  
 596          while ($row = $this->db->sql_fetchrow($result))
 597          {
 598              $id_ary[] = (int) $row[$field];
 599          }
 600          $this->db->sql_freeresult($result);
 601  
 602          $id_ary = array_unique($id_ary);
 603  
 604          // if the total result count is not cached yet, retrieve it from the db
 605          if (!$result_count)
 606          {
 607              $sql_found_rows = 'SELECT FOUND_ROWS() as result_count';
 608              $result = $this->db->sql_query($sql_found_rows);
 609              $result_count = (int) $this->db->sql_fetchfield('result_count');
 610              $this->db->sql_freeresult($result);
 611  
 612              if (!$result_count)
 613              {
 614                  return false;
 615              }
 616          }
 617  
 618          if ($start >= $result_count)
 619          {
 620              $start = floor(($result_count - 1) / $per_page) * $per_page;
 621  
 622              $result = $this->db->sql_query_limit($sql, $this->config['search_block_size'], $start);
 623  
 624              while ($row = $this->db->sql_fetchrow($result))
 625              {
 626                  $id_ary[] = (int) $row[$field];
 627              }
 628              $this->db->sql_freeresult($result);
 629  
 630              $id_ary = array_unique($id_ary);
 631          }
 632  
 633          // store the ids, from start on then delete anything that isn't on the current page because we only need ids for one page
 634          $this->save_ids($search_key, implode(' ', $this->split_words), $author_ary, $result_count, $id_ary, $start, $sort_dir);
 635          $id_ary = array_slice($id_ary, 0, (int) $per_page);
 636  
 637          return $result_count;
 638      }
 639  
 640      /**
 641      * Performs a search on an author's posts without caring about message contents. Depends on display specific params
 642      *
 643      * @param    string        $type                contains either posts or topics depending on what should be searched for
 644      * @param    boolean        $firstpost_only        if true, only topic starting posts will be considered
 645      * @param    array        $sort_by_sql        contains SQL code for the ORDER BY part of a query
 646      * @param    string        $sort_key            is the key of $sort_by_sql for the selected sorting
 647      * @param    string        $sort_dir            is either a or d representing ASC and DESC
 648      * @param    string        $sort_days            specifies the maximum amount of days a post may be old
 649      * @param    array        $ex_fid_ary            specifies an array of forum ids which should not be searched
 650      * @param    string        $post_visibility    specifies which types of posts the user can view in which forums
 651      * @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
 652      * @param    array        $author_ary            an array of author ids
 653      * @param    string        $author_name        specifies the author match, when ANONYMOUS is also a search-match
 654      * @param    array        &$id_ary            passed by reference, to be filled with ids for the page specified by $start and $per_page, should be ordered
 655      * @param    int            $start                indicates the first index of the page
 656      * @param    int            $per_page            number of ids each page is supposed to contain
 657      * @return    boolean|int                        total number of results
 658      */
 659  	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)
 660      {
 661          // No author? No posts
 662          if (!sizeof($author_ary))
 663          {
 664              return 0;
 665          }
 666  
 667          // generate a search_key from all the options to identify the results
 668          $search_key_array = array(
 669              '',
 670              $type,
 671              ($firstpost_only) ? 'firstpost' : '',
 672              '',
 673              '',
 674              $sort_days,
 675              $sort_key,
 676              $topic_id,
 677              implode(',', $ex_fid_ary),
 678              $post_visibility,
 679              implode(',', $author_ary),
 680              $author_name,
 681          );
 682  
 683          /**
 684          * Allow changing the search_key for cached results
 685          *
 686          * @event core.search_mysql_by_author_modify_search_key
 687          * @var    array    search_key_array    Array with search parameters to generate the search_key
 688          * @var    string    type                Searching type ('posts', 'topics')
 689          * @var    boolean    firstpost_only        Flag indicating if only topic starting posts are considered
 690          * @var    int        sort_days            Time, in days, of the oldest possible post to list
 691          * @var    string    sort_key            The sort type used from the possible sort types
 692          * @var    int        topic_id            Limit the search to this topic_id only
 693          * @var    array    ex_fid_ary            Which forums not to search on
 694          * @var    string    post_visibility        Post visibility data
 695          * @var    array    author_ary            Array of user_id containing the users to filter the results to
 696          * @var    string    author_name            The username to search on
 697          * @since 3.1.7-RC1
 698          */
 699          $vars = array(
 700              'search_key_array',
 701              'type',
 702              'firstpost_only',
 703              'sort_days',
 704              'sort_key',
 705              'topic_id',
 706              'ex_fid_ary',
 707              'post_visibility',
 708              'author_ary',
 709              'author_name',
 710          );
 711          extract($this->phpbb_dispatcher->trigger_event('core.search_mysql_by_author_modify_search_key', compact($vars)));
 712  
 713          $search_key = md5(implode('#', $search_key_array));
 714  
 715          if ($start < 0)
 716          {
 717              $start = 0;
 718          }
 719  
 720          // try reading the results from cache
 721          $result_count = 0;
 722          if ($this->obtain_ids($search_key, $result_count, $id_ary, $start, $per_page, $sort_dir) == SEARCH_RESULT_IN_CACHE)
 723          {
 724              return $result_count;
 725          }
 726  
 727          $id_ary = array();
 728  
 729          // Create some display specific sql strings
 730          if ($author_name)
 731          {
 732              // first one matches post of registered users, second one guests and deleted users
 733              $sql_author = '(' . $this->db->sql_in_set('p.poster_id', array_diff($author_ary, array(ANONYMOUS)), false, true) . ' OR p.post_username ' . $author_name . ')';
 734          }
 735          else
 736          {
 737              $sql_author = $this->db->sql_in_set('p.poster_id', $author_ary);
 738          }
 739          $sql_fora        = (sizeof($ex_fid_ary)) ? ' AND ' . $this->db->sql_in_set('p.forum_id', $ex_fid_ary, true) : '';
 740          $sql_topic_id    = ($topic_id) ? ' AND p.topic_id = ' . (int) $topic_id : '';
 741          $sql_time        = ($sort_days) ? ' AND p.post_time >= ' . (time() - ($sort_days * 86400)) : '';
 742          $sql_firstpost = ($firstpost_only) ? ' AND p.post_id = t.topic_first_post_id' : '';
 743  
 744          // Build sql strings for sorting
 745          $sql_sort = $sort_by_sql[$sort_key] . (($sort_dir == 'a') ? ' ASC' : ' DESC');
 746          $sql_sort_table = $sql_sort_join = '';
 747          switch ($sql_sort[0])
 748          {
 749              case 'u':
 750                  $sql_sort_table    = USERS_TABLE . ' u, ';
 751                  $sql_sort_join    = ($type == 'posts') ? ' AND u.user_id = p.poster_id ' : ' AND u.user_id = t.topic_poster ';
 752              break;
 753  
 754              case 't':
 755                  $sql_sort_table    = ($type == 'posts' && !$firstpost_only) ? TOPICS_TABLE . ' t, ' : '';
 756                  $sql_sort_join    = ($type == 'posts' && !$firstpost_only) ? ' AND t.topic_id = p.topic_id ' : '';
 757              break;
 758  
 759              case 'f':
 760                  $sql_sort_table    = FORUMS_TABLE . ' f, ';
 761                  $sql_sort_join    = ' AND f.forum_id = p.forum_id ';
 762              break;
 763          }
 764  
 765          $m_approve_fid_sql = ' AND ' . $post_visibility;
 766  
 767          /**
 768          * Allow changing the query used to search for posts by author in fulltext_mysql
 769          *
 770          * @event core.search_mysql_author_query_before
 771          * @var    int        result_count        The previous result count for the format of the query.
 772          *                                    Set to 0 to force a re-count
 773          * @var    string    sql_sort_table        CROSS JOIN'ed table to allow doing the sort chosen
 774          * @var    string    sql_sort_join        Condition to define how to join the CROSS JOIN'ed table specifyed in sql_sort_table
 775          * @var    string    type                Either "posts" or "topics" specifying the type of search being made
 776          * @var    array    author_ary            Array of user_id containing the users to filter the results to
 777          * @var    string    author_name            An extra username to search on
 778          * @var    string    sql_author            SQL WHERE condition for the post author ids
 779          * @var    int        topic_id            Limit the search to this topic_id only
 780          * @var    string    sql_topic_id        SQL of topic_id
 781          * @var    string    sort_by_sql            The possible predefined sort types
 782          * @var    string    sort_key            The sort type used from the possible sort types
 783          * @var    string    sort_dir            "a" for ASC or "d" dor DESC for the sort order used
 784          * @var    string    sql_sort            The result SQL when processing sort_by_sql + sort_key + sort_dir
 785          * @var    string    sort_days            Time, in days, that the oldest post showing can have
 786          * @var    string    sql_time            The SQL to search on the time specifyed by sort_days
 787          * @var    bool    firstpost_only        Wether or not to search only on the first post of the topics
 788          * @var    string    sql_firstpost        The SQL with the conditions to join the tables when using firstpost_only
 789          * @var    array    ex_fid_ary            Forum ids that must not be searched on
 790          * @var    array    sql_fora            SQL query for ex_fid_ary
 791          * @var    string    m_approve_fid_sql    WHERE clause condition on post_visibility restrictions
 792          * @var    int        start                How many posts to skip in the search results (used for pagination)
 793          * @since 3.1.5-RC1
 794          */
 795          $vars = array(
 796              'result_count',
 797              'sql_sort_table',
 798              'sql_sort_join',
 799              'type',
 800              'author_ary',
 801              'author_name',
 802              'sql_author',
 803              'topic_id',
 804              'sql_topic_id',
 805              'sort_by_sql',
 806              'sort_key',
 807              'sort_dir',
 808              'sql_sort',
 809              'sort_days',
 810              'sql_time',
 811              'firstpost_only',
 812              'sql_firstpost',
 813              'ex_fid_ary',
 814              'sql_fora',
 815              'm_approve_fid_sql',
 816              'start',
 817          );
 818          extract($this->phpbb_dispatcher->trigger_event('core.search_mysql_author_query_before', compact($vars)));
 819  
 820          // If the cache was completely empty count the results
 821          $calc_results = ($result_count) ? '' : 'SQL_CALC_FOUND_ROWS ';
 822  
 823          // Build the query for really selecting the post_ids
 824          if ($type == 'posts')
 825          {
 826              $sql = "SELECT {$calc_results}p.post_id
 827                  FROM " . $sql_sort_table . POSTS_TABLE . ' p' . (($firstpost_only) ? ', ' . TOPICS_TABLE . ' t ' : ' ') . "
 828                  WHERE $sql_author
 829                      $sql_topic_id
 830                      $sql_firstpost
 831                      $m_approve_fid_sql
 832                      $sql_fora
 833                      $sql_sort_join
 834                      $sql_time
 835                  ORDER BY $sql_sort";
 836              $field = 'post_id';
 837          }
 838          else
 839          {
 840              $sql = "SELECT {$calc_results}t.topic_id
 841                  FROM " . $sql_sort_table . TOPICS_TABLE . ' t, ' . POSTS_TABLE . " p
 842                  WHERE $sql_author
 843                      $sql_topic_id
 844                      $sql_firstpost
 845                      $m_approve_fid_sql
 846                      $sql_fora
 847                      AND t.topic_id = p.topic_id
 848                      $sql_sort_join
 849                      $sql_time
 850                  GROUP BY t.topic_id
 851                  ORDER BY $sql_sort";
 852              $field = 'topic_id';
 853          }
 854  
 855          // Only read one block of posts from the db and then cache it
 856          $result = $this->db->sql_query_limit($sql, $this->config['search_block_size'], $start);
 857  
 858          while ($row = $this->db->sql_fetchrow($result))
 859          {
 860              $id_ary[] = (int) $row[$field];
 861          }
 862          $this->db->sql_freeresult($result);
 863  
 864          // retrieve the total result count if needed
 865          if (!$result_count)
 866          {
 867              $sql_found_rows = 'SELECT FOUND_ROWS() as result_count';
 868              $result = $this->db->sql_query($sql_found_rows);
 869              $result_count = (int) $this->db->sql_fetchfield('result_count');
 870              $this->db->sql_freeresult($result);
 871  
 872              if (!$result_count)
 873              {
 874                  return false;
 875              }
 876          }
 877  
 878          if ($start >= $result_count)
 879          {
 880              $start = floor(($result_count - 1) / $per_page) * $per_page;
 881  
 882              $result = $this->db->sql_query_limit($sql, $this->config['search_block_size'], $start);
 883              while ($row = $this->db->sql_fetchrow($result))
 884              {
 885                  $id_ary[] = (int) $row[$field];
 886              }
 887              $this->db->sql_freeresult($result);
 888  
 889              $id_ary = array_unique($id_ary);
 890          }
 891  
 892          if (sizeof($id_ary))
 893          {
 894              $this->save_ids($search_key, '', $author_ary, $result_count, $id_ary, $start, $sort_dir);
 895              $id_ary = array_slice($id_ary, 0, $per_page);
 896  
 897              return $result_count;
 898          }
 899          return false;
 900      }
 901  
 902      /**
 903      * Destroys cached search results, that contained one of the new words in a post so the results won't be outdated
 904      *
 905      * @param    string        $mode contains the post mode: edit, post, reply, quote ...
 906      * @param    int            $post_id    contains the post id of the post to index
 907      * @param    string        $message    contains the post text of the post
 908      * @param    string        $subject    contains the subject of the post to index
 909      * @param    int            $poster_id    contains the user id of the poster
 910      * @param    int            $forum_id    contains the forum id of parent forum of the post
 911      */
 912  	public function index($mode, $post_id, &$message, &$subject, $poster_id, $forum_id)
 913      {
 914          // Split old and new post/subject to obtain array of words
 915          $split_text = $this->split_message($message);
 916          $split_title = ($subject) ? $this->split_message($subject) : array();
 917  
 918          $words = array_unique(array_merge($split_text, $split_title));
 919  
 920          unset($split_text);
 921          unset($split_title);
 922  
 923          // destroy cached search results containing any of the words removed or added
 924          $this->destroy_cache($words, array($poster_id));
 925  
 926          unset($words);
 927      }
 928  
 929      /**
 930      * Destroy cached results, that might be outdated after deleting a post
 931      */
 932  	public function index_remove($post_ids, $author_ids, $forum_ids)
 933      {
 934          $this->destroy_cache(array(), array_unique($author_ids));
 935      }
 936  
 937      /**
 938      * Destroy old cache entries
 939      */
 940  	public function tidy()
 941      {
 942          // destroy too old cached search results
 943          $this->destroy_cache(array());
 944  
 945          set_config('search_last_gc', time(), true);
 946      }
 947  
 948      /**
 949      * Create fulltext index
 950      *
 951      * @return string|bool error string is returned incase of errors otherwise false
 952      */
 953  	public function create_index($acp_module, $u_action)
 954      {
 955          // Make sure we can actually use MySQL with fulltext indexes
 956          if ($error = $this->init())
 957          {
 958              return $error;
 959          }
 960  
 961          if (empty($this->stats))
 962          {
 963              $this->get_stats();
 964          }
 965  
 966          $alter_list = array();
 967  
 968          if (!isset($this->stats['post_subject']))
 969          {
 970              $alter_entry = array();
 971              if ($this->db->get_sql_layer() == 'mysqli' || version_compare($this->db->sql_server_info(true), '4.1.3', '>='))
 972              {
 973                  $alter_entry[] = 'MODIFY post_subject varchar(255) COLLATE utf8_unicode_ci DEFAULT \'\' NOT NULL';
 974              }
 975              else
 976              {
 977                  $alter_entry[] = 'MODIFY post_subject text NOT NULL';
 978              }
 979              $alter_entry[] = 'ADD FULLTEXT (post_subject)';
 980              $alter_list[] = $alter_entry;
 981          }
 982  
 983          if (!isset($this->stats['post_content']))
 984          {
 985              $alter_entry = array();
 986              if ($this->db->get_sql_layer() == 'mysqli' || version_compare($this->db->sql_server_info(true), '4.1.3', '>='))
 987              {
 988                  $alter_entry[] = 'MODIFY post_text mediumtext COLLATE utf8_unicode_ci NOT NULL';
 989              }
 990              else
 991              {
 992                  $alter_entry[] = 'MODIFY post_text mediumtext NOT NULL';
 993              }
 994  
 995              $alter_entry[] = 'ADD FULLTEXT post_content (post_text, post_subject)';
 996              $alter_list[] = $alter_entry;
 997          }
 998  
 999          if (sizeof($alter_list))
1000          {
1001              foreach ($alter_list as $alter)
1002              {
1003                  $this->db->sql_query('ALTER TABLE ' . POSTS_TABLE . ' ' . implode(', ', $alter));
1004              }
1005          }
1006  
1007          $this->db->sql_query('TRUNCATE TABLE ' . SEARCH_RESULTS_TABLE);
1008  
1009          return false;
1010      }
1011  
1012      /**
1013      * Drop fulltext index
1014      *
1015      * @return string|bool error string is returned incase of errors otherwise false
1016      */
1017  	public function delete_index($acp_module, $u_action)
1018      {
1019          // Make sure we can actually use MySQL with fulltext indexes
1020          if ($error = $this->init())
1021          {
1022              return $error;
1023          }
1024  
1025          if (empty($this->stats))
1026          {
1027              $this->get_stats();
1028          }
1029  
1030          $alter = array();
1031  
1032          if (isset($this->stats['post_subject']))
1033          {
1034              $alter[] = 'DROP INDEX post_subject';
1035          }
1036  
1037          if (isset($this->stats['post_content']))
1038          {
1039              $alter[] = 'DROP INDEX post_content';
1040          }
1041  
1042          if (sizeof($alter))
1043          {
1044              $this->db->sql_query('ALTER TABLE ' . POSTS_TABLE . ' ' . implode(', ', $alter));
1045          }
1046  
1047          $this->db->sql_query('TRUNCATE TABLE ' . SEARCH_RESULTS_TABLE);
1048  
1049          return false;
1050      }
1051  
1052      /**
1053      * Returns true if both FULLTEXT indexes exist
1054      */
1055  	public function index_created()
1056      {
1057          if (empty($this->stats))
1058          {
1059              $this->get_stats();
1060          }
1061  
1062          return isset($this->stats['post_subject']) && isset($this->stats['post_content']);
1063      }
1064  
1065      /**
1066      * Returns an associative array containing information about the indexes
1067      */
1068  	public function index_stats()
1069      {
1070          if (empty($this->stats))
1071          {
1072              $this->get_stats();
1073          }
1074  
1075          return array(
1076              $this->user->lang['FULLTEXT_MYSQL_TOTAL_POSTS']            => ($this->index_created()) ? $this->stats['total_posts'] : 0,
1077          );
1078      }
1079  
1080      /**
1081       * Computes the stats and store them in the $this->stats associative array
1082       */
1083  	protected function get_stats()
1084      {
1085          if (strpos($this->db->get_sql_layer(), 'mysql') === false)
1086          {
1087              $this->stats = array();
1088              return;
1089          }
1090  
1091          $sql = 'SHOW INDEX
1092              FROM ' . POSTS_TABLE;
1093          $result = $this->db->sql_query($sql);
1094  
1095          while ($row = $this->db->sql_fetchrow($result))
1096          {
1097              // deal with older MySQL versions which didn't use Index_type
1098              $index_type = (isset($row['Index_type'])) ? $row['Index_type'] : $row['Comment'];
1099  
1100              if ($index_type == 'FULLTEXT')
1101              {
1102                  if ($row['Key_name'] == 'post_subject')
1103                  {
1104                      $this->stats['post_subject'] = $row;
1105                  }
1106                  else if ($row['Key_name'] == 'post_content')
1107                  {
1108                      $this->stats['post_content'] = $row;
1109                  }
1110              }
1111          }
1112          $this->db->sql_freeresult($result);
1113  
1114          $this->stats['total_posts'] = empty($this->stats) ? 0 : $this->db->get_estimated_row_count(POSTS_TABLE);
1115      }
1116  
1117      /**
1118      * Display a note, that UTF-8 support is not available with certain versions of PHP
1119      *
1120      * @return associative array containing template and config variables
1121      */
1122  	public function acp()
1123      {
1124          $tpl = '
1125          <dl>
1126              <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>
1127              <dd>' . $this->config['fulltext_mysql_min_word_len'] . '</dd>
1128          </dl>
1129          <dl>
1130              <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>
1131              <dd>' . $this->config['fulltext_mysql_max_word_len'] . '</dd>
1132          </dl>
1133          ';
1134  
1135          // These are fields required in the config table
1136          return array(
1137              'tpl'        => $tpl,
1138              'config'    => array()
1139          );
1140      }
1141  }


Generated: Thu Jan 11 00:25:41 2018 Cross-referenced by PHPXref 0.7.1