Verzeichnisstruktur phpBB-3.1.0
- Veröffentlicht
- 27.10.2014
So funktioniert es
|
Auf das letzte Element klicken. Dies geht jeweils ein Schritt zurück |
Auf das Icon klicken, dies öffnet das Verzeichnis. Nochmal klicken schließt das Verzeichnis. |
|
(Beispiel Datei-Icons)
|
Auf das Icon klicken um den Quellcode anzuzeigen |
fulltext_mysql.php
001 <?php
002 /**
003 *
004 * This file is part of the phpBB Forum Software package.
005 *
006 * @copyright (c) phpBB Limited <https://www.phpbb.com>
007 * @license GNU General Public License, version 2 (GPL-2.0)
008 *
009 * For full copyright and license information, please see
010 * the docs/CREDITS.txt file.
011 *
012 */
013
014 namespace phpbb\search;
015
016 /**
017 * Fulltext search for MySQL
018 */
019 class fulltext_mysql extends \phpbb\search\base
020 {
021 /**
022 * Associative array holding index stats
023 * @var array
024 */
025 protected $stats = array();
026
027 /**
028 * Holds the words entered by user, obtained by splitting the entered query on whitespace
029 * @var array
030 */
031 protected $split_words = array();
032
033 /**
034 * Config object
035 * @var \phpbb\config\config
036 */
037 protected $config;
038
039 /**
040 * Database connection
041 * @var \phpbb\db\driver\driver_interface
042 */
043 protected $db;
044
045 /**
046 * User object
047 * @var \phpbb\user
048 */
049 protected $user;
050
051 /**
052 * Associative array stores the min and max word length to be searched
053 * @var array
054 */
055 protected $word_length = array();
056
057 /**
058 * Contains tidied search query.
059 * Operators are prefixed in search query and common words excluded
060 * @var string
061 */
062 protected $search_query;
063
064 /**
065 * Contains common words.
066 * Common words are words with length less/more than min/max length
067 * @var array
068 */
069 protected $common_words = array();
070
071 /**
072 * Constructor
073 * Creates a new \phpbb\search\fulltext_mysql, which is used as a search backend
074 *
075 * @param string|bool $error Any error that occurs is passed on through this reference variable otherwise false
076 * @param string $phpbb_root_path Relative path to phpBB root
077 * @param string $phpEx PHP file extension
078 * @param \phpbb\auth\auth $auth Auth object
079 * @param \phpbb\config\config $config Config object
080 * @param \phpbb\db\driver\driver_interface Database object
081 * @param \phpbb\user $user User object
082 */
083 public function __construct(&$error, $phpbb_root_path, $phpEx, $auth, $config, $db, $user)
084 {
085 $this->config = $config;
086 $this->db = $db;
087 $this->user = $user;
088
089 $this->word_length = array('min' => $this->config['fulltext_mysql_min_word_len'], 'max' => $this->config['fulltext_mysql_max_word_len']);
090
091 /**
092 * Load the UTF tools
093 */
094 if (!function_exists('utf8_strlen'))
095 {
096 include($phpbb_root_path . 'includes/utf/utf_tools.' . $phpEx);
097 }
098
099 $error = false;
100 }
101
102 /**
103 * Returns the name of this search backend to be displayed to administrators
104 *
105 * @return string Name
106 */
107 public function get_name()
108 {
109 return 'MySQL Fulltext';
110 }
111
112 /**
113 * Returns the search_query
114 *
115 * @return string search query
116 */
117 public function get_search_query()
118 {
119 return $this->search_query;
120 }
121
122 /**
123 * Returns the common_words array
124 *
125 * @return array common words that are ignored by search backend
126 */
127 public function get_common_words()
128 {
129 return $this->common_words;
130 }
131
132 /**
133 * Returns the word_length array
134 *
135 * @return array min and max word length for searching
136 */
137 public function get_word_length()
138 {
139 return $this->word_length;
140 }
141
142 /**
143 * Checks for correct MySQL version and stores min/max word length in the config
144 *
145 * @return string|bool Language key of the error/incompatiblity occurred
146 */
147 public function init()
148 {
149 if ($this->db->get_sql_layer() != 'mysql4' && $this->db->get_sql_layer() != 'mysqli')
150 {
151 return $this->user->lang['FULLTEXT_MYSQL_INCOMPATIBLE_DATABASE'];
152 }
153
154 $result = $this->db->sql_query('SHOW TABLE STATUS LIKE \'' . POSTS_TABLE . '\'');
155 $info = $this->db->sql_fetchrow($result);
156 $this->db->sql_freeresult($result);
157
158 $engine = '';
159 if (isset($info['Engine']))
160 {
161 $engine = $info['Engine'];
162 }
163 else if (isset($info['Type']))
164 {
165 $engine = $info['Type'];
166 }
167
168 $fulltext_supported =
169 $engine === 'MyISAM' ||
170 // FULLTEXT is supported on InnoDB since MySQL 5.6.4 according to
171 // http://dev.mysql.com/doc/refman/5.6/en/innodb-storage-engine.html
172 $engine === 'InnoDB' &&
173 phpbb_version_compare($this->db->sql_server_info(true), '5.6.4', '>=');
174
175 if (!$fulltext_supported)
176 {
177 return $this->user->lang['FULLTEXT_MYSQL_NOT_SUPPORTED'];
178 }
179
180 $sql = 'SHOW VARIABLES
181 LIKE \'ft\_%\'';
182 $result = $this->db->sql_query($sql);
183
184 $mysql_info = array();
185 while ($row = $this->db->sql_fetchrow($result))
186 {
187 $mysql_info[$row['Variable_name']] = $row['Value'];
188 }
189 $this->db->sql_freeresult($result);
190
191 set_config('fulltext_mysql_max_word_len', $mysql_info['ft_max_word_len']);
192 set_config('fulltext_mysql_min_word_len', $mysql_info['ft_min_word_len']);
193
194 return false;
195 }
196
197 /**
198 * Splits keywords entered by a user into an array of words stored in $this->split_words
199 * Stores the tidied search query in $this->search_query
200 *
201 * @param string &$keywords Contains the keyword as entered by the user
202 * @param string $terms is either 'all' or 'any'
203 * @return bool false if no valid keywords were found and otherwise true
204 */
205 public function split_keywords(&$keywords, $terms)
206 {
207 if ($terms == 'all')
208 {
209 $match = array('#\sand\s#iu', '#\sor\s#iu', '#\snot\s#iu', '#(^|\s)\+#', '#(^|\s)-#', '#(^|\s)\|#');
210 $replace = array(' +', ' |', ' -', ' +', ' -', ' |');
211
212 $keywords = preg_replace($match, $replace, $keywords);
213 }
214
215 // Filter out as above
216 $split_keywords = preg_replace("#[\n\r\t]+#", ' ', trim(htmlspecialchars_decode($keywords)));
217
218 // Split words
219 $split_keywords = preg_replace('#([^\p{L}\p{N}\'*"()])#u', '$1$1', str_replace('\'\'', '\' \'', trim($split_keywords)));
220 $matches = array();
221 preg_match_all('#(?:[^\p{L}\p{N}*"()]|^)([+\-|]?(?:[\p{L}\p{N}*"()]+\'?)*[\p{L}\p{N}*"()])(?:[^\p{L}\p{N}*"()]|$)#u', $split_keywords, $matches);
222 $this->split_words = $matches[1];
223
224 // We limit the number of allowed keywords to minimize load on the database
225 if ($this->config['max_num_search_keywords'] && sizeof($this->split_words) > $this->config['max_num_search_keywords'])
226 {
227 trigger_error($this->user->lang('MAX_NUM_SEARCH_KEYWORDS_REFINE', (int) $this->config['max_num_search_keywords'], sizeof($this->split_words)));
228 }
229
230 // to allow phrase search, we need to concatenate quoted words
231 $tmp_split_words = array();
232 $phrase = '';
233 foreach ($this->split_words as $word)
234 {
235 if ($phrase)
236 {
237 $phrase .= ' ' . $word;
238 if (strpos($word, '"') !== false && substr_count($word, '"') % 2 == 1)
239 {
240 $tmp_split_words[] = $phrase;
241 $phrase = '';
242 }
243 }
244 else if (strpos($word, '"') !== false && substr_count($word, '"') % 2 == 1)
245 {
246 $phrase = $word;
247 }
248 else
249 {
250 $tmp_split_words[] = $word;
251 }
252 }
253 if ($phrase)
254 {
255 $tmp_split_words[] = $phrase;
256 }
257
258 $this->split_words = $tmp_split_words;
259
260 unset($tmp_split_words);
261 unset($phrase);
262
263 foreach ($this->split_words as $i => $word)
264 {
265 $clean_word = preg_replace('#^[+\-|"]#', '', $word);
266
267 // check word length
268 $clean_len = utf8_strlen(str_replace('*', '', $clean_word));
269 if (($clean_len < $this->config['fulltext_mysql_min_word_len']) || ($clean_len > $this->config['fulltext_mysql_max_word_len']))
270 {
271 $this->common_words[] = $word;
272 unset($this->split_words[$i]);
273 }
274 }
275
276 if ($terms == 'any')
277 {
278 $this->search_query = '';
279 foreach ($this->split_words as $word)
280 {
281 if ((strpos($word, '+') === 0) || (strpos($word, '-') === 0) || (strpos($word, '|') === 0))
282 {
283 $word = substr($word, 1);
284 }
285 $this->search_query .= $word . ' ';
286 }
287 }
288 else
289 {
290 $this->search_query = '';
291 foreach ($this->split_words as $word)
292 {
293 if ((strpos($word, '+') === 0) || (strpos($word, '-') === 0))
294 {
295 $this->search_query .= $word . ' ';
296 }
297 else if (strpos($word, '|') === 0)
298 {
299 $this->search_query .= substr($word, 1) . ' ';
300 }
301 else
302 {
303 $this->search_query .= '+' . $word . ' ';
304 }
305 }
306 }
307
308 $this->search_query = utf8_htmlspecialchars($this->search_query);
309
310 if ($this->search_query)
311 {
312 $this->split_words = array_values($this->split_words);
313 sort($this->split_words);
314 return true;
315 }
316 return false;
317 }
318
319 /**
320 * Turns text into an array of words
321 * @param string $text contains post text/subject
322 */
323 public function split_message($text)
324 {
325 // Split words
326 $text = preg_replace('#([^\p{L}\p{N}\'*])#u', '$1$1', str_replace('\'\'', '\' \'', trim($text)));
327 $matches = array();
328 preg_match_all('#(?:[^\p{L}\p{N}*]|^)([+\-|]?(?:[\p{L}\p{N}*]+\'?)*[\p{L}\p{N}*])(?:[^\p{L}\p{N}*]|$)#u', $text, $matches);
329 $text = $matches[1];
330
331 // remove too short or too long words
332 $text = array_values($text);
333 for ($i = 0, $n = sizeof($text); $i < $n; $i++)
334 {
335 $text[$i] = trim($text[$i]);
336 if (utf8_strlen($text[$i]) < $this->config['fulltext_mysql_min_word_len'] || utf8_strlen($text[$i]) > $this->config['fulltext_mysql_max_word_len'])
337 {
338 unset($text[$i]);
339 }
340 }
341
342 return array_values($text);
343 }
344
345 /**
346 * Performs a search on keywords depending on display specific params. You have to run split_keywords() first
347 *
348 * @param string $type contains either posts or topics depending on what should be searched for
349 * @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)
350 * @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)
351 * @param array $sort_by_sql contains SQL code for the ORDER BY part of a query
352 * @param string $sort_key is the key of $sort_by_sql for the selected sorting
353 * @param string $sort_dir is either a or d representing ASC and DESC
354 * @param string $sort_days specifies the maximum amount of days a post may be old
355 * @param array $ex_fid_ary specifies an array of forum ids which should not be searched
356 * @param string $post_visibility specifies which types of posts the user can view in which forums
357 * @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
358 * @param array $author_ary an array of author ids if the author should be ignored during the search the array is empty
359 * @param string $author_name specifies the author match, when ANONYMOUS is also a search-match
360 * @param array &$id_ary passed by reference, to be filled with ids for the page specified by $start and $per_page, should be ordered
361 * @param int $start indicates the first index of the page
362 * @param int $per_page number of ids each page is supposed to contain
363 * @return boolean|int total number of results
364 */
365 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)
366 {
367 // No keywords? No posts
368 if (!$this->search_query)
369 {
370 return false;
371 }
372
373 // generate a search_key from all the options to identify the results
374 $search_key = md5(implode('#', array(
375 implode(', ', $this->split_words),
376 $type,
377 $fields,
378 $terms,
379 $sort_days,
380 $sort_key,
381 $topic_id,
382 implode(',', $ex_fid_ary),
383 $post_visibility,
384 implode(',', $author_ary)
385 )));
386
387 if ($start < 0)
388 {
389 $start = 0;
390 }
391
392 // try reading the results from cache
393 $result_count = 0;
394 if ($this->obtain_ids($search_key, $result_count, $id_ary, $start, $per_page, $sort_dir) == SEARCH_RESULT_IN_CACHE)
395 {
396 return $result_count;
397 }
398
399 $id_ary = array();
400
401 $join_topic = ($type == 'posts') ? false : true;
402
403 // Build sql strings for sorting
404 $sql_sort = $sort_by_sql[$sort_key] . (($sort_dir == 'a') ? ' ASC' : ' DESC');
405 $sql_sort_table = $sql_sort_join = '';
406
407 switch ($sql_sort[0])
408 {
409 case 'u':
410 $sql_sort_table = USERS_TABLE . ' u, ';
411 $sql_sort_join = ($type == 'posts') ? ' AND u.user_id = p.poster_id ' : ' AND u.user_id = t.topic_poster ';
412 break;
413
414 case 't':
415 $join_topic = true;
416 break;
417
418 case 'f':
419 $sql_sort_table = FORUMS_TABLE . ' f, ';
420 $sql_sort_join = ' AND f.forum_id = p.forum_id ';
421 break;
422 }
423
424 // Build some display specific sql strings
425 switch ($fields)
426 {
427 case 'titleonly':
428 $sql_match = 'p.post_subject';
429 $sql_match_where = ' AND p.post_id = t.topic_first_post_id';
430 $join_topic = true;
431 break;
432
433 case 'msgonly':
434 $sql_match = 'p.post_text';
435 $sql_match_where = '';
436 break;
437
438 case 'firstpost':
439 $sql_match = 'p.post_subject, p.post_text';
440 $sql_match_where = ' AND p.post_id = t.topic_first_post_id';
441 $join_topic = true;
442 break;
443
444 default:
445 $sql_match = 'p.post_subject, p.post_text';
446 $sql_match_where = '';
447 break;
448 }
449
450 $sql_select = (!$result_count) ? 'SQL_CALC_FOUND_ROWS ' : '';
451 $sql_select = ($type == 'posts') ? $sql_select . 'p.post_id' : 'DISTINCT ' . $sql_select . 't.topic_id';
452 $sql_from = ($join_topic) ? TOPICS_TABLE . ' t, ' : '';
453 $field = ($type == 'posts') ? 'post_id' : 'topic_id';
454 if (sizeof($author_ary) && $author_name)
455 {
456 // first one matches post of registered users, second one guests and deleted users
457 $sql_author = ' AND (' . $this->db->sql_in_set('p.poster_id', array_diff($author_ary, array(ANONYMOUS)), false, true) . ' OR p.post_username ' . $author_name . ')';
458 }
459 else if (sizeof($author_ary))
460 {
461 $sql_author = ' AND ' . $this->db->sql_in_set('p.poster_id', $author_ary);
462 }
463 else
464 {
465 $sql_author = '';
466 }
467
468 $sql_where_options = $sql_sort_join;
469 $sql_where_options .= ($topic_id) ? ' AND p.topic_id = ' . $topic_id : '';
470 $sql_where_options .= ($join_topic) ? ' AND t.topic_id = p.topic_id' : '';
471 $sql_where_options .= (sizeof($ex_fid_ary)) ? ' AND ' . $this->db->sql_in_set('p.forum_id', $ex_fid_ary, true) : '';
472 $sql_where_options .= ' AND ' . $post_visibility;
473 $sql_where_options .= $sql_author;
474 $sql_where_options .= ($sort_days) ? ' AND p.post_time >= ' . (time() - ($sort_days * 86400)) : '';
475 $sql_where_options .= $sql_match_where;
476
477 $sql = "SELECT $sql_select
478 FROM $sql_from$sql_sort_table" . POSTS_TABLE . " p
479 WHERE MATCH ($sql_match) AGAINST ('" . $this->db->sql_escape(htmlspecialchars_decode($this->search_query)) . "' IN BOOLEAN MODE)
480 $sql_where_options
481 ORDER BY $sql_sort";
482 $result = $this->db->sql_query_limit($sql, $this->config['search_block_size'], $start);
483
484 while ($row = $this->db->sql_fetchrow($result))
485 {
486 $id_ary[] = (int) $row[$field];
487 }
488 $this->db->sql_freeresult($result);
489
490 $id_ary = array_unique($id_ary);
491
492 // if the total result count is not cached yet, retrieve it from the db
493 if (!$result_count)
494 {
495 $sql_found_rows = 'SELECT FOUND_ROWS() as result_count';
496 $result = $this->db->sql_query($sql_found_rows);
497 $result_count = (int) $this->db->sql_fetchfield('result_count');
498 $this->db->sql_freeresult($result);
499
500 if (!$result_count)
501 {
502 return false;
503 }
504 }
505
506 if ($start >= $result_count)
507 {
508 $start = floor(($result_count - 1) / $per_page) * $per_page;
509
510 $result = $this->db->sql_query_limit($sql, $this->config['search_block_size'], $start);
511
512 while ($row = $this->db->sql_fetchrow($result))
513 {
514 $id_ary[] = (int) $row[$field];
515 }
516 $this->db->sql_freeresult($result);
517
518 $id_ary = array_unique($id_ary);
519 }
520
521 // store the ids, from start on then delete anything that isn't on the current page because we only need ids for one page
522 $this->save_ids($search_key, implode(' ', $this->split_words), $author_ary, $result_count, $id_ary, $start, $sort_dir);
523 $id_ary = array_slice($id_ary, 0, (int) $per_page);
524
525 return $result_count;
526 }
527
528 /**
529 * Performs a search on an author's posts without caring about message contents. Depends on display specific params
530 *
531 * @param string $type contains either posts or topics depending on what should be searched for
532 * @param boolean $firstpost_only if true, only topic starting posts will be considered
533 * @param array $sort_by_sql contains SQL code for the ORDER BY part of a query
534 * @param string $sort_key is the key of $sort_by_sql for the selected sorting
535 * @param string $sort_dir is either a or d representing ASC and DESC
536 * @param string $sort_days specifies the maximum amount of days a post may be old
537 * @param array $ex_fid_ary specifies an array of forum ids which should not be searched
538 * @param string $post_visibility specifies which types of posts the user can view in which forums
539 * @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
540 * @param array $author_ary an array of author ids
541 * @param string $author_name specifies the author match, when ANONYMOUS is also a search-match
542 * @param array &$id_ary passed by reference, to be filled with ids for the page specified by $start and $per_page, should be ordered
543 * @param int $start indicates the first index of the page
544 * @param int $per_page number of ids each page is supposed to contain
545 * @return boolean|int total number of results
546 */
547 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)
548 {
549 // No author? No posts
550 if (!sizeof($author_ary))
551 {
552 return 0;
553 }
554
555 // generate a search_key from all the options to identify the results
556 $search_key = md5(implode('#', array(
557 '',
558 $type,
559 ($firstpost_only) ? 'firstpost' : '',
560 '',
561 '',
562 $sort_days,
563 $sort_key,
564 $topic_id,
565 implode(',', $ex_fid_ary),
566 $post_visibility,
567 implode(',', $author_ary),
568 $author_name,
569 )));
570
571 if ($start < 0)
572 {
573 $start = 0;
574 }
575
576 // try reading the results from cache
577 $result_count = 0;
578 if ($this->obtain_ids($search_key, $result_count, $id_ary, $start, $per_page, $sort_dir) == SEARCH_RESULT_IN_CACHE)
579 {
580 return $result_count;
581 }
582
583 $id_ary = array();
584
585 // Create some display specific sql strings
586 if ($author_name)
587 {
588 // first one matches post of registered users, second one guests and deleted users
589 $sql_author = '(' . $this->db->sql_in_set('p.poster_id', array_diff($author_ary, array(ANONYMOUS)), false, true) . ' OR p.post_username ' . $author_name . ')';
590 }
591 else
592 {
593 $sql_author = $this->db->sql_in_set('p.poster_id', $author_ary);
594 }
595 $sql_fora = (sizeof($ex_fid_ary)) ? ' AND ' . $this->db->sql_in_set('p.forum_id', $ex_fid_ary, true) : '';
596 $sql_topic_id = ($topic_id) ? ' AND p.topic_id = ' . (int) $topic_id : '';
597 $sql_time = ($sort_days) ? ' AND p.post_time >= ' . (time() - ($sort_days * 86400)) : '';
598 $sql_firstpost = ($firstpost_only) ? ' AND p.post_id = t.topic_first_post_id' : '';
599
600 // Build sql strings for sorting
601 $sql_sort = $sort_by_sql[$sort_key] . (($sort_dir == 'a') ? ' ASC' : ' DESC');
602 $sql_sort_table = $sql_sort_join = '';
603 switch ($sql_sort[0])
604 {
605 case 'u':
606 $sql_sort_table = USERS_TABLE . ' u, ';
607 $sql_sort_join = ($type == 'posts') ? ' AND u.user_id = p.poster_id ' : ' AND u.user_id = t.topic_poster ';
608 break;
609
610 case 't':
611 $sql_sort_table = ($type == 'posts' && !$firstpost_only) ? TOPICS_TABLE . ' t, ' : '';
612 $sql_sort_join = ($type == 'posts' && !$firstpost_only) ? ' AND t.topic_id = p.topic_id ' : '';
613 break;
614
615 case 'f':
616 $sql_sort_table = FORUMS_TABLE . ' f, ';
617 $sql_sort_join = ' AND f.forum_id = p.forum_id ';
618 break;
619 }
620
621 $m_approve_fid_sql = ' AND ' . $post_visibility;
622
623 // If the cache was completely empty count the results
624 $calc_results = ($result_count) ? '' : 'SQL_CALC_FOUND_ROWS ';
625
626 // Build the query for really selecting the post_ids
627 if ($type == 'posts')
628 {
629 $sql = "SELECT {$calc_results}p.post_id
630 FROM " . $sql_sort_table . POSTS_TABLE . ' p' . (($firstpost_only) ? ', ' . TOPICS_TABLE . ' t ' : ' ') . "
631 WHERE $sql_author
632 $sql_topic_id
633 $sql_firstpost
634 $m_approve_fid_sql
635 $sql_fora
636 $sql_sort_join
637 $sql_time
638 ORDER BY $sql_sort";
639 $field = 'post_id';
640 }
641 else
642 {
643 $sql = "SELECT {$calc_results}t.topic_id
644 FROM " . $sql_sort_table . TOPICS_TABLE . ' t, ' . POSTS_TABLE . " p
645 WHERE $sql_author
646 $sql_topic_id
647 $sql_firstpost
648 $m_approve_fid_sql
649 $sql_fora
650 AND t.topic_id = p.topic_id
651 $sql_sort_join
652 $sql_time
653 GROUP BY t.topic_id
654 ORDER BY $sql_sort";
655 $field = 'topic_id';
656 }
657
658 // Only read one block of posts from the db and then cache it
659 $result = $this->db->sql_query_limit($sql, $this->config['search_block_size'], $start);
660
661 while ($row = $this->db->sql_fetchrow($result))
662 {
663 $id_ary[] = (int) $row[$field];
664 }
665 $this->db->sql_freeresult($result);
666
667 // retrieve the total result count if needed
668 if (!$result_count)
669 {
670 $sql_found_rows = 'SELECT FOUND_ROWS() as result_count';
671 $result = $this->db->sql_query($sql_found_rows);
672 $result_count = (int) $this->db->sql_fetchfield('result_count');
673 $this->db->sql_freeresult($result);
674
675 if (!$result_count)
676 {
677 return false;
678 }
679 }
680
681 if ($start >= $result_count)
682 {
683 $start = floor(($result_count - 1) / $per_page) * $per_page;
684
685 $result = $this->db->sql_query_limit($sql, $this->config['search_block_size'], $start);
686 while ($row = $this->db->sql_fetchrow($result))
687 {
688 $id_ary[] = (int) $row[$field];
689 }
690 $this->db->sql_freeresult($result);
691
692 $id_ary = array_unique($id_ary);
693 }
694
695 if (sizeof($id_ary))
696 {
697 $this->save_ids($search_key, '', $author_ary, $result_count, $id_ary, $start, $sort_dir);
698 $id_ary = array_slice($id_ary, 0, $per_page);
699
700 return $result_count;
701 }
702 return false;
703 }
704
705 /**
706 * Destroys cached search results, that contained one of the new words in a post so the results won't be outdated
707 *
708 * @param string $mode contains the post mode: edit, post, reply, quote ...
709 * @param int $post_id contains the post id of the post to index
710 * @param string $message contains the post text of the post
711 * @param string $subject contains the subject of the post to index
712 * @param int $poster_id contains the user id of the poster
713 * @param int $forum_id contains the forum id of parent forum of the post
714 */
715 public function index($mode, $post_id, &$message, &$subject, $poster_id, $forum_id)
716 {
717 // Split old and new post/subject to obtain array of words
718 $split_text = $this->split_message($message);
719 $split_title = ($subject) ? $this->split_message($subject) : array();
720
721 $words = array_unique(array_merge($split_text, $split_title));
722
723 unset($split_text);
724 unset($split_title);
725
726 // destroy cached search results containing any of the words removed or added
727 $this->destroy_cache($words, array($poster_id));
728
729 unset($words);
730 }
731
732 /**
733 * Destroy cached results, that might be outdated after deleting a post
734 */
735 public function index_remove($post_ids, $author_ids, $forum_ids)
736 {
737 $this->destroy_cache(array(), array_unique($author_ids));
738 }
739
740 /**
741 * Destroy old cache entries
742 */
743 public function tidy()
744 {
745 // destroy too old cached search results
746 $this->destroy_cache(array());
747
748 set_config('search_last_gc', time(), true);
749 }
750
751 /**
752 * Create fulltext index
753 *
754 * @return string|bool error string is returned incase of errors otherwise false
755 */
756 public function create_index($acp_module, $u_action)
757 {
758 // Make sure we can actually use MySQL with fulltext indexes
759 if ($error = $this->init())
760 {
761 return $error;
762 }
763
764 if (empty($this->stats))
765 {
766 $this->get_stats();
767 }
768
769 $alter = array();
770
771 if (!isset($this->stats['post_subject']))
772 {
773 if ($this->db->get_sql_layer() == 'mysqli' || version_compare($this->db->sql_server_info(true), '4.1.3', '>='))
774 {
775 $alter[] = 'MODIFY post_subject varchar(255) COLLATE utf8_unicode_ci DEFAULT \'\' NOT NULL';
776 }
777 else
778 {
779 $alter[] = 'MODIFY post_subject text NOT NULL';
780 }
781 $alter[] = 'ADD FULLTEXT (post_subject)';
782 }
783
784 if (!isset($this->stats['post_content']))
785 {
786 if ($this->db->get_sql_layer() == 'mysqli' || version_compare($this->db->sql_server_info(true), '4.1.3', '>='))
787 {
788 $alter[] = 'MODIFY post_text mediumtext COLLATE utf8_unicode_ci NOT NULL';
789 }
790 else
791 {
792 $alter[] = 'MODIFY post_text mediumtext NOT NULL';
793 }
794
795 $alter[] = 'ADD FULLTEXT post_content (post_text, post_subject)';
796 }
797
798 if (sizeof($alter))
799 {
800 $this->db->sql_query('ALTER TABLE ' . POSTS_TABLE . ' ' . implode(', ', $alter));
801 }
802
803 $this->db->sql_query('TRUNCATE TABLE ' . SEARCH_RESULTS_TABLE);
804
805 return false;
806 }
807
808 /**
809 * Drop fulltext index
810 *
811 * @return string|bool error string is returned incase of errors otherwise false
812 */
813 public function delete_index($acp_module, $u_action)
814 {
815 // Make sure we can actually use MySQL with fulltext indexes
816 if ($error = $this->init())
817 {
818 return $error;
819 }
820
821 if (empty($this->stats))
822 {
823 $this->get_stats();
824 }
825
826 $alter = array();
827
828 if (isset($this->stats['post_subject']))
829 {
830 $alter[] = 'DROP INDEX post_subject';
831 }
832
833 if (isset($this->stats['post_content']))
834 {
835 $alter[] = 'DROP INDEX post_content';
836 }
837
838 if (sizeof($alter))
839 {
840 $this->db->sql_query('ALTER TABLE ' . POSTS_TABLE . ' ' . implode(', ', $alter));
841 }
842
843 $this->db->sql_query('TRUNCATE TABLE ' . SEARCH_RESULTS_TABLE);
844
845 return false;
846 }
847
848 /**
849 * Returns true if both FULLTEXT indexes exist
850 */
851 public function index_created()
852 {
853 if (empty($this->stats))
854 {
855 $this->get_stats();
856 }
857
858 return isset($this->stats['post_subject']) && isset($this->stats['post_content']);
859 }
860
861 /**
862 * Returns an associative array containing information about the indexes
863 */
864 public function index_stats()
865 {
866 if (empty($this->stats))
867 {
868 $this->get_stats();
869 }
870
871 return array(
872 $this->user->lang['FULLTEXT_MYSQL_TOTAL_POSTS'] => ($this->index_created()) ? $this->stats['total_posts'] : 0,
873 );
874 }
875
876 /**
877 * Computes the stats and store them in the $this->stats associative array
878 */
879 protected function get_stats()
880 {
881 if (strpos($this->db->get_sql_layer(), 'mysql') === false)
882 {
883 $this->stats = array();
884 return;
885 }
886
887 $sql = 'SHOW INDEX
888 FROM ' . POSTS_TABLE;
889 $result = $this->db->sql_query($sql);
890
891 while ($row = $this->db->sql_fetchrow($result))
892 {
893 // deal with older MySQL versions which didn't use Index_type
894 $index_type = (isset($row['Index_type'])) ? $row['Index_type'] : $row['Comment'];
895
896 if ($index_type == 'FULLTEXT')
897 {
898 if ($row['Key_name'] == 'post_subject')
899 {
900 $this->stats['post_subject'] = $row;
901 }
902 else if ($row['Key_name'] == 'post_content')
903 {
904 $this->stats['post_content'] = $row;
905 }
906 }
907 }
908 $this->db->sql_freeresult($result);
909
910 $this->stats['total_posts'] = empty($this->stats) ? 0 : $this->db->get_estimated_row_count(POSTS_TABLE);
911 }
912
913 /**
914 * Display a note, that UTF-8 support is not available with certain versions of PHP
915 *
916 * @return associative array containing template and config variables
917 */
918 public function acp()
919 {
920 $tpl = '
921 <dl>
922 <dt><label>' . $this->user->lang['MIN_SEARCH_CHARS'] . $this->user->lang['COLON'] . '</label><br /><span>' . $this->user->lang['FULLTEXT_MYSQL_MIN_SEARCH_CHARS_EXPLAIN'] . '</span></dt>
923 <dd>' . $this->config['fulltext_mysql_min_word_len'] . '</dd>
924 </dl>
925 <dl>
926 <dt><label>' . $this->user->lang['MAX_SEARCH_CHARS'] . $this->user->lang['COLON'] . '</label><br /><span>' . $this->user->lang['FULLTEXT_MYSQL_MAX_SEARCH_CHARS_EXPLAIN'] . '</span></dt>
927 <dd>' . $this->config['fulltext_mysql_max_word_len'] . '</dd>
928 </dl>
929 ';
930
931 // These are fields required in the config table
932 return array(
933 'tpl' => $tpl,
934 'config' => array()
935 );
936 }
937 }
938