phpBB
Statistics
| Revision:

root / trunk / phpBB / includes / search / fulltext_mysql.php

History | View | Annotate | Download (26.1 kB)

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