[ Index ]

PHP Cross Reference of phpBB-3.3.11-deutsch

title

Body

[close]

/phpbb/search/ -> fulltext_native.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  * phpBB's own db driven fulltext search, version 2
  18  */
  19  class fulltext_native extends \phpbb\search\base
  20  {
  21      const UTF8_HANGUL_FIRST = "\xEA\xB0\x80";
  22      const UTF8_HANGUL_LAST = "\xED\x9E\xA3";
  23      const UTF8_CJK_FIRST = "\xE4\xB8\x80";
  24      const UTF8_CJK_LAST = "\xE9\xBE\xBB";
  25      const UTF8_CJK_B_FIRST = "\xF0\xA0\x80\x80";
  26      const UTF8_CJK_B_LAST = "\xF0\xAA\x9B\x96";
  27  
  28      /**
  29       * Associative array holding index stats
  30       * @var array
  31       */
  32      protected $stats = array();
  33  
  34      /**
  35       * Associative array stores the min and max word length to be searched
  36       * @var array
  37       */
  38      protected $word_length = array();
  39  
  40      /**
  41       * Contains tidied search query.
  42       * Operators are prefixed in search query and common words excluded
  43       * @var string
  44       */
  45      protected $search_query;
  46  
  47      /**
  48       * Contains common words.
  49       * Common words are words with length less/more than min/max length
  50       * @var array
  51       */
  52      protected $common_words = array();
  53  
  54      /**
  55       * Post ids of posts containing words that are to be included
  56       * @var array
  57       */
  58      protected $must_contain_ids = array();
  59  
  60      /**
  61       * Post ids of posts containing words that should not be included
  62       * @var array
  63       */
  64      protected $must_not_contain_ids = array();
  65  
  66      /**
  67       * Post ids of posts containing at least one word that needs to be excluded
  68       * @var array
  69       */
  70      protected $must_exclude_one_ids = array();
  71  
  72      /**
  73       * Relative path to board root
  74       * @var string
  75       */
  76      protected $phpbb_root_path;
  77  
  78      /**
  79       * PHP Extension
  80       * @var string
  81       */
  82      protected $php_ext;
  83  
  84      /**
  85       * Config object
  86       * @var \phpbb\config\config
  87       */
  88      protected $config;
  89  
  90      /**
  91       * Database connection
  92       * @var \phpbb\db\driver\driver_interface
  93       */
  94      protected $db;
  95  
  96      /**
  97       * phpBB event dispatcher object
  98       * @var \phpbb\event\dispatcher_interface
  99       */
 100      protected $phpbb_dispatcher;
 101  
 102      /**
 103       * User object
 104       * @var \phpbb\user
 105       */
 106      protected $user;
 107  
 108      /**
 109      * Initialises the fulltext_native search backend with min/max word length
 110      *
 111      * @param    boolean|string    &$error    is passed by reference and should either be set to false on success or an error message on failure
 112      * @param    string    $phpbb_root_path    phpBB root path
 113      * @param    string    $phpEx    PHP file extension
 114      * @param    \phpbb\auth\auth    $auth    Auth object
 115      * @param    \phpbb\config\config    $config    Config object
 116      * @param    \phpbb\db\driver\driver_interface    $db    Database object
 117      * @param    \phpbb\user    $user    User object
 118      * @param    \phpbb\event\dispatcher_interface    $phpbb_dispatcher    Event dispatcher object
 119      */
 120  	public function __construct(&$error, $phpbb_root_path, $phpEx, $auth, $config, $db, $user, $phpbb_dispatcher)
 121      {
 122          $this->phpbb_root_path = $phpbb_root_path;
 123          $this->php_ext = $phpEx;
 124          $this->config = $config;
 125          $this->db = $db;
 126          $this->phpbb_dispatcher = $phpbb_dispatcher;
 127          $this->user = $user;
 128  
 129          $this->word_length = array('min' => (int) $this->config['fulltext_native_min_chars'], 'max' => (int) $this->config['fulltext_native_max_chars']);
 130  
 131          /**
 132          * Load the UTF tools
 133          */
 134          if (!function_exists('utf8_decode_ncr'))
 135          {
 136              include($this->phpbb_root_path . 'includes/utf/utf_tools.' . $this->php_ext);
 137          }
 138  
 139          $error = false;
 140      }
 141  
 142      /**
 143      * Returns the name of this search backend to be displayed to administrators
 144      *
 145      * @return string Name
 146      */
 147  	public function get_name()
 148      {
 149          return 'phpBB Native Fulltext';
 150      }
 151  
 152      /**
 153       * Returns the search_query
 154       *
 155       * @return string search query
 156       */
 157  	public function get_search_query()
 158      {
 159          return $this->search_query;
 160      }
 161  
 162      /**
 163       * Returns the common_words array
 164       *
 165       * @return array common words that are ignored by search backend
 166       */
 167  	public function get_common_words()
 168      {
 169          return $this->common_words;
 170      }
 171  
 172      /**
 173       * Returns the word_length array
 174       *
 175       * @return array min and max word length for searching
 176       */
 177  	public function get_word_length()
 178      {
 179          return $this->word_length;
 180      }
 181  
 182      /**
 183      * This function fills $this->search_query with the cleaned user search query
 184      *
 185      * If $terms is 'any' then the words will be extracted from the search query
 186      * and combined with | inside brackets. They will afterwards be treated like
 187      * an standard search query.
 188      *
 189      * Then it analyses the query and fills the internal arrays $must_not_contain_ids,
 190      * $must_contain_ids and $must_exclude_one_ids which are later used by keyword_search()
 191      *
 192      * @param    string    $keywords    contains the search query string as entered by the user
 193      * @param    string    $terms        is either 'all' (use search query as entered, default words to 'must be contained in post')
 194      *     or 'any' (find all posts containing at least one of the given words)
 195      * @return    boolean                false if no valid keywords were found and otherwise true
 196      */
 197  	public function split_keywords($keywords, $terms)
 198      {
 199          $tokens = '+-|()* ';
 200  
 201          $keywords = trim($this->cleanup($keywords, $tokens));
 202  
 203          // allow word|word|word without brackets
 204          if ((strpos($keywords, ' ') === false) && (strpos($keywords, '|') !== false) && (strpos($keywords, '(') === false))
 205          {
 206              $keywords = '(' . $keywords . ')';
 207          }
 208  
 209          $open_bracket = $space = false;
 210          for ($i = 0, $n = strlen($keywords); $i < $n; $i++)
 211          {
 212              if ($open_bracket !== false)
 213              {
 214                  switch ($keywords[$i])
 215                  {
 216                      case ')':
 217                          if ($open_bracket + 1 == $i)
 218                          {
 219                              $keywords[$i - 1] = '|';
 220                              $keywords[$i] = '|';
 221                          }
 222                          $open_bracket = false;
 223                      break;
 224                      case '(':
 225                          $keywords[$i] = '|';
 226                      break;
 227                      case '+':
 228                      case '-':
 229                      case ' ':
 230                          $keywords[$i] = '|';
 231                      break;
 232                      case '*':
 233                          // $i can never be 0 here since $open_bracket is initialised to false
 234                          if (strpos($tokens, $keywords[$i - 1]) !== false && ($i + 1 === $n || strpos($tokens, $keywords[$i + 1]) !== false))
 235                          {
 236                              $keywords[$i] = '|';
 237                          }
 238                      break;
 239                  }
 240              }
 241              else
 242              {
 243                  switch ($keywords[$i])
 244                  {
 245                      case ')':
 246                          $keywords[$i] = ' ';
 247                      break;
 248                      case '(':
 249                          $open_bracket = $i;
 250                          $space = false;
 251                      break;
 252                      case '|':
 253                          $keywords[$i] = ' ';
 254                      break;
 255                      case '-':
 256                          // Ignore hyphen if followed by a space
 257                          if (isset($keywords[$i + 1]) && $keywords[$i + 1] == ' ')
 258                          {
 259                              $keywords[$i] = ' ';
 260                          }
 261                          else
 262                          {
 263                              $space = $keywords[$i];
 264                          }
 265                      break;
 266                      case '+':
 267                          $space = $keywords[$i];
 268                      break;
 269                      case ' ':
 270                          if ($space !== false)
 271                          {
 272                              $keywords[$i] = $space;
 273                          }
 274                      break;
 275                      default:
 276                          $space = false;
 277                  }
 278              }
 279          }
 280  
 281          if ($open_bracket !== false)
 282          {
 283              $keywords .= ')';
 284          }
 285  
 286          $match = array(
 287              '#  +#',
 288              '#\|\|+#',
 289              '#(\+|\-)(?:\+|\-)+#',
 290              '#\(\|#',
 291              '#\|\)#',
 292          );
 293          $replace = array(
 294              ' ',
 295              '|',
 296              '$1',
 297              '(',
 298              ')',
 299          );
 300  
 301          $keywords = preg_replace($match, $replace, $keywords);
 302          $num_keywords = count(explode(' ', $keywords));
 303  
 304          // We limit the number of allowed keywords to minimize load on the database
 305          if ($this->config['max_num_search_keywords'] && $num_keywords > $this->config['max_num_search_keywords'])
 306          {
 307              trigger_error($this->user->lang('MAX_NUM_SEARCH_KEYWORDS_REFINE', (int) $this->config['max_num_search_keywords'], $num_keywords));
 308          }
 309  
 310          // $keywords input format: each word separated by a space, words in a bracket are not separated
 311  
 312          // the user wants to search for any word, convert the search query
 313          if ($terms == 'any')
 314          {
 315              $words = array();
 316  
 317              preg_match_all('#([^\\s+\\-|()]+)(?:$|[\\s+\\-|()])#u', $keywords, $words);
 318              if (count($words[1]))
 319              {
 320                  $keywords = '(' . implode('|', $words[1]) . ')';
 321              }
 322          }
 323  
 324          // Remove non trailing wildcards from each word to prevent a full table scan (it's now using the database index)
 325          $match = '#\*(?!$|\s)#';
 326          $replace = '$1';
 327          $keywords = preg_replace($match, $replace, $keywords);
 328  
 329          // Only allow one wildcard in the search query to limit the database load
 330          $match = '#\*#';
 331          $replace = '$1';
 332          $count_wildcards = substr_count($keywords, '*');
 333  
 334          // Reverse the string to remove all wildcards except the first one
 335          $keywords = strrev(preg_replace($match, $replace, strrev($keywords), $count_wildcards - 1));
 336          unset($count_wildcards);
 337  
 338          // set the search_query which is shown to the user
 339          $this->search_query = $keywords;
 340  
 341          $exact_words = array();
 342          preg_match_all('#([^\\s+\\-|()]+)(?:$|[\\s+\\-|()])#u', $keywords, $exact_words);
 343          $exact_words = $exact_words[1];
 344  
 345          $common_ids = $words = array();
 346  
 347          if (count($exact_words))
 348          {
 349              $sql = 'SELECT word_id, word_text, word_common
 350                  FROM ' . SEARCH_WORDLIST_TABLE . '
 351                  WHERE ' . $this->db->sql_in_set('word_text', $exact_words) . '
 352                  ORDER BY word_count ASC';
 353              $result = $this->db->sql_query($sql);
 354  
 355              // store an array of words and ids, remove common words
 356              while ($row = $this->db->sql_fetchrow($result))
 357              {
 358                  if ($row['word_common'])
 359                  {
 360                      $this->common_words[] = $row['word_text'];
 361                      $common_ids[$row['word_text']] = (int) $row['word_id'];
 362                      continue;
 363                  }
 364  
 365                  $words[$row['word_text']] = (int) $row['word_id'];
 366              }
 367              $this->db->sql_freeresult($result);
 368          }
 369  
 370          // Handle +, - without preceding whitespace character
 371          $match        = array('#(\S)\+#', '#(\S)-#');
 372          $replace    = array('$1 +', '$1 +');
 373  
 374          $keywords = preg_replace($match, $replace, $keywords);
 375  
 376          // now analyse the search query, first split it using the spaces
 377          $query = explode(' ', $keywords);
 378  
 379          $this->must_contain_ids = array();
 380          $this->must_not_contain_ids = array();
 381          $this->must_exclude_one_ids = array();
 382  
 383          foreach ($query as $word)
 384          {
 385              if (empty($word))
 386              {
 387                  continue;
 388              }
 389  
 390              // words which should not be included
 391              if ($word[0] == '-')
 392              {
 393                  $word = substr($word, 1);
 394  
 395                  // a group of which at least one may not be in the resulting posts
 396                  if (isset($word[0]) && $word[0] == '(')
 397                  {
 398                      $word = array_unique(explode('|', substr($word, 1, -1)));
 399                      $mode = 'must_exclude_one';
 400                  }
 401                  // one word which should not be in the resulting posts
 402                  else
 403                  {
 404                      $mode = 'must_not_contain';
 405                  }
 406                  $ignore_no_id = true;
 407              }
 408              // words which have to be included
 409              else
 410              {
 411                  // no prefix is the same as a +prefix
 412                  if ($word[0] == '+')
 413                  {
 414                      $word = substr($word, 1);
 415                  }
 416  
 417                  // a group of words of which at least one word should be in every resulting post
 418                  if (isset($word[0]) && $word[0] == '(')
 419                  {
 420                      $word = array_unique(explode('|', substr($word, 1, -1)));
 421                  }
 422                  $ignore_no_id = false;
 423                  $mode = 'must_contain';
 424              }
 425  
 426              if (empty($word))
 427              {
 428                  continue;
 429              }
 430  
 431              // if this is an array of words then retrieve an id for each
 432              if (is_array($word))
 433              {
 434                  $non_common_words = array();
 435                  $id_words = array();
 436                  foreach ($word as $i => $word_part)
 437                  {
 438                      if (strpos($word_part, '*') !== false)
 439                      {
 440                          $len = utf8_strlen(str_replace('*', '', $word_part));
 441                          if ($len >= $this->word_length['min'] && $len <= $this->word_length['max'])
 442                          {
 443                              $id_words[] = '\'' . $this->db->sql_escape(str_replace('*', '%', $word_part)) . '\'';
 444                              $non_common_words[] = $word_part;
 445                          }
 446                          else
 447                          {
 448                              $this->common_words[] = $word_part;
 449                          }
 450                      }
 451                      else if (isset($words[$word_part]))
 452                      {
 453                          $id_words[] = $words[$word_part];
 454                          $non_common_words[] = $word_part;
 455                      }
 456                      else
 457                      {
 458                          $len = utf8_strlen($word_part);
 459                          if ($len < $this->word_length['min'] || $len > $this->word_length['max'])
 460                          {
 461                              $this->common_words[] = $word_part;
 462                          }
 463                      }
 464                  }
 465                  if (count($id_words))
 466                  {
 467                      sort($id_words);
 468                      if (count($id_words) > 1)
 469                      {
 470                          $this->{$mode . '_ids'}[] = $id_words;
 471                      }
 472                      else
 473                      {
 474                          $mode = ($mode == 'must_exclude_one') ? 'must_not_contain' : $mode;
 475                          $this->{$mode . '_ids'}[] = $id_words[0];
 476                      }
 477                  }
 478                  // throw an error if we shall not ignore unexistant words
 479                  else if (!$ignore_no_id && count($non_common_words))
 480                  {
 481                      trigger_error(sprintf($this->user->lang['WORDS_IN_NO_POST'], implode($this->user->lang['COMMA_SEPARATOR'], $non_common_words)));
 482                  }
 483                  unset($non_common_words);
 484              }
 485              // else we only need one id
 486              else if (($wildcard = strpos($word, '*') !== false) || isset($words[$word]))
 487              {
 488                  if ($wildcard)
 489                  {
 490                      $len = utf8_strlen(str_replace('*', '', $word));
 491                      if ($len >= $this->word_length['min'] && $len <= $this->word_length['max'])
 492                      {
 493                          $this->{$mode . '_ids'}[] = '\'' . $this->db->sql_escape(str_replace('*', '%', $word)) . '\'';
 494                      }
 495                      else
 496                      {
 497                          $this->common_words[] = $word;
 498                      }
 499                  }
 500                  else
 501                  {
 502                      $this->{$mode . '_ids'}[] = $words[$word];
 503                  }
 504              }
 505              else
 506              {
 507                  if (!isset($common_ids[$word]))
 508                  {
 509                      $len = utf8_strlen($word);
 510                      if ($len < $this->word_length['min'] || $len > $this->word_length['max'])
 511                      {
 512                          $this->common_words[] = $word;
 513                      }
 514                  }
 515              }
 516          }
 517  
 518          // Return true if all words are not common words
 519          if (count($exact_words) - count($this->common_words) > 0)
 520          {
 521              return true;
 522          }
 523          return false;
 524      }
 525  
 526      /**
 527      * Performs a search on keywords depending on display specific params. You have to run split_keywords() first
 528      *
 529      * @param    string        $type                contains either posts or topics depending on what should be searched for
 530      * @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)
 531      * @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)
 532      * @param    array        $sort_by_sql        contains SQL code for the ORDER BY part of a query
 533      * @param    string        $sort_key            is the key of $sort_by_sql for the selected sorting
 534      * @param    string        $sort_dir            is either a or d representing ASC and DESC
 535      * @param    string        $sort_days            specifies the maximum amount of days a post may be old
 536      * @param    array        $ex_fid_ary            specifies an array of forum ids which should not be searched
 537      * @param    string        $post_visibility    specifies which types of posts the user can view in which forums
 538      * @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
 539      * @param    array        $author_ary            an array of author ids if the author should be ignored during the search the array is empty
 540      * @param    string        $author_name        specifies the author match, when ANONYMOUS is also a search-match
 541      * @param    array        &$id_ary            passed by reference, to be filled with ids for the page specified by $start and $per_page, should be ordered
 542      * @param    int            $start                indicates the first index of the page
 543      * @param    int            $per_page            number of ids each page is supposed to contain
 544      * @return    boolean|int                        total number of results
 545      */
 546  	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)
 547      {
 548          // No keywords? No posts.
 549          if (empty($this->search_query))
 550          {
 551              return false;
 552          }
 553  
 554          // we can't search for negatives only
 555          if (empty($this->must_contain_ids))
 556          {
 557              return false;
 558          }
 559  
 560          $must_contain_ids = $this->must_contain_ids;
 561          $must_not_contain_ids = $this->must_not_contain_ids;
 562          $must_exclude_one_ids = $this->must_exclude_one_ids;
 563  
 564          sort($must_contain_ids);
 565          sort($must_not_contain_ids);
 566          sort($must_exclude_one_ids);
 567  
 568          // generate a search_key from all the options to identify the results
 569          $search_key_array = array(
 570              serialize($must_contain_ids),
 571              serialize($must_not_contain_ids),
 572              serialize($must_exclude_one_ids),
 573              $type,
 574              $fields,
 575              $terms,
 576              $sort_days,
 577              $sort_key,
 578              $topic_id,
 579              implode(',', $ex_fid_ary),
 580              $post_visibility,
 581              implode(',', $author_ary),
 582              $author_name,
 583          );
 584  
 585          /**
 586          * Allow changing the search_key for cached results
 587          *
 588          * @event core.search_native_by_keyword_modify_search_key
 589          * @var    array    search_key_array    Array with search parameters to generate the search_key
 590          * @var    array    must_contain_ids    Array with post ids of posts containing words that are to be included
 591          * @var    array    must_not_contain_ids    Array with post ids of posts containing words that should not be included
 592          * @var    array    must_exclude_one_ids    Array with post ids of posts containing at least one word that needs to be excluded
 593          * @var    string    type                Searching type ('posts', 'topics')
 594          * @var    string    fields                Searching fields ('titleonly', 'msgonly', 'firstpost', 'all')
 595          * @var    string    terms                Searching terms ('all', 'any')
 596          * @var    int        sort_days            Time, in days, of the oldest possible post to list
 597          * @var    string    sort_key            The sort type used from the possible sort types
 598          * @var    int        topic_id            Limit the search to this topic_id only
 599          * @var    array    ex_fid_ary            Which forums not to search on
 600          * @var    string    post_visibility        Post visibility data
 601          * @var    array    author_ary            Array of user_id containing the users to filter the results to
 602          * @since 3.1.7-RC1
 603          */
 604          $vars = array(
 605              'search_key_array',
 606              'must_contain_ids',
 607              'must_not_contain_ids',
 608              'must_exclude_one_ids',
 609              'type',
 610              'fields',
 611              'terms',
 612              'sort_days',
 613              'sort_key',
 614              'topic_id',
 615              'ex_fid_ary',
 616              'post_visibility',
 617              'author_ary',
 618          );
 619          extract($this->phpbb_dispatcher->trigger_event('core.search_native_by_keyword_modify_search_key', compact($vars)));
 620  
 621          $search_key = md5(implode('#', $search_key_array));
 622  
 623          // try reading the results from cache
 624          $total_results = 0;
 625          if ($this->obtain_ids($search_key, $total_results, $id_ary, $start, $per_page, $sort_dir) == SEARCH_RESULT_IN_CACHE)
 626          {
 627              return $total_results;
 628          }
 629  
 630          $id_ary = array();
 631  
 632          $sql_where = array();
 633          $m_num = 0;
 634          $w_num = 0;
 635  
 636          $sql_array = array(
 637              'SELECT'    => ($type == 'posts') ? 'DISTINCT p.post_id' : 'DISTINCT p.topic_id',
 638              'FROM'        => array(
 639                  SEARCH_WORDMATCH_TABLE    => array(),
 640                  SEARCH_WORDLIST_TABLE    => array(),
 641              ),
 642              'LEFT_JOIN' => array(array(
 643                  'FROM'    => array(POSTS_TABLE => 'p'),
 644                  'ON'    => 'm0.post_id = p.post_id',
 645              )),
 646          );
 647  
 648          $title_match = '';
 649          $left_join_topics = false;
 650          $group_by = true;
 651          // Build some display specific sql strings
 652          switch ($fields)
 653          {
 654              case 'titleonly':
 655                  $title_match = 'title_match = 1';
 656                  $group_by = false;
 657              // no break
 658              case 'firstpost':
 659                  $left_join_topics = true;
 660                  $sql_where[] = 'p.post_id = t.topic_first_post_id';
 661              break;
 662  
 663              case 'msgonly':
 664                  $title_match = 'title_match = 0';
 665                  $group_by = false;
 666              break;
 667          }
 668  
 669          if ($type == 'topics')
 670          {
 671              $left_join_topics = true;
 672              $group_by = true;
 673          }
 674  
 675          /**
 676          * @todo Add a query optimizer (handle stuff like "+(4|3) +4")
 677          */
 678  
 679          foreach ($this->must_contain_ids as $subquery)
 680          {
 681              if (is_array($subquery))
 682              {
 683                  $group_by = true;
 684  
 685                  $word_id_sql = array();
 686                  $word_ids = array();
 687                  foreach ($subquery as $id)
 688                  {
 689                      if (is_string($id))
 690                      {
 691                          $sql_array['LEFT_JOIN'][] = array(
 692                              'FROM'    => array(SEARCH_WORDLIST_TABLE => 'w' . $w_num),
 693                              'ON'    => "w$w_num.word_text LIKE $id"
 694                          );
 695                          $word_ids[] = "w$w_num.word_id";
 696  
 697                          $w_num++;
 698                      }
 699                      else
 700                      {
 701                          $word_ids[] = $id;
 702                      }
 703                  }
 704  
 705                  $sql_where[] = $this->db->sql_in_set("m$m_num.word_id", $word_ids);
 706  
 707                  unset($word_id_sql);
 708                  unset($word_ids);
 709              }
 710              else if (is_string($subquery))
 711              {
 712                  $sql_array['FROM'][SEARCH_WORDLIST_TABLE][] = 'w' . $w_num;
 713  
 714                  $sql_where[] = "w$w_num.word_text LIKE $subquery";
 715                  $sql_where[] = "m$m_num.word_id = w$w_num.word_id";
 716  
 717                  $group_by = true;
 718                  $w_num++;
 719              }
 720              else
 721              {
 722                  $sql_where[] = "m$m_num.word_id = $subquery";
 723              }
 724  
 725              $sql_array['FROM'][SEARCH_WORDMATCH_TABLE][] = 'm' . $m_num;
 726  
 727              if ($title_match)
 728              {
 729                  $sql_where[] = "m$m_num.$title_match";
 730              }
 731  
 732              if ($m_num != 0)
 733              {
 734                  $sql_where[] = "m$m_num.post_id = m0.post_id";
 735              }
 736              $m_num++;
 737          }
 738  
 739          foreach ($this->must_not_contain_ids as $key => $subquery)
 740          {
 741              if (is_string($subquery))
 742              {
 743                  $sql_array['LEFT_JOIN'][] = array(
 744                      'FROM'    => array(SEARCH_WORDLIST_TABLE => 'w' . $w_num),
 745                      'ON'    => "w$w_num.word_text LIKE $subquery"
 746                  );
 747  
 748                  $this->must_not_contain_ids[$key] = "w$w_num.word_id";
 749  
 750                  $group_by = true;
 751                  $w_num++;
 752              }
 753          }
 754  
 755          if (count($this->must_not_contain_ids))
 756          {
 757              $sql_array['LEFT_JOIN'][] = array(
 758                  'FROM'    => array(SEARCH_WORDMATCH_TABLE => 'm' . $m_num),
 759                  'ON'    => $this->db->sql_in_set("m$m_num.word_id", $this->must_not_contain_ids) . (($title_match) ? " AND m$m_num.$title_match" : '') . " AND m$m_num.post_id = m0.post_id"
 760              );
 761  
 762              $sql_where[] = "m$m_num.word_id IS NULL";
 763              $m_num++;
 764          }
 765  
 766          foreach ($this->must_exclude_one_ids as $ids)
 767          {
 768              $is_null_joins = array();
 769              foreach ($ids as $id)
 770              {
 771                  if (is_string($id))
 772                  {
 773                      $sql_array['LEFT_JOIN'][] = array(
 774                          'FROM'    => array(SEARCH_WORDLIST_TABLE => 'w' . $w_num),
 775                          'ON'    => "w$w_num.word_text LIKE $id"
 776                      );
 777                      $id = "w$w_num.word_id";
 778  
 779                      $group_by = true;
 780                      $w_num++;
 781                  }
 782  
 783                  $sql_array['LEFT_JOIN'][] = array(
 784                      'FROM'    => array(SEARCH_WORDMATCH_TABLE => 'm' . $m_num),
 785                      'ON'    => "m$m_num.word_id = $id AND m$m_num.post_id = m0.post_id" . (($title_match) ? " AND m$m_num.$title_match" : '')
 786                  );
 787                  $is_null_joins[] = "m$m_num.word_id IS NULL";
 788  
 789                  $m_num++;
 790              }
 791              $sql_where[] = '(' . implode(' OR ', $is_null_joins) . ')';
 792          }
 793  
 794          $sql_where[] = $post_visibility;
 795  
 796          $search_query = $this->search_query;
 797          $must_exclude_one_ids = $this->must_exclude_one_ids;
 798          $must_not_contain_ids = $this->must_not_contain_ids;
 799          $must_contain_ids = $this->must_contain_ids;
 800  
 801          $sql_sort_table = $sql_sort_join = $sql_match = $sql_match_where = $sql_sort = '';
 802  
 803          /**
 804          * Allow changing the query used for counting for posts using fulltext_native
 805          *
 806          * @event core.search_native_keywords_count_query_before
 807          * @var    string    search_query            The parsed keywords used for this search
 808          * @var    array    must_not_contain_ids    Ids that cannot be taken into account for the results
 809          * @var    array    must_exclude_one_ids    Ids that cannot be on the results
 810          * @var    array    must_contain_ids        Ids that must be on the results
 811          * @var    int        total_results            The previous result count for the format of the query
 812          *                                        Set to 0 to force a re-count
 813          * @var    array    sql_array                The data on how to search in the DB at this point
 814          * @var    bool    left_join_topics        Whether or not TOPICS_TABLE should be CROSS JOIN'ED
 815          * @var    array    author_ary                Array of user_id containing the users to filter the results to
 816          * @var    string    author_name                An extra username to search on (!empty(author_ary) must be true, to be relevant)
 817          * @var    array    ex_fid_ary                Which forums not to search on
 818          * @var    int        topic_id                Limit the search to this topic_id only
 819          * @var    string    sql_sort_table            Extra tables to include in the SQL query.
 820          *                                        Used in conjunction with sql_sort_join
 821          * @var    string    sql_sort_join            SQL conditions to join all the tables used together.
 822          *                                        Used in conjunction with sql_sort_table
 823          * @var    int        sort_days                Time, in days, of the oldest possible post to list
 824          * @var    string    sql_where                An array of the current WHERE clause conditions
 825          * @var    string    sql_match                Which columns to do the search on
 826          * @var    string    sql_match_where            Extra conditions to use to properly filter the matching process
 827          * @var    bool    group_by                Whether or not the SQL query requires a GROUP BY for the elements in the SELECT clause
 828          * @var    string    sort_by_sql                The possible predefined sort types
 829          * @var    string    sort_key                The sort type used from the possible sort types
 830          * @var    string    sort_dir                "a" for ASC or "d" dor DESC for the sort order used
 831          * @var    string    sql_sort                The result SQL when processing sort_by_sql + sort_key + sort_dir
 832          * @var    int        start                    How many posts to skip in the search results (used for pagination)
 833          * @since 3.1.5-RC1
 834          */
 835          $vars = array(
 836              'search_query',
 837              'must_not_contain_ids',
 838              'must_exclude_one_ids',
 839              'must_contain_ids',
 840              'total_results',
 841              'sql_array',
 842              'left_join_topics',
 843              'author_ary',
 844              'author_name',
 845              'ex_fid_ary',
 846              'topic_id',
 847              'sql_sort_table',
 848              'sql_sort_join',
 849              'sort_days',
 850              'sql_where',
 851              'sql_match',
 852              'sql_match_where',
 853              'group_by',
 854              'sort_by_sql',
 855              'sort_key',
 856              'sort_dir',
 857              'sql_sort',
 858              'start',
 859          );
 860          extract($this->phpbb_dispatcher->trigger_event('core.search_native_keywords_count_query_before', compact($vars)));
 861  
 862          if ($topic_id)
 863          {
 864              $sql_where[] = 'p.topic_id = ' . $topic_id;
 865          }
 866  
 867          if (count($author_ary))
 868          {
 869              if ($author_name)
 870              {
 871                  // first one matches post of registered users, second one guests and deleted users
 872                  $sql_author = '(' . $this->db->sql_in_set('p.poster_id', array_diff($author_ary, array(ANONYMOUS)), false, true) . ' OR p.post_username ' . $author_name . ')';
 873              }
 874              else
 875              {
 876                  $sql_author = $this->db->sql_in_set('p.poster_id', $author_ary);
 877              }
 878              $sql_where[] = $sql_author;
 879          }
 880  
 881          if (count($ex_fid_ary))
 882          {
 883              $sql_where[] = $this->db->sql_in_set('p.forum_id', $ex_fid_ary, true);
 884          }
 885  
 886          if ($sort_days)
 887          {
 888              $sql_where[] = 'p.post_time >= ' . (time() - ($sort_days * 86400));
 889          }
 890  
 891          $sql_array['WHERE'] = implode(' AND ', $sql_where);
 892  
 893          $is_mysql = false;
 894          // if the total result count is not cached yet, retrieve it from the db
 895          if (!$total_results)
 896          {
 897              $sql = '';
 898              $sql_array_count = $sql_array;
 899  
 900              if ($left_join_topics)
 901              {
 902                  $sql_array_count['LEFT_JOIN'][] = array(
 903                      'FROM'    => array(TOPICS_TABLE => 't'),
 904                      'ON'    => 'p.topic_id = t.topic_id'
 905                  );
 906              }
 907  
 908              switch ($this->db->get_sql_layer())
 909              {
 910                  case 'mysqli':
 911                      $is_mysql = true;
 912  
 913                  break;
 914  
 915                  case 'sqlite3':
 916                      $sql_array_count['SELECT'] = ($type == 'posts') ? 'DISTINCT p.post_id' : 'DISTINCT p.topic_id';
 917                      $sql = 'SELECT COUNT(' . (($type == 'posts') ? 'post_id' : 'topic_id') . ') as total_results
 918                              FROM (' . $this->db->sql_build_query('SELECT', $sql_array_count) . ')';
 919  
 920                  // no break
 921  
 922                  default:
 923                      $sql_array_count['SELECT'] = ($type == 'posts') ? 'COUNT(DISTINCT p.post_id) AS total_results' : 'COUNT(DISTINCT p.topic_id) AS total_results';
 924                      $sql = (!$sql) ? $this->db->sql_build_query('SELECT', $sql_array_count) : $sql;
 925  
 926                      $result = $this->db->sql_query($sql);
 927                      $total_results = (int) $this->db->sql_fetchfield('total_results');
 928                      $this->db->sql_freeresult($result);
 929  
 930                      if (!$total_results)
 931                      {
 932                          return false;
 933                      }
 934                  break;
 935              }
 936  
 937              unset($sql_array_count, $sql);
 938          }
 939  
 940          // Build sql strings for sorting
 941          $sql_sort = $sort_by_sql[$sort_key] . (($sort_dir == 'a') ? ' ASC' : ' DESC');
 942  
 943          switch ($sql_sort[0])
 944          {
 945              case 'u':
 946                  $sql_array['FROM'][USERS_TABLE] = 'u';
 947                  $sql_where[] = 'u.user_id = p.poster_id ';
 948              break;
 949  
 950              case 't':
 951                  $left_join_topics = true;
 952              break;
 953  
 954              case 'f':
 955                  $sql_array['FROM'][FORUMS_TABLE] = 'f';
 956                  $sql_where[] = 'f.forum_id = p.forum_id';
 957              break;
 958          }
 959  
 960          if ($left_join_topics)
 961          {
 962              $sql_array['LEFT_JOIN'][] = array(
 963                  'FROM'    => array(TOPICS_TABLE => 't'),
 964                  'ON'    => 'p.topic_id = t.topic_id'
 965              );
 966          }
 967  
 968          $sql_array['WHERE'] = implode(' AND ', $sql_where);
 969          $sql_array['GROUP_BY'] = ($group_by) ? (($type == 'posts') ? 'p.post_id' : 'p.topic_id') . ', ' . $sort_by_sql[$sort_key] : '';
 970          $sql_array['ORDER_BY'] = $sql_sort;
 971          $sql_array['SELECT'] .= $sort_by_sql[$sort_key] ? ", {$sort_by_sql[$sort_key]}" : '';
 972  
 973          unset($sql_where, $sql_sort, $group_by);
 974  
 975          $sql = $this->db->sql_build_query('SELECT', $sql_array);
 976          $result = $this->db->sql_query_limit($sql, $this->config['search_block_size'], $start);
 977  
 978          while ($row = $this->db->sql_fetchrow($result))
 979          {
 980              $id_ary[] = (int) $row[(($type == 'posts') ? 'post_id' : 'topic_id')];
 981          }
 982          $this->db->sql_freeresult($result);
 983  
 984          // If using mysql and the total result count is not calculated yet, get it from the db
 985          if (!$total_results && $is_mysql)
 986          {
 987              $sql_count = str_replace("SELECT {$sql_array['SELECT']}", "SELECT COUNT({$sql_array['SELECT']}) as total_results", $sql);
 988              $result = $this->db->sql_query($sql_count);
 989              $total_results = $sql_array['GROUP_BY'] ? count($this->db->sql_fetchrowset($result)) : $this->db->sql_fetchfield('total_results');
 990              $this->db->sql_freeresult($result);
 991  
 992              if (!$total_results)
 993              {
 994                  return false;
 995              }
 996          }
 997  
 998          if ($start >= $total_results)
 999          {
1000              $start = floor(($total_results - 1) / $per_page) * $per_page;
1001  
1002              $result = $this->db->sql_query_limit($sql, $this->config['search_block_size'], $start);
1003  
1004              while ($row = $this->db->sql_fetchrow($result))
1005              {
1006                  $id_ary[] = (int) $row[(($type == 'posts') ? 'post_id' : 'topic_id')];
1007              }
1008              $this->db->sql_freeresult($result);
1009          }
1010  
1011          // store the ids, from start on then delete anything that isn't on the current page because we only need ids for one page
1012          $this->save_ids($search_key, $this->search_query, $author_ary, $total_results, $id_ary, $start, $sort_dir);
1013          $id_ary = array_slice($id_ary, 0, (int) $per_page);
1014  
1015          return $total_results;
1016      }
1017  
1018      /**
1019      * Performs a search on an author's posts without caring about message contents. Depends on display specific params
1020      *
1021      * @param    string        $type                contains either posts or topics depending on what should be searched for
1022      * @param    boolean        $firstpost_only        if true, only topic starting posts will be considered
1023      * @param    array        $sort_by_sql        contains SQL code for the ORDER BY part of a query
1024      * @param    string        $sort_key            is the key of $sort_by_sql for the selected sorting
1025      * @param    string        $sort_dir            is either a or d representing ASC and DESC
1026      * @param    string        $sort_days            specifies the maximum amount of days a post may be old
1027      * @param    array        $ex_fid_ary            specifies an array of forum ids which should not be searched
1028      * @param    string        $post_visibility    specifies which types of posts the user can view in which forums
1029      * @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
1030      * @param    array        $author_ary            an array of author ids
1031      * @param    string        $author_name        specifies the author match, when ANONYMOUS is also a search-match
1032      * @param    array        &$id_ary            passed by reference, to be filled with ids for the page specified by $start and $per_page, should be ordered
1033      * @param    int            $start                indicates the first index of the page
1034      * @param    int            $per_page            number of ids each page is supposed to contain
1035      * @return    boolean|int                        total number of results
1036      */
1037  	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)
1038      {
1039          // No author? No posts
1040          if (!count($author_ary))
1041          {
1042              return 0;
1043          }
1044  
1045          // generate a search_key from all the options to identify the results
1046          $search_key_array = array(
1047              '',
1048              $type,
1049              ($firstpost_only) ? 'firstpost' : '',
1050              '',
1051              '',
1052              $sort_days,
1053              $sort_key,
1054              $topic_id,
1055              implode(',', $ex_fid_ary),
1056              $post_visibility,
1057              implode(',', $author_ary),
1058              $author_name,
1059          );
1060  
1061          /**
1062          * Allow changing the search_key for cached results
1063          *
1064          * @event core.search_native_by_author_modify_search_key
1065          * @var    array    search_key_array    Array with search parameters to generate the search_key
1066          * @var    string    type                Searching type ('posts', 'topics')
1067          * @var    boolean    firstpost_only        Flag indicating if only topic starting posts are considered
1068          * @var    int        sort_days            Time, in days, of the oldest possible post to list
1069          * @var    string    sort_key            The sort type used from the possible sort types
1070          * @var    int        topic_id            Limit the search to this topic_id only
1071          * @var    array    ex_fid_ary            Which forums not to search on
1072          * @var    string    post_visibility        Post visibility data
1073          * @var    array    author_ary            Array of user_id containing the users to filter the results to
1074          * @var    string    author_name            The username to search on
1075          * @since 3.1.7-RC1
1076          */
1077          $vars = array(
1078              'search_key_array',
1079              'type',
1080              'firstpost_only',
1081              'sort_days',
1082              'sort_key',
1083              'topic_id',
1084              'ex_fid_ary',
1085              'post_visibility',
1086              'author_ary',
1087              'author_name',
1088          );
1089          extract($this->phpbb_dispatcher->trigger_event('core.search_native_by_author_modify_search_key', compact($vars)));
1090  
1091          $search_key = md5(implode('#', $search_key_array));
1092  
1093          // try reading the results from cache
1094          $total_results = 0;
1095          if ($this->obtain_ids($search_key, $total_results, $id_ary, $start, $per_page, $sort_dir) == SEARCH_RESULT_IN_CACHE)
1096          {
1097              return $total_results;
1098          }
1099  
1100          $id_ary = array();
1101  
1102          // Create some display specific sql strings
1103          if ($author_name)
1104          {
1105              // first one matches post of registered users, second one guests and deleted users
1106              $sql_author = '(' . $this->db->sql_in_set('p.poster_id', array_diff($author_ary, array(ANONYMOUS)), false, true) . ' OR p.post_username ' . $author_name . ')';
1107          }
1108          else
1109          {
1110              $sql_author = $this->db->sql_in_set('p.poster_id', $author_ary);
1111          }
1112          $sql_fora        = (count($ex_fid_ary)) ? ' AND ' . $this->db->sql_in_set('p.forum_id', $ex_fid_ary, true) : '';
1113          $sql_time        = ($sort_days) ? ' AND p.post_time >= ' . (time() - ($sort_days * 86400)) : '';
1114          $sql_topic_id    = ($topic_id) ? ' AND p.topic_id = ' . (int) $topic_id : '';
1115          $sql_firstpost = ($firstpost_only) ? ' AND p.post_id = t.topic_first_post_id' : '';
1116          $post_visibility = ($post_visibility) ? ' AND ' . $post_visibility : '';
1117  
1118          // Build sql strings for sorting
1119          $sql_sort = $sort_by_sql[$sort_key] . (($sort_dir == 'a') ? ' ASC' : ' DESC');
1120          $sql_sort_table = $sql_sort_join = '';
1121          switch ($sql_sort[0])
1122          {
1123              case 'u':
1124                  $sql_sort_table    = USERS_TABLE . ' u, ';
1125                  $sql_sort_join    = ' AND u.user_id = p.poster_id ';
1126              break;
1127  
1128              case 't':
1129                  $sql_sort_table    = ($type == 'posts' && !$firstpost_only) ? TOPICS_TABLE . ' t, ' : '';
1130                  $sql_sort_join    = ($type == 'posts' && !$firstpost_only) ? ' AND t.topic_id = p.topic_id ' : '';
1131              break;
1132  
1133              case 'f':
1134                  $sql_sort_table    = FORUMS_TABLE . ' f, ';
1135                  $sql_sort_join    = ' AND f.forum_id = p.forum_id ';
1136              break;
1137          }
1138  
1139          $select = ($type == 'posts') ? 'p.post_id' : 't.topic_id';
1140          $select .= $sort_by_sql[$sort_key] ? ", {$sort_by_sql[$sort_key]}" : '';
1141          $is_mysql = false;
1142  
1143          /**
1144          * Allow changing the query used to search for posts by author in fulltext_native
1145          *
1146          * @event core.search_native_author_count_query_before
1147          * @var    int        total_results        The previous result count for the format of the query.
1148          *                                    Set to 0 to force a re-count
1149          * @var    string    type                The type of search being made
1150          * @var    string    select                SQL SELECT clause for what to get
1151          * @var    string    sql_sort_table        CROSS JOIN'ed table to allow doing the sort chosen
1152          * @var    string    sql_sort_join        Condition to define how to join the CROSS JOIN'ed table specifyed in sql_sort_table
1153          * @var    array    sql_author            SQL WHERE condition for the post author ids
1154          * @var    int        topic_id            Limit the search to this topic_id only
1155          * @var    string    sort_by_sql            The possible predefined sort types
1156          * @var    string    sort_key            The sort type used from the possible sort types
1157          * @var    string    sort_dir            "a" for ASC or "d" dor DESC for the sort order used
1158          * @var    string    sql_sort            The result SQL when processing sort_by_sql + sort_key + sort_dir
1159          * @var    string    sort_days            Time, in days, that the oldest post showing can have
1160          * @var    string    sql_time            The SQL to search on the time specifyed by sort_days
1161          * @var    bool    firstpost_only        Wether or not to search only on the first post of the topics
1162          * @var    string    sql_firstpost        The SQL used in the WHERE claused to filter by firstpost.
1163          * @var    array    ex_fid_ary            Forum ids that must not be searched on
1164          * @var    array    sql_fora            SQL query for ex_fid_ary
1165          * @var    int        start                How many posts to skip in the search results (used for pagination)
1166          * @since 3.1.5-RC1
1167          */
1168          $vars = array(
1169              'total_results',
1170              'type',
1171              'select',
1172              'sql_sort_table',
1173              'sql_sort_join',
1174              'sql_author',
1175              'topic_id',
1176              'sort_by_sql',
1177              'sort_key',
1178              'sort_dir',
1179              'sql_sort',
1180              'sort_days',
1181              'sql_time',
1182              'firstpost_only',
1183              'sql_firstpost',
1184              'ex_fid_ary',
1185              'sql_fora',
1186              'start',
1187          );
1188          extract($this->phpbb_dispatcher->trigger_event('core.search_native_author_count_query_before', compact($vars)));
1189  
1190          // If the cache was completely empty count the results
1191          if (!$total_results)
1192          {
1193              switch ($this->db->get_sql_layer())
1194              {
1195                  case 'mysqli':
1196                      $is_mysql = true;
1197                  break;
1198  
1199                  default:
1200                      if ($type == 'posts')
1201                      {
1202                          $sql = 'SELECT COUNT(p.post_id) as total_results
1203                              FROM ' . POSTS_TABLE . ' p' . (($firstpost_only) ? ', ' . TOPICS_TABLE . ' t ' : ' ') . "
1204                              WHERE $sql_author
1205                                  $sql_topic_id
1206                                  $sql_firstpost
1207                                  $post_visibility
1208                                  $sql_fora
1209                                  $sql_time";
1210                      }
1211                      else
1212                      {
1213                          if ($this->db->get_sql_layer() == 'sqlite3')
1214                          {
1215                              $sql = 'SELECT COUNT(topic_id) as total_results
1216                                  FROM (SELECT DISTINCT t.topic_id';
1217                          }
1218                          else
1219                          {
1220                              $sql = 'SELECT COUNT(DISTINCT t.topic_id) as total_results';
1221                          }
1222  
1223                          $sql .= ' FROM ' . TOPICS_TABLE . ' t, ' . POSTS_TABLE . " p
1224                              WHERE $sql_author
1225                                  $sql_topic_id
1226                                  $sql_firstpost
1227                                  $post_visibility
1228                                  $sql_fora
1229                                  AND t.topic_id = p.topic_id
1230                                  $sql_time" . ($this->db->get_sql_layer() == 'sqlite3' ? ')' : '');
1231                      }
1232                      $result = $this->db->sql_query($sql);
1233  
1234                      $total_results = (int) $this->db->sql_fetchfield('total_results');
1235                      $this->db->sql_freeresult($result);
1236  
1237                      if (!$total_results)
1238                      {
1239                          return false;
1240                      }
1241                  break;
1242              }
1243          }
1244  
1245          // Build the query for really selecting the post_ids
1246          if ($type == 'posts')
1247          {
1248              $sql = "SELECT $select
1249                  FROM " . $sql_sort_table . POSTS_TABLE . ' p' . (($firstpost_only) ? ', ' . TOPICS_TABLE . ' t' : '') . "
1250                  WHERE $sql_author
1251                      $sql_topic_id
1252                      $sql_firstpost
1253                      $post_visibility
1254                      $sql_fora
1255                      $sql_sort_join
1256                      $sql_time
1257                  ORDER BY $sql_sort";
1258              $field = 'post_id';
1259          }
1260          else
1261          {
1262              $sql = "SELECT $select
1263                  FROM " . $sql_sort_table . TOPICS_TABLE . ' t, ' . POSTS_TABLE . " p
1264                  WHERE $sql_author
1265                      $sql_topic_id
1266                      $sql_firstpost
1267                      $post_visibility
1268                      $sql_fora
1269                      AND t.topic_id = p.topic_id
1270                      $sql_sort_join
1271                      $sql_time
1272                  GROUP BY t.topic_id, " . $sort_by_sql[$sort_key] . '
1273                  ORDER BY ' . $sql_sort;
1274              $field = 'topic_id';
1275          }
1276  
1277          // Only read one block of posts from the db and then cache it
1278          $result = $this->db->sql_query_limit($sql, $this->config['search_block_size'], $start);
1279  
1280          while ($row = $this->db->sql_fetchrow($result))
1281          {
1282              $id_ary[] = (int) $row[$field];
1283          }
1284          $this->db->sql_freeresult($result);
1285  
1286          if (!$total_results && $is_mysql)
1287          {
1288              $sql_count = str_replace("SELECT $select", "SELECT COUNT(*) as total_results", $sql);
1289              $result = $this->db->sql_query($sql_count);
1290              $total_results = ($type == 'posts') ? (int) $this->db->sql_fetchfield('total_results') : count($this->db->sql_fetchrowset($result));
1291              $this->db->sql_freeresult($result);
1292  
1293              if (!$total_results)
1294              {
1295                  return false;
1296              }
1297          }
1298  
1299          if ($start >= $total_results)
1300          {
1301              $start = floor(($total_results - 1) / $per_page) * $per_page;
1302  
1303              $result = $this->db->sql_query_limit($sql, $this->config['search_block_size'], $start);
1304  
1305              while ($row = $this->db->sql_fetchrow($result))
1306              {
1307                  $id_ary[] = (int) $row[$field];
1308              }
1309              $this->db->sql_freeresult($result);
1310          }
1311  
1312          if (count($id_ary))
1313          {
1314              $this->save_ids($search_key, '', $author_ary, $total_results, $id_ary, $start, $sort_dir);
1315              $id_ary = array_slice($id_ary, 0, $per_page);
1316  
1317              return $total_results;
1318          }
1319          return false;
1320      }
1321  
1322      /**
1323      * Split a text into words of a given length
1324      *
1325      * The text is converted to UTF-8, cleaned up, and split. Then, words that
1326      * conform to the defined length range are returned in an array.
1327      *
1328      * NOTE: duplicates are NOT removed from the return array
1329      *
1330      * @param    string    $text    Text to split, encoded in UTF-8
1331      * @return    array            Array of UTF-8 words
1332      */
1333  	public function split_message($text)
1334      {
1335          $match = $words = array();
1336  
1337          /**
1338          * Taken from the original code
1339          */
1340          // Do not index code
1341          $match[] = '#\[code(?:=.*?)?(\:?[0-9a-z]{5,})\].*?\[\/code(\:?[0-9a-z]{5,})\]#is';
1342          // BBcode
1343          $match[] = '#\[\/?[a-z0-9\*\+\-]+(?:=.*?)?(?::[a-z])?(\:?[0-9a-z]{5,})\]#';
1344  
1345          $min = $this->word_length['min'];
1346  
1347          $isset_min = $min - 1;
1348  
1349          /**
1350          * Clean up the string, remove HTML tags, remove BBCodes
1351          */
1352          $word = strtok($this->cleanup(preg_replace($match, ' ', strip_tags($text)), -1), ' ');
1353  
1354          while (strlen($word))
1355          {
1356              if (strlen($word) > 255 || strlen($word) <= $isset_min)
1357              {
1358                  /**
1359                  * Words longer than 255 bytes are ignored. This will have to be
1360                  * changed whenever we change the length of search_wordlist.word_text
1361                  *
1362                  * Words shorter than $isset_min bytes are ignored, too
1363                  */
1364                  $word = strtok(' ');
1365                  continue;
1366              }
1367  
1368              $len = utf8_strlen($word);
1369  
1370              /**
1371              * Test whether the word is too short to be indexed.
1372              *
1373              * Note that this limit does NOT apply to CJK and Hangul
1374              */
1375              if ($len < $min)
1376              {
1377                  /**
1378                  * Note: this could be optimized. If the codepoint is lower than Hangul's range
1379                  * we know that it will also be lower than CJK ranges
1380                  */
1381                  if ((strncmp($word, self::UTF8_HANGUL_FIRST, 3) < 0 || strncmp($word, self::UTF8_HANGUL_LAST, 3) > 0)
1382                      && (strncmp($word, self::UTF8_CJK_FIRST, 3) < 0 || strncmp($word, self::UTF8_CJK_LAST, 3) > 0)
1383                      && (strncmp($word, self::UTF8_CJK_B_FIRST, 4) < 0 || strncmp($word, self::UTF8_CJK_B_LAST, 4) > 0))
1384                  {
1385                      $word = strtok(' ');
1386                      continue;
1387                  }
1388              }
1389  
1390              $words[] = $word;
1391              $word = strtok(' ');
1392          }
1393  
1394          return $words;
1395      }
1396  
1397      /**
1398      * Updates wordlist and wordmatch tables when a message is posted or changed
1399      *
1400      * @param    string    $mode        Contains the post mode: edit, post, reply, quote
1401      * @param    int        $post_id    The id of the post which is modified/created
1402      * @param    string    &$message    New or updated post content
1403      * @param    string    &$subject    New or updated post subject
1404      * @param    int        $poster_id    Post author's user id
1405      * @param    int        $forum_id    The id of the forum in which the post is located
1406      */
1407  	public function index($mode, $post_id, &$message, &$subject, $poster_id, $forum_id)
1408      {
1409          if (!$this->config['fulltext_native_load_upd'])
1410          {
1411              /**
1412              * The search indexer is disabled, return
1413              */
1414              return;
1415          }
1416  
1417          // Split old and new post/subject to obtain array of 'words'
1418          $split_text = $this->split_message($message);
1419          $split_title = $this->split_message($subject);
1420  
1421          $cur_words = array('post' => array(), 'title' => array());
1422  
1423          $words = array();
1424          if ($mode == 'edit')
1425          {
1426              $words['add']['post'] = array();
1427              $words['add']['title'] = array();
1428              $words['del']['post'] = array();
1429              $words['del']['title'] = array();
1430  
1431              $sql = 'SELECT w.word_id, w.word_text, m.title_match
1432                  FROM ' . SEARCH_WORDLIST_TABLE . ' w, ' . SEARCH_WORDMATCH_TABLE . " m
1433                  WHERE m.post_id = $post_id
1434                      AND w.word_id = m.word_id";
1435              $result = $this->db->sql_query($sql);
1436  
1437              while ($row = $this->db->sql_fetchrow($result))
1438              {
1439                  $which = ($row['title_match']) ? 'title' : 'post';
1440                  $cur_words[$which][$row['word_text']] = $row['word_id'];
1441              }
1442              $this->db->sql_freeresult($result);
1443  
1444              $words['add']['post'] = array_diff($split_text, array_keys($cur_words['post']));
1445              $words['add']['title'] = array_diff($split_title, array_keys($cur_words['title']));
1446              $words['del']['post'] = array_diff(array_keys($cur_words['post']), $split_text);
1447              $words['del']['title'] = array_diff(array_keys($cur_words['title']), $split_title);
1448          }
1449          else
1450          {
1451              $words['add']['post'] = $split_text;
1452              $words['add']['title'] = $split_title;
1453              $words['del']['post'] = array();
1454              $words['del']['title'] = array();
1455          }
1456  
1457          /**
1458          * Event to modify method arguments and words before the native search index is updated
1459          *
1460          * @event core.search_native_index_before
1461          * @var string    mode                Contains the post mode: edit, post, reply, quote
1462          * @var int        post_id                The id of the post which is modified/created
1463          * @var string    message                New or updated post content
1464          * @var string    subject                New or updated post subject
1465          * @var int        poster_id            Post author's user id
1466          * @var int        forum_id            The id of the forum in which the post is located
1467          * @var array    words                Grouped lists of words added to or remove from the index
1468          * @var array    split_text            Array of words from the message
1469          * @var array    split_title            Array of words from the title
1470          * @var array    cur_words            Array of words currently in the index for comparing to new words
1471          *                                     when mode is edit. Empty for other modes.
1472          * @since 3.2.3-RC1
1473          */
1474          $vars = array(
1475              'mode',
1476              'post_id',
1477              'message',
1478              'subject',
1479              'poster_id',
1480              'forum_id',
1481              'words',
1482              'split_text',
1483              'split_title',
1484              'cur_words',
1485          );
1486          extract($this->phpbb_dispatcher->trigger_event('core.search_native_index_before', compact($vars)));
1487  
1488          unset($split_text);
1489          unset($split_title);
1490  
1491          // Get unique words from the above arrays
1492          $unique_add_words = array_unique(array_merge($words['add']['post'], $words['add']['title']));
1493  
1494          // We now have unique arrays of all words to be added and removed and
1495          // individual arrays of added and removed words for text and title. What
1496          // we need to do now is add the new words (if they don't already exist)
1497          // and then add (or remove) matches between the words and this post
1498          if (count($unique_add_words))
1499          {
1500              $sql = 'SELECT word_id, word_text
1501                  FROM ' . SEARCH_WORDLIST_TABLE . '
1502                  WHERE ' . $this->db->sql_in_set('word_text', $unique_add_words);
1503              $result = $this->db->sql_query($sql);
1504  
1505              $word_ids = array();
1506              while ($row = $this->db->sql_fetchrow($result))
1507              {
1508                  $word_ids[$row['word_text']] = $row['word_id'];
1509              }
1510              $this->db->sql_freeresult($result);
1511              $new_words = array_diff($unique_add_words, array_keys($word_ids));
1512  
1513              $this->db->sql_transaction('begin');
1514              if (count($new_words))
1515              {
1516                  $sql_ary = array();
1517  
1518                  foreach ($new_words as $word)
1519                  {
1520                      $sql_ary[] = array('word_text' => (string) $word, 'word_count' => 0);
1521                  }
1522                  $this->db->sql_return_on_error(true);
1523                  $this->db->sql_multi_insert(SEARCH_WORDLIST_TABLE, $sql_ary);
1524                  $this->db->sql_return_on_error(false);
1525              }
1526              unset($new_words, $sql_ary);
1527          }
1528          else
1529          {
1530              $this->db->sql_transaction('begin');
1531          }
1532  
1533          // now update the search match table, remove links to removed words and add links to new words
1534          foreach ($words['del'] as $word_in => $word_ary)
1535          {
1536              $title_match = ($word_in == 'title') ? 1 : 0;
1537  
1538              if (count($word_ary))
1539              {
1540                  $sql_in = array();
1541                  foreach ($word_ary as $word)
1542                  {
1543                      $sql_in[] = $cur_words[$word_in][$word];
1544                  }
1545  
1546                  $sql = 'DELETE FROM ' . SEARCH_WORDMATCH_TABLE . '
1547                      WHERE ' . $this->db->sql_in_set('word_id', $sql_in) . '
1548                          AND post_id = ' . intval($post_id) . "
1549                          AND title_match = $title_match";
1550                  $this->db->sql_query($sql);
1551  
1552                  $sql = 'UPDATE ' . SEARCH_WORDLIST_TABLE . '
1553                      SET word_count = word_count - 1
1554                      WHERE ' . $this->db->sql_in_set('word_id', $sql_in) . '
1555                          AND word_count > 0';
1556                  $this->db->sql_query($sql);
1557  
1558                  unset($sql_in);
1559              }
1560          }
1561  
1562          $this->db->sql_return_on_error(true);
1563          foreach ($words['add'] as $word_in => $word_ary)
1564          {
1565              $title_match = ($word_in == 'title') ? 1 : 0;
1566  
1567              if (count($word_ary))
1568              {
1569                  $sql = 'INSERT INTO ' . SEARCH_WORDMATCH_TABLE . ' (post_id, word_id, title_match)
1570                      SELECT ' . (int) $post_id . ', word_id, ' . (int) $title_match . '
1571                      FROM ' . SEARCH_WORDLIST_TABLE . '
1572                      WHERE ' . $this->db->sql_in_set('word_text', $word_ary);
1573                  $this->db->sql_query($sql);
1574  
1575                  $sql = 'UPDATE ' . SEARCH_WORDLIST_TABLE . '
1576                      SET word_count = word_count + 1
1577                      WHERE ' . $this->db->sql_in_set('word_text', $word_ary);
1578                  $this->db->sql_query($sql);
1579              }
1580          }
1581          $this->db->sql_return_on_error(false);
1582  
1583          $this->db->sql_transaction('commit');
1584  
1585          // destroy cached search results containing any of the words removed or added
1586          $this->destroy_cache(array_unique(array_merge($words['add']['post'], $words['add']['title'], $words['del']['post'], $words['del']['title'])), array($poster_id));
1587  
1588          unset($unique_add_words);
1589          unset($words);
1590          unset($cur_words);
1591      }
1592  
1593      /**
1594      * Removes entries from the wordmatch table for the specified post_ids
1595      */
1596  	public function index_remove($post_ids, $author_ids, $forum_ids)
1597      {
1598          if (count($post_ids))
1599          {
1600              $sql = 'SELECT w.word_id, w.word_text, m.title_match
1601                  FROM ' . SEARCH_WORDMATCH_TABLE . ' m, ' . SEARCH_WORDLIST_TABLE . ' w
1602                  WHERE ' . $this->db->sql_in_set('m.post_id', $post_ids) . '
1603                      AND w.word_id = m.word_id';
1604              $result = $this->db->sql_query($sql);
1605  
1606              $message_word_ids = $title_word_ids = $word_texts = array();
1607              while ($row = $this->db->sql_fetchrow($result))
1608              {
1609                  if ($row['title_match'])
1610                  {
1611                      $title_word_ids[] = $row['word_id'];
1612                  }
1613                  else
1614                  {
1615                      $message_word_ids[] = $row['word_id'];
1616                  }
1617                  $word_texts[] = $row['word_text'];
1618              }
1619              $this->db->sql_freeresult($result);
1620  
1621              if (count($title_word_ids))
1622              {
1623                  $sql = 'UPDATE ' . SEARCH_WORDLIST_TABLE . '
1624                      SET word_count = word_count - 1
1625                      WHERE ' . $this->db->sql_in_set('word_id', $title_word_ids) . '
1626                          AND word_count > 0';
1627                  $this->db->sql_query($sql);
1628              }
1629  
1630              if (count($message_word_ids))
1631              {
1632                  $sql = 'UPDATE ' . SEARCH_WORDLIST_TABLE . '
1633                      SET word_count = word_count - 1
1634                      WHERE ' . $this->db->sql_in_set('word_id', $message_word_ids) . '
1635                          AND word_count > 0';
1636                  $this->db->sql_query($sql);
1637              }
1638  
1639              unset($title_word_ids);
1640              unset($message_word_ids);
1641  
1642              $sql = 'DELETE FROM ' . SEARCH_WORDMATCH_TABLE . '
1643                  WHERE ' . $this->db->sql_in_set('post_id', $post_ids);
1644              $this->db->sql_query($sql);
1645          }
1646  
1647          $this->destroy_cache(array_unique($word_texts), array_unique($author_ids));
1648      }
1649  
1650      /**
1651      * Tidy up indexes: Tag 'common words' and remove
1652      * words no longer referenced in the match table
1653      */
1654  	public function tidy()
1655      {
1656          // Is the fulltext indexer disabled? If yes then we need not
1657          // carry on ... it's okay ... I know when I'm not wanted boo hoo
1658          if (!$this->config['fulltext_native_load_upd'])
1659          {
1660              $this->config->set('search_last_gc', time(), false);
1661              return;
1662          }
1663  
1664          $destroy_cache_words = array();
1665  
1666          // Remove common words
1667          if ($this->config['num_posts'] >= 100 && $this->config['fulltext_native_common_thres'])
1668          {
1669              $common_threshold = ((double) $this->config['fulltext_native_common_thres']) / 100.0;
1670              // First, get the IDs of common words
1671              $sql = 'SELECT word_id, word_text
1672                  FROM ' . SEARCH_WORDLIST_TABLE . '
1673                  WHERE word_count > ' . floor($this->config['num_posts'] * $common_threshold) . '
1674                      OR word_common = 1';
1675              $result = $this->db->sql_query($sql);
1676  
1677              $sql_in = array();
1678              while ($row = $this->db->sql_fetchrow($result))
1679              {
1680                  $sql_in[] = $row['word_id'];
1681                  $destroy_cache_words[] = $row['word_text'];
1682              }
1683              $this->db->sql_freeresult($result);
1684  
1685              if (count($sql_in))
1686              {
1687                  // Flag the words
1688                  $sql = 'UPDATE ' . SEARCH_WORDLIST_TABLE . '
1689                      SET word_common = 1
1690                      WHERE ' . $this->db->sql_in_set('word_id', $sql_in);
1691                  $this->db->sql_query($sql);
1692  
1693                  // by setting search_last_gc to the new time here we make sure that if a user reloads because the
1694                  // following query takes too long, he won't run into it again
1695                  $this->config->set('search_last_gc', time(), false);
1696  
1697                  // Delete the matches
1698                  $sql = 'DELETE FROM ' . SEARCH_WORDMATCH_TABLE . '
1699                      WHERE ' . $this->db->sql_in_set('word_id', $sql_in);
1700                  $this->db->sql_query($sql);
1701              }
1702              unset($sql_in);
1703          }
1704  
1705          if (count($destroy_cache_words))
1706          {
1707              // destroy cached search results containing any of the words that are now common or were removed
1708              $this->destroy_cache(array_unique($destroy_cache_words));
1709          }
1710  
1711          $this->config->set('search_last_gc', time(), false);
1712      }
1713  
1714      /**
1715      * Deletes all words from the index
1716      */
1717  	public function delete_index($acp_module, $u_action)
1718      {
1719          $sql_queries = [];
1720  
1721          switch ($this->db->get_sql_layer())
1722          {
1723              case 'sqlite3':
1724                  $sql_queries[] = 'DELETE FROM ' . SEARCH_WORDLIST_TABLE;
1725                  $sql_queries[] = 'DELETE FROM ' . SEARCH_WORDMATCH_TABLE;
1726                  $sql_queries[] = 'DELETE FROM ' . SEARCH_RESULTS_TABLE;
1727              break;
1728  
1729              default:
1730                  $sql_queries[] = 'TRUNCATE TABLE ' . SEARCH_WORDLIST_TABLE;
1731                  $sql_queries[] = 'TRUNCATE TABLE ' . SEARCH_WORDMATCH_TABLE;
1732                  $sql_queries[] = 'TRUNCATE TABLE ' . SEARCH_RESULTS_TABLE;
1733              break;
1734          }
1735  
1736          $stats = $this->stats;
1737  
1738          /**
1739          * Event to modify SQL queries before the native search index is deleted
1740          *
1741          * @event core.search_native_delete_index_before
1742          * @var array    sql_queries            Array with queries for deleting the search index
1743          * @var array    stats                Array with statistics of the current index (read only)
1744          * @since 3.2.3-RC1
1745          */
1746          $vars = array(
1747              'sql_queries',
1748              'stats',
1749          );
1750          extract($this->phpbb_dispatcher->trigger_event('core.search_native_delete_index_before', compact($vars)));
1751  
1752          foreach ($sql_queries as $sql_query)
1753          {
1754              $this->db->sql_query($sql_query);
1755          }
1756      }
1757  
1758      /**
1759      * Returns true if both FULLTEXT indexes exist
1760      */
1761  	public function index_created()
1762      {
1763          if (!count($this->stats))
1764          {
1765              $this->get_stats();
1766          }
1767  
1768          return ($this->stats['total_words'] && $this->stats['total_matches']) ? true : false;
1769      }
1770  
1771      /**
1772      * Returns an associative array containing information about the indexes
1773      */
1774  	public function index_stats()
1775      {
1776          if (!count($this->stats))
1777          {
1778              $this->get_stats();
1779          }
1780  
1781          return array(
1782              $this->user->lang['TOTAL_WORDS']        => $this->stats['total_words'],
1783              $this->user->lang['TOTAL_MATCHES']    => $this->stats['total_matches']);
1784      }
1785  
1786  	protected function get_stats()
1787      {
1788          $this->stats['total_words']        = $this->db->get_estimated_row_count(SEARCH_WORDLIST_TABLE);
1789          $this->stats['total_matches']    = $this->db->get_estimated_row_count(SEARCH_WORDMATCH_TABLE);
1790      }
1791  
1792      /**
1793      * Clean up a text to remove non-alphanumeric characters
1794      *
1795      * This method receives a UTF-8 string, normalizes and validates it, replaces all
1796      * non-alphanumeric characters with strings then returns the result.
1797      *
1798      * Any number of "allowed chars" can be passed as a UTF-8 string in NFC.
1799      *
1800      * @param    string    $text            Text to split, in UTF-8 (not normalized or sanitized)
1801      * @param    string    $allowed_chars    String of special chars to allow
1802      * @param    string    $encoding        Text encoding
1803      * @return    string                    Cleaned up text, only alphanumeric chars are left
1804      */
1805  	protected function cleanup($text, $allowed_chars = null, $encoding = 'utf-8')
1806      {
1807          static $conv = array(), $conv_loaded = array();
1808          $allow = array();
1809  
1810          // Convert the text to UTF-8
1811          $encoding = strtolower($encoding);
1812          if ($encoding != 'utf-8')
1813          {
1814              $text = utf8_recode($text, $encoding);
1815          }
1816  
1817          $utf_len_mask = array(
1818              "\xC0"    =>    2,
1819              "\xD0"    =>    2,
1820              "\xE0"    =>    3,
1821              "\xF0"    =>    4
1822          );
1823  
1824          /**
1825          * Replace HTML entities and NCRs
1826          */
1827          $text = html_entity_decode(utf8_decode_ncr($text), ENT_QUOTES);
1828  
1829          /**
1830          * Normalize to NFC
1831          */
1832          $text = \Normalizer::normalize($text);
1833  
1834          /**
1835          * The first thing we do is:
1836          *
1837          * - convert ASCII-7 letters to lowercase
1838          * - remove the ASCII-7 non-alpha characters
1839          * - remove the bytes that should not appear in a valid UTF-8 string: 0xC0,
1840          *   0xC1 and 0xF5-0xFF
1841          *
1842          * @todo in theory, the third one is already taken care of during normalization and those chars should have been replaced by Unicode replacement chars
1843          */
1844          $sb_match    = "ISTCPAMELRDOJBNHFGVWUQKYXZ\r\n\t!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0B\x0C\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F\xC0\xC1\xF5\xF6\xF7\xF8\xF9\xFA\xFB\xFC\xFD\xFE\xFF";
1845          $sb_replace    = 'istcpamelrdojbnhfgvwuqkyxz                                                                              ';
1846  
1847          /**
1848          * This is the list of legal ASCII chars, it is automatically extended
1849          * with ASCII chars from $allowed_chars
1850          */
1851          $legal_ascii = ' eaisntroludcpmghbfvq10xy2j9kw354867z';
1852  
1853          /**
1854          * Prepare an array containing the extra chars to allow
1855          */
1856          if (isset($allowed_chars[0]))
1857          {
1858              $pos = 0;
1859              $len = strlen($allowed_chars);
1860              do
1861              {
1862                  $c = $allowed_chars[$pos];
1863  
1864                  if ($c < "\x80")
1865                  {
1866                      /**
1867                      * ASCII char
1868                      */
1869                      $sb_pos = strpos($sb_match, $c);
1870                      if (is_int($sb_pos))
1871                      {
1872                          /**
1873                          * Remove the char from $sb_match and its corresponding
1874                          * replacement in $sb_replace
1875                          */
1876                          $sb_match = substr($sb_match, 0, $sb_pos) . substr($sb_match, $sb_pos + 1);
1877                          $sb_replace = substr($sb_replace, 0, $sb_pos) . substr($sb_replace, $sb_pos + 1);
1878                          $legal_ascii .= $c;
1879                      }
1880  
1881                      ++$pos;
1882                  }
1883                  else
1884                  {
1885                      /**
1886                      * UTF-8 char
1887                      */
1888                      $utf_len = $utf_len_mask[$c & "\xF0"];
1889                      $allow[substr($allowed_chars, $pos, $utf_len)] = 1;
1890                      $pos += $utf_len;
1891                  }
1892              }
1893              while ($pos < $len);
1894          }
1895  
1896          $text = strtr($text, $sb_match, $sb_replace);
1897          $ret = '';
1898  
1899          $pos = 0;
1900          $len = strlen($text);
1901  
1902          do
1903          {
1904              /**
1905              * Do all consecutive ASCII chars at once
1906              */
1907              if ($spn = strspn($text, $legal_ascii, $pos))
1908              {
1909                  $ret .= substr($text, $pos, $spn);
1910                  $pos += $spn;
1911              }
1912  
1913              if ($pos >= $len)
1914              {
1915                  return $ret;
1916              }
1917  
1918              /**
1919              * Capture the UTF char
1920              */
1921              $utf_len = $utf_len_mask[$text[$pos] & "\xF0"];
1922              $utf_char = substr($text, $pos, $utf_len);
1923              $pos += $utf_len;
1924  
1925              if (($utf_char >= self::UTF8_HANGUL_FIRST && $utf_char <= self::UTF8_HANGUL_LAST)
1926                  || ($utf_char >= self::UTF8_CJK_FIRST && $utf_char <= self::UTF8_CJK_LAST)
1927                  || ($utf_char >= self::UTF8_CJK_B_FIRST && $utf_char <= self::UTF8_CJK_B_LAST))
1928              {
1929                  /**
1930                  * All characters within these ranges are valid
1931                  *
1932                  * We separate them with a space in order to index each character
1933                  * individually
1934                  */
1935                  $ret .= ' ' . $utf_char . ' ';
1936                  continue;
1937              }
1938  
1939              if (isset($allow[$utf_char]))
1940              {
1941                  /**
1942                  * The char is explicitly allowed
1943                  */
1944                  $ret .= $utf_char;
1945                  continue;
1946              }
1947  
1948              if (isset($conv[$utf_char]))
1949              {
1950                  /**
1951                  * The char is mapped to something, maybe to itself actually
1952                  */
1953                  $ret .= $conv[$utf_char];
1954                  continue;
1955              }
1956  
1957              /**
1958              * The char isn't mapped, but did we load its conversion table?
1959              *
1960              * The search indexer table is split into blocks. The block number of
1961              * each char is equal to its codepoint right-shifted for 11 bits. It
1962              * means that out of the 11, 16 or 21 meaningful bits of a 2-, 3- or
1963              * 4- byte sequence we only keep the leftmost 0, 5 or 10 bits. Thus,
1964              * all UTF chars encoded in 2 bytes are in the same first block.
1965              */
1966              if (isset($utf_char[2]))
1967              {
1968                  if (isset($utf_char[3]))
1969                  {
1970                      /**
1971                      * 1111 0nnn 10nn nnnn 10nx xxxx 10xx xxxx
1972                      * 0000 0111 0011 1111 0010 0000
1973                      */
1974                      $idx = ((ord($utf_char[0]) & 0x07) << 7) | ((ord($utf_char[1]) & 0x3F) << 1) | ((ord($utf_char[2]) & 0x20) >> 5);
1975                  }
1976                  else
1977                  {
1978                      /**
1979                      * 1110 nnnn 10nx xxxx 10xx xxxx
1980                      * 0000 0111 0010 0000
1981                      */
1982                      $idx = ((ord($utf_char[0]) & 0x07) << 1) | ((ord($utf_char[1]) & 0x20) >> 5);
1983                  }
1984              }
1985              else
1986              {
1987                  /**
1988                  * 110x xxxx 10xx xxxx
1989                  * 0000 0000 0000 0000
1990                  */
1991                  $idx = 0;
1992              }
1993  
1994              /**
1995              * Check if the required conv table has been loaded already
1996              */
1997              if (!isset($conv_loaded[$idx]))
1998              {
1999                  $conv_loaded[$idx] = 1;
2000                  $file = $this->phpbb_root_path . 'includes/utf/data/search_indexer_' . $idx . '.' . $this->php_ext;
2001  
2002                  if (file_exists($file))
2003                  {
2004                      $conv += include($file);
2005                  }
2006              }
2007  
2008              if (isset($conv[$utf_char]))
2009              {
2010                  $ret .= $conv[$utf_char];
2011              }
2012              else
2013              {
2014                  /**
2015                  * We add an entry to the conversion table so that we
2016                  * don't have to convert to codepoint and perform the checks
2017                  * that are above this block
2018                  */
2019                  $conv[$utf_char] = ' ';
2020                  $ret .= ' ';
2021              }
2022          }
2023          while (1);
2024  
2025          return $ret;
2026      }
2027  
2028      /**
2029      * Returns a list of options for the ACP to display
2030      */
2031  	public function acp()
2032      {
2033          /**
2034          * if we need any options, copied from fulltext_native for now, will have to be adjusted or removed
2035          */
2036  
2037          $tpl = '
2038          <dl>
2039              <dt><label for="fulltext_native_load_upd">' . $this->user->lang['YES_SEARCH_UPDATE'] . $this->user->lang['COLON'] . '</label><br /><span>' . $this->user->lang['YES_SEARCH_UPDATE_EXPLAIN'] . '</span></dt>
2040              <dd><label><input type="radio" id="fulltext_native_load_upd" name="config[fulltext_native_load_upd]" value="1"' . (($this->config['fulltext_native_load_upd']) ? ' checked="checked"' : '') . ' class="radio" /> ' . $this->user->lang['YES'] . '</label><label><input type="radio" name="config[fulltext_native_load_upd]" value="0"' . ((!$this->config['fulltext_native_load_upd']) ? ' checked="checked"' : '') . ' class="radio" /> ' . $this->user->lang['NO'] . '</label></dd>
2041          </dl>
2042          <dl>
2043              <dt><label for="fulltext_native_min_chars">' . $this->user->lang['MIN_SEARCH_CHARS'] . $this->user->lang['COLON'] . '</label><br /><span>' . $this->user->lang['MIN_SEARCH_CHARS_EXPLAIN'] . '</span></dt>
2044              <dd><input id="fulltext_native_min_chars" type="number" min="0" max="255" name="config[fulltext_native_min_chars]" value="' . (int) $this->config['fulltext_native_min_chars'] . '" /></dd>
2045          </dl>
2046          <dl>
2047              <dt><label for="fulltext_native_max_chars">' . $this->user->lang['MAX_SEARCH_CHARS'] . $this->user->lang['COLON'] . '</label><br /><span>' . $this->user->lang['MAX_SEARCH_CHARS_EXPLAIN'] . '</span></dt>
2048              <dd><input id="fulltext_native_max_chars" type="number" min="0" max="255" name="config[fulltext_native_max_chars]" value="' . (int) $this->config['fulltext_native_max_chars'] . '" /></dd>
2049          </dl>
2050          <dl>
2051              <dt><label for="fulltext_native_common_thres">' . $this->user->lang['COMMON_WORD_THRESHOLD'] . $this->user->lang['COLON'] . '</label><br /><span>' . $this->user->lang['COMMON_WORD_THRESHOLD_EXPLAIN'] . '</span></dt>
2052              <dd><input id="fulltext_native_common_thres" type="text" name="config[fulltext_native_common_thres]" value="' . (double) $this->config['fulltext_native_common_thres'] . '" /> %</dd>
2053          </dl>
2054          ';
2055  
2056          // These are fields required in the config table
2057          return array(
2058              'tpl'        => $tpl,
2059              'config'    => array('fulltext_native_load_upd' => 'bool', 'fulltext_native_min_chars' => 'integer:0:255', 'fulltext_native_max_chars' => 'integer:0:255', 'fulltext_native_common_thres' => 'double:0:100')
2060          );
2061      }
2062  }


Generated: Sat Nov 4 14:26:03 2023 Cross-referenced by PHPXref 0.7.1