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