Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 265
0.00% covered (danger)
0.00%
0 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
phpbb_functional_search_base
0.00% covered (danger)
0.00%
0 / 265
0.00% covered (danger)
0.00%
0 / 15
1806
0.00% covered (danger)
0.00%
0 / 1
 assert_search_found
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 assert_search_found_topics
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 assert_search_posts_by_author
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 assert_search_topics_by_author
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 assert_search_posts_by_author_id
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 assert_search_topics_by_author_id
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 assert_search_in_topic
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 assert_search_in_forum
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 assert_search_topics_in_forum
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 assert_search_not_found
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 assert_search_for_author_not_found
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 test_search_backend
0.00% covered (danger)
0.00%
0 / 69
0.00% covered (danger)
0.00%
0 / 1
56
 test_caching_search_results
0.00% covered (danger)
0.00%
0 / 95
0.00% covered (danger)
0.00%
0 / 1
12
 create_search_index
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
20
 delete_search_index
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
20
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
14/**
15* @group functional
16*/
17abstract class phpbb_functional_search_base extends phpbb_functional_test_case
18{
19    protected $search_backend;
20
21    protected function assert_search_found($keywords, $posts_found, $words_highlighted, $sort_key = '')
22    {
23        $this->purge_cache();
24        $crawler = self::request('GET', 'search.php?keywords=' . $keywords . ($sort_key ? "&sk=$sort_key" : ''));
25        $this->assertEquals($posts_found, $crawler->filter('.postbody')->count(), $this->search_backend);
26        $this->assertEquals($words_highlighted, $crawler->filter('.posthilit')->count(), $this->search_backend);
27        $this->assertStringContainsString("Search found $posts_found match", $crawler->filter('.searchresults-title')->text(), $this->search_backend);
28    }
29
30    protected function assert_search_found_topics($keywords, $topics_found, $sort_key = '')
31    {
32        $this->purge_cache();
33        $crawler = self::request('GET', 'search.php?sr=topics&keywords=' . $keywords . ($sort_key ? "&sk=$sort_key" : ''));
34        $this->assertEquals($topics_found, $crawler->filter('.row')->count(), $this->search_backend);
35        $this->assertStringContainsString("Search found $topics_found match", $crawler->filter('.searchresults-title')->text(), $this->search_backend);
36    }
37
38    protected function assert_search_posts_by_author($author, $posts_found, $sort_key = '')
39    {
40        $this->purge_cache();
41        $crawler = self::request('GET', 'search.php?author=' . $author . ($sort_key ? "&sk=$sort_key" : ''));
42        $this->assertEquals($posts_found, $crawler->filter('.postbody')->count(), $this->search_backend);
43        $this->assertStringContainsString("Search found $posts_found match", $crawler->filter('.searchresults-title')->text(), $this->search_backend);
44    }
45
46    protected function assert_search_topics_by_author($author, $topics_found, $sort_key = '')
47    {
48        $this->purge_cache();
49        $crawler = self::request('GET', 'search.php?sr=topics&author=' . $author . ($sort_key ? "&sk=$sort_key" : ''));
50        $this->assertEquals($topics_found, $crawler->filter('.row')->count(), $this->search_backend);
51        $this->assertStringContainsString("Search found $topics_found match", $crawler->filter('.searchresults-title')->text(), $this->search_backend);
52    }
53
54    protected function assert_search_posts_by_author_id($author_id, $posts_found, $sort_key = '', $sort_dir = '')
55    {
56        // Test obtaining data from cache if sorting direction is set
57        if (!$sort_dir)
58        {
59            $this->purge_cache();
60        }
61        $crawler = self::request('GET', 'search.php?author_id=' . $author_id . ($sort_key ? "&sk=$sort_key" : '') . ($sort_dir ? "&sk=$sort_dir" : ''));
62        $this->assertEquals($posts_found, $crawler->filter('.postbody')->count(), $this->search_backend);
63        $this->assertStringContainsString("Search found $posts_found match", $crawler->filter('.searchresults-title')->text(), $this->search_backend);
64    }
65
66    protected function assert_search_topics_by_author_id($author_id, $topics_found, $sort_key = '', $sort_dir = '')
67    {
68        // Test obtaining data from cache if sorting direction is set
69        if (!$sort_dir)
70        {
71            $this->purge_cache();
72        }
73        $crawler = self::request('GET', 'search.php?sr=topics&author_id=' . $author_id . ($sort_key ? "&sk=$sort_key" : '') . ($sort_dir ? "&sk=$sort_dir" : ''));
74        $this->assertEquals($topics_found, $crawler->filter('.row')->count(), $this->search_backend);
75        $this->assertStringContainsString("Search found $topics_found match", $crawler->filter('.searchresults-title')->text(), $this->search_backend);
76    }
77
78    protected function assert_search_in_topic($topic_id, $keywords, $posts_found, $sort_key = '')
79    {
80        $this->purge_cache();
81        $crawler = self::request('GET', "search.php?t=$topic_id&sf=msgonly&keywords=$keywords" . ($sort_key ? "&sk=$sort_key" : ''));
82        $this->assertEquals($posts_found, $crawler->filter('.postbody')->count(), $this->search_backend);
83        $this->assertStringContainsString("Search found $posts_found match", $crawler->filter('.searchresults-title')->text(), $this->search_backend);
84    }
85
86    protected function assert_search_in_forum($forum_id, $keywords, $posts_found, $sort_key = '')
87    {
88        $this->purge_cache();
89        $crawler = self::request('GET', "search.php?fid[]=$forum_id&keywords=$keywords" . ($sort_key ? "&sk=$sort_key" : ''));
90        $this->assertEquals($posts_found, $crawler->filter('.postbody')->count(), $this->search_backend);
91        $this->assertStringContainsString("Search found $posts_found match", $crawler->filter('.searchresults-title')->text(), $this->search_backend);
92    }
93
94    protected function assert_search_topics_in_forum($forum_id, $keywords, $topics_found, $sort_key = '')
95    {
96        $this->purge_cache();
97        $crawler = self::request('GET', "search.php?fid[]=$forum_id&sr=topics&keywords=$keywords" . ($sort_key ? "&sk=$sort_key" : ''));
98        $this->assertEquals($topics_found, $crawler->filter('.row')->count(), $this->search_backend);
99        $this->assertStringContainsString("Search found $topics_found match", $crawler->filter('.searchresults-title')->text(), $this->search_backend);
100    }
101
102    protected function assert_search_not_found($keywords)
103    {
104        $crawler = self::request('GET', 'search.php?keywords=' . $keywords);
105        $this->assertEquals(0, $crawler->filter('.postbody')->count(), $this->search_backend);
106        $split_keywords_string = str_replace('+', ' ', $keywords);
107        $this->assertEquals($split_keywords_string, $crawler->filter('#keywords')->attr('value'), $this->search_backend);
108    }
109
110    protected function assert_search_for_author_not_found($author)
111    {
112        $this->add_lang('search');
113        $crawler = self::request('GET', 'search.php?author=' . $author);
114        $this->assertContainsLang('NO_SEARCH_RESULTS', $crawler->text(), $this->search_backend);
115    }
116
117    public function test_search_backend()
118    {
119        $this->add_lang('common');
120
121        // Create a new standard user if needed, topic and post to test searh for author
122        if (!$searchforauthoruser_id = $this->user_exists('searchforauthoruser'))
123        {
124            $searchforauthoruser_id = $this->create_user('searchforauthoruser');
125        }
126        else
127        {
128            $searchforauthoruser_id = key($searchforauthoruser_id);
129        }
130        $this->remove_user_group('NEWLY_REGISTERED', ['searchforauthoruser']);
131        $this->set_flood_interval(0);
132        $this->login('searchforauthoruser');
133        $topic_by_author = $this->create_topic(2, 'Test Topic from searchforauthoruser', 'This is a test topic posted by searchforauthoruser to test searching by author.');
134        $this->create_post(2, $topic_by_author['topic_id'], 'Re: Test Topic from searchforauthoruser', 'This is a test post posted by searchforauthoruser');
135        $this->logout();
136
137        $this->login();
138        $this->admin_login();
139
140        $post = $this->create_topic(2, 'Test Topic 1 foosubject', 'This is a test topic posted by the barsearch testing framework.');
141        $topic_multiple_results_count1 = $this->create_topic(2, 'Test Topic for multiple search results', 'This is a test topic posted to test multiple results count.');
142        $this->create_post(2, $topic_multiple_results_count1['topic_id'], 'Re: Test Topic for multiple search results', 'This is a test post 2 posted to test multiple results count.');
143        $topic_multiple_results_count2 = $this->create_topic(2, 'Test Topic 2 for multiple search results', 'This is a test topic 2 posted to test multiple results count.');
144        $this->set_flood_interval(15);
145
146        $crawler = self::request('GET', 'adm/index.php?i=acp_search&mode=settings&sid=' . $this->sid);
147        $form = $crawler->selectButton($this->lang('SUBMIT'))->form();
148        $values = $form->getValues();
149
150        if ($values["config[search_type]"] != $this->search_backend)
151        {
152            $values["config[search_type]"] = $this->search_backend;
153
154            if (strpos($this->search_backend, 'fulltext_sphinx'))
155            {
156                // Set board Sphinx id in according to respective setup-sphinx.sh $ID value
157                $values["config[fulltext_sphinx_id]"] = 'saw9zf2fdhp1goue';
158            }
159
160            try
161            {
162                $form->setValues($values);
163            }
164            catch(\InvalidArgumentException $e)
165            {
166                // Search backed is not supported because don't appear in the select
167                $this->delete_topic($post['topic_id']);
168                $this->delete_topic($topic_by_author['topic_id']);
169                $this->delete_topic($topic_multiple_results_count1['topic_id']);
170                $this->delete_topic($topic_multiple_results_count2['topic_id']);
171                $this->markTestSkipped("Search backend is not supported/running");
172            }
173
174            $crawler = self::submit($form);
175            $this->purge_cache();
176
177            $form = $crawler->selectButton($this->lang('YES'))->form();
178            $values = $form->getValues();
179            $crawler = self::submit($form);
180
181            // Unknown error selecting search backend
182            if ($crawler->filter('.errorbox')->count() > 0)
183            {
184                $this->fail('Error when trying to select available search backend');
185            }
186
187            $this->create_search_index();
188        }
189
190        $this->logout();
191
192        foreach (['', 'a', 't', 'f', 'i', 's'] as $sort_key)
193        {
194            $this->assert_search_found('phpbb+installation', 1, 4, $sort_key);
195            $this->assert_search_found('foosubject+barsearch', 1, 2, $sort_key);
196            $this->assert_search_found('barsearch-testing', 1, 2, $sort_key); // test hyphen ignored
197            $this->assert_search_found('barsearch+-+testing', 1, 2, $sort_key); // test hyphen wrapped with space ignored
198            $this->assert_search_found('multiple+results+count', 3, 15, $sort_key); // test multiple results count - posts
199            $this->assert_search_found_topics('multiple+results+count', 2, $sort_key); // test multiple results count - topics
200            $this->assert_search_found_topics('phpbb+installation', 1, $sort_key);
201            $this->assert_search_found_topics('foosubject+barsearch', 1, $sort_key);
202
203            $this->assert_search_in_forum(2, 'multiple+search+results', 3, $sort_key); // test multiple results count - forum search - posts
204            $this->assert_search_topics_in_forum(2, 'multiple+search+results', 2, $sort_key); // test multiple results count - forum search - topics
205            $this->assert_search_in_topic((int) $topic_multiple_results_count1['topic_id'], 'multiple+results', 2, $sort_key); // test multiple results count - topic search
206
207            $this->assert_search_posts_by_author('searchforauthoruser', 2, $sort_key);
208            $this->assert_search_topics_by_author('searchforauthoruser', 1, $sort_key);
209
210            $this->assert_search_posts_by_author_id($searchforauthoruser_id, 2, $sort_key);
211            $this->assert_search_topics_by_author_id($searchforauthoruser_id, 1, $sort_key);
212            $this->assert_search_posts_by_author_id($searchforauthoruser_id, 2, $sort_key, 'a'); //search asc order
213            $this->assert_search_topics_by_author_id($searchforauthoruser_id, 1, $sort_key, 'a'); // search asc order
214        }
215
216        $this->assert_search_not_found('loremipsumdedo');
217        $this->assert_search_not_found('loremipsumdedo+-'); // test search query ending with the space followed by hyphen
218        $this->assert_search_not_found('barsearch+-testing'); // test excluding keyword
219        $this->assert_search_for_author_not_found('authornotexists');
220
221        $this->login();
222        $this->admin_login();
223        $this->delete_search_index();
224        $this->delete_topic($post['topic_id']);
225        $this->delete_topic($topic_by_author['topic_id']);
226        $this->delete_topic($topic_multiple_results_count1['topic_id']);
227        $this->delete_topic($topic_multiple_results_count2['topic_id']);
228    }
229
230    public function test_caching_search_results()
231    {
232        global $phpbb_root_path;
233
234        // Sphinx search doesn't use phpBB search results caching
235        if (strpos($this->search_backend, 'fulltext_sphinx'))
236        {
237            $this->markTestSkipped("Sphinx search doesn't use phpBB search results caching");
238        }
239
240        $this->purge_cache();
241        $this->login();
242        $this->admin_login();
243
244        $crawler = self::request('GET', 'search.php?author_id=2&sr=posts');
245        $posts_found_text = $crawler->filter('.searchresults-title')->text();
246
247        // Get total user's post count
248        preg_match('!(\d+)!', $posts_found_text, $matches);
249        $posts_count = (int) $matches[1];
250
251        $this->assertStringContainsString("Search found $posts_count matches", $posts_found_text, $this->search_backend);
252
253        // Set this value to cache less results than total count
254        $sql = 'UPDATE ' . CONFIG_TABLE . '
255            SET config_value = ' . floor($posts_count / 3) . "
256            WHERE config_name = '" . $this->db->sql_escape('search_block_size') . "'";
257        $this->db->sql_query($sql);
258
259        // Temporarily set posts_per_page to the value allowing to get several pages (4+)
260        $crawler = self::request('GET', 'adm/index.php?sid=' . $this->sid . '&i=acp_board&mode=post');
261        $form = $crawler->selectButton('Submit')->form();
262        $values = $form->getValues();
263        $current_posts_per_page = $values['config[posts_per_page]'];
264        $values['config[posts_per_page]'] = floor($posts_count / 10);
265        $form->setValues($values);
266        $crawler = self::submit($form);
267        $this->assertEquals(1, $crawler->filter('.successbox')->count(), $this->search_backend);
268
269        // Now actually test caching search results
270        $this->purge_cache();
271
272        // Default sort direction is 'd' (descending), browse  the 1st page
273        $crawler = self::request('GET', 'search.php?author_id=2&sr=posts');
274        $pagination = $crawler->filter('.pagination')->eq(0);
275        $posts_found_text = $pagination->text();
276
277        $this->assertStringContainsString("Search found $posts_count matches", $posts_found_text, $this->search_backend);
278
279        // Filter all search result page links on the 1st page
280        $pagination = $pagination->filter('li > a')->reduce(
281            function ($node, $i)
282            {
283                return ($node->attr('class') == 'button');
284            }
285        );
286
287        // Get last page number
288        $last_page = (int) $pagination->last()->text();
289
290        // Browse the last search page
291        $crawler = self::$client->click($pagination->selectLink($last_page)->link());
292        $pagination = $crawler->filter('.pagination')->eq(0);
293
294        // Filter all search result page links on the last page
295        $pagination = $pagination->filter('li > a')->reduce(
296            function ($node, $i)
297            {
298                return ($node->attr('class') == 'button');
299            }
300        );
301
302        // Now change sort direction to ascending
303        $form = $crawler->selectButton('sort')->form();
304        $values = $form->getValues();
305        $values['sd'] = 'a';
306        $form->setValues($values);
307        $crawler = self::submit($form);
308
309        $pagination = $crawler->filter('.pagination')->eq(0);
310
311        // Filter all search result page links on the 1st page with new sort direction
312        $pagination = $pagination->filter('li > a')->reduce(
313            function ($node, $i)
314            {
315                return ($node->attr('class') == 'button');
316            }
317        );
318
319        // Browse the rest of search results pages with new sort direction
320        $pages = range(2, $last_page);
321        foreach ($pages as $page_number)
322        {
323            $crawler = self::$client->click($pagination->selectLink($page_number)->link());
324            $pagination = $crawler->filter('.pagination')->eq(0);
325            $pagination = $pagination->filter('li > a')->reduce(
326                function ($node, $i)
327                {
328                    return ($node->attr('class') == 'button');
329                }
330            );
331        }
332
333        // Get search results cache varname
334        $finder = new \Symfony\Component\Finder\Finder();
335        $finder
336            ->name('data_search_results_*.php')
337            ->files()
338            ->in($phpbb_root_path . 'cache/' . PHPBB_ENVIRONMENT);
339        $iterator = $finder->getIterator();
340        $iterator->rewind();
341        $cache_filename = $iterator->current();
342        $cache_varname = substr($cache_filename->getBasename('.php'), 4);
343
344        // Get cached post ids data
345        $cache = $this->get_cache_driver();
346        $post_ids_cached = $cache->get($cache_varname);
347
348        $cached_results_count = count($post_ids_cached) - 2; // Don't count '-1' and '-2' indexes
349
350        $post_ids_cached_backup = $post_ids_cached;
351
352        // Cached data still should have initial 'd' sort direction
353        $this->assertTrue($post_ids_cached[-2] === 'd', $this->search_backend);
354
355        // Cached search results count should be equal to displayed on search results page
356        $this->assertEquals($posts_count, $post_ids_cached[-1], $this->search_backend);
357
358        // Actual cached data array count should be equal to displayed on search results page too
359        $this->assertEquals($posts_count, $cached_results_count, $this->search_backend);
360
361        // Cached data array shouldn't change after removing duplicates. That is, it shouldn't have any duplicates.
362        unset($post_ids_cached[-2], $post_ids_cached[-1]);
363        unset($post_ids_cached_backup[-2], $post_ids_cached_backup[-1]);
364        $post_ids_cached = array_unique($post_ids_cached);
365        $this->assertEquals($post_ids_cached_backup, $post_ids_cached, $this->search_backend);
366
367        // Restore this value to default
368        $sql = 'UPDATE ' . CONFIG_TABLE . "
369            SET config_value = 250
370            WHERE config_name = '" . $this->db->sql_escape('search_block_size') . "'";
371        $this->db->sql_query($sql);
372
373        // Restore posts_per_page value
374        $crawler = self::request('GET', 'adm/index.php?sid=' . $this->sid . '&i=acp_board&mode=post');
375        $form = $crawler->selectButton('Submit')->form();
376        $values = $form->getValues();
377        $values['config[posts_per_page]'] = $current_posts_per_page;
378        $form->setValues($values);
379        $crawler = self::submit($form);
380        $this->assertEquals(1, $crawler->filter('.successbox')->count(), $this->search_backend);
381    }
382
383    protected function create_search_index($backend = null)
384    {
385        $this->add_lang('acp/search');
386        $search_type = $backend ?? $this->search_backend;
387        $crawler = self::request('GET', 'adm/index.php?i=acp_search&mode=index&sid=' . $this->sid);
388        $form = $crawler->selectButton($this->lang('CREATE_INDEX'))->form();
389        $form_values = $form->getValues();
390        $form_values = array_merge($form_values,
391            [
392                'search_type'    => $search_type,
393                'action'        => 'create',
394            ]
395        );
396        $form->setValues($form_values);
397        $crawler = self::submit($form);
398
399        $meta_refresh = $crawler->filter('meta[http-equiv="refresh"]');
400
401        if ($meta_refresh->count() > 0)
402        {
403            // Wait for posts to be fully indexed
404            while ($meta_refresh->count() > 0)
405            {
406                preg_match('#url=.+/(adm+.+)#', $meta_refresh->attr('content'), $match);
407                $url = $match[1];
408                $crawler = self::request('POST', $url);
409                $meta_refresh = $crawler->filter('meta[http-equiv="refresh"]');
410            }
411        }
412
413        $this->assertContainsLang('SEARCH_INDEX_CREATED', $crawler->text());
414
415        // Ensure search index has been actually created
416        $crawler = self::request('GET', 'adm/index.php?i=acp_search&mode=index&sid=' . $this->sid);
417        $posts_indexed = (int) $crawler->filter('#acp_search_index_' . str_replace('\\', '-', $search_type) . ' td')->reduce(
418            function ($node, $i) {
419                // Find the value of total posts indexed
420                return (strpos($node->text(), $this->lang('FULLTEXT_MYSQL_TOTAL_POSTS')) !== false  || strpos($node->text(), $this->lang('TOTAL_WORDS')) !== false);
421            })
422        ->nextAll()->eq(0)->text();
423        $this->assertTrue($posts_indexed > 0);
424    }
425
426    protected function delete_search_index()
427    {
428        $this->add_lang('acp/search');
429        $crawler = self::request('GET', 'adm/index.php?i=acp_search&mode=index&sid=' . $this->sid);
430        $form = $crawler->selectButton($this->lang('DELETE_INDEX'))->form();
431        $form_values = $form->getValues();
432        $form_values = array_merge($form_values,
433            [
434                'search_type'    => $this->search_backend,
435                'action'        => 'delete',
436            ]
437        );
438        $form->setValues($form_values);
439        $crawler = self::submit($form);
440
441        $meta_refresh = $crawler->filter('meta[http-equiv="refresh"]');
442
443        if ($meta_refresh->count() > 0)
444        {
445            // Wait for index to be fully deleted
446            while ($meta_refresh->count() > 0)
447            {
448                preg_match('#url=.+/(adm+.+)#', $meta_refresh->attr('content'), $match);
449                $url = $match[1];
450                $crawler = self::request('POST', $url);
451                $meta_refresh = $crawler->filter('meta[http-equiv="refresh"]');
452            }
453        }
454
455        $this->assertContainsLang('SEARCH_INDEX_REMOVED', $crawler->text());
456
457        // Ensure search index has been actually removed
458        $crawler = self::request('GET', 'adm/index.php?i=acp_search&mode=index&sid=' . $this->sid);
459        $posts_indexed = (int) $crawler->filter('#acp_search_index_' . str_replace('\\', '-', $this->search_backend) . ' td')->reduce(
460            function ($node, $i) {
461                // Find the value of total posts indexed
462                return (strpos($node->text(), $this->lang('FULLTEXT_MYSQL_TOTAL_POSTS')) !== false  || strpos($node->text(), $this->lang('TOTAL_WORDS')) !== false);
463            })
464        ->nextAll()->eq(0)->text();
465        $this->assertEquals(0, $posts_indexed);
466    }
467}