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