Verzeichnisstruktur phpBB-3.3.15


Veröffentlicht
28.08.2024

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: 02.04.2025, 15:02 - Dateigröße: 37.96 KiB


0001  <?php
0002  /**
0003  *
0004  * This file is part of the phpBB Forum Software package.
0005  *
0006  * @copyright (c) phpBB Limited <https://www.phpbb.com>
0007  * @license GNU General Public License, version 2 (GPL-2.0)
0008  *
0009  * For full copyright and license information, please see
0010  * the docs/CREDITS.txt file.
0011  *
0012  */
0013   
0014  namespace phpbb\search;
0015   
0016  /**
0017  * Fulltext search for MySQL
0018  */
0019  class fulltext_mysql extends \phpbb\search\base
0020  {
0021      /**
0022       * Associative array holding index stats
0023       * @var array
0024       */
0025      protected $stats = array();
0026   
0027      /**
0028       * Holds the words entered by user, obtained by splitting the entered query on whitespace
0029       * @var array
0030       */
0031      protected $split_words = array();
0032   
0033      /**
0034       * Config object
0035       * @var \phpbb\config\config
0036       */
0037      protected $config;
0038   
0039      /**
0040       * Database connection
0041       * @var \phpbb\db\driver\driver_interface
0042       */
0043      protected $db;
0044   
0045      /**
0046       * phpBB event dispatcher object
0047       * @var \phpbb\event\dispatcher_interface
0048       */
0049      protected $phpbb_dispatcher;
0050   
0051      /**
0052       * User object
0053       * @var \phpbb\user
0054       */
0055      protected $user;
0056   
0057      /**
0058       * Associative array stores the min and max word length to be searched
0059       * @var array
0060       */
0061      protected $word_length = array();
0062   
0063      /**
0064       * Contains tidied search query.
0065       * Operators are prefixed in search query and common words excluded
0066       * @var string
0067       */
0068      protected $search_query;
0069   
0070      /**
0071       * Contains common words.
0072       * Common words are words with length less/more than min/max length
0073       * @var array
0074       */
0075      protected $common_words = array();
0076   
0077      /**
0078       * Constructor
0079       * Creates a new \phpbb\search\fulltext_mysql, which is used as a search backend
0080       *
0081       * @param string|bool $error Any error that occurs is passed on through this reference variable otherwise false
0082       * @param string $phpbb_root_path Relative path to phpBB root
0083       * @param string $phpEx PHP file extension
0084       * @param \phpbb\auth\auth $auth Auth object
0085       * @param \phpbb\config\config $config Config object
0086       * @param \phpbb\db\driver\driver_interface $db Database object
0087       * @param \phpbb\user $user User object
0088       * @param \phpbb\event\dispatcher_interface    $phpbb_dispatcher    Event dispatcher object
0089       */
0090      public function __construct(&$error, $phpbb_root_path, $phpEx, $auth, $config, $db, $user, $phpbb_dispatcher)
0091      {
0092          $this->config = $config;
0093          $this->db = $db;
0094          $this->phpbb_dispatcher = $phpbb_dispatcher;
0095          $this->user = $user;
0096   
0097          $this->word_length = array('min' => $this->config['fulltext_mysql_min_word_len'], 'max' => $this->config['fulltext_mysql_max_word_len']);
0098   
0099          /**
0100           * Load the UTF tools
0101           */
0102          if (!function_exists('utf8_strlen'))
0103          {
0104              include($phpbb_root_path . 'includes/utf/utf_tools.' . $phpEx);
0105          }
0106   
0107          $error = false;
0108      }
0109   
0110      /**
0111      * Returns the name of this search backend to be displayed to administrators
0112      *
0113      * @return string Name
0114      */
0115      public function get_name()
0116      {
0117          return 'MySQL Fulltext';
0118      }
0119   
0120      /**
0121       * Returns the search_query
0122       *
0123       * @return string search query
0124       */
0125      public function get_search_query()
0126      {
0127          return $this->search_query;
0128      }
0129   
0130      /**
0131       * Returns the common_words array
0132       *
0133       * @return array common words that are ignored by search backend
0134       */
0135      public function get_common_words()
0136      {
0137          return $this->common_words;
0138      }
0139   
0140      /**
0141       * Returns the word_length array
0142       *
0143       * @return array min and max word length for searching
0144       */
0145      public function get_word_length()
0146      {
0147          return $this->word_length;
0148      }
0149   
0150      /**
0151      * Checks for correct MySQL version and stores min/max word length in the config
0152      *
0153      * @return string|bool Language key of the error/incompatibility occurred
0154      */
0155      public function init()
0156      {
0157          if ($this->db->get_sql_layer() != 'mysqli')
0158          {
0159              return $this->user->lang['FULLTEXT_MYSQL_INCOMPATIBLE_DATABASE'];
0160          }
0161   
0162          $result = $this->db->sql_query('SHOW TABLE STATUS LIKE \'' . POSTS_TABLE . '\'');
0163          $info = $this->db->sql_fetchrow($result);
0164          $this->db->sql_freeresult($result);
0165   
0166          $engine = '';
0167          if (isset($info['Engine']))
0168          {
0169              $engine = $info['Engine'];
0170          }
0171          else if (isset($info['Type']))
0172          {
0173              $engine = $info['Type'];
0174          }
0175   
0176          $fulltext_supported = $engine === 'Aria' || $engine === 'MyISAM'
0177              /**
0178               * FULLTEXT is supported on InnoDB since MySQL 5.6.4 according to
0179               * http://dev.mysql.com/doc/refman/5.6/en/innodb-storage-engine.html
0180               * We also require https://bugs.mysql.com/bug.php?id=67004 to be
0181               * fixed for proper overall operation. Hence we require 5.6.8.
0182               */
0183              || $engine === 'InnoDB'
0184              && phpbb_version_compare($this->db->sql_server_info(true), '5.6.8', '>=');
0185   
0186          if (!$fulltext_supported)
0187          {
0188              return $this->user->lang['FULLTEXT_MYSQL_NOT_SUPPORTED'];
0189          }
0190   
0191          $sql = 'SHOW VARIABLES
0192              LIKE \'%ft\_%\'';
0193          $result = $this->db->sql_query($sql);
0194   
0195          $mysql_info = array();
0196          while ($row = $this->db->sql_fetchrow($result))
0197          {
0198              $mysql_info[$row['Variable_name']] = $row['Value'];
0199          }
0200          $this->db->sql_freeresult($result);
0201   
0202          if ($engine === 'MyISAM')
0203          {
0204              $this->config->set('fulltext_mysql_max_word_len', $mysql_info['ft_max_word_len']);
0205              $this->config->set('fulltext_mysql_min_word_len', $mysql_info['ft_min_word_len']);
0206          }
0207          else if ($engine === 'InnoDB')
0208          {
0209              $this->config->set('fulltext_mysql_max_word_len', $mysql_info['innodb_ft_max_token_size']);
0210              $this->config->set('fulltext_mysql_min_word_len', $mysql_info['innodb_ft_min_token_size']);
0211          }
0212   
0213          return false;
0214      }
0215   
0216      /**
0217      * Splits keywords entered by a user into an array of words stored in $this->split_words
0218      * Stores the tidied search query in $this->search_query
0219      *
0220      * @param string &$keywords Contains the keyword as entered by the user
0221      * @param string $terms is either 'all' or 'any'
0222      * @return bool false if no valid keywords were found and otherwise true
0223      */
0224      public function split_keywords(&$keywords, $terms)
0225      {
0226          if ($terms == 'all')
0227          {
0228              $match        = array('#\sand\s#iu', '#\sor\s#iu', '#\snot\s#iu', '#(^|\s)\+#', '#(^|\s)-#', '#(^|\s)\|#');
0229              $replace    = array(' +', ' |', ' -', ' +', ' -', ' |');
0230   
0231              $keywords = preg_replace($match, $replace, $keywords);
0232          }
0233   
0234          // Filter out as above
0235          $split_keywords = preg_replace("#[\n\r\t]+#", ' ', trim(html_entity_decode($keywords, ENT_COMPAT)));
0236   
0237          // Split words
0238          $split_keywords = preg_replace('#([^\p{L}\p{N}\'*"()])#u', '$1$1', str_replace('\'\'', '\' \'', trim($split_keywords)));
0239          $matches = array();
0240          preg_match_all('#(?:[^\p{L}\p{N}*"()]|^)([+\-|]?(?:[\p{L}\p{N}*"()]+\'?)*[\p{L}\p{N}*"()])(?:[^\p{L}\p{N}*"()]|$)#u', $split_keywords, $matches);
0241          $this->split_words = $matches[1];
0242   
0243          // We limit the number of allowed keywords to minimize load on the database
0244          if ($this->config['max_num_search_keywords'] && count($this->split_words) > $this->config['max_num_search_keywords'])
0245          {
0246              trigger_error($this->user->lang('MAX_NUM_SEARCH_KEYWORDS_REFINE', (int) $this->config['max_num_search_keywords'], count($this->split_words)));
0247          }
0248   
0249          // to allow phrase search, we need to concatenate quoted words
0250          $tmp_split_words = array();
0251          $phrase = '';
0252          foreach ($this->split_words as $word)
0253          {
0254              if ($phrase)
0255              {
0256                  $phrase .= ' ' . $word;
0257                  if (strpos($word, '"') !== false && substr_count($word, '"') % 2 == 1)
0258                  {
0259                      $tmp_split_words[] = $phrase;
0260                      $phrase = '';
0261                  }
0262              }
0263              else if (strpos($word, '"') !== false && substr_count($word, '"') % 2 == 1)
0264              {
0265                  $phrase = $word;
0266              }
0267              else
0268              {
0269                  $tmp_split_words[] = $word;
0270              }
0271          }
0272          if ($phrase)
0273          {
0274              $tmp_split_words[] = $phrase;
0275          }
0276   
0277          $this->split_words = $tmp_split_words;
0278   
0279          unset($tmp_split_words);
0280          unset($phrase);
0281   
0282          foreach ($this->split_words as $i => $word)
0283          {
0284              // Check for not allowed search queries for InnoDB.
0285              // We assume similar restrictions for MyISAM, which is usually even
0286              // slower but not as restrictive as InnoDB.
0287              // InnoDB full-text search does not support the use of a leading
0288              // plus sign with wildcard ('+*'), a plus and minus sign
0289              // combination ('+-'), or leading a plus and minus sign combination.
0290              // InnoDB full-text search only supports leading plus or minus signs.
0291              // For example, InnoDB supports '+apple' but does not support 'apple+'.
0292              // Specifying a trailing plus or minus sign causes InnoDB to report
0293              // a syntax error. InnoDB full-text search does not support the use
0294              // of multiple operators on a single search word, as in this example:
0295              // '++apple'. Use of multiple operators on a single search word
0296              // returns a syntax error to standard out.
0297              // Also, ensure that the wildcard character is only used at the
0298              // end of the line as it's intended by MySQL.
0299              if (preg_match('#^(\+[+-]|\+\*|.+[+-]$|.+\*(?!$))#', $word))
0300              {
0301                  unset($this->split_words[$i]);
0302                  continue;
0303              }
0304   
0305              $clean_word = preg_replace('#^[+\-|"]#', '', $word);
0306   
0307              // check word length
0308              $clean_len = utf8_strlen(str_replace('*', '', $clean_word));
0309              if (($clean_len < $this->config['fulltext_mysql_min_word_len']) || ($clean_len > $this->config['fulltext_mysql_max_word_len']))
0310              {
0311                  $this->common_words[] = $word;
0312                  unset($this->split_words[$i]);
0313              }
0314          }
0315   
0316          if ($terms == 'any')
0317          {
0318              $this->search_query = '';
0319              foreach ($this->split_words as $word)
0320              {
0321                  if ((strpos($word, '+') === 0) || (strpos($word, '-') === 0) || (strpos($word, '|') === 0))
0322                  {
0323                      $word = substr($word, 1);
0324                  }
0325                  $this->search_query .= $word . ' ';
0326              }
0327          }
0328          else
0329          {
0330              $this->search_query = '';
0331              foreach ($this->split_words as $word)
0332              {
0333                  if ((strpos($word, '+') === 0) || (strpos($word, '-') === 0))
0334                  {
0335                      $this->search_query .= $word . ' ';
0336                  }
0337                  else if (strpos($word, '|') === 0)
0338                  {
0339                      $this->search_query .= substr($word, 1) . ' ';
0340                  }
0341                  else
0342                  {
0343                      $this->search_query .= '+' . $word . ' ';
0344                  }
0345              }
0346          }
0347   
0348          $this->search_query = utf8_htmlspecialchars($this->search_query);
0349   
0350          if ($this->search_query)
0351          {
0352              $this->split_words = array_values($this->split_words);
0353              sort($this->split_words);
0354              return true;
0355          }
0356          return false;
0357      }
0358   
0359      /**
0360      * Turns text into an array of words
0361      * @param string $text contains post text/subject
0362      */
0363      public function split_message($text)
0364      {
0365          // Split words
0366          $text = preg_replace('#([^\p{L}\p{N}\'*])#u', '$1$1', str_replace('\'\'', '\' \'', trim($text)));
0367          $matches = array();
0368          preg_match_all('#(?:[^\p{L}\p{N}*]|^)([+\-|]?(?:[\p{L}\p{N}*]+\'?)*[\p{L}\p{N}*])(?:[^\p{L}\p{N}*]|$)#u', $text, $matches);
0369          $text = $matches[1];
0370   
0371          // remove too short or too long words
0372          $text = array_values($text);
0373          for ($i = 0, $n = count($text); $i < $n; $i++)
0374          {
0375              $text[$i] = trim($text[$i]);
0376              if (utf8_strlen($text[$i]) < $this->config['fulltext_mysql_min_word_len'] || utf8_strlen($text[$i]) > $this->config['fulltext_mysql_max_word_len'])
0377              {
0378                  unset($text[$i]);
0379              }
0380          }
0381   
0382          return array_values($text);
0383      }
0384   
0385      /**
0386      * Performs a search on keywords depending on display specific params. You have to run split_keywords() first
0387      *
0388      * @param    string        $type                contains either posts or topics depending on what should be searched for
0389      * @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)
0390      * @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)
0391      * @param    array        $sort_by_sql        contains SQL code for the ORDER BY part of a query
0392      * @param    string        $sort_key            is the key of $sort_by_sql for the selected sorting
0393      * @param    string        $sort_dir            is either a or d representing ASC and DESC
0394      * @param    string        $sort_days            specifies the maximum amount of days a post may be old
0395      * @param    array        $ex_fid_ary            specifies an array of forum ids which should not be searched
0396      * @param    string        $post_visibility    specifies which types of posts the user can view in which forums
0397      * @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
0398      * @param    array        $author_ary            an array of author ids if the author should be ignored during the search the array is empty
0399      * @param    string        $author_name        specifies the author match, when ANONYMOUS is also a search-match
0400      * @param    array        &$id_ary            passed by reference, to be filled with ids for the page specified by $start and $per_page, should be ordered
0401      * @param    int            $start                indicates the first index of the page
0402      * @param    int            $per_page            number of ids each page is supposed to contain
0403      * @return    boolean|int                        total number of results
0404      */
0405      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)
0406      {
0407          // No keywords? No posts
0408          if (!$this->search_query)
0409          {
0410              return false;
0411          }
0412   
0413          // generate a search_key from all the options to identify the results
0414          $search_key_array = array(
0415              implode(', ', $this->split_words),
0416              $type,
0417              $fields,
0418              $terms,
0419              $sort_days,
0420              $sort_key,
0421              $topic_id,
0422              implode(',', $ex_fid_ary),
0423              $post_visibility,
0424              implode(',', $author_ary)
0425          );
0426   
0427          /**
0428          * Allow changing the search_key for cached results
0429          *
0430          * @event core.search_mysql_by_keyword_modify_search_key
0431          * @var    array    search_key_array    Array with search parameters to generate the search_key
0432          * @var    string    type                Searching type ('posts', 'topics')
0433          * @var    string    fields                Searching fields ('titleonly', 'msgonly', 'firstpost', 'all')
0434          * @var    string    terms                Searching terms ('all', 'any')
0435          * @var    int        sort_days            Time, in days, of the oldest possible post to list
0436          * @var    string    sort_key            The sort type used from the possible sort types
0437          * @var    int        topic_id            Limit the search to this topic_id only
0438          * @var    array    ex_fid_ary            Which forums not to search on
0439          * @var    string    post_visibility        Post visibility data
0440          * @var    array    author_ary            Array of user_id containing the users to filter the results to
0441          * @since 3.1.7-RC1
0442          */
0443          $vars = array(
0444              'search_key_array',
0445              'type',
0446              'fields',
0447              'terms',
0448              'sort_days',
0449              'sort_key',
0450              'topic_id',
0451              'ex_fid_ary',
0452              'post_visibility',
0453              'author_ary',
0454          );
0455          extract($this->phpbb_dispatcher->trigger_event('core.search_mysql_by_keyword_modify_search_key', compact($vars)));
0456   
0457          $search_key = md5(implode('#', $search_key_array));
0458   
0459          if ($start < 0)
0460          {
0461              $start = 0;
0462          }
0463   
0464          // try reading the results from cache
0465          $result_count = 0;
0466          if ($this->obtain_ids($search_key, $result_count, $id_ary, $start, $per_page, $sort_dir) == SEARCH_RESULT_IN_CACHE)
0467          {
0468              return $result_count;
0469          }
0470   
0471          $id_ary = array();
0472   
0473          $join_topic = ($type == 'posts') ? false : true;
0474   
0475          // Build sql strings for sorting
0476          $sql_sort = $sort_by_sql[$sort_key] . (($sort_dir == 'a') ? ' ASC' : ' DESC');
0477          $sql_sort_table = $sql_sort_join = '';
0478   
0479          switch ($sql_sort[0])
0480          {
0481              case 'u':
0482                  $sql_sort_table    = USERS_TABLE . ' u, ';
0483                  $sql_sort_join    = ($type == 'posts') ? ' AND u.user_id = p.poster_id ' : ' AND u.user_id = t.topic_poster ';
0484              break;
0485   
0486              case 't':
0487                  $join_topic = true;
0488              break;
0489   
0490              case 'f':
0491                  $sql_sort_table    = FORUMS_TABLE . ' f, ';
0492                  $sql_sort_join    = ' AND f.forum_id = p.forum_id ';
0493              break;
0494          }
0495   
0496          // Build some display specific sql strings
0497          switch ($fields)
0498          {
0499              case 'titleonly':
0500                  $sql_match = 'p.post_subject';
0501                  $sql_match_where = ' AND p.post_id = t.topic_first_post_id';
0502                  $join_topic = true;
0503              break;
0504   
0505              case 'msgonly':
0506                  $sql_match = 'p.post_text';
0507                  $sql_match_where = '';
0508              break;
0509   
0510              case 'firstpost':
0511                  $sql_match = 'p.post_subject, p.post_text';
0512                  $sql_match_where = ' AND p.post_id = t.topic_first_post_id';
0513                  $join_topic = true;
0514              break;
0515   
0516              default:
0517                  $sql_match = 'p.post_subject, p.post_text';
0518                  $sql_match_where = '';
0519              break;
0520          }
0521   
0522          $search_query = $this->search_query;
0523   
0524          /**
0525          * Allow changing the query used to search for posts using fulltext_mysql
0526          *
0527          * @event core.search_mysql_keywords_main_query_before
0528          * @var    string    search_query        The parsed keywords used for this search
0529          * @var    int        result_count        The previous result count for the format of the query.
0530          *                                    Set to 0 to force a re-count
0531          * @var    bool    join_topic            Weather or not TOPICS_TABLE should be CROSS JOIN'ED
0532          * @var    array    author_ary            Array of user_id containing the users to filter the results to
0533          * @var    string    author_name            An extra username to search on (!empty(author_ary) must be true, to be relevant)
0534          * @var    array    ex_fid_ary            Which forums not to search on
0535          * @var    int        topic_id            Limit the search to this topic_id only
0536          * @var    string    sql_sort_table        Extra tables to include in the SQL query.
0537          *                                    Used in conjunction with sql_sort_join
0538          * @var    string    sql_sort_join        SQL conditions to join all the tables used together.
0539          *                                    Used in conjunction with sql_sort_table
0540          * @var    int        sort_days            Time, in days, of the oldest possible post to list
0541          * @var    string    sql_match            Which columns to do the search on.
0542          * @var    string    sql_match_where        Extra conditions to use to properly filter the matching process
0543          * @var    string    sort_by_sql            The possible predefined sort types
0544          * @var    string    sort_key            The sort type used from the possible sort types
0545          * @var    string    sort_dir            "a" for ASC or "d" dor DESC for the sort order used
0546          * @var    string    sql_sort            The result SQL when processing sort_by_sql + sort_key + sort_dir
0547          * @var    int        start                How many posts to skip in the search results (used for pagination)
0548          * @since 3.1.5-RC1
0549          */
0550          $vars = array(
0551              'search_query',
0552              'result_count',
0553              'join_topic',
0554              'author_ary',
0555              'author_name',
0556              'ex_fid_ary',
0557              'topic_id',
0558              'sql_sort_table',
0559              'sql_sort_join',
0560              'sort_days',
0561              'sql_match',
0562              'sql_match_where',
0563              'sort_by_sql',
0564              'sort_key',
0565              'sort_dir',
0566              'sql_sort',
0567              'start',
0568          );
0569          extract($this->phpbb_dispatcher->trigger_event('core.search_mysql_keywords_main_query_before', compact($vars)));
0570   
0571          $sql_select            = ($type == 'posts') ? 'DISTINCT p.post_id' : 'DISTINCT t.topic_id';
0572          $sql_select            .= $sort_by_sql[$sort_key] ? "{$sort_by_sql[$sort_key]}" : '';
0573          $sql_from            = ($join_topic) ? TOPICS_TABLE . ' t, ' : '';
0574          $field                = ($type == 'posts') ? 'post_id' : 'topic_id';
0575          if (count($author_ary) && $author_name)
0576          {
0577              // first one matches post of registered users, second one guests and deleted users
0578              $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 . ')';
0579          }
0580          else if (count($author_ary))
0581          {
0582              $sql_author = ' AND ' . $this->db->sql_in_set('p.poster_id', $author_ary);
0583          }
0584          else
0585          {
0586              $sql_author = '';
0587          }
0588   
0589          $sql_where_options = $sql_sort_join;
0590          $sql_where_options .= ($topic_id) ? ' AND p.topic_id = ' . $topic_id : '';
0591          $sql_where_options .= ($join_topic) ? ' AND t.topic_id = p.topic_id' : '';
0592          $sql_where_options .= (count($ex_fid_ary)) ? ' AND ' . $this->db->sql_in_set('p.forum_id', $ex_fid_ary, true) : '';
0593          $sql_where_options .= ' AND ' . $post_visibility;
0594          $sql_where_options .= $sql_author;
0595          $sql_where_options .= ($sort_days) ? ' AND p.post_time >= ' . (time() - ($sort_days * 86400)) : '';
0596          $sql_where_options .= $sql_match_where;
0597   
0598          $sql = "SELECT $sql_select
0599              FROM $sql_from$sql_sort_table" . POSTS_TABLE . " p
0600              WHERE MATCH ($sql_match) AGAINST ('" . $this->db->sql_escape(html_entity_decode($this->search_query, ENT_COMPAT)) . "' IN BOOLEAN MODE)
0601                  $sql_where_options
0602              ORDER BY $sql_sort";
0603          $this->db->sql_return_on_error(true);
0604          $result = $this->db->sql_query_limit($sql, $this->config['search_block_size'], $start);
0605   
0606          while ($row = $this->db->sql_fetchrow($result))
0607          {
0608              $id_ary[] = (int) $row[$field];
0609          }
0610          $this->db->sql_freeresult($result);
0611   
0612          $id_ary = array_unique($id_ary);
0613          // if the total result count is not cached yet, retrieve it from the db
0614          if (!$result_count && count($id_ary))
0615          {
0616              $sql_found_rows = str_replace("SELECT $sql_select", "SELECT COUNT($sql_select) as result_count", $sql);
0617              $result = $this->db->sql_query($sql_found_rows);
0618              $result_count = (int) $this->db->sql_fetchfield('result_count');
0619              $this->db->sql_freeresult($result);
0620   
0621              if (!$result_count)
0622              {
0623                  return false;
0624              }
0625          }
0626   
0627          if ($start >= $result_count)
0628          {
0629              $start = floor(($result_count - 1) / $per_page) * $per_page;
0630   
0631              $result = $this->db->sql_query_limit($sql, $this->config['search_block_size'], $start);
0632   
0633              while ($row = $this->db->sql_fetchrow($result))
0634              {
0635                  $id_ary[] = (int) $row[$field];
0636              }
0637              $this->db->sql_freeresult($result);
0638   
0639              $id_ary = array_unique($id_ary);
0640          }
0641   
0642          // store the ids, from start on then delete anything that isn't on the current page because we only need ids for one page
0643          $this->save_ids($search_key, implode(' ', $this->split_words), $author_ary, $result_count, $id_ary, $start, $sort_dir);
0644          $id_ary = array_slice($id_ary, 0, (int) $per_page);
0645   
0646          return $result_count;
0647      }
0648   
0649      /**
0650      * Performs a search on an author's posts without caring about message contents. Depends on display specific params
0651      *
0652      * @param    string        $type                contains either posts or topics depending on what should be searched for
0653      * @param    boolean        $firstpost_only        if true, only topic starting posts will be considered
0654      * @param    array        $sort_by_sql        contains SQL code for the ORDER BY part of a query
0655      * @param    string        $sort_key            is the key of $sort_by_sql for the selected sorting
0656      * @param    string        $sort_dir            is either a or d representing ASC and DESC
0657      * @param    string        $sort_days            specifies the maximum amount of days a post may be old
0658      * @param    array        $ex_fid_ary            specifies an array of forum ids which should not be searched
0659      * @param    string        $post_visibility    specifies which types of posts the user can view in which forums
0660      * @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
0661      * @param    array        $author_ary            an array of author ids
0662      * @param    string        $author_name        specifies the author match, when ANONYMOUS is also a search-match
0663      * @param    array        &$id_ary            passed by reference, to be filled with ids for the page specified by $start and $per_page, should be ordered
0664      * @param    int            $start                indicates the first index of the page
0665      * @param    int            $per_page            number of ids each page is supposed to contain
0666      * @return    boolean|int                        total number of results
0667      */
0668      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)
0669      {
0670          // No author? No posts
0671          if (!count($author_ary))
0672          {
0673              return 0;
0674          }
0675   
0676          // generate a search_key from all the options to identify the results
0677          $search_key_array = array(
0678              '',
0679              $type,
0680              ($firstpost_only) ? 'firstpost' : '',
0681              '',
0682              '',
0683              $sort_days,
0684              $sort_key,
0685              $topic_id,
0686              implode(',', $ex_fid_ary),
0687              $post_visibility,
0688              implode(',', $author_ary),
0689              $author_name,
0690          );
0691   
0692          /**
0693          * Allow changing the search_key for cached results
0694          *
0695          * @event core.search_mysql_by_author_modify_search_key
0696          * @var    array    search_key_array    Array with search parameters to generate the search_key
0697          * @var    string    type                Searching type ('posts', 'topics')
0698          * @var    boolean    firstpost_only        Flag indicating if only topic starting posts are considered
0699          * @var    int        sort_days            Time, in days, of the oldest possible post to list
0700          * @var    string    sort_key            The sort type used from the possible sort types
0701          * @var    int        topic_id            Limit the search to this topic_id only
0702          * @var    array    ex_fid_ary            Which forums not to search on
0703          * @var    string    post_visibility        Post visibility data
0704          * @var    array    author_ary            Array of user_id containing the users to filter the results to
0705          * @var    string    author_name            The username to search on
0706          * @since 3.1.7-RC1
0707          */
0708          $vars = array(
0709              'search_key_array',
0710              'type',
0711              'firstpost_only',
0712              'sort_days',
0713              'sort_key',
0714              'topic_id',
0715              'ex_fid_ary',
0716              'post_visibility',
0717              'author_ary',
0718              'author_name',
0719          );
0720          extract($this->phpbb_dispatcher->trigger_event('core.search_mysql_by_author_modify_search_key', compact($vars)));
0721   
0722          $search_key = md5(implode('#', $search_key_array));
0723   
0724          if ($start < 0)
0725          {
0726              $start = 0;
0727          }
0728   
0729          // try reading the results from cache
0730          $result_count = 0;
0731          if ($this->obtain_ids($search_key, $result_count, $id_ary, $start, $per_page, $sort_dir) == SEARCH_RESULT_IN_CACHE)
0732          {
0733              return $result_count;
0734          }
0735   
0736          $id_ary = array();
0737   
0738          // Create some display specific sql strings
0739          if ($author_name)
0740          {
0741              // first one matches post of registered users, second one guests and deleted users
0742              $sql_author = '(' . $this->db->sql_in_set('p.poster_id', array_diff($author_ary, array(ANONYMOUS)), false, true) . ' OR p.post_username ' . $author_name . ')';
0743          }
0744          else
0745          {
0746              $sql_author = $this->db->sql_in_set('p.poster_id', $author_ary);
0747          }
0748          $sql_fora        = (count($ex_fid_ary)) ? ' AND ' . $this->db->sql_in_set('p.forum_id', $ex_fid_ary, true) : '';
0749          $sql_topic_id    = ($topic_id) ? ' AND p.topic_id = ' . (int) $topic_id : '';
0750          $sql_time        = ($sort_days) ? ' AND p.post_time >= ' . (time() - ($sort_days * 86400)) : '';
0751          $sql_firstpost = ($firstpost_only) ? ' AND p.post_id = t.topic_first_post_id' : '';
0752   
0753          // Build sql strings for sorting
0754          $sql_sort = $sort_by_sql[$sort_key] . (($sort_dir == 'a') ? ' ASC' : ' DESC');
0755          $sql_sort_table = $sql_sort_join = '';
0756          switch ($sql_sort[0])
0757          {
0758              case 'u':
0759                  $sql_sort_table    = USERS_TABLE . ' u, ';
0760                  $sql_sort_join    = ($type == 'posts') ? ' AND u.user_id = p.poster_id ' : ' AND u.user_id = t.topic_poster ';
0761              break;
0762   
0763              case 't':
0764                  $sql_sort_table    = ($type == 'posts' && !$firstpost_only) ? TOPICS_TABLE . ' t, ' : '';
0765                  $sql_sort_join    = ($type == 'posts' && !$firstpost_only) ? ' AND t.topic_id = p.topic_id ' : '';
0766              break;
0767   
0768              case 'f':
0769                  $sql_sort_table    = FORUMS_TABLE . ' f, ';
0770                  $sql_sort_join    = ' AND f.forum_id = p.forum_id ';
0771              break;
0772          }
0773   
0774          $m_approve_fid_sql = ' AND ' . $post_visibility;
0775   
0776          /**
0777          * Allow changing the query used to search for posts by author in fulltext_mysql
0778          *
0779          * @event core.search_mysql_author_query_before
0780          * @var    int        result_count        The previous result count for the format of the query.
0781          *                                    Set to 0 to force a re-count
0782          * @var    string    sql_sort_table        CROSS JOIN'ed table to allow doing the sort chosen
0783          * @var    string    sql_sort_join        Condition to define how to join the CROSS JOIN'ed table specifyed in sql_sort_table
0784          * @var    string    type                Either "posts" or "topics" specifying the type of search being made
0785          * @var    array    author_ary            Array of user_id containing the users to filter the results to
0786          * @var    string    author_name            An extra username to search on
0787          * @var    string    sql_author            SQL WHERE condition for the post author ids
0788          * @var    int        topic_id            Limit the search to this topic_id only
0789          * @var    string    sql_topic_id        SQL of topic_id
0790          * @var    string    sort_by_sql            The possible predefined sort types
0791          * @var    string    sort_key            The sort type used from the possible sort types
0792          * @var    string    sort_dir            "a" for ASC or "d" dor DESC for the sort order used
0793          * @var    string    sql_sort            The result SQL when processing sort_by_sql + sort_key + sort_dir
0794          * @var    string    sort_days            Time, in days, that the oldest post showing can have
0795          * @var    string    sql_time            The SQL to search on the time specifyed by sort_days
0796          * @var    bool    firstpost_only        Wether or not to search only on the first post of the topics
0797          * @var    string    sql_firstpost        The SQL with the conditions to join the tables when using firstpost_only
0798          * @var    array    ex_fid_ary            Forum ids that must not be searched on
0799          * @var    array    sql_fora            SQL query for ex_fid_ary
0800          * @var    string    m_approve_fid_sql    WHERE clause condition on post_visibility restrictions
0801          * @var    int        start                How many posts to skip in the search results (used for pagination)
0802          * @since 3.1.5-RC1
0803          */
0804          $vars = array(
0805              'result_count',
0806              'sql_sort_table',
0807              'sql_sort_join',
0808              'type',
0809              'author_ary',
0810              'author_name',
0811              'sql_author',
0812              'topic_id',
0813              'sql_topic_id',
0814              'sort_by_sql',
0815              'sort_key',
0816              'sort_dir',
0817              'sql_sort',
0818              'sort_days',
0819              'sql_time',
0820              'firstpost_only',
0821              'sql_firstpost',
0822              'ex_fid_ary',
0823              'sql_fora',
0824              'm_approve_fid_sql',
0825              'start',
0826          );
0827          extract($this->phpbb_dispatcher->trigger_event('core.search_mysql_author_query_before', compact($vars)));
0828   
0829          // If the cache was completely empty count the results
0830          $sql_select    = ($type == 'posts') ? 'p.post_id' : 't.topic_id';
0831          $sql_select    .= $sort_by_sql[$sort_key] ? "{$sort_by_sql[$sort_key]}" : '';
0832   
0833          // Build the query for really selecting the post_ids
0834          if ($type == 'posts')
0835          {
0836              $sql = "SELECT $sql_select
0837                  FROM " . $sql_sort_table . POSTS_TABLE . ' p' . (($firstpost_only) ? ', ' . TOPICS_TABLE . ' t ' : ' ') . "
0838                  WHERE $sql_author
0839                      $sql_topic_id
0840                      $sql_firstpost
0841                      $m_approve_fid_sql
0842                      $sql_fora
0843                      $sql_sort_join
0844                      $sql_time
0845                  ORDER BY $sql_sort";
0846              $field = 'post_id';
0847          }
0848          else
0849          {
0850              $sql = "SELECT $sql_select
0851                  FROM " . $sql_sort_table . TOPICS_TABLE . ' t, ' . POSTS_TABLE . " p
0852                  WHERE $sql_author
0853                      $sql_topic_id
0854                      $sql_firstpost
0855                      $m_approve_fid_sql
0856                      $sql_fora
0857                      AND t.topic_id = p.topic_id
0858                      $sql_sort_join
0859                      $sql_time
0860                  GROUP BY $sql_select
0861                  ORDER BY $sql_sort";
0862              $field = 'topic_id';
0863          }
0864   
0865          // Only read one block of posts from the db and then cache it
0866          $result = $this->db->sql_query_limit($sql, $this->config['search_block_size'], $start);
0867   
0868          while ($row = $this->db->sql_fetchrow($result))
0869          {
0870              $id_ary[] = (int) $row[$field];
0871          }
0872          $this->db->sql_freeresult($result);
0873   
0874          // retrieve the total result count if needed
0875          if (!$result_count)
0876          {
0877              $sql_found_rows = str_replace("SELECT $sql_select", "SELECT COUNT(*) as result_count", $sql);
0878              $result = $this->db->sql_query($sql_found_rows);
0879              $result_count = ($type == 'posts') ? (int) $this->db->sql_fetchfield('result_count') : count($this->db->sql_fetchrowset($result));
0880   
0881              $this->db->sql_freeresult($result);
0882   
0883              if (!$result_count)
0884              {
0885                  return false;
0886              }
0887          }
0888   
0889          if ($start >= $result_count)
0890          {
0891              $start = floor(($result_count - 1) / $per_page) * $per_page;
0892   
0893              $result = $this->db->sql_query_limit($sql, $this->config['search_block_size'], $start);
0894              while ($row = $this->db->sql_fetchrow($result))
0895              {
0896                  $id_ary[] = (int) $row[$field];
0897              }
0898              $this->db->sql_freeresult($result);
0899   
0900              $id_ary = array_unique($id_ary);
0901          }
0902   
0903          if (count($id_ary))
0904          {
0905              $this->save_ids($search_key, '', $author_ary, $result_count, $id_ary, $start, $sort_dir);
0906              $id_ary = array_slice($id_ary, 0, $per_page);
0907   
0908              return $result_count;
0909          }
0910          return false;
0911      }
0912   
0913      /**
0914      * Destroys cached search results, that contained one of the new words in a post so the results won't be outdated
0915      *
0916      * @param    string        $mode contains the post mode: edit, post, reply, quote ...
0917      * @param    int            $post_id    contains the post id of the post to index
0918      * @param    string        $message    contains the post text of the post
0919      * @param    string        $subject    contains the subject of the post to index
0920      * @param    int            $poster_id    contains the user id of the poster
0921      * @param    int            $forum_id    contains the forum id of parent forum of the post
0922      */
0923      public function index($mode, $post_id, &$message, &$subject, $poster_id, $forum_id)
0924      {
0925          // Split old and new post/subject to obtain array of words
0926          $split_text = $this->split_message($message);
0927          $split_title = ($subject) ? $this->split_message($subject) : array();
0928   
0929          $words = array_unique(array_merge($split_text, $split_title));
0930   
0931          /**
0932          * Event to modify method arguments and words before the MySQL search index is updated
0933          *
0934          * @event core.search_mysql_index_before
0935          * @var string    mode                Contains the post mode: edit, post, reply, quote
0936          * @var int        post_id                The id of the post which is modified/created
0937          * @var string    message                New or updated post content
0938          * @var string    subject                New or updated post subject
0939          * @var int        poster_id            Post author's user id
0940          * @var int        forum_id            The id of the forum in which the post is located
0941          * @var array    words                List of words added to the index
0942          * @var array    split_text            Array of words from the message
0943          * @var array    split_title            Array of words from the title
0944          * @since 3.2.3-RC1
0945          */
0946          $vars = array(
0947              'mode',
0948              'post_id',
0949              'message',
0950              'subject',
0951              'poster_id',
0952              'forum_id',
0953              'words',
0954              'split_text',
0955              'split_title',
0956          );
0957          extract($this->phpbb_dispatcher->trigger_event('core.search_mysql_index_before', compact($vars)));
0958   
0959          unset($split_text);
0960          unset($split_title);
0961   
0962          // destroy cached search results containing any of the words removed or added
0963          $this->destroy_cache($words, array($poster_id));
0964   
0965          unset($words);
0966      }
0967   
0968      /**
0969      * Destroy cached results, that might be outdated after deleting a post
0970      */
0971      public function index_remove($post_ids, $author_ids, $forum_ids)
0972      {
0973          $this->destroy_cache(array(), array_unique($author_ids));
0974      }
0975   
0976      /**
0977      * Destroy old cache entries
0978      */
0979      public function tidy()
0980      {
0981          // destroy too old cached search results
0982          $this->destroy_cache(array());
0983   
0984          $this->config->set('search_last_gc', time(), false);
0985      }
0986   
0987      /**
0988      * Create fulltext index
0989      *
0990      * @return string|bool error string is returned incase of errors otherwise false
0991      */
0992      public function create_index($acp_module, $u_action)
0993      {
0994          // Make sure we can actually use MySQL with fulltext indexes
0995          if ($error = $this->init())
0996          {
0997              return $error;
0998          }
0999   
1000          if (empty($this->stats))
1001          {
1002              $this->get_stats();
1003          }
1004   
1005          $alter_list = array();
1006   
1007          if (!isset($this->stats['post_subject']))
1008          {
1009              $alter_entry = array();
1010              $alter_entry[] = 'MODIFY post_subject varchar(255) COLLATE utf8_unicode_ci DEFAULT \'\' NOT NULL';
1011              $alter_entry[] = 'ADD FULLTEXT (post_subject)';
1012              $alter_list[] = $alter_entry;
1013          }
1014   
1015          if (!isset($this->stats['post_content']))
1016          {
1017              $alter_entry = array();
1018              $alter_entry[] = 'MODIFY post_text mediumtext COLLATE utf8_unicode_ci NOT NULL';
1019              $alter_entry[] = 'ADD FULLTEXT post_content (post_text, post_subject)';
1020              $alter_list[] = $alter_entry;
1021          }
1022   
1023          $sql_queries = [];
1024   
1025          foreach ($alter_list as $alter)
1026          {
1027              $sql_queries[] = 'ALTER TABLE ' . POSTS_TABLE . ' ' . implode(', ', $alter);
1028          }
1029   
1030          if (!isset($this->stats['post_text']))
1031          {
1032              $sql_queries[] = 'ALTER TABLE ' . POSTS_TABLE . ' ADD FULLTEXT post_text (post_text)';
1033          }
1034   
1035          $stats = $this->stats;
1036   
1037          /**
1038          * Event to modify SQL queries before the MySQL search index is created
1039          *
1040          * @event core.search_mysql_create_index_before
1041          * @var array    sql_queries            Array with queries for creating the search index
1042          * @var array    stats                Array with statistics of the current index (read only)
1043          * @since 3.2.3-RC1
1044          */
1045          $vars = array(
1046              'sql_queries',
1047              'stats',
1048          );
1049          extract($this->phpbb_dispatcher->trigger_event('core.search_mysql_create_index_before', compact($vars)));
1050   
1051          foreach ($sql_queries as $sql_query)
1052          {
1053              $this->db->sql_query($sql_query);
1054          }
1055   
1056          $this->db->sql_query('TRUNCATE TABLE ' . SEARCH_RESULTS_TABLE);
1057   
1058          return false;
1059      }
1060   
1061      /**
1062      * Drop fulltext index
1063      *
1064      * @return string|bool error string is returned incase of errors otherwise false
1065      */
1066      public function delete_index($acp_module, $u_action)
1067      {
1068          // Make sure we can actually use MySQL with fulltext indexes
1069          if ($error = $this->init())
1070          {
1071              return $error;
1072          }
1073   
1074          if (empty($this->stats))
1075          {
1076              $this->get_stats();
1077          }
1078   
1079          $alter = array();
1080   
1081          if (isset($this->stats['post_subject']))
1082          {
1083              $alter[] = 'DROP INDEX post_subject';
1084          }
1085   
1086          if (isset($this->stats['post_content']))
1087          {
1088              $alter[] = 'DROP INDEX post_content';
1089          }
1090   
1091          if (isset($this->stats['post_text']))
1092          {
1093              $alter[] = 'DROP INDEX post_text';
1094          }
1095   
1096          $sql_queries = [];
1097   
1098          if (count($alter))
1099          {
1100              $sql_queries[] = 'ALTER TABLE ' . POSTS_TABLE . ' ' . implode(', ', $alter);
1101          }
1102   
1103          $stats = $this->stats;
1104   
1105          /**
1106          * Event to modify SQL queries before the MySQL search index is deleted
1107          *
1108          * @event core.search_mysql_delete_index_before
1109          * @var array    sql_queries            Array with queries for deleting the search index
1110          * @var array    stats                Array with statistics of the current index (read only)
1111          * @since 3.2.3-RC1
1112          */
1113          $vars = array(
1114              'sql_queries',
1115              'stats',
1116          );
1117          extract($this->phpbb_dispatcher->trigger_event('core.search_mysql_delete_index_before', compact($vars)));
1118   
1119          foreach ($sql_queries as $sql_query)
1120          {
1121              $this->db->sql_query($sql_query);
1122          }
1123   
1124          $this->db->sql_query('TRUNCATE TABLE ' . SEARCH_RESULTS_TABLE);
1125   
1126          return false;
1127      }
1128   
1129      /**
1130      * Returns true if both FULLTEXT indexes exist
1131      */
1132      public function index_created()
1133      {
1134          if (empty($this->stats))
1135          {
1136              $this->get_stats();
1137          }
1138   
1139          return isset($this->stats['post_subject']) && isset($this->stats['post_content']) && isset($this->stats['post_text']);
1140      }
1141   
1142      /**
1143      * Returns an associative array containing information about the indexes
1144      */
1145      public function index_stats()
1146      {
1147          if (empty($this->stats))
1148          {
1149              $this->get_stats();
1150          }
1151   
1152          return array(
1153              $this->user->lang['FULLTEXT_MYSQL_TOTAL_POSTS']            => ($this->index_created()) ? $this->stats['total_posts'] : 0,
1154          );
1155      }
1156   
1157      /**
1158       * Computes the stats and store them in the $this->stats associative array
1159       */
1160      protected function get_stats()
1161      {
1162          if (strpos($this->db->get_sql_layer(), 'mysql') === false)
1163          {
1164              $this->stats = array();
1165              return;
1166          }
1167   
1168          $sql = 'SHOW INDEX
1169              FROM ' . POSTS_TABLE;
1170          $result = $this->db->sql_query($sql);
1171   
1172          while ($row = $this->db->sql_fetchrow($result))
1173          {
1174              // deal with older MySQL versions which didn't use Index_type
1175              $index_type = (isset($row['Index_type'])) ? $row['Index_type'] : $row['Comment'];
1176   
1177              if ($index_type == 'FULLTEXT')
1178              {
1179                  if ($row['Key_name'] == 'post_subject')
1180                  {
1181                      $this->stats['post_subject'] = $row;
1182                  }
1183                  else if ($row['Key_name'] == 'post_text')
1184                  {
1185                      $this->stats['post_text'] = $row;
1186                  }
1187                  else if ($row['Key_name'] == 'post_content')
1188                  {
1189                      $this->stats['post_content'] = $row;
1190                  }
1191              }
1192          }
1193          $this->db->sql_freeresult($result);
1194   
1195          $this->stats['total_posts'] = empty($this->stats) ? 0 : $this->db->get_estimated_row_count(POSTS_TABLE);
1196      }
1197   
1198      /**
1199      * Display a note, that UTF-8 support is not available with certain versions of PHP
1200      *
1201      * @return associative array containing template and config variables
1202      */
1203      public function acp()
1204      {
1205          $tpl = '
1206          <dl>
1207              <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>
1208              <dd>' . $this->config['fulltext_mysql_min_word_len'] . '</dd>
1209          </dl>
1210          <dl>
1211              <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>
1212              <dd>' . $this->config['fulltext_mysql_max_word_len'] . '</dd>
1213          </dl>
1214          ';
1215   
1216          // These are fields required in the config table
1217          return array(
1218              'tpl'        => $tpl,
1219              'config'    => array()
1220          );
1221      }
1222  }
1223