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.
Auf den Verzeichnisnamen klicken, dies zeigt nur das Verzeichnis mit Inhalt an

(Beispiel Datei-Icons)

Auf das Icon klicken um den Quellcode anzuzeigen

fulltext_mysql.php

Zuletzt modifiziert: 09.10.2024, 12:52 - Dateigröße: 26.96 KiB


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