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