[ Index ]

PHP Cross Reference of phpBB-3.2.8-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    \phpbb\event\dispatcher_interface    $phpbb_dispatcher    Event dispatcher object
 113      */
 114  	public function __construct(&$error, $phpbb_root_path, $phpEx, $auth, $config, $db, $user, $phpbb_dispatcher)
 115      {
 116          $this->phpbb_root_path = $phpbb_root_path;
 117          $this->php_ext = $phpEx;
 118          $this->config = $config;
 119          $this->db = $db;
 120          $this->phpbb_dispatcher = $phpbb_dispatcher;
 121          $this->user = $user;
 122  
 123          $this->word_length = array('min' => (int) $this->config['fulltext_native_min_chars'], 'max' => (int) $this->config['fulltext_native_max_chars']);
 124  
 125          /**
 126          * Load the UTF tools
 127          */
 128          if (!function_exists('utf8_decode_ncr'))
 129          {
 130              include($this->phpbb_root_path . 'includes/utf/utf_tools.' . $this->php_ext);
 131          }
 132  
 133          $error = false;
 134      }
 135  
 136      /**
 137      * Returns the name of this search backend to be displayed to administrators
 138      *
 139      * @return string Name
 140      */
 141  	public function get_name()
 142      {
 143          return 'phpBB Native Fulltext';
 144      }
 145  
 146      /**
 147       * Returns the search_query
 148       *
 149       * @return string search query
 150       */
 151  	public function get_search_query()
 152      {
 153          return $this->search_query;
 154      }
 155  
 156      /**
 157       * Returns the common_words array
 158       *
 159       * @return array common words that are ignored by search backend
 160       */
 161  	public function get_common_words()
 162      {
 163          return $this->common_words;
 164      }
 165  
 166      /**
 167       * Returns the word_length array
 168       *
 169       * @return array min and max word length for searching
 170       */
 171  	public function get_word_length()
 172      {
 173          return $this->word_length;
 174      }
 175  
 176      /**
 177      * This function fills $this->search_query with the cleaned user search query
 178      *
 179      * If $terms is 'any' then the words will be extracted from the search query
 180      * and combined with | inside brackets. They will afterwards be treated like
 181      * an standard search query.
 182      *
 183      * Then it analyses the query and fills the internal arrays $must_not_contain_ids,
 184      * $must_contain_ids and $must_exclude_one_ids which are later used by keyword_search()
 185      *
 186      * @param    string    $keywords    contains the search query string as entered by the user
 187      * @param    string    $terms        is either 'all' (use search query as entered, default words to 'must be contained in post')
 188      *     or 'any' (find all posts containing at least one of the given words)
 189      * @return    boolean                false if no valid keywords were found and otherwise true
 190      */
 191  	public function split_keywords($keywords, $terms)
 192      {
 193          $tokens = '+-|()* ';
 194  
 195          $keywords = trim($this->cleanup($keywords, $tokens));
 196  
 197          // allow word|word|word without brackets
 198          if ((strpos($keywords, ' ') === false) && (strpos($keywords, '|') !== false) && (strpos($keywords, '(') === false))
 199          {
 200              $keywords = '(' . $keywords . ')';
 201          }
 202  
 203          $open_bracket = $space = false;
 204          for ($i = 0, $n = strlen($keywords); $i < $n; $i++)
 205          {
 206              if ($open_bracket !== false)
 207              {
 208                  switch ($keywords[$i])
 209                  {
 210                      case ')':
 211                          if ($open_bracket + 1 == $i)
 212                          {
 213                              $keywords[$i - 1] = '|';
 214                              $keywords[$i] = '|';
 215                          }
 216                          $open_bracket = false;
 217                      break;
 218                      case '(':
 219                          $keywords[$i] = '|';
 220                      break;
 221                      case '+':
 222                      case '-':
 223                      case ' ':
 224                          $keywords[$i] = '|';
 225                      break;
 226                      case '*':
 227                          // $i can never be 0 here since $open_bracket is initialised to false
 228                          if (strpos($tokens, $keywords[$i - 1]) !== false && ($i + 1 === $n || strpos($tokens, $keywords[$i + 1]) !== false))
 229                          {
 230                              $keywords[$i] = '|';
 231                          }
 232                      break;
 233                  }
 234              }
 235              else
 236              {
 237                  switch ($keywords[$i])
 238                  {
 239                      case ')':
 240                          $keywords[$i] = ' ';
 241                      break;
 242                      case '(':
 243                          $open_bracket = $i;
 244                          $space = false;
 245                      break;
 246                      case '|':
 247                          $keywords[$i] = ' ';
 248                      break;
 249                      case '-':
 250                      case '+':
 251                          $space = $keywords[$i];
 252                      break;
 253                      case ' ':
 254                          if ($space !== false)
 255                          {
 256                              $keywords[$i] = $space;
 257                          }
 258                      break;
 259                      default:
 260                          $space = false;
 261                  }
 262              }
 263          }
 264  
 265          if ($open_bracket !== false)
 266          {
 267              $keywords .= ')';
 268          }
 269  
 270          $match = array(
 271              '#  +#',
 272              '#\|\|+#',
 273              '#(\+|\-)(?:\+|\-)+#',
 274              '#\(\|#',
 275              '#\|\)#',
 276          );
 277          $replace = array(
 278              ' ',
 279              '|',
 280              '$1',
 281              '(',
 282              ')',
 283          );
 284  
 285          $keywords = preg_replace($match, $replace, $keywords);
 286          $num_keywords = count(explode(' ', $keywords));
 287  
 288          // We limit the number of allowed keywords to minimize load on the database
 289          if ($this->config['max_num_search_keywords'] && $num_keywords > $this->config['max_num_search_keywords'])
 290          {
 291              trigger_error($this->user->lang('MAX_NUM_SEARCH_KEYWORDS_REFINE', (int) $this->config['max_num_search_keywords'], $num_keywords));
 292          }
 293  
 294          // $keywords input format: each word separated by a space, words in a bracket are not separated
 295  
 296          // the user wants to search for any word, convert the search query
 297          if ($terms == 'any')
 298          {
 299              $words = array();
 300  
 301              preg_match_all('#([^\\s+\\-|()]+)(?:$|[\\s+\\-|()])#u', $keywords, $words);
 302              if (count($words[1]))
 303              {
 304                  $keywords = '(' . implode('|', $words[1]) . ')';
 305              }
 306          }
 307  
 308          // Remove non trailing wildcards from each word to prevent a full table scan (it's now using the database index)
 309          $match = '#\*(?!$|\s)#';
 310          $replace = '$1';
 311          $keywords = preg_replace($match, $replace, $keywords);
 312  
 313          // Only allow one wildcard in the search query to limit the database load
 314          $match = '#\*#';
 315          $replace = '$1';
 316          $count_wildcards = substr_count($keywords, '*');
 317  
 318          // Reverse the string to remove all wildcards except the first one
 319          $keywords = strrev(preg_replace($match, $replace, strrev($keywords), $count_wildcards - 1));
 320          unset($count_wildcards);
 321  
 322          // set the search_query which is shown to the user
 323          $this->search_query = $keywords;
 324  
 325          $exact_words = array();
 326          preg_match_all('#([^\\s+\\-|()]+)(?:$|[\\s+\\-|()])#u', $keywords, $exact_words);
 327          $exact_words = $exact_words[1];
 328  
 329          $common_ids = $words = array();
 330  
 331          if (count($exact_words))
 332          {
 333              $sql = 'SELECT word_id, word_text, word_common
 334                  FROM ' . SEARCH_WORDLIST_TABLE . '
 335                  WHERE ' . $this->db->sql_in_set('word_text', $exact_words) . '
 336                  ORDER BY word_count ASC';
 337              $result = $this->db->sql_query($sql);
 338  
 339              // store an array of words and ids, remove common words
 340              while ($row = $this->db->sql_fetchrow($result))
 341              {
 342                  if ($row['word_common'])
 343                  {
 344                      $this->common_words[] = $row['word_text'];
 345                      $common_ids[$row['word_text']] = (int) $row['word_id'];
 346                      continue;
 347                  }
 348  
 349                  $words[$row['word_text']] = (int) $row['word_id'];
 350              }
 351              $this->db->sql_freeresult($result);
 352          }
 353  
 354          // Handle +, - without preceeding whitespace character
 355          $match        = array('#(\S)\+#', '#(\S)-#');
 356          $replace    = array('$1 +', '$1 +');
 357  
 358          $keywords = preg_replace($match, $replace, $keywords);
 359  
 360          // now analyse the search query, first split it using the spaces
 361          $query = explode(' ', $keywords);
 362  
 363          $this->must_contain_ids = array();
 364          $this->must_not_contain_ids = array();
 365          $this->must_exclude_one_ids = array();
 366  
 367          foreach ($query as $word)
 368          {
 369              if (empty($word))
 370              {
 371                  continue;
 372              }
 373  
 374              // words which should not be included
 375              if ($word[0] == '-')
 376              {
 377                  $word = substr($word, 1);
 378  
 379                  // a group of which at least one may not be in the resulting posts
 380                  if ($word[0] == '(')
 381                  {
 382                      $word = array_unique(explode('|', substr($word, 1, -1)));
 383                      $mode = 'must_exclude_one';
 384                  }
 385                  // one word which should not be in the resulting posts
 386                  else
 387                  {
 388                      $mode = 'must_not_contain';
 389                  }
 390                  $ignore_no_id = true;
 391              }
 392              // words which have to be included
 393              else
 394              {
 395                  // no prefix is the same as a +prefix
 396                  if ($word[0] == '+')
 397                  {
 398                      $word = substr($word, 1);
 399                  }
 400  
 401                  // a group of words of which at least one word should be in every resulting post
 402                  if ($word[0] == '(')
 403                  {
 404                      $word = array_unique(explode('|', substr($word, 1, -1)));
 405                  }
 406                  $ignore_no_id = false;
 407                  $mode = 'must_contain';
 408              }
 409  
 410              if (empty($word))
 411              {
 412                  continue;
 413              }
 414  
 415              // if this is an array of words then retrieve an id for each
 416              if (is_array($word))
 417              {
 418                  $non_common_words = array();
 419                  $id_words = array();
 420                  foreach ($word as $i => $word_part)
 421                  {
 422                      if (strpos($word_part, '*') !== false)
 423                      {
 424                          $len = utf8_strlen(str_replace('*', '', $word_part));
 425                          if ($len >= $this->word_length['min'] && $len <= $this->word_length['max'])
 426                          {
 427                              $id_words[] = '\'' . $this->db->sql_escape(str_replace('*', '%', $word_part)) . '\'';
 428                              $non_common_words[] = $word_part;
 429                          }
 430                          else
 431                          {
 432                              $this->common_words[] = $word_part;
 433                          }
 434                      }
 435                      else if (isset($words[$word_part]))
 436                      {
 437                          $id_words[] = $words[$word_part];
 438                          $non_common_words[] = $word_part;
 439                      }
 440                      else
 441                      {
 442                          $len = utf8_strlen($word_part);
 443                          if ($len < $this->word_length['min'] || $len > $this->word_length['max'])
 444                          {
 445                              $this->common_words[] = $word_part;
 446                          }
 447                      }
 448                  }
 449                  if (count($id_words))
 450                  {
 451                      sort($id_words);
 452                      if (count($id_words) > 1)
 453                      {
 454                          $this->{$mode . '_ids'}[] = $id_words;
 455                      }
 456                      else
 457                      {
 458                          $mode = ($mode == 'must_exclude_one') ? 'must_not_contain' : $mode;
 459                          $this->{$mode . '_ids'}[] = $id_words[0];
 460                      }
 461                  }
 462                  // throw an error if we shall not ignore unexistant words
 463                  else if (!$ignore_no_id && count($non_common_words))
 464                  {
 465                      trigger_error(sprintf($this->user->lang['WORDS_IN_NO_POST'], implode($this->user->lang['COMMA_SEPARATOR'], $non_common_words)));
 466                  }
 467                  unset($non_common_words);
 468              }
 469              // else we only need one id
 470              else if (($wildcard = strpos($word, '*') !== false) || isset($words[$word]))
 471              {
 472                  if ($wildcard)
 473                  {
 474                      $len = utf8_strlen(str_replace('*', '', $word));
 475                      if ($len >= $this->word_length['min'] && $len <= $this->word_length['max'])
 476                      {
 477                          $this->{$mode . '_ids'}[] = '\'' . $this->db->sql_escape(str_replace('*', '%', $word)) . '\'';
 478                      }
 479                      else
 480                      {
 481                          $this->common_words[] = $word;
 482                      }
 483                  }
 484                  else
 485                  {
 486                      $this->{$mode . '_ids'}[] = $words[$word];
 487                  }
 488              }
 489              else
 490              {
 491                  if (!isset($common_ids[$word]))
 492                  {
 493                      $len = utf8_strlen($word);
 494                      if ($len < $this->word_length['min'] || $len > $this->word_length['max'])
 495                      {
 496                          $this->common_words[] = $word;
 497                      }
 498                  }
 499              }
 500          }
 501  
 502          // Return true if all words are not common words
 503          if (count($exact_words) - count($this->common_words) > 0)
 504          {
 505              return true;
 506          }
 507          return false;
 508      }
 509  
 510      /**
 511      * Performs a search on keywords depending on display specific params. You have to run split_keywords() first
 512      *
 513      * @param    string        $type                contains either posts or topics depending on what should be searched for
 514      * @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)
 515      * @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)
 516      * @param    array        $sort_by_sql        contains SQL code for the ORDER BY part of a query
 517      * @param    string        $sort_key            is the key of $sort_by_sql for the selected sorting
 518      * @param    string        $sort_dir            is either a or d representing ASC and DESC
 519      * @param    string        $sort_days            specifies the maximum amount of days a post may be old
 520      * @param    array        $ex_fid_ary            specifies an array of forum ids which should not be searched
 521      * @param    string        $post_visibility    specifies which types of posts the user can view in which forums
 522      * @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
 523      * @param    array        $author_ary            an array of author ids if the author should be ignored during the search the array is empty
 524      * @param    string        $author_name        specifies the author match, when ANONYMOUS is also a search-match
 525      * @param    array        &$id_ary            passed by reference, to be filled with ids for the page specified by $start and $per_page, should be ordered
 526      * @param    int            $start                indicates the first index of the page
 527      * @param    int            $per_page            number of ids each page is supposed to contain
 528      * @return    boolean|int                        total number of results
 529      */
 530  	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)
 531      {
 532          // No keywords? No posts.
 533          if (empty($this->search_query))
 534          {
 535              return false;
 536          }
 537  
 538          // we can't search for negatives only
 539          if (empty($this->must_contain_ids))
 540          {
 541              return false;
 542          }
 543  
 544          $must_contain_ids = $this->must_contain_ids;
 545          $must_not_contain_ids = $this->must_not_contain_ids;
 546          $must_exclude_one_ids = $this->must_exclude_one_ids;
 547  
 548          sort($must_contain_ids);
 549          sort($must_not_contain_ids);
 550          sort($must_exclude_one_ids);
 551  
 552          // generate a search_key from all the options to identify the results
 553          $search_key_array = array(
 554              serialize($must_contain_ids),
 555              serialize($must_not_contain_ids),
 556              serialize($must_exclude_one_ids),
 557              $type,
 558              $fields,
 559              $terms,
 560              $sort_days,
 561              $sort_key,
 562              $topic_id,
 563              implode(',', $ex_fid_ary),
 564              $post_visibility,
 565              implode(',', $author_ary),
 566              $author_name,
 567          );
 568  
 569          /**
 570          * Allow changing the search_key for cached results
 571          *
 572          * @event core.search_native_by_keyword_modify_search_key
 573          * @var    array    search_key_array    Array with search parameters to generate the search_key
 574          * @var    array    must_contain_ids    Array with post ids of posts containing words that are to be included
 575          * @var    array    must_not_contain_ids    Array with post ids of posts containing words that should not be included
 576          * @var    array    must_exclude_one_ids    Array with post ids of posts containing at least one word that needs to be excluded
 577          * @var    string    type                Searching type ('posts', 'topics')
 578          * @var    string    fields                Searching fields ('titleonly', 'msgonly', 'firstpost', 'all')
 579          * @var    string    terms                Searching terms ('all', 'any')
 580          * @var    int        sort_days            Time, in days, of the oldest possible post to list
 581          * @var    string    sort_key            The sort type used from the possible sort types
 582          * @var    int        topic_id            Limit the search to this topic_id only
 583          * @var    array    ex_fid_ary            Which forums not to search on
 584          * @var    string    post_visibility        Post visibility data
 585          * @var    array    author_ary            Array of user_id containing the users to filter the results to
 586          * @since 3.1.7-RC1
 587          */
 588          $vars = array(
 589              'search_key_array',
 590              'must_contain_ids',
 591              'must_not_contain_ids',
 592              'must_exclude_one_ids',
 593              'type',
 594              'fields',
 595              'terms',
 596              'sort_days',
 597              'sort_key',
 598              'topic_id',
 599              'ex_fid_ary',
 600              'post_visibility',
 601              'author_ary',
 602          );
 603          extract($this->phpbb_dispatcher->trigger_event('core.search_native_by_keyword_modify_search_key', compact($vars)));
 604  
 605          $search_key = md5(implode('#', $search_key_array));
 606  
 607          // try reading the results from cache
 608          $total_results = 0;
 609          if ($this->obtain_ids($search_key, $total_results, $id_ary, $start, $per_page, $sort_dir) == SEARCH_RESULT_IN_CACHE)
 610          {
 611              return $total_results;
 612          }
 613  
 614          $id_ary = array();
 615  
 616          $sql_where = array();
 617          $m_num = 0;
 618          $w_num = 0;
 619  
 620          $sql_array = array(
 621              'SELECT'    => ($type == 'posts') ? 'p.post_id' : 'p.topic_id',
 622              'FROM'        => array(
 623                  SEARCH_WORDMATCH_TABLE    => array(),
 624                  SEARCH_WORDLIST_TABLE    => array(),
 625              ),
 626              'LEFT_JOIN' => array(array(
 627                  'FROM'    => array(POSTS_TABLE => 'p'),
 628                  'ON'    => 'm0.post_id = p.post_id',
 629              )),
 630          );
 631  
 632          $title_match = '';
 633          $left_join_topics = false;
 634          $group_by = true;
 635          // Build some display specific sql strings
 636          switch ($fields)
 637          {
 638              case 'titleonly':
 639                  $title_match = 'title_match = 1';
 640                  $group_by = false;
 641              // no break
 642              case 'firstpost':
 643                  $left_join_topics = true;
 644                  $sql_where[] = 'p.post_id = t.topic_first_post_id';
 645              break;
 646  
 647              case 'msgonly':
 648                  $title_match = 'title_match = 0';
 649                  $group_by = false;
 650              break;
 651          }
 652  
 653          if ($type == 'topics')
 654          {
 655              $left_join_topics = true;
 656              $group_by = true;
 657          }
 658  
 659          /**
 660          * @todo Add a query optimizer (handle stuff like "+(4|3) +4")
 661          */
 662  
 663          foreach ($this->must_contain_ids as $subquery)
 664          {
 665              if (is_array($subquery))
 666              {
 667                  $group_by = true;
 668  
 669                  $word_id_sql = array();
 670                  $word_ids = array();
 671                  foreach ($subquery as $id)
 672                  {
 673                      if (is_string($id))
 674                      {
 675                          $sql_array['LEFT_JOIN'][] = array(
 676                              'FROM'    => array(SEARCH_WORDLIST_TABLE => 'w' . $w_num),
 677                              'ON'    => "w$w_num.word_text LIKE $id"
 678                          );
 679                          $word_ids[] = "w$w_num.word_id";
 680  
 681                          $w_num++;
 682                      }
 683                      else
 684                      {
 685                          $word_ids[] = $id;
 686                      }
 687                  }
 688  
 689                  $sql_where[] = $this->db->sql_in_set("m$m_num.word_id", $word_ids);
 690  
 691                  unset($word_id_sql);
 692                  unset($word_ids);
 693              }
 694              else if (is_string($subquery))
 695              {
 696                  $sql_array['FROM'][SEARCH_WORDLIST_TABLE][] = 'w' . $w_num;
 697  
 698                  $sql_where[] = "w$w_num.word_text LIKE $subquery";
 699                  $sql_where[] = "m$m_num.word_id = w$w_num.word_id";
 700  
 701                  $group_by = true;
 702                  $w_num++;
 703              }
 704              else
 705              {
 706                  $sql_where[] = "m$m_num.word_id = $subquery";
 707              }
 708  
 709              $sql_array['FROM'][SEARCH_WORDMATCH_TABLE][] = 'm' . $m_num;
 710  
 711              if ($title_match)
 712              {
 713                  $sql_where[] = "m$m_num.$title_match";
 714              }
 715  
 716              if ($m_num != 0)
 717              {
 718                  $sql_where[] = "m$m_num.post_id = m0.post_id";
 719              }
 720              $m_num++;
 721          }
 722  
 723          foreach ($this->must_not_contain_ids as $key => $subquery)
 724          {
 725              if (is_string($subquery))
 726              {
 727                  $sql_array['LEFT_JOIN'][] = array(
 728                      'FROM'    => array(SEARCH_WORDLIST_TABLE => 'w' . $w_num),
 729                      'ON'    => "w$w_num.word_text LIKE $subquery"
 730                  );
 731  
 732                  $this->must_not_contain_ids[$key] = "w$w_num.word_id";
 733  
 734                  $group_by = true;
 735                  $w_num++;
 736              }
 737          }
 738  
 739          if (count($this->must_not_contain_ids))
 740          {
 741              $sql_array['LEFT_JOIN'][] = array(
 742                  'FROM'    => array(SEARCH_WORDMATCH_TABLE => 'm' . $m_num),
 743                  '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"
 744              );
 745  
 746              $sql_where[] = "m$m_num.word_id IS NULL";
 747              $m_num++;
 748          }
 749  
 750          foreach ($this->must_exclude_one_ids as $ids)
 751          {
 752              $is_null_joins = array();
 753              foreach ($ids as $id)
 754              {
 755                  if (is_string($id))
 756                  {
 757                      $sql_array['LEFT_JOIN'][] = array(
 758                          'FROM'    => array(SEARCH_WORDLIST_TABLE => 'w' . $w_num),
 759                          'ON'    => "w$w_num.word_text LIKE $id"
 760                      );
 761                      $id = "w$w_num.word_id";
 762  
 763                      $group_by = true;
 764                      $w_num++;
 765                  }
 766  
 767                  $sql_array['LEFT_JOIN'][] = array(
 768                      'FROM'    => array(SEARCH_WORDMATCH_TABLE => 'm' . $m_num),
 769                      'ON'    => "m$m_num.word_id = $id AND m$m_num.post_id = m0.post_id" . (($title_match) ? " AND m$m_num.$title_match" : '')
 770                  );
 771                  $is_null_joins[] = "m$m_num.word_id IS NULL";
 772  
 773                  $m_num++;
 774              }
 775              $sql_where[] = '(' . implode(' OR ', $is_null_joins) . ')';
 776          }
 777  
 778          $sql_where[] = $post_visibility;
 779  
 780          $search_query = $this->search_query;
 781          $must_exclude_one_ids = $this->must_exclude_one_ids;
 782          $must_not_contain_ids = $this->must_not_contain_ids;
 783          $must_contain_ids = $this->must_contain_ids;
 784  
 785          /**
 786          * Allow changing the query used for counting for posts using fulltext_native
 787          *
 788          * @event core.search_native_keywords_count_query_before
 789          * @var    string    search_query            The parsed keywords used for this search
 790          * @var    array    must_not_contain_ids    Ids that cannot be taken into account for the results
 791          * @var    array    must_exclude_one_ids    Ids that cannot be on the results
 792          * @var    array    must_contain_ids        Ids that must be on the results
 793          * @var    int        total_results            The previous result count for the format of the query
 794          *                                        Set to 0 to force a re-count
 795          * @var    array    sql_array                The data on how to search in the DB at this point
 796          * @var    bool    left_join_topics        Whether or not TOPICS_TABLE should be CROSS JOIN'ED
 797          * @var    array    author_ary                Array of user_id containing the users to filter the results to
 798          * @var    string    author_name                An extra username to search on (!empty(author_ary) must be true, to be relevant)
 799          * @var    array    ex_fid_ary                Which forums not to search on
 800          * @var    int        topic_id                Limit the search to this topic_id only
 801          * @var    string    sql_sort_table            Extra tables to include in the SQL query.
 802          *                                        Used in conjunction with sql_sort_join
 803          * @var    string    sql_sort_join            SQL conditions to join all the tables used together.
 804          *                                        Used in conjunction with sql_sort_table
 805          * @var    int        sort_days                Time, in days, of the oldest possible post to list
 806          * @var    string    sql_where                An array of the current WHERE clause conditions
 807          * @var    string    sql_match                Which columns to do the search on
 808          * @var    string    sql_match_where            Extra conditions to use to properly filter the matching process
 809          * @var    bool    group_by                Whether or not the SQL query requires a GROUP BY for the elements in the SELECT clause
 810          * @var    string    sort_by_sql                The possible predefined sort types
 811          * @var    string    sort_key                The sort type used from the possible sort types
 812          * @var    string    sort_dir                "a" for ASC or "d" dor DESC for the sort order used
 813          * @var    string    sql_sort                The result SQL when processing sort_by_sql + sort_key + sort_dir
 814          * @var    int        start                    How many posts to skip in the search results (used for pagination)
 815          * @since 3.1.5-RC1
 816          */
 817          $vars = array(
 818              'search_query',
 819              'must_not_contain_ids',
 820              'must_exclude_one_ids',
 821              'must_contain_ids',
 822              'total_results',
 823              'sql_array',
 824              'left_join_topics',
 825              'author_ary',
 826              'author_name',
 827              'ex_fid_ary',
 828              'topic_id',
 829              'sql_sort_table',
 830              'sql_sort_join',
 831              'sort_days',
 832              'sql_where',
 833              'sql_match',
 834              'sql_match_where',
 835              'group_by',
 836              'sort_by_sql',
 837              'sort_key',
 838              'sort_dir',
 839              'sql_sort',
 840              'start',
 841          );
 842          extract($this->phpbb_dispatcher->trigger_event('core.search_native_keywords_count_query_before', compact($vars)));
 843  
 844          if ($topic_id)
 845          {
 846              $sql_where[] = 'p.topic_id = ' . $topic_id;
 847          }
 848  
 849          if (count($author_ary))
 850          {
 851              if ($author_name)
 852              {
 853                  // first one matches post of registered users, second one guests and deleted users
 854                  $sql_author = '(' . $this->db->sql_in_set('p.poster_id', array_diff($author_ary, array(ANONYMOUS)), false, true) . ' OR p.post_username ' . $author_name . ')';
 855              }
 856              else
 857              {
 858                  $sql_author = $this->db->sql_in_set('p.poster_id', $author_ary);
 859              }
 860              $sql_where[] = $sql_author;
 861          }
 862  
 863          if (count($ex_fid_ary))
 864          {
 865              $sql_where[] = $this->db->sql_in_set('p.forum_id', $ex_fid_ary, true);
 866          }
 867  
 868          if ($sort_days)
 869          {
 870              $sql_where[] = 'p.post_time >= ' . (time() - ($sort_days * 86400));
 871          }
 872  
 873          $sql_array['WHERE'] = implode(' AND ', $sql_where);
 874  
 875          $is_mysql = false;
 876          // if the total result count is not cached yet, retrieve it from the db
 877          if (!$total_results)
 878          {
 879              $sql = '';
 880              $sql_array_count = $sql_array;
 881  
 882              if ($left_join_topics)
 883              {
 884                  $sql_array_count['LEFT_JOIN'][] = array(
 885                      'FROM'    => array(TOPICS_TABLE => 't'),
 886                      'ON'    => 'p.topic_id = t.topic_id'
 887                  );
 888              }
 889  
 890              switch ($this->db->get_sql_layer())
 891              {
 892                  case 'mysql4':
 893                  case 'mysqli':
 894  
 895                      // 3.x does not support SQL_CALC_FOUND_ROWS
 896                      // $sql_array['SELECT'] = 'SQL_CALC_FOUND_ROWS ' . $sql_array['SELECT'];
 897                      $is_mysql = true;
 898  
 899                  break;
 900  
 901                  case 'sqlite3':
 902                      $sql_array_count['SELECT'] = ($type == 'posts') ? 'DISTINCT p.post_id' : 'DISTINCT p.topic_id';
 903                      $sql = 'SELECT COUNT(' . (($type == 'posts') ? 'post_id' : 'topic_id') . ') as total_results
 904                              FROM (' . $this->db->sql_build_query('SELECT', $sql_array_count) . ')';
 905  
 906                  // no break
 907  
 908                  default:
 909                      $sql_array_count['SELECT'] = ($type == 'posts') ? 'COUNT(DISTINCT p.post_id) AS total_results' : 'COUNT(DISTINCT p.topic_id) AS total_results';
 910                      $sql = (!$sql) ? $this->db->sql_build_query('SELECT', $sql_array_count) : $sql;
 911  
 912                      $result = $this->db->sql_query($sql);
 913                      $total_results = (int) $this->db->sql_fetchfield('total_results');
 914                      $this->db->sql_freeresult($result);
 915  
 916                      if (!$total_results)
 917                      {
 918                          return false;
 919                      }
 920                  break;
 921              }
 922  
 923              unset($sql_array_count, $sql);
 924          }
 925  
 926          // Build sql strings for sorting
 927          $sql_sort = $sort_by_sql[$sort_key] . (($sort_dir == 'a') ? ' ASC' : ' DESC');
 928  
 929          switch ($sql_sort[0])
 930          {
 931              case 'u':
 932                  $sql_array['FROM'][USERS_TABLE] = 'u';
 933                  $sql_where[] = 'u.user_id = p.poster_id ';
 934              break;
 935  
 936              case 't':
 937                  $left_join_topics = true;
 938              break;
 939  
 940              case 'f':
 941                  $sql_array['FROM'][FORUMS_TABLE] = 'f';
 942                  $sql_where[] = 'f.forum_id = p.forum_id';
 943              break;
 944          }
 945  
 946          if ($left_join_topics)
 947          {
 948              $sql_array['LEFT_JOIN'][] = array(
 949                  'FROM'    => array(TOPICS_TABLE => 't'),
 950                  'ON'    => 'p.topic_id = t.topic_id'
 951              );
 952          }
 953  
 954          // if using mysql and the total result count is not calculated yet, get it from the db
 955          if (!$total_results && $is_mysql)
 956          {
 957              // Also count rows for the query as if there was not LIMIT. Add SQL_CALC_FOUND_ROWS to SQL
 958              $sql_array['SELECT'] = 'SQL_CALC_FOUND_ROWS ' . $sql_array['SELECT'];
 959          }
 960  
 961          $sql_array['WHERE'] = implode(' AND ', $sql_where);
 962          $sql_array['GROUP_BY'] = ($group_by) ? (($type == 'posts') ? 'p.post_id' : 'p.topic_id') . ', ' . $sort_by_sql[$sort_key] : '';
 963          $sql_array['ORDER_BY'] = $sql_sort;
 964  
 965          unset($sql_where, $sql_sort, $group_by);
 966  
 967          $sql = $this->db->sql_build_query('SELECT', $sql_array);
 968          $result = $this->db->sql_query_limit($sql, $this->config['search_block_size'], $start);
 969  
 970          while ($row = $this->db->sql_fetchrow($result))
 971          {
 972              $id_ary[] = (int) $row[(($type == 'posts') ? 'post_id' : 'topic_id')];
 973          }
 974          $this->db->sql_freeresult($result);
 975  
 976          if (!$total_results && $is_mysql)
 977          {
 978              // Get the number of results as calculated by MySQL
 979              $sql_count = 'SELECT FOUND_ROWS() as total_results';
 980              $result = $this->db->sql_query($sql_count);
 981              $total_results = (int) $this->db->sql_fetchfield('total_results');
 982              $this->db->sql_freeresult($result);
 983  
 984              if (!$total_results)
 985              {
 986                  return false;
 987              }
 988          }
 989  
 990          if ($start >= $total_results)
 991          {
 992              $start = floor(($total_results - 1) / $per_page) * $per_page;
 993  
 994              $result = $this->db->sql_query_limit($sql, $this->config['search_block_size'], $start);
 995  
 996              while ($row = $this->db->sql_fetchrow($result))
 997              {
 998                  $id_ary[] = (int) $row[(($type == 'posts') ? 'post_id' : 'topic_id')];
 999              }
1000              $this->db->sql_freeresult($result);
1001  
1002          }
1003  
1004          // store the ids, from start on then delete anything that isn't on the current page because we only need ids for one page
1005          $this->save_ids($search_key, $this->search_query, $author_ary, $total_results, $id_ary, $start, $sort_dir);
1006          $id_ary = array_slice($id_ary, 0, (int) $per_page);
1007  
1008          return $total_results;
1009      }
1010  
1011      /**
1012      * Performs a search on an author's posts without caring about message contents. Depends on display specific params
1013      *
1014      * @param    string        $type                contains either posts or topics depending on what should be searched for
1015      * @param    boolean        $firstpost_only        if true, only topic starting posts will be considered
1016      * @param    array        $sort_by_sql        contains SQL code for the ORDER BY part of a query
1017      * @param    string        $sort_key            is the key of $sort_by_sql for the selected sorting
1018      * @param    string        $sort_dir            is either a or d representing ASC and DESC
1019      * @param    string        $sort_days            specifies the maximum amount of days a post may be old
1020      * @param    array        $ex_fid_ary            specifies an array of forum ids which should not be searched
1021      * @param    string        $post_visibility    specifies which types of posts the user can view in which forums
1022      * @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
1023      * @param    array        $author_ary            an array of author ids
1024      * @param    string        $author_name        specifies the author match, when ANONYMOUS is also a search-match
1025      * @param    array        &$id_ary            passed by reference, to be filled with ids for the page specified by $start and $per_page, should be ordered
1026      * @param    int            $start                indicates the first index of the page
1027      * @param    int            $per_page            number of ids each page is supposed to contain
1028      * @return    boolean|int                        total number of results
1029      */
1030  	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)
1031      {
1032          // No author? No posts
1033          if (!count($author_ary))
1034          {
1035              return 0;
1036          }
1037  
1038          // generate a search_key from all the options to identify the results
1039          $search_key_array = array(
1040              '',
1041              $type,
1042              ($firstpost_only) ? 'firstpost' : '',
1043              '',
1044              '',
1045              $sort_days,
1046              $sort_key,
1047              $topic_id,
1048              implode(',', $ex_fid_ary),
1049              $post_visibility,
1050              implode(',', $author_ary),
1051              $author_name,
1052          );
1053  
1054          /**
1055          * Allow changing the search_key for cached results
1056          *
1057          * @event core.search_native_by_author_modify_search_key
1058          * @var    array    search_key_array    Array with search parameters to generate the search_key
1059          * @var    string    type                Searching type ('posts', 'topics')
1060          * @var    boolean    firstpost_only        Flag indicating if only topic starting posts are considered
1061          * @var    int        sort_days            Time, in days, of the oldest possible post to list
1062          * @var    string    sort_key            The sort type used from the possible sort types
1063          * @var    int        topic_id            Limit the search to this topic_id only
1064          * @var    array    ex_fid_ary            Which forums not to search on
1065          * @var    string    post_visibility        Post visibility data
1066          * @var    array    author_ary            Array of user_id containing the users to filter the results to
1067          * @var    string    author_name            The username to search on
1068          * @since 3.1.7-RC1
1069          */
1070          $vars = array(
1071              'search_key_array',
1072              'type',
1073              'firstpost_only',
1074              'sort_days',
1075              'sort_key',
1076              'topic_id',
1077              'ex_fid_ary',
1078              'post_visibility',
1079              'author_ary',
1080              'author_name',
1081          );
1082          extract($this->phpbb_dispatcher->trigger_event('core.search_native_by_author_modify_search_key', compact($vars)));
1083  
1084          $search_key = md5(implode('#', $search_key_array));
1085  
1086          // try reading the results from cache
1087          $total_results = 0;
1088          if ($this->obtain_ids($search_key, $total_results, $id_ary, $start, $per_page, $sort_dir) == SEARCH_RESULT_IN_CACHE)
1089          {
1090              return $total_results;
1091          }
1092  
1093          $id_ary = array();
1094  
1095          // Create some display specific sql strings
1096          if ($author_name)
1097          {
1098              // first one matches post of registered users, second one guests and deleted users
1099              $sql_author = '(' . $this->db->sql_in_set('p.poster_id', array_diff($author_ary, array(ANONYMOUS)), false, true) . ' OR p.post_username ' . $author_name . ')';
1100          }
1101          else
1102          {
1103              $sql_author = $this->db->sql_in_set('p.poster_id', $author_ary);
1104          }
1105          $sql_fora        = (count($ex_fid_ary)) ? ' AND ' . $this->db->sql_in_set('p.forum_id', $ex_fid_ary, true) : '';
1106          $sql_time        = ($sort_days) ? ' AND p.post_time >= ' . (time() - ($sort_days * 86400)) : '';
1107          $sql_topic_id    = ($topic_id) ? ' AND p.topic_id = ' . (int) $topic_id : '';
1108          $sql_firstpost = ($firstpost_only) ? ' AND p.post_id = t.topic_first_post_id' : '';
1109          $post_visibility = ($post_visibility) ? ' AND ' . $post_visibility : '';
1110  
1111          // Build sql strings for sorting
1112          $sql_sort = $sort_by_sql[$sort_key] . (($sort_dir == 'a') ? ' ASC' : ' DESC');
1113          $sql_sort_table = $sql_sort_join = '';
1114          switch ($sql_sort[0])
1115          {
1116              case 'u':
1117                  $sql_sort_table    = USERS_TABLE . ' u, ';
1118                  $sql_sort_join    = ' AND u.user_id = p.poster_id ';
1119              break;
1120  
1121              case 't':
1122                  $sql_sort_table    = ($type == 'posts' && !$firstpost_only) ? TOPICS_TABLE . ' t, ' : '';
1123                  $sql_sort_join    = ($type == 'posts' && !$firstpost_only) ? ' AND t.topic_id = p.topic_id ' : '';
1124              break;
1125  
1126              case 'f':
1127                  $sql_sort_table    = FORUMS_TABLE . ' f, ';
1128                  $sql_sort_join    = ' AND f.forum_id = p.forum_id ';
1129              break;
1130          }
1131  
1132          $select = ($type == 'posts') ? 'p.post_id' : 't.topic_id';
1133          $is_mysql = false;
1134  
1135          /**
1136          * Allow changing the query used to search for posts by author in fulltext_native
1137          *
1138          * @event core.search_native_author_count_query_before
1139          * @var    int        total_results        The previous result count for the format of the query.
1140          *                                    Set to 0 to force a re-count
1141          * @var    string    type                The type of search being made
1142          * @var    string    select                SQL SELECT clause for what to get
1143          * @var    string    sql_sort_table        CROSS JOIN'ed table to allow doing the sort chosen
1144          * @var    string    sql_sort_join        Condition to define how to join the CROSS JOIN'ed table specifyed in sql_sort_table
1145          * @var    array    sql_author            SQL WHERE condition for the post author ids
1146          * @var    int        topic_id            Limit the search to this topic_id only
1147          * @var    string    sort_by_sql            The possible predefined sort types
1148          * @var    string    sort_key            The sort type used from the possible sort types
1149          * @var    string    sort_dir            "a" for ASC or "d" dor DESC for the sort order used
1150          * @var    string    sql_sort            The result SQL when processing sort_by_sql + sort_key + sort_dir
1151          * @var    string    sort_days            Time, in days, that the oldest post showing can have
1152          * @var    string    sql_time            The SQL to search on the time specifyed by sort_days
1153          * @var    bool    firstpost_only        Wether or not to search only on the first post of the topics
1154          * @var    string    sql_firstpost        The SQL used in the WHERE claused to filter by firstpost.
1155          * @var    array    ex_fid_ary            Forum ids that must not be searched on
1156          * @var    array    sql_fora            SQL query for ex_fid_ary
1157          * @var    int        start                How many posts to skip in the search results (used for pagination)
1158          * @since 3.1.5-RC1
1159          */
1160          $vars = array(
1161              'total_results',
1162              'type',
1163              'select',
1164              'sql_sort_table',
1165              'sql_sort_join',
1166              'sql_author',
1167              'topic_id',
1168              'sort_by_sql',
1169              'sort_key',
1170              'sort_dir',
1171              'sql_sort',
1172              'sort_days',
1173              'sql_time',
1174              'firstpost_only',
1175              'sql_firstpost',
1176              'ex_fid_ary',
1177              'sql_fora',
1178              'start',
1179          );
1180          extract($this->phpbb_dispatcher->trigger_event('core.search_native_author_count_query_before', compact($vars)));
1181  
1182          // If the cache was completely empty count the results
1183          if (!$total_results)
1184          {
1185              switch ($this->db->get_sql_layer())
1186              {
1187                  case 'mysql4':
1188                  case 'mysqli':
1189  //                    $select = 'SQL_CALC_FOUND_ROWS ' . $select;
1190                      $is_mysql = true;
1191                  break;
1192  
1193                  default:
1194                      if ($type == 'posts')
1195                      {
1196                          $sql = 'SELECT COUNT(p.post_id) as total_results
1197                              FROM ' . POSTS_TABLE . ' p' . (($firstpost_only) ? ', ' . TOPICS_TABLE . ' t ' : ' ') . "
1198                              WHERE $sql_author
1199                                  $sql_topic_id
1200                                  $sql_firstpost
1201                                  $post_visibility
1202                                  $sql_fora
1203                                  $sql_time";
1204                      }
1205                      else
1206                      {
1207                          if ($this->db->get_sql_layer() == 'sqlite3')
1208                          {
1209                              $sql = 'SELECT COUNT(topic_id) as total_results
1210                                  FROM (SELECT DISTINCT t.topic_id';
1211                          }
1212                          else
1213                          {
1214                              $sql = 'SELECT COUNT(DISTINCT t.topic_id) as total_results';
1215                          }
1216  
1217                          $sql .= ' FROM ' . TOPICS_TABLE . ' t, ' . POSTS_TABLE . " p
1218                              WHERE $sql_author
1219                                  $sql_topic_id
1220                                  $sql_firstpost
1221                                  $post_visibility
1222                                  $sql_fora
1223                                  AND t.topic_id = p.topic_id
1224                                  $sql_time" . ($this->db->get_sql_layer() == 'sqlite3' ? ')' : '');
1225                      }
1226                      $result = $this->db->sql_query($sql);
1227  
1228                      $total_results = (int) $this->db->sql_fetchfield('total_results');
1229                      $this->db->sql_freeresult($result);
1230  
1231                      if (!$total_results)
1232                      {
1233                          return false;
1234                      }
1235                  break;
1236              }
1237          }
1238  
1239          // Build the query for really selecting the post_ids
1240          if ($type == 'posts')
1241          {
1242              $sql = "SELECT $select
1243                  FROM " . $sql_sort_table . POSTS_TABLE . ' p' . (($firstpost_only) ? ', ' . TOPICS_TABLE . ' t' : '') . "
1244                  WHERE $sql_author
1245                      $sql_topic_id
1246                      $sql_firstpost
1247                      $post_visibility
1248                      $sql_fora
1249                      $sql_sort_join
1250                      $sql_time
1251                  ORDER BY $sql_sort";
1252              $field = 'post_id';
1253          }
1254          else
1255          {
1256              $sql = "SELECT $select
1257                  FROM " . $sql_sort_table . TOPICS_TABLE . ' t, ' . POSTS_TABLE . " p
1258                  WHERE $sql_author
1259                      $sql_topic_id
1260                      $sql_firstpost
1261                      $post_visibility
1262                      $sql_fora
1263                      AND t.topic_id = p.topic_id
1264                      $sql_sort_join
1265                      $sql_time
1266                  GROUP BY t.topic_id, " . $sort_by_sql[$sort_key] . '
1267                  ORDER BY ' . $sql_sort;
1268              $field = 'topic_id';
1269          }
1270  
1271          // Only read one block of posts from the db and then cache it
1272          $result = $this->db->sql_query_limit($sql, $this->config['search_block_size'], $start);
1273  
1274          while ($row = $this->db->sql_fetchrow($result))
1275          {
1276              $id_ary[] = (int) $row[$field];
1277          }
1278          $this->db->sql_freeresult($result);
1279  
1280          if (!$total_results && $is_mysql)
1281          {
1282              // Count rows for the executed queries. Replace $select within $sql with SQL_CALC_FOUND_ROWS, and run it.
1283              $sql_calc = str_replace('SELECT ' . $select, 'SELECT SQL_CALC_FOUND_ROWS ' . $select, $sql);
1284  
1285              $result = $this->db->sql_query($sql_calc);
1286              $this->db->sql_freeresult($result);
1287  
1288              $sql_count = 'SELECT FOUND_ROWS() as total_results';
1289              $result = $this->db->sql_query($sql_count);
1290              $total_results = (int) $this->db->sql_fetchfield('total_results');
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 = htmlspecialchars_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 Oct 26 11:28:42 2019 Cross-referenced by PHPXref 0.7.1