[ Index ]

PHP Cross Reference of phpBB-3.3.14-deutsch

title

Body

[close]

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

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


Generated: Mon Nov 25 19:05:08 2024 Cross-referenced by PHPXref 0.7.1