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_postgres.php

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