PageRenderTime 51ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 0ms

/phpBB/phpbb/search/fulltext_mysql.php

https://github.com/Jipem/phpbb
PHP | 931 lines | 598 code | 126 blank | 207 comment | 106 complexity | ab99734c7cbcc356e83034a18bd09e96 MD5 | raw file
Possible License(s): AGPL-1.0
  1. <?php
  2. /**
  3. *
  4. * This file is part of the phpBB Forum Software package.
  5. *
  6. * @copyright (c) phpBB Limited <https://www.phpbb.com>
  7. * @license GNU General Public License, version 2 (GPL-2.0)
  8. *
  9. * For full copyright and license information, please see
  10. * the docs/CREDITS.txt file.
  11. *
  12. */
  13. namespace phpbb\search;
  14. /**
  15. * Fulltext search for MySQL
  16. */
  17. class fulltext_mysql extends \phpbb\search\base
  18. {
  19. /**
  20. * Associative array holding index stats
  21. * @var array
  22. */
  23. protected $stats = array();
  24. /**
  25. * Holds the words entered by user, obtained by splitting the entered query on whitespace
  26. * @var array
  27. */
  28. protected $split_words = array();
  29. /**
  30. * Config object
  31. * @var \phpbb\config\config
  32. */
  33. protected $config;
  34. /**
  35. * Database connection
  36. * @var \phpbb\db\driver\driver_interface
  37. */
  38. protected $db;
  39. /**
  40. * User object
  41. * @var \phpbb\user
  42. */
  43. protected $user;
  44. /**
  45. * Associative array stores the min and max word length to be searched
  46. * @var array
  47. */
  48. protected $word_length = array();
  49. /**
  50. * Contains tidied search query.
  51. * Operators are prefixed in search query and common words excluded
  52. * @var string
  53. */
  54. protected $search_query;
  55. /**
  56. * Contains common words.
  57. * Common words are words with length less/more than min/max length
  58. * @var array
  59. */
  60. protected $common_words = array();
  61. /**
  62. * Constructor
  63. * Creates a new \phpbb\search\fulltext_mysql, which is used as a search backend
  64. *
  65. * @param string|bool $error Any error that occurs is passed on through this reference variable otherwise false
  66. */
  67. public function __construct(&$error, $phpbb_root_path, $phpEx, $auth, $config, $db, $user)
  68. {
  69. $this->config = $config;
  70. $this->db = $db;
  71. $this->user = $user;
  72. $this->word_length = array('min' => $this->config['fulltext_mysql_min_word_len'], 'max' => $this->config['fulltext_mysql_max_word_len']);
  73. /**
  74. * Load the UTF tools
  75. */
  76. if (!function_exists('utf8_strlen'))
  77. {
  78. include($phpbb_root_path . 'includes/utf/utf_tools.' . $phpEx);
  79. }
  80. $error = false;
  81. }
  82. /**
  83. * Returns the name of this search backend to be displayed to administrators
  84. *
  85. * @return string Name
  86. */
  87. public function get_name()
  88. {
  89. return 'MySQL Fulltext';
  90. }
  91. /**
  92. * Returns the search_query
  93. *
  94. * @return string search query
  95. */
  96. public function get_search_query()
  97. {
  98. return $this->search_query;
  99. }
  100. /**
  101. * Returns the common_words array
  102. *
  103. * @return array common words that are ignored by search backend
  104. */
  105. public function get_common_words()
  106. {
  107. return $this->common_words;
  108. }
  109. /**
  110. * Returns the word_length array
  111. *
  112. * @return array min and max word length for searching
  113. */
  114. public function get_word_length()
  115. {
  116. return $this->word_length;
  117. }
  118. /**
  119. * Checks for correct MySQL version and stores min/max word length in the config
  120. *
  121. * @return string|bool Language key of the error/incompatiblity occurred
  122. */
  123. public function init()
  124. {
  125. if ($this->db->sql_layer != 'mysql4' && $this->db->sql_layer != 'mysqli')
  126. {
  127. return $this->user->lang['FULLTEXT_MYSQL_INCOMPATIBLE_DATABASE'];
  128. }
  129. $result = $this->db->sql_query('SHOW TABLE STATUS LIKE \'' . POSTS_TABLE . '\'');
  130. $info = $this->db->sql_fetchrow($result);
  131. $this->db->sql_freeresult($result);
  132. $engine = '';
  133. if (isset($info['Engine']))
  134. {
  135. $engine = $info['Engine'];
  136. }
  137. else if (isset($info['Type']))
  138. {
  139. $engine = $info['Type'];
  140. }
  141. $fulltext_supported =
  142. $engine === 'MyISAM' ||
  143. // FULLTEXT is supported on InnoDB since MySQL 5.6.4 according to
  144. // http://dev.mysql.com/doc/refman/5.6/en/innodb-storage-engine.html
  145. $engine === 'InnoDB' &&
  146. phpbb_version_compare($this->db->sql_server_info(true), '5.6.4', '>=');
  147. if (!$fulltext_supported)
  148. {
  149. return $this->user->lang['FULLTEXT_MYSQL_NOT_SUPPORTED'];
  150. }
  151. $sql = 'SHOW VARIABLES
  152. LIKE \'ft\_%\'';
  153. $result = $this->db->sql_query($sql);
  154. $mysql_info = array();
  155. while ($row = $this->db->sql_fetchrow($result))
  156. {
  157. $mysql_info[$row['Variable_name']] = $row['Value'];
  158. }
  159. $this->db->sql_freeresult($result);
  160. set_config('fulltext_mysql_max_word_len', $mysql_info['ft_max_word_len']);
  161. set_config('fulltext_mysql_min_word_len', $mysql_info['ft_min_word_len']);
  162. return false;
  163. }
  164. /**
  165. * Splits keywords entered by a user into an array of words stored in $this->split_words
  166. * Stores the tidied search query in $this->search_query
  167. *
  168. * @param string &$keywords Contains the keyword as entered by the user
  169. * @param string $terms is either 'all' or 'any'
  170. * @return bool false if no valid keywords were found and otherwise true
  171. */
  172. public function split_keywords(&$keywords, $terms)
  173. {
  174. if ($terms == 'all')
  175. {
  176. $match = array('#\sand\s#iu', '#\sor\s#iu', '#\snot\s#iu', '#(^|\s)\+#', '#(^|\s)-#', '#(^|\s)\|#');
  177. $replace = array(' +', ' |', ' -', ' +', ' -', ' |');
  178. $keywords = preg_replace($match, $replace, $keywords);
  179. }
  180. // Filter out as above
  181. $split_keywords = preg_replace("#[\n\r\t]+#", ' ', trim(htmlspecialchars_decode($keywords)));
  182. // Split words
  183. $split_keywords = preg_replace('#([^\p{L}\p{N}\'*"()])#u', '$1$1', str_replace('\'\'', '\' \'', trim($split_keywords)));
  184. $matches = array();
  185. preg_match_all('#(?:[^\p{L}\p{N}*"()]|^)([+\-|]?(?:[\p{L}\p{N}*"()]+\'?)*[\p{L}\p{N}*"()])(?:[^\p{L}\p{N}*"()]|$)#u', $split_keywords, $matches);
  186. $this->split_words = $matches[1];
  187. // We limit the number of allowed keywords to minimize load on the database
  188. if ($this->config['max_num_search_keywords'] && sizeof($this->split_words) > $this->config['max_num_search_keywords'])
  189. {
  190. trigger_error($this->user->lang('MAX_NUM_SEARCH_KEYWORDS_REFINE', (int) $this->config['max_num_search_keywords'], sizeof($this->split_words)));
  191. }
  192. // to allow phrase search, we need to concatenate quoted words
  193. $tmp_split_words = array();
  194. $phrase = '';
  195. foreach ($this->split_words as $word)
  196. {
  197. if ($phrase)
  198. {
  199. $phrase .= ' ' . $word;
  200. if (strpos($word, '"') !== false && substr_count($word, '"') % 2 == 1)
  201. {
  202. $tmp_split_words[] = $phrase;
  203. $phrase = '';
  204. }
  205. }
  206. else if (strpos($word, '"') !== false && substr_count($word, '"') % 2 == 1)
  207. {
  208. $phrase = $word;
  209. }
  210. else
  211. {
  212. $tmp_split_words[] = $word;
  213. }
  214. }
  215. if ($phrase)
  216. {
  217. $tmp_split_words[] = $phrase;
  218. }
  219. $this->split_words = $tmp_split_words;
  220. unset($tmp_split_words);
  221. unset($phrase);
  222. foreach ($this->split_words as $i => $word)
  223. {
  224. $clean_word = preg_replace('#^[+\-|"]#', '', $word);
  225. // check word length
  226. $clean_len = utf8_strlen(str_replace('*', '', $clean_word));
  227. if (($clean_len < $this->config['fulltext_mysql_min_word_len']) || ($clean_len > $this->config['fulltext_mysql_max_word_len']))
  228. {
  229. $this->common_words[] = $word;
  230. unset($this->split_words[$i]);
  231. }
  232. }
  233. if ($terms == 'any')
  234. {
  235. $this->search_query = '';
  236. foreach ($this->split_words as $word)
  237. {
  238. if ((strpos($word, '+') === 0) || (strpos($word, '-') === 0) || (strpos($word, '|') === 0))
  239. {
  240. $word = substr($word, 1);
  241. }
  242. $this->search_query .= $word . ' ';
  243. }
  244. }
  245. else
  246. {
  247. $this->search_query = '';
  248. foreach ($this->split_words as $word)
  249. {
  250. if ((strpos($word, '+') === 0) || (strpos($word, '-') === 0))
  251. {
  252. $this->search_query .= $word . ' ';
  253. }
  254. else if (strpos($word, '|') === 0)
  255. {
  256. $this->search_query .= substr($word, 1) . ' ';
  257. }
  258. else
  259. {
  260. $this->search_query .= '+' . $word . ' ';
  261. }
  262. }
  263. }
  264. $this->search_query = utf8_htmlspecialchars($this->search_query);
  265. if ($this->search_query)
  266. {
  267. $this->split_words = array_values($this->split_words);
  268. sort($this->split_words);
  269. return true;
  270. }
  271. return false;
  272. }
  273. /**
  274. * Turns text into an array of words
  275. * @param string $text contains post text/subject
  276. */
  277. public function split_message($text)
  278. {
  279. // Split words
  280. $text = preg_replace('#([^\p{L}\p{N}\'*])#u', '$1$1', str_replace('\'\'', '\' \'', trim($text)));
  281. $matches = array();
  282. preg_match_all('#(?:[^\p{L}\p{N}*]|^)([+\-|]?(?:[\p{L}\p{N}*]+\'?)*[\p{L}\p{N}*])(?:[^\p{L}\p{N}*]|$)#u', $text, $matches);
  283. $text = $matches[1];
  284. // remove too short or too long words
  285. $text = array_values($text);
  286. for ($i = 0, $n = sizeof($text); $i < $n; $i++)
  287. {
  288. $text[$i] = trim($text[$i]);
  289. if (utf8_strlen($text[$i]) < $this->config['fulltext_mysql_min_word_len'] || utf8_strlen($text[$i]) > $this->config['fulltext_mysql_max_word_len'])
  290. {
  291. unset($text[$i]);
  292. }
  293. }
  294. return array_values($text);
  295. }
  296. /**
  297. * Performs a search on keywords depending on display specific params. You have to run split_keywords() first
  298. *
  299. * @param string $type contains either posts or topics depending on what should be searched for
  300. * @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)
  301. * @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)
  302. * @param array $sort_by_sql contains SQL code for the ORDER BY part of a query
  303. * @param string $sort_key is the key of $sort_by_sql for the selected sorting
  304. * @param string $sort_dir is either a or d representing ASC and DESC
  305. * @param string $sort_days specifies the maximum amount of days a post may be old
  306. * @param array $ex_fid_ary specifies an array of forum ids which should not be searched
  307. * @param string $post_visibility specifies which types of posts the user can view in which forums
  308. * @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
  309. * @param array $author_ary an array of author ids if the author should be ignored during the search the array is empty
  310. * @param string $author_name specifies the author match, when ANONYMOUS is also a search-match
  311. * @param array &$id_ary passed by reference, to be filled with ids for the page specified by $start and $per_page, should be ordered
  312. * @param int $start indicates the first index of the page
  313. * @param int $per_page number of ids each page is supposed to contain
  314. * @return boolean|int total number of results
  315. */
  316. 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)
  317. {
  318. // No keywords? No posts
  319. if (!$this->search_query)
  320. {
  321. return false;
  322. }
  323. // generate a search_key from all the options to identify the results
  324. $search_key = md5(implode('#', array(
  325. implode(', ', $this->split_words),
  326. $type,
  327. $fields,
  328. $terms,
  329. $sort_days,
  330. $sort_key,
  331. $topic_id,
  332. implode(',', $ex_fid_ary),
  333. $post_visibility,
  334. implode(',', $author_ary)
  335. )));
  336. if ($start < 0)
  337. {
  338. $start = 0;
  339. }
  340. // try reading the results from cache
  341. $result_count = 0;
  342. if ($this->obtain_ids($search_key, $result_count, $id_ary, $start, $per_page, $sort_dir) == SEARCH_RESULT_IN_CACHE)
  343. {
  344. return $result_count;
  345. }
  346. $id_ary = array();
  347. $join_topic = ($type == 'posts') ? false : true;
  348. // Build sql strings for sorting
  349. $sql_sort = $sort_by_sql[$sort_key] . (($sort_dir == 'a') ? ' ASC' : ' DESC');
  350. $sql_sort_table = $sql_sort_join = '';
  351. switch ($sql_sort[0])
  352. {
  353. case 'u':
  354. $sql_sort_table = USERS_TABLE . ' u, ';
  355. $sql_sort_join = ($type == 'posts') ? ' AND u.user_id = p.poster_id ' : ' AND u.user_id = t.topic_poster ';
  356. break;
  357. case 't':
  358. $join_topic = true;
  359. break;
  360. case 'f':
  361. $sql_sort_table = FORUMS_TABLE . ' f, ';
  362. $sql_sort_join = ' AND f.forum_id = p.forum_id ';
  363. break;
  364. }
  365. // Build some display specific sql strings
  366. switch ($fields)
  367. {
  368. case 'titleonly':
  369. $sql_match = 'p.post_subject';
  370. $sql_match_where = ' AND p.post_id = t.topic_first_post_id';
  371. $join_topic = true;
  372. break;
  373. case 'msgonly':
  374. $sql_match = 'p.post_text';
  375. $sql_match_where = '';
  376. break;
  377. case 'firstpost':
  378. $sql_match = 'p.post_subject, p.post_text';
  379. $sql_match_where = ' AND p.post_id = t.topic_first_post_id';
  380. $join_topic = true;
  381. break;
  382. default:
  383. $sql_match = 'p.post_subject, p.post_text';
  384. $sql_match_where = '';
  385. break;
  386. }
  387. $sql_select = (!$result_count) ? 'SQL_CALC_FOUND_ROWS ' : '';
  388. $sql_select = ($type == 'posts') ? $sql_select . 'p.post_id' : 'DISTINCT ' . $sql_select . 't.topic_id';
  389. $sql_from = ($join_topic) ? TOPICS_TABLE . ' t, ' : '';
  390. $field = ($type == 'posts') ? 'post_id' : 'topic_id';
  391. if (sizeof($author_ary) && $author_name)
  392. {
  393. // first one matches post of registered users, second one guests and deleted users
  394. $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 . ')';
  395. }
  396. else if (sizeof($author_ary))
  397. {
  398. $sql_author = ' AND ' . $this->db->sql_in_set('p.poster_id', $author_ary);
  399. }
  400. else
  401. {
  402. $sql_author = '';
  403. }
  404. $sql_where_options = $sql_sort_join;
  405. $sql_where_options .= ($topic_id) ? ' AND p.topic_id = ' . $topic_id : '';
  406. $sql_where_options .= ($join_topic) ? ' AND t.topic_id = p.topic_id' : '';
  407. $sql_where_options .= (sizeof($ex_fid_ary)) ? ' AND ' . $this->db->sql_in_set('p.forum_id', $ex_fid_ary, true) : '';
  408. $sql_where_options .= ' AND ' . $post_visibility;
  409. $sql_where_options .= $sql_author;
  410. $sql_where_options .= ($sort_days) ? ' AND p.post_time >= ' . (time() - ($sort_days * 86400)) : '';
  411. $sql_where_options .= $sql_match_where;
  412. $sql = "SELECT $sql_select
  413. FROM $sql_from$sql_sort_table" . POSTS_TABLE . " p
  414. WHERE MATCH ($sql_match) AGAINST ('" . $this->db->sql_escape(htmlspecialchars_decode($this->search_query)) . "' IN BOOLEAN MODE)
  415. $sql_where_options
  416. ORDER BY $sql_sort";
  417. $result = $this->db->sql_query_limit($sql, $this->config['search_block_size'], $start);
  418. while ($row = $this->db->sql_fetchrow($result))
  419. {
  420. $id_ary[] = (int) $row[$field];
  421. }
  422. $this->db->sql_freeresult($result);
  423. $id_ary = array_unique($id_ary);
  424. // if the total result count is not cached yet, retrieve it from the db
  425. if (!$result_count)
  426. {
  427. $sql_found_rows = 'SELECT FOUND_ROWS() as result_count';
  428. $result = $this->db->sql_query($sql_found_rows);
  429. $result_count = (int) $this->db->sql_fetchfield('result_count');
  430. $this->db->sql_freeresult($result);
  431. if (!$result_count)
  432. {
  433. return false;
  434. }
  435. }
  436. if ($start >= $result_count)
  437. {
  438. $start = floor(($result_count - 1) / $per_page) * $per_page;
  439. $result = $this->db->sql_query_limit($sql, $this->config['search_block_size'], $start);
  440. while ($row = $this->db->sql_fetchrow($result))
  441. {
  442. $id_ary[] = (int) $row[$field];
  443. }
  444. $this->db->sql_freeresult($result);
  445. $id_ary = array_unique($id_ary);
  446. }
  447. // store the ids, from start on then delete anything that isn't on the current page because we only need ids for one page
  448. $this->save_ids($search_key, implode(' ', $this->split_words), $author_ary, $result_count, $id_ary, $start, $sort_dir);
  449. $id_ary = array_slice($id_ary, 0, (int) $per_page);
  450. return $result_count;
  451. }
  452. /**
  453. * Performs a search on an author's posts without caring about message contents. Depends on display specific params
  454. *
  455. * @param string $type contains either posts or topics depending on what should be searched for
  456. * @param boolean $firstpost_only if true, only topic starting posts will be considered
  457. * @param array $sort_by_sql contains SQL code for the ORDER BY part of a query
  458. * @param string $sort_key is the key of $sort_by_sql for the selected sorting
  459. * @param string $sort_dir is either a or d representing ASC and DESC
  460. * @param string $sort_days specifies the maximum amount of days a post may be old
  461. * @param array $ex_fid_ary specifies an array of forum ids which should not be searched
  462. * @param string $post_visibility specifies which types of posts the user can view in which forums
  463. * @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
  464. * @param array $author_ary an array of author ids
  465. * @param string $author_name specifies the author match, when ANONYMOUS is also a search-match
  466. * @param array &$id_ary passed by reference, to be filled with ids for the page specified by $start and $per_page, should be ordered
  467. * @param int $start indicates the first index of the page
  468. * @param int $per_page number of ids each page is supposed to contain
  469. * @return boolean|int total number of results
  470. */
  471. 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)
  472. {
  473. // No author? No posts
  474. if (!sizeof($author_ary))
  475. {
  476. return 0;
  477. }
  478. // generate a search_key from all the options to identify the results
  479. $search_key = md5(implode('#', array(
  480. '',
  481. $type,
  482. ($firstpost_only) ? 'firstpost' : '',
  483. '',
  484. '',
  485. $sort_days,
  486. $sort_key,
  487. $topic_id,
  488. implode(',', $ex_fid_ary),
  489. $post_visibility,
  490. implode(',', $author_ary),
  491. $author_name,
  492. )));
  493. if ($start < 0)
  494. {
  495. $start = 0;
  496. }
  497. // try reading the results from cache
  498. $result_count = 0;
  499. if ($this->obtain_ids($search_key, $result_count, $id_ary, $start, $per_page, $sort_dir) == SEARCH_RESULT_IN_CACHE)
  500. {
  501. return $result_count;
  502. }
  503. $id_ary = array();
  504. // Create some display specific sql strings
  505. if ($author_name)
  506. {
  507. // first one matches post of registered users, second one guests and deleted users
  508. $sql_author = '(' . $this->db->sql_in_set('p.poster_id', array_diff($author_ary, array(ANONYMOUS)), false, true) . ' OR p.post_username ' . $author_name . ')';
  509. }
  510. else
  511. {
  512. $sql_author = $this->db->sql_in_set('p.poster_id', $author_ary);
  513. }
  514. $sql_fora = (sizeof($ex_fid_ary)) ? ' AND ' . $this->db->sql_in_set('p.forum_id', $ex_fid_ary, true) : '';
  515. $sql_topic_id = ($topic_id) ? ' AND p.topic_id = ' . (int) $topic_id : '';
  516. $sql_time = ($sort_days) ? ' AND p.post_time >= ' . (time() - ($sort_days * 86400)) : '';
  517. $sql_firstpost = ($firstpost_only) ? ' AND p.post_id = t.topic_first_post_id' : '';
  518. // Build sql strings for sorting
  519. $sql_sort = $sort_by_sql[$sort_key] . (($sort_dir == 'a') ? ' ASC' : ' DESC');
  520. $sql_sort_table = $sql_sort_join = '';
  521. switch ($sql_sort[0])
  522. {
  523. case 'u':
  524. $sql_sort_table = USERS_TABLE . ' u, ';
  525. $sql_sort_join = ($type == 'posts') ? ' AND u.user_id = p.poster_id ' : ' AND u.user_id = t.topic_poster ';
  526. break;
  527. case 't':
  528. $sql_sort_table = ($type == 'posts' && !$firstpost_only) ? TOPICS_TABLE . ' t, ' : '';
  529. $sql_sort_join = ($type == 'posts' && !$firstpost_only) ? ' AND t.topic_id = p.topic_id ' : '';
  530. break;
  531. case 'f':
  532. $sql_sort_table = FORUMS_TABLE . ' f, ';
  533. $sql_sort_join = ' AND f.forum_id = p.forum_id ';
  534. break;
  535. }
  536. $m_approve_fid_sql = ' AND ' . $post_visibility;
  537. // If the cache was completely empty count the results
  538. $calc_results = ($result_count) ? '' : 'SQL_CALC_FOUND_ROWS ';
  539. // Build the query for really selecting the post_ids
  540. if ($type == 'posts')
  541. {
  542. $sql = "SELECT {$calc_results}p.post_id
  543. FROM " . $sql_sort_table . POSTS_TABLE . ' p' . (($firstpost_only) ? ', ' . TOPICS_TABLE . ' t ' : ' ') . "
  544. WHERE $sql_author
  545. $sql_topic_id
  546. $sql_firstpost
  547. $m_approve_fid_sql
  548. $sql_fora
  549. $sql_sort_join
  550. $sql_time
  551. ORDER BY $sql_sort";
  552. $field = 'post_id';
  553. }
  554. else
  555. {
  556. $sql = "SELECT {$calc_results}t.topic_id
  557. FROM " . $sql_sort_table . TOPICS_TABLE . ' t, ' . POSTS_TABLE . " p
  558. WHERE $sql_author
  559. $sql_topic_id
  560. $sql_firstpost
  561. $m_approve_fid_sql
  562. $sql_fora
  563. AND t.topic_id = p.topic_id
  564. $sql_sort_join
  565. $sql_time
  566. GROUP BY t.topic_id
  567. ORDER BY $sql_sort";
  568. $field = 'topic_id';
  569. }
  570. // Only read one block of posts from the db and then cache it
  571. $result = $this->db->sql_query_limit($sql, $this->config['search_block_size'], $start);
  572. while ($row = $this->db->sql_fetchrow($result))
  573. {
  574. $id_ary[] = (int) $row[$field];
  575. }
  576. $this->db->sql_freeresult($result);
  577. // retrieve the total result count if needed
  578. if (!$result_count)
  579. {
  580. $sql_found_rows = 'SELECT FOUND_ROWS() as result_count';
  581. $result = $this->db->sql_query($sql_found_rows);
  582. $result_count = (int) $this->db->sql_fetchfield('result_count');
  583. $this->db->sql_freeresult($result);
  584. if (!$result_count)
  585. {
  586. return false;
  587. }
  588. }
  589. if ($start >= $result_count)
  590. {
  591. $start = floor(($result_count - 1) / $per_page) * $per_page;
  592. $result = $this->db->sql_query_limit($sql, $this->config['search_block_size'], $start);
  593. while ($row = $this->db->sql_fetchrow($result))
  594. {
  595. $id_ary[] = (int) $row[$field];
  596. }
  597. $this->db->sql_freeresult($result);
  598. $id_ary = array_unique($id_ary);
  599. }
  600. if (sizeof($id_ary))
  601. {
  602. $this->save_ids($search_key, '', $author_ary, $result_count, $id_ary, $start, $sort_dir);
  603. $id_ary = array_slice($id_ary, 0, $per_page);
  604. return $result_count;
  605. }
  606. return false;
  607. }
  608. /**
  609. * Destroys cached search results, that contained one of the new words in a post so the results won't be outdated
  610. *
  611. * @param string $mode contains the post mode: edit, post, reply, quote ...
  612. * @param int $post_id contains the post id of the post to index
  613. * @param string $message contains the post text of the post
  614. * @param string $subject contains the subject of the post to index
  615. * @param int $poster_id contains the user id of the poster
  616. * @param int $forum_id contains the forum id of parent forum of the post
  617. */
  618. public function index($mode, $post_id, &$message, &$subject, $poster_id, $forum_id)
  619. {
  620. // Split old and new post/subject to obtain array of words
  621. $split_text = $this->split_message($message);
  622. $split_title = ($subject) ? $this->split_message($subject) : array();
  623. $words = array_unique(array_merge($split_text, $split_title));
  624. unset($split_text);
  625. unset($split_title);
  626. // destroy cached search results containing any of the words removed or added
  627. $this->destroy_cache($words, array($poster_id));
  628. unset($words);
  629. }
  630. /**
  631. * Destroy cached results, that might be outdated after deleting a post
  632. */
  633. public function index_remove($post_ids, $author_ids, $forum_ids)
  634. {
  635. $this->destroy_cache(array(), array_unique($author_ids));
  636. }
  637. /**
  638. * Destroy old cache entries
  639. */
  640. public function tidy()
  641. {
  642. // destroy too old cached search results
  643. $this->destroy_cache(array());
  644. set_config('search_last_gc', time(), true);
  645. }
  646. /**
  647. * Create fulltext index
  648. *
  649. * @return string|bool error string is returned incase of errors otherwise false
  650. */
  651. public function create_index($acp_module, $u_action)
  652. {
  653. // Make sure we can actually use MySQL with fulltext indexes
  654. if ($error = $this->init())
  655. {
  656. return $error;
  657. }
  658. if (empty($this->stats))
  659. {
  660. $this->get_stats();
  661. }
  662. $alter = array();
  663. if (!isset($this->stats['post_subject']))
  664. {
  665. if ($this->db->sql_layer == 'mysqli' || version_compare($this->db->sql_server_info(true), '4.1.3', '>='))
  666. {
  667. $alter[] = 'MODIFY post_subject varchar(255) COLLATE utf8_unicode_ci DEFAULT \'\' NOT NULL';
  668. }
  669. else
  670. {
  671. $alter[] = 'MODIFY post_subject text NOT NULL';
  672. }
  673. $alter[] = 'ADD FULLTEXT (post_subject)';
  674. }
  675. if (!isset($this->stats['post_content']))
  676. {
  677. if ($this->db->sql_layer == 'mysqli' || version_compare($this->db->sql_server_info(true), '4.1.3', '>='))
  678. {
  679. $alter[] = 'MODIFY post_text mediumtext COLLATE utf8_unicode_ci NOT NULL';
  680. }
  681. else
  682. {
  683. $alter[] = 'MODIFY post_text mediumtext NOT NULL';
  684. }
  685. $alter[] = 'ADD FULLTEXT post_content (post_text, post_subject)';
  686. }
  687. if (sizeof($alter))
  688. {
  689. $this->db->sql_query('ALTER TABLE ' . POSTS_TABLE . ' ' . implode(', ', $alter));
  690. }
  691. $this->db->sql_query('TRUNCATE TABLE ' . SEARCH_RESULTS_TABLE);
  692. return false;
  693. }
  694. /**
  695. * Drop fulltext index
  696. *
  697. * @return string|bool error string is returned incase of errors otherwise false
  698. */
  699. public function delete_index($acp_module, $u_action)
  700. {
  701. // Make sure we can actually use MySQL with fulltext indexes
  702. if ($error = $this->init())
  703. {
  704. return $error;
  705. }
  706. if (empty($this->stats))
  707. {
  708. $this->get_stats();
  709. }
  710. $alter = array();
  711. if (isset($this->stats['post_subject']))
  712. {
  713. $alter[] = 'DROP INDEX post_subject';
  714. }
  715. if (isset($this->stats['post_content']))
  716. {
  717. $alter[] = 'DROP INDEX post_content';
  718. }
  719. if (sizeof($alter))
  720. {
  721. $this->db->sql_query('ALTER TABLE ' . POSTS_TABLE . ' ' . implode(', ', $alter));
  722. }
  723. $this->db->sql_query('TRUNCATE TABLE ' . SEARCH_RESULTS_TABLE);
  724. return false;
  725. }
  726. /**
  727. * Returns true if both FULLTEXT indexes exist
  728. */
  729. public function index_created()
  730. {
  731. if (empty($this->stats))
  732. {
  733. $this->get_stats();
  734. }
  735. return isset($this->stats['post_subject']) && isset($this->stats['post_content']);
  736. }
  737. /**
  738. * Returns an associative array containing information about the indexes
  739. */
  740. public function index_stats()
  741. {
  742. if (empty($this->stats))
  743. {
  744. $this->get_stats();
  745. }
  746. return array(
  747. $this->user->lang['FULLTEXT_MYSQL_TOTAL_POSTS'] => ($this->index_created()) ? $this->stats['total_posts'] : 0,
  748. );
  749. }
  750. /**
  751. * Computes the stats and store them in the $this->stats associative array
  752. */
  753. protected function get_stats()
  754. {
  755. if (strpos($this->db->sql_layer, 'mysql') === false)
  756. {
  757. $this->stats = array();
  758. return;
  759. }
  760. $sql = 'SHOW INDEX
  761. FROM ' . POSTS_TABLE;
  762. $result = $this->db->sql_query($sql);
  763. while ($row = $this->db->sql_fetchrow($result))
  764. {
  765. // deal with older MySQL versions which didn't use Index_type
  766. $index_type = (isset($row['Index_type'])) ? $row['Index_type'] : $row['Comment'];
  767. if ($index_type == 'FULLTEXT')
  768. {
  769. if ($row['Key_name'] == 'post_subject')
  770. {
  771. $this->stats['post_subject'] = $row;
  772. }
  773. else if ($row['Key_name'] == 'post_content')
  774. {
  775. $this->stats['post_content'] = $row;
  776. }
  777. }
  778. }
  779. $this->db->sql_freeresult($result);
  780. $this->stats['total_posts'] = empty($this->stats) ? 0 : $this->db->get_estimated_row_count(POSTS_TABLE);
  781. }
  782. /**
  783. * Display a note, that UTF-8 support is not available with certain versions of PHP
  784. *
  785. * @return associative array containing template and config variables
  786. */
  787. public function acp()
  788. {
  789. $tpl = '
  790. <dl>
  791. <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>
  792. <dd>' . $this->config['fulltext_mysql_min_word_len'] . '</dd>
  793. </dl>
  794. <dl>
  795. <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>
  796. <dd>' . $this->config['fulltext_mysql_max_word_len'] . '</dd>
  797. </dl>
  798. ';
  799. // These are fields required in the config table
  800. return array(
  801. 'tpl' => $tpl,
  802. 'config' => array()
  803. );
  804. }
  805. }