PageRenderTime 59ms CodeModel.GetById 28ms RepoModel.GetById 0ms app.codeStats 0ms

/forum/includes/search/fulltext_mysql.php

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