Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 793
0.00% covered (danger)
0.00%
0 / 57
CRAP
0.00% covered (danger)
0.00%
0 / 1
phpbb_functional_test_case
0.00% covered (danger)
0.00%
0 / 793
0.00% covered (danger)
0.00%
0 / 57
23256
0.00% covered (danger)
0.00%
0 / 1
 __get
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 setUpBeforeClass
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
12
 setup_extensions
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setUp
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
30
 tearDownAfterClass
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 request
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 submit
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
30
 get_content
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 bootstrap
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_db
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 get_db_doctrine
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 get_cache_driver
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 purge_cache
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 get_extension_manager
0.00% covered (danger)
0.00%
0 / 41
0.00% covered (danger)
0.00%
0 / 1
2
 get_messenger_method_email
0.00% covered (danger)
0.00%
0 / 94
0.00% covered (danger)
0.00%
0 / 1
2
 install_board
0.00% covered (danger)
0.00%
0 / 99
0.00% covered (danger)
0.00%
0 / 1
182
 install_ext
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
20
 disable_ext
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
12
 delete_ext_data
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
12
 uninstall_ext
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 recreate_database
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 add_style
0.00% covered (danger)
0.00%
0 / 52
0.00% covered (danger)
0.00%
0 / 1
12
 delete_style
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 create_user
0.00% covered (danger)
0.00%
0 / 33
0.00% covered (danger)
0.00%
0 / 1
20
 get_group_id
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 get_search_type
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 remove_user_group
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
12
 add_user_group
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
12
 login
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
20
 logout
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 admin_login
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
42
 add_lang
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 add_lang_ext
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 lang
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 assertContainsLang
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 assertNotContainsLang
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 assert_response_html
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 assert_response_xml
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 assert_response_status_code
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 assert_filter
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 assert_checkbox_is_checked
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 assert_checkbox_is_unchecked
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 assert_find_one_checkbox
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 create_topic
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 create_post
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 submit_post
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
20
 create_private_message
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
6
 submit_message
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
12
 delete_topic
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 delete_post
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 get_parameter_from_link
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
42
 get_passwords_manager
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
2
 get_quickmod_page
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 get_hidden_fields
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
 get_logged_in_user
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 set_flood_interval
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
56
 user_exists
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
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*/
13use Symfony\Component\BrowserKit\CookieJar;
14use Symfony\Component\BrowserKit\HttpBrowser;
15use Symfony\Component\HttpClient\HttpClient;
16use Symfony\Component\HttpClient\NativeHttpClient;
17use Symfony\Contracts\HttpClient\HttpClientInterface;
18
19require_once __DIR__ . '/mock/phpbb_mock_null_installer_task.php';
20
21class phpbb_functional_test_case extends phpbb_test_case
22{
23    /** @var HttpClientInterface */
24    protected static $http_client;
25
26    /** @var HttpBrowser */
27    protected static $client;
28    protected static $cookieJar;
29    protected static $root_url;
30    protected static $install_success = false;
31
32    protected $cache = null;
33    protected $db_doctrine = null;
34    protected $extension_manager = null;
35
36    /**
37    * Session ID for current test's session (each test makes its own)
38    * @var string
39    */
40    protected static $session_id;
41
42    /**
43    * Language array used by phpBB
44    * @var array
45    */
46    protected static $lang_ary = [];
47
48    protected static $config = array();
49    protected static $already_installed = false;
50    protected static $db_connection = null;
51
52    public function __get($property)
53    {
54        if ($property === 'sid')
55        {
56            return self::$session_id ??= null;
57        }
58
59        if ($property === 'db')
60        {
61            return self::$db_connection ??= null;
62        }
63
64        if ($property === 'lang')
65        {
66            return self::$lang_ary;
67        }
68
69        return null;
70    }
71
72    public static function setUpBeforeClass(): void
73    {
74        parent::setUpBeforeClass();
75
76        self::$config = phpbb_test_case_helpers::get_test_config();
77
78        // Important: this is used both for installation and by
79        // test cases for querying the tables.
80        // Therefore table prefix must be set before a board is
81        // installed, and also before each test case is run.
82        self::$config['table_prefix'] = 'phpbb_';
83
84        if (!isset(self::$config['phpbb_functional_url']))
85        {
86            self::markTestSkipped('phpbb_functional_url was not set in test_config and wasn\'t set as PHPBB_FUNCTIONAL_URL environment variable either.');
87        }
88
89        self::$root_url = self::$config['phpbb_functional_url'];
90
91        if (!self::$already_installed)
92        {
93            self::install_board();
94            self::$already_installed = true;
95        }
96
97        global $cache;
98        $cache = new phpbb_mock_null_cache;
99        self::get_db();
100
101        // Special flag for testing without possibility to run into lock scenario.
102        // Unset entry and add it back if lock behavior for posting should be tested.
103        // Unset ci_tests_no_lock_posting from config
104        $sql = 'INSERT INTO ' . CONFIG_TABLE . " (config_name, config_value) VALUES ('ci_tests_no_lock_posting', '1')";
105        self::$db_connection->sql_query($sql);
106    }
107
108    /**
109    * @return array List of extensions that should be set up
110    */
111    protected static function setup_extensions()
112    {
113        return array();
114    }
115
116    protected function setUp(): void
117    {
118        parent::setUp();
119
120        $this->setBackupStaticPropertiesExcludeList([
121            'phpbb_functional_test_case' => ['config', 'already_installed'],
122        ]);
123
124        if (!self::$install_success)
125        {
126            $this->fail('Installing phpBB has failed.');
127        }
128
129        $this->bootstrap();
130
131        self::$cookieJar = new CookieJar;
132        // Optimize HTTP client for Windows platform
133        if (strtolower(substr(PHP_OS, 0, 3)) === 'win') {
134            self::$http_client = new NativeHttpClient([
135                'timeout' => 30,
136                'max_duration' => 60,
137            ]);
138        } else {
139            self::$http_client = HttpClient::create([
140                'timeout' => 60,
141            ]);
142        }
143        self::$client = new HttpBrowser(self::$http_client, null, self::$cookieJar);
144
145        // Disable SSL verification for local development with self-signed certificates
146        if (isset(self::$config['path_to_ssl_cert']))
147        {
148            $guzzle_client = new \GuzzleHttp\Client(['verify' => self::$config['path_to_ssl_cert']]);
149            self::$client->setClient($guzzle_client);
150        }
151
152        // Clear the language array so that things
153        // that were added in other tests are gone
154        self::$lang_ary = [];
155        self::add_lang('common');
156
157        foreach (static::setup_extensions() as $extension)
158        {
159            self::install_ext($extension);
160        }
161    }
162
163    public static function tearDownAfterClass(): void
164    {
165        parent::tearDownAfterClass();
166
167        global $cache;
168        $cache = new phpbb_mock_null_cache;
169
170        if (self::$db_connection instanceof \phpbb\db\driver\driver_interface)
171        {
172            // Unset ci_tests_no_lock_posting from config
173            $sql = 'DELETE FROM ' . CONFIG_TABLE . "
174            WHERE config_name = 'ci_tests_no_lock_posting'";
175            self::$db_connection->sql_query($sql);
176
177            // Close the database connections again this test
178            self::$db_connection->sql_close();
179            self::$db_connection = null;
180        }
181    }
182
183    /**
184    * Perform a request to page
185    *
186    * @param string    $method        HTTP Method
187    * @param string    $path        Page path, relative from phpBB root path
188    * @param array $form_data    An array of form field values
189    * @param bool    $assert_response_html    Should we perform standard assertions for a normal html page
190    * @return Symfony\Component\DomCrawler\Crawler
191    */
192    public static function request($method, $path, $form_data = array(), $assert_response_html = true)
193    {
194        $crawler = self::$client->request($method, self::$root_url . $path, $form_data);
195
196        if ($assert_response_html)
197        {
198            self::assert_response_html();
199        }
200
201        return $crawler;
202    }
203
204    /**
205    * Submits a form
206    *
207    * @param Symfony\Component\DomCrawler\Form $form A Form instance
208    * @param array $values An array of form field values
209    * @param bool    $assert_response_html    Should we perform standard assertions for a normal html page
210    * @return Symfony\Component\DomCrawler\Crawler
211    */
212    public static function submit(Symfony\Component\DomCrawler\Form $form, array $values = array(), $assert_response_html = true)
213    {
214        // Remove files from form if no file was submitted
215        // See: https://github.com/symfony/symfony/issues/49014
216        foreach ($form->getFiles() as $field_name => $value)
217        {
218            if (!$value['name'] && !$value['tmp_name'])
219            {
220                $form->remove($field_name);
221            }
222        }
223
224        $crawler = self::$client->submit($form, $values);
225
226        if ($assert_response_html)
227        {
228            self::assert_response_html();
229        }
230
231        return $crawler;
232    }
233
234    /**
235    * Get Client Content
236    *
237    * @return string HTML page
238    */
239    public static function get_content()
240    {
241        return (string) self::$client->getResponse()->getContent();
242    }
243
244    // bootstrap, called after board is set up
245    // once per test case class
246    // test cases can override this
247    protected function bootstrap()
248    {
249    }
250
251    /**
252     * @return \phpbb\db\driver\driver_interface
253     */
254    protected static function get_db()
255    {
256        global $phpbb_root_path, $phpEx;
257        // so we don't reopen an open connection
258        if (!(self::$db_connection instanceof \phpbb\db\driver\driver_interface))
259        {
260            $dbms = self::$config['dbms'];
261            self::$db_connection = new $dbms();
262            self::$db_connection->sql_connect(self::$config['dbhost'], self::$config['dbuser'], self::$config['dbpasswd'], self::$config['dbname'], self::$config['dbport'], true);
263        }
264
265        return self::$db_connection;
266    }
267
268    protected function get_db_doctrine()
269    {
270        // so we don't reopen an open connection
271        if (!($this->db_doctrine instanceof \Doctrine\DBAL\Connection))
272        {
273            $this->db_doctrine = \phpbb\db\doctrine\connection_factory::get_connection_from_params(self::$config['dbms'], self::$config['dbhost'], self::$config['dbuser'], self::$config['dbpasswd'], self::$config['dbname'], self::$config['dbport']);
274        }
275        return $this->db_doctrine;
276    }
277
278    protected function get_cache_driver()
279    {
280        if (!$this->cache)
281        {
282            global $phpbb_container, $phpbb_root_path;
283
284            $phpbb_container = new phpbb_mock_container_builder();
285            $phpbb_container->setParameter('core.environment', PHPBB_ENVIRONMENT);
286            $phpbb_container->setParameter('core.cache_dir', $phpbb_root_path . 'cache/' . PHPBB_ENVIRONMENT . '/');
287
288            $this->cache = new \phpbb\cache\driver\file;
289        }
290
291        return $this->cache;
292    }
293
294    protected function purge_cache()
295    {
296        $cache = $this->get_cache_driver();
297
298        $cache->purge();
299        $cache->unload();
300        $cache->load();
301    }
302
303    protected function get_extension_manager()
304    {
305        global $phpbb_root_path, $phpEx;
306
307        $config = new \phpbb\config\config(array('version' => PHPBB_VERSION));
308        $this->get_db();
309        $db_doctrine = $this->get_db_doctrine();
310        $factory = new \phpbb\db\tools\factory();
311        $finder_factory = new \phpbb\finder\factory(null, false, $phpbb_root_path, $phpEx);
312        $db_tools = $factory->get($db_doctrine);
313        $db_tools->set_table_prefix(self::$config['table_prefix']);
314
315        $container = new phpbb_mock_container_builder();
316        $migrator = new \phpbb\db\migrator(
317            $container,
318            $config,
319            $this->db,
320            $db_tools,
321            self::$config['table_prefix'] . 'migrations',
322            $phpbb_root_path,
323            $phpEx,
324            self::$config['table_prefix'],
325            phpbb_database_test_case::get_core_tables(),
326            array(),
327            new \phpbb\db\migration\helper()
328        );
329        $phpbb_dispatcher = new phpbb_mock_event_dispatcher();
330        $container->set('migrator', $migrator);
331        $container->set('event_dispatcher', $phpbb_dispatcher);
332        $cache = $this->getMockBuilder('\phpbb\cache\service')
333            ->setConstructorArgs([$this->get_cache_driver(), $config, $this->db, $phpbb_dispatcher, $phpbb_root_path, $phpEx])
334            ->onlyMethods(['deferred_purge'])
335            ->getMock();
336        $cache->method('deferred_purge')
337            ->willReturnCallback([$cache, 'purge']);
338
339        $extension_manager = new \phpbb\extension\manager(
340            $container,
341            $this->db,
342            $config,
343            $finder_factory,
344            self::$config['table_prefix'] . 'ext',
345            __DIR__ . '/',
346            $cache
347        );
348
349        return $extension_manager;
350    }
351
352    protected static function get_messenger_method_email($container)
353    {
354        global $phpbb_root_path, $phpEx;
355
356        $config = new \phpbb\config\config(
357            [
358                'version' => PHPBB_VERSION,
359                'email_enable' => false,
360                'email_package_size' => 0,
361                'smtp_delivery' => 0,
362                'default_lang' => 'en',
363            ]
364        );
365
366        $lang_loader = new \phpbb\language\language_file_loader($phpbb_root_path, $phpEx);
367        $lang = new \phpbb\language\language($lang_loader);
368        $user = new \phpbb\user($lang, '\phpbb\datetime');
369        $container->set('user', $user);
370        $container->set('language', $lang);
371
372        $assets_bag = new \phpbb\template\assets_bag();
373        $container->set('assets.bag', $assets_bag);
374
375        $phpbb_dispatcher = new phpbb_mock_event_dispatcher();
376        $container->set('dispatcher', $phpbb_dispatcher);
377
378        $core_cache_dir = $phpbb_root_path . 'cache/' . PHPBB_ENVIRONMENT . '/';
379        $container->setParameter('core.cache_dir', $core_cache_dir);
380
381        $core_messenger_queue_file = $core_cache_dir . 'queue.' . $phpEx;
382        $container->setParameter('core.messenger_queue_file', $core_messenger_queue_file);
383
384        $messenger_method_collection = new \phpbb\di\service_collection($container);
385        $messenger_method_collection->add('messenger.method.email');
386        $container->set('messenger.method_collection', $messenger_method_collection);
387
388        $messenger_queue = new \phpbb\messenger\queue($config, $phpbb_dispatcher, $messenger_method_collection, $core_messenger_queue_file);
389        $container->set('messenger.queue', $messenger_queue);
390
391        $request = new phpbb_mock_request;
392        $container->set('request', $request);
393
394        $symfony_request = new \phpbb\symfony_request(
395            $request
396        );
397
398        $phpbb_path_helper = new \phpbb\path_helper(
399            $symfony_request,
400            $request,
401            $phpbb_root_path,
402            $phpEx
403        );
404        $container->set('path_helper', $phpbb_path_helper);
405
406        $dbms = self::$config['dbms'];
407        $db = new $dbms();
408        $db->sql_connect(self::$config['dbhost'], self::$config['dbuser'], self::$config['dbpasswd'], self::$config['dbname'], self::$config['dbport']);
409
410        $extension_manager = new phpbb_mock_extension_manager($phpbb_root_path, [], $container);
411        $container->set('ext.manager', $extension_manager);
412
413        $context = new \phpbb\template\context();
414        $cache_path = $phpbb_root_path . 'cache/' . PHPBB_ENVIRONMENT . '/twig';
415        $container->setParameter('core.template.cache_path', $cache_path);
416        $filesystem = new \phpbb\filesystem\filesystem();
417        $container->set('filesystem', $filesystem);
418
419        $twig = new \phpbb\template\twig\environment(
420            $assets_bag,
421            $config,
422            $filesystem,
423            $phpbb_path_helper,
424            $cache_path,
425            null,
426            new \phpbb\template\twig\loader(''),
427            $phpbb_dispatcher,
428            [
429                'cache'            => false,
430                'debug'            => false,
431                'auto_reload'    => true,
432                'autoescape'    => false,
433            ]
434        );
435        $twig_extension = new \phpbb\template\twig\extension($context, $twig, $lang);
436        $container->set('template.twig.extensions.phpbb', $twig_extension);
437
438        $twig_extensions_collection = new \phpbb\di\service_collection($container);
439        $twig_extensions_collection->add('template.twig.extensions.phpbb');
440        $container->set('template.twig.extensions.collection', $twig_extensions_collection);
441
442        $twig->addExtension($twig_extension);
443        $twig_lexer = new \phpbb\template\twig\lexer($twig);
444        $container->set('template.twig.lexer', $twig_lexer);
445
446        $auth = new \phpbb\auth\auth();
447        $log = new \phpbb\log\log($db, $user, $auth, $phpbb_dispatcher, $phpbb_root_path, 'adm/', $phpEx, LOG_TABLE);
448        $container->set('log', $log);
449
450        $email_method = new \phpbb\messenger\method\email(
451            $assets_bag,
452            $config,
453            $phpbb_dispatcher,
454            $lang,
455            $messenger_queue,
456            $phpbb_path_helper,
457            $request,
458            $twig_extensions_collection,
459            $twig_lexer,
460            $user,
461            $phpbb_root_path,
462            $cache_path,
463            $extension_manager,
464            $log
465        );
466        $container->set('messenger.method.email', $email_method);
467    }
468
469    protected static function install_board()
470    {
471        global $phpbb_root_path, $phpEx;
472
473        self::recreate_database(self::$config);
474
475        $config_file = $phpbb_root_path . "config.$phpEx";
476        $config_file_dev = $phpbb_root_path . "config_dev.$phpEx";
477        $config_file_test = $phpbb_root_path . "config_test.$phpEx";
478
479        if (file_exists($config_file))
480        {
481            if (!file_exists($config_file_dev))
482            {
483                rename($config_file, $config_file_dev);
484            }
485            else
486            {
487                unlink($config_file);
488            }
489        }
490
491        $install_config_file = $phpbb_root_path . 'store/install_config.php';
492
493        if (file_exists($install_config_file))
494        {
495            unlink($install_config_file);
496        }
497
498        $container_builder = new \phpbb\di\container_builder($phpbb_root_path, $phpEx);
499        $container = $container_builder
500            ->with_environment('installer')
501            ->without_extensions()
502            ->without_cache()
503            ->with_custom_parameters([
504                'core.disable_super_globals' => false,
505                'installer.create_config_file.options' => [
506                    'debug' => true,
507                    'environment' => 'test',
508                ],
509                'cache.driver.class' => 'phpbb\cache\driver\file'
510            ])
511            ->with_config(new \phpbb\config_php_file($phpbb_root_path, $phpEx))
512            ->without_compiled_container()
513            ->get_container();
514
515        $container->register('installer.install_finish.notify_user')->setSynthetic(true);
516        $container->set('installer.install_finish.notify_user', new phpbb_mock_null_installer_task());
517        $container->register('installer.install_finish.install_extensions')->setSynthetic(true);
518        $container->set('installer.install_finish.install_extensions', new phpbb_mock_null_installer_task());
519        self::get_messenger_method_email($container);
520        $container->compile();
521
522        $language = $container->get('language');
523        $language->add_lang(array('common', 'acp/common', 'acp/board', 'install', 'posting'));
524
525        $iohandler_factory = $container->get('installer.helper.iohandler_factory');
526        $iohandler_factory->set_environment('cli');
527        $iohandler = $iohandler_factory->get();
528
529        $parseURL = parse_url(self::$config['phpbb_functional_url']);
530
531        $output = new \Symfony\Component\Console\Output\NullOutput();
532        $style = new \Symfony\Component\Console\Style\SymfonyStyle(
533            new \Symfony\Component\Console\Input\ArrayInput(array()),
534            $output
535        );
536        $iohandler->set_style($style, $output);
537
538        $installer = $container->get('installer.installer.install');
539        $installer->set_iohandler($iohandler);
540
541        // Set data
542        $iohandler->set_input('admin_name', 'admin');
543        $iohandler->set_input('admin_pass1', 'adminadmin');
544        $iohandler->set_input('admin_pass2', 'adminadmin');
545        $iohandler->set_input('board_email', 'nobody@example.com');
546        $iohandler->set_input('submit_admin', 'submit');
547
548        $iohandler->set_input('default_lang', 'en');
549        $iohandler->set_input('board_name', 'yourdomain.com');
550        $iohandler->set_input('board_description', 'A short text to describe your forum');
551        $iohandler->set_input('submit_board', 'submit');
552
553        $iohandler->set_input('dbms', str_replace('phpbb\db\driver\\', '',  self::$config['dbms']));
554        $iohandler->set_input('dbhost', self::$config['dbhost']);
555        $iohandler->set_input('dbport', self::$config['dbport']);
556        $iohandler->set_input('dbuser', self::$config['dbuser']);
557        $iohandler->set_input('dbpasswd', self::$config['dbpasswd']);
558        $iohandler->set_input('dbname', self::$config['dbname']);
559        $iohandler->set_input('table_prefix', self::$config['table_prefix']);
560        $iohandler->set_input('submit_database', 'submit');
561
562        $iohandler->set_input('email_enable', true);
563        $iohandler->set_input('smtp_delivery', '1');
564        $iohandler->set_input('smtp_host', '');
565        $iohandler->set_input('smtp_user', 'nxuser');
566        $iohandler->set_input('smtp_pass', 'nxpass');
567        $iohandler->set_input('submit_email', 'submit');
568
569        $iohandler->set_input('cookie_secure', (strpos(self::$root_url, 'https://') === 0) ? '1' : '0');
570        $iohandler->set_input('server_protocol', '0');
571        $iohandler->set_input('force_server_vars', $parseURL['scheme'] . '://');
572        $iohandler->set_input('server_name', $parseURL['host']);
573        $iohandler->set_input('server_port', isset($parseURL['port']) ? (int) $parseURL['port'] : 80);
574        $iohandler->set_input('script_path', $parseURL['path']);
575        $iohandler->set_input('submit_server', 'submit');
576
577        try
578        {
579            $installer->run();
580        }
581        catch (\Throwable $e)
582        {
583            // Do nothing but catch the exception as PHPUnit here throws
584            // "PHPUnit\Event\Code\NoTestCaseObjectOnCallStackException: Cannot find TestCase object on call stack"
585        }
586
587        copy($config_file, $config_file_test);
588
589        self::$install_success = true;
590
591        if (file_exists($phpbb_root_path . 'store/install_config.php'))
592        {
593            self::$install_success = false;
594            @unlink($phpbb_root_path . 'store/install_config.php');
595        }
596
597        if (file_exists($phpbb_root_path . 'cache/install_lock'))
598        {
599            @unlink($phpbb_root_path . 'cache/install_lock');
600        }
601
602        global $phpbb_container;
603        if (!empty($phpbb_container))
604        {
605            $phpbb_container->reset();
606        }
607
608        // Purge cache to remove cached files
609        $phpbb_container = new phpbb_mock_container_builder();
610        $phpbb_container->setParameter('core.environment', PHPBB_ENVIRONMENT);
611        $phpbb_container->setParameter('core.cache_dir', $phpbb_root_path . 'cache/' . PHPBB_ENVIRONMENT . '/');
612
613        $cache = new \phpbb\cache\driver\file;
614        $cache->purge();
615
616        $blacklist = ['phpbb_class_loader_mock', 'phpbb_class_loader_ext', 'phpbb_class_loader'];
617
618        foreach (array_keys($GLOBALS) as $key)
619        {
620            if (is_object($GLOBALS[$key]) && !in_array($key, $blacklist, true))
621            {
622                unset($GLOBALS[$key]);
623            }
624        }
625    }
626
627    public static function install_ext($extension)
628    {
629        self::get_db();
630        $sql = 'SELECT ext_active
631            FROM ' . EXT_TABLE . "
632            WHERE ext_name = '" . self::$db_connection->sql_escape($extension). "'";
633        $result = self::$db_connection->sql_query($sql);
634        $status = (bool) self::$db_connection->sql_fetchfield('ext_active');
635        self::$db_connection->sql_freeresult($result);
636
637        if (!$status)
638        {
639            self::add_lang('acp/extensions');
640
641            if (self::get_logged_in_user())
642            {
643                self::logout();
644            }
645            self::login();
646            self::admin_login();
647
648            $ext_path = str_replace('/', '%2F', $extension);
649
650            $crawler = self::request('GET', 'adm/index.php?i=acp_extensions&mode=main&action=enable_pre&ext_name=' . $ext_path . '&sid=' . self::$session_id);
651            self::assertGreaterThan(1, $crawler->filter('div.main fieldset.submit-buttons input')->count());
652
653            $form = $crawler->selectButton(self::lang('EXTENSION_ENABLE'))->form();
654            $crawler = self::submit($form);
655
656            $meta_refresh = $crawler->filter('meta[http-equiv="refresh"]');
657
658            // Wait for extension to be fully enabled
659            while (count($meta_refresh))
660            {
661                preg_match('#url=.+/(adm+.+)#', $meta_refresh->attr('content'), $match);
662                $url = $match[1];
663                $crawler = self::request('POST', $url);
664                $meta_refresh = $crawler->filter('meta[http-equiv="refresh"]');
665            }
666
667            self::assertContainsLang('EXTENSION_ENABLE_SUCCESS', $crawler->filter('div.successbox')->text());
668
669            self::logout();
670        }
671    }
672
673    public static function disable_ext($extension)
674    {
675        self::add_lang('acp/extensions');
676
677        if (self::get_logged_in_user())
678        {
679            self::logout();
680        }
681        self::login();
682        self::admin_login();
683
684        $ext_path = str_replace('/', '%2F', $extension);
685
686        $crawler = self::request('GET', 'adm/index.php?i=acp_extensions&mode=main&action=disable_pre&ext_name=' . $ext_path . '&sid=' . self::$session_id);
687        self::assertGreaterThan(1, $crawler->filter('div.main fieldset.submit-buttons input')->count());
688
689        $form = $crawler->selectButton(self::lang('EXTENSION_DISABLE'))->form();
690        $crawler = self::submit($form);
691
692        $meta_refresh = $crawler->filter('meta[http-equiv="refresh"]');
693
694        // Wait for extension to be fully disabled
695        while (count($meta_refresh))
696        {
697            preg_match('#url=.+/(adm+.+)#', $meta_refresh->attr('content'), $match);
698            $url = $match[1];
699            $crawler = self::request('POST', $url);
700            $meta_refresh = $crawler->filter('meta[http-equiv="refresh"]');
701        }
702
703        self::assertContainsLang('EXTENSION_DISABLE_SUCCESS', $crawler->filter('div.successbox')->text());
704
705        self::logout();
706    }
707
708    public static function delete_ext_data($extension)
709    {
710        self::add_lang('acp/extensions');
711
712        if (self::get_logged_in_user())
713        {
714            self::logout();
715        }
716        self::login();
717        self::admin_login();
718
719        $ext_path = str_replace('/', '%2F', $extension);
720
721        $crawler = self::request('GET', 'adm/index.php?i=acp_extensions&mode=main&action=delete_data_pre&ext_name=' . $ext_path . '&sid=' . self::$session_id);
722        self::assertGreaterThan(1, $crawler->filter('div.main fieldset.submit-buttons input')->count());
723
724        $form = $crawler->selectButton(self::lang('EXTENSION_DELETE_DATA'))->form();
725        $crawler = self::submit($form);
726
727        $meta_refresh = $crawler->filter('meta[http-equiv="refresh"]');
728
729        // Wait for extension to be fully enabled
730        while (count($meta_refresh))
731        {
732            preg_match('#url=.+/(adm+.+)#', $meta_refresh->attr('content'), $match);
733            $url = $match[1];
734            $crawler = self::request('POST', $url);
735            $meta_refresh = $crawler->filter('meta[http-equiv="refresh"]');
736        }
737
738        self::assertContainsLang('EXTENSION_DELETE_DATA_SUCCESS', $crawler->filter('div.successbox')->text());
739
740        self::logout();
741    }
742
743    public static function uninstall_ext($extension)
744    {
745        self::disable_ext($extension);
746        self::delete_ext_data($extension);
747    }
748
749    private static function recreate_database($config)
750    {
751        $db_conn_mgr = new phpbb_database_test_connection_manager($config);
752        $db_conn_mgr->recreate_db();
753    }
754
755    /**
756    * Creates a new style
757    *
758    * @param int $style_id Style ID
759    * @param string $style_path Style directory
760    * @param int $parent_style_id Parent style id. Default = 1
761    * @param string $parent_style_path Parent style directory. Default = 'prosilver'
762    */
763    protected function add_style($style_id, $style_path, $parent_style_id = 1, $parent_style_path = 'prosilver')
764    {
765        global $phpbb_root_path;
766
767        $this->get_db();
768        if (version_compare(PHPBB_VERSION, '3.1.0-dev', '<'))
769        {
770            $sql = 'INSERT INTO ' . STYLES_TABLE . ' ' . $this->db->sql_build_array('INSERT', array(
771                'style_id' => $style_id,
772                'style_name' => $style_path,
773                'style_copyright' => '',
774                'style_active' => 1,
775                'template_id' => $style_id,
776                'theme_id' => $style_id,
777                'imageset_id' => $style_id,
778            ));
779            $this->db->sql_query($sql);
780
781            $sql = 'INSERT INTO ' . STYLES_IMAGESET_TABLE . ' ' . $this->db->sql_build_array('INSERT', array(
782                'imageset_id' => $style_id,
783                'imageset_name' => $style_path,
784                'imageset_copyright' => '',
785                'imageset_path' => $style_path,
786            ));
787            $this->db->sql_query($sql);
788
789            $sql = 'INSERT INTO ' . STYLES_TEMPLATE_TABLE . ' ' . $this->db->sql_build_array('INSERT', array(
790                'template_id' => $style_id,
791                'template_name' => $style_path,
792                'template_copyright' => '',
793                'template_path' => $style_path,
794                'bbcode_bitfield' => 'kNg=',
795                'template_inherits_id' => $parent_style_id,
796                'template_inherit_path' => $parent_style_path,
797            ));
798            $this->db->sql_query($sql);
799
800            $sql = 'INSERT INTO ' . STYLES_THEME_TABLE . ' ' . $this->db->sql_build_array('INSERT', array(
801                'theme_id' => $style_id,
802                'theme_name' => $style_path,
803                'theme_copyright' => '',
804                'theme_path' => $style_path,
805                'theme_storedb' => 0,
806                'theme_mtime' => 0,
807                'theme_data' => '',
808            ));
809            $this->db->sql_query($sql);
810
811            if ($style_path != 'prosilver')
812            {
813                @mkdir($phpbb_root_path . 'styles/' . $style_path, 0777);
814                @mkdir($phpbb_root_path . 'styles/' . $style_path . '/template', 0777);
815            }
816        }
817        else
818        {
819            $this->db->sql_multi_insert(STYLES_TABLE, array(array(
820                'style_name' => $style_path,
821                'style_copyright' => '',
822                'style_active' => 1,
823                'style_path' => $style_path,
824                'bbcode_bitfield' => 'kNg=',
825                'style_parent_id' => $parent_style_id,
826                'style_parent_tree' => $parent_style_path,
827            )));
828        }
829    }
830
831    /**
832    * Remove temporary style created by add_style()
833    *
834    * @param int $style_id Style ID
835    * @param string $style_path Style directory
836    */
837    protected function delete_style($style_id, $style_path)
838    {
839        global $phpbb_root_path;
840
841        $this->get_db();
842        $this->db->sql_query('DELETE FROM ' . STYLES_TABLE . ' WHERE style_id = ' . $style_id);
843        if (version_compare(PHPBB_VERSION, '3.1.0-dev', '<'))
844        {
845            $this->db->sql_query('DELETE FROM ' . STYLES_IMAGESET_TABLE . ' WHERE imageset_id = ' . $style_id);
846            $this->db->sql_query('DELETE FROM ' . STYLES_TEMPLATE_TABLE . ' WHERE template_id = ' . $style_id);
847            $this->db->sql_query('DELETE FROM ' . STYLES_THEME_TABLE . ' WHERE theme_id = ' . $style_id);
848
849            if ($style_path != 'prosilver')
850            {
851                @rmdir($phpbb_root_path . 'styles/' . $style_path . '/template');
852                @rmdir($phpbb_root_path . 'styles/' . $style_path);
853            }
854        }
855    }
856
857    /**
858    * Creates a new user with limited permissions
859    *
860    * @param string $username Also doubles up as the user's password
861    * @param string $email User email (defaults to nobody@example.com)
862    * @return int ID of created user
863    */
864    protected function create_user($username, $email = 'nobody@example.com')
865    {
866        // Required by unique_id
867        global $config;
868
869        $config = new \phpbb\config\config(array());
870
871        /*
872        * Add required config entries to the config array to prevent
873        * set_config() sending an INSERT query for already existing entries,
874        * resulting in a SQL error.
875        * This is because set_config() first sends an UPDATE query, then checks
876        * sql_affectedrows() which can be 0 (e.g. on MySQL) when the new
877        * data is already there.
878        */
879        $config['newest_user_colour'] = '';
880        $config['rand_seed'] = '';
881        $config['rand_seed_last_update'] = time() + 600;
882
883        // Prevent new user to have an invalid style
884        $config['default_style'] = 1;
885
886        // Required by user_add
887        global $db, $cache, $phpbb_dispatcher, $phpbb_container;
888        $db = $this->get_db();
889        if (!function_exists('phpbb_mock_null_cache'))
890        {
891            require_once(__DIR__ . '/../mock/null_cache.php');
892        }
893        $cache = new phpbb_mock_null_cache;
894
895        $cache_driver = new \phpbb\cache\driver\dummy();
896        $phpbb_container = new phpbb_mock_container_builder();
897        $phpbb_container->set('cache.driver', $cache_driver);
898        $phpbb_notifications = new phpbb_mock_notification_manager();
899        $phpbb_container->set('notification_manager', $phpbb_notifications);
900
901        if (!function_exists('utf_clean_string'))
902        {
903            require_once(__DIR__ . '/../../phpBB/includes/utf/utf_tools.php');
904        }
905        if (!function_exists('user_add'))
906        {
907            require_once(__DIR__ . '/../../phpBB/includes/functions_user.php');
908        }
909
910        $phpbb_dispatcher = new phpbb_mock_event_dispatcher();
911        $passwords_manager = $this->get_passwords_manager();
912
913        $user_row = array(
914            'username' => $username,
915            'group_id' => 2,
916            'user_email' => $email,
917            'user_type' => 0,
918            'user_lang' => 'en',
919            'user_timezone' => 'UTC',
920            'user_dateformat' => 'r',
921            'user_password' => $passwords_manager->hash($username . $username),
922        );
923        return user_add($user_row);
924    }
925
926    /**
927     * Get group ID
928     *
929     * @param string $group_name Group name
930     * @return int Group id of specified group name
931     */
932    protected function get_group_id($group_name)
933    {
934        $sql = 'SELECT group_id
935            FROM ' . GROUPS_TABLE . "
936            WHERE group_name = '" . $this->db->sql_escape($group_name) . "'";
937        $result = $this->db->sql_query($sql);
938        $group_id = (int) $this->db->sql_fetchfield('group_id');
939        $this->db->sql_freeresult($result);
940
941        return $group_id;
942    }
943
944    /**
945     * Get current board's search type
946     *
947     * @return string Current search type setting
948     */
949    protected function get_search_type()
950    {
951        $sql = 'SELECT config_value as search_type
952            FROM ' . CONFIG_TABLE . "
953            WHERE config_name = '" . $this->db->sql_escape('search_type') . "'";
954        $result = $this->db->sql_query($sql);
955        $search_type = $this->db->sql_fetchfield('search_type');
956        $this->db->sql_freeresult($result);
957
958        return $search_type;
959    }
960
961    protected function remove_user_group($group_name, $usernames)
962    {
963        global $db, $cache, $auth, $config, $phpbb_dispatcher, $phpbb_log, $phpbb_container, $user, $phpbb_root_path, $phpEx;
964
965        $config = new \phpbb\config\config(array());
966        $config['coppa_enable'] = 0;
967
968        $db = $this->get_db();
969        $phpbb_dispatcher = new phpbb_mock_event_dispatcher();
970
971        $user = $this->createMock('\phpbb\user');
972        $user->data['user_id'] = 2; // admin
973        $user->ip = '';
974
975        $auth = $this->createMock('\phpbb\auth\auth');
976
977        $phpbb_log = new \phpbb\log\log($db, $user, $auth, $phpbb_dispatcher, $phpbb_root_path, 'adm/', $phpEx, LOG_TABLE);
978        $cache = new phpbb_mock_null_cache;
979
980        $cache_driver = new \phpbb\cache\driver\dummy();
981        $phpbb_container = new phpbb_mock_container_builder();
982        $phpbb_container->set('cache.driver', $cache_driver);
983        $phpbb_container->set('notification_manager', new phpbb_mock_notification_manager());
984
985        if (!function_exists('utf_clean_string'))
986        {
987            require_once(__DIR__ . '/../../phpBB/includes/utf/utf_tools.php');
988        }
989        if (!function_exists('group_user_del'))
990        {
991            require_once(__DIR__ . '/../../phpBB/includes/functions_user.php');
992        }
993
994        $group_id = $this->get_group_id($group_name);
995
996        return group_user_del($group_id, false, $usernames, $group_name);
997    }
998
999    protected function add_user_group($group_name, $usernames, $default = false, $leader = false)
1000    {
1001        global $db, $cache, $auth, $config, $phpbb_dispatcher, $phpbb_log, $phpbb_container, $user, $phpbb_root_path, $phpEx;
1002
1003        $config = new \phpbb\config\config(array());
1004        $config['coppa_enable'] = 0;
1005
1006        $db = $this->get_db();
1007        $phpbb_dispatcher = new phpbb_mock_event_dispatcher();
1008
1009        $user = $this->createMock('\phpbb\user');
1010        $user->data['user_id'] = 2; // admin
1011        $user->ip = '';
1012
1013        $auth = $this->createMock('\phpbb\auth\auth');
1014
1015        $phpbb_log = new \phpbb\log\log($db, $user, $auth, $phpbb_dispatcher, $phpbb_root_path, 'adm/', $phpEx, LOG_TABLE);
1016        $cache = new phpbb_mock_null_cache;
1017
1018        $cache_driver = new \phpbb\cache\driver\dummy();
1019        $phpbb_container = $this->createMock('Symfony\Component\DependencyInjection\ContainerInterface');
1020        $phpbb_container
1021            ->expects($this->any())
1022            ->method('get')
1023            ->with('cache.driver')
1024            ->will($this->returnValue($cache_driver));
1025
1026        if (!function_exists('utf_clean_string'))
1027        {
1028            require_once(__DIR__ . '/../../phpBB/includes/utf/utf_tools.php');
1029        }
1030        if (!function_exists('group_user_del'))
1031        {
1032            require_once(__DIR__ . '/../../phpBB/includes/functions_user.php');
1033        }
1034
1035        $group_id = $this->get_group_id($group_name);
1036
1037        return group_user_add($group_id, false, $usernames, $group_name, $default, $leader);
1038    }
1039
1040    protected static function login($username = 'admin', $autologin = false)
1041    {
1042        self::add_lang('ucp');
1043
1044        $crawler = self::request('GET', 'ucp.php?mode=login');
1045        $button = $crawler->selectButton(self::lang('LOGIN'));
1046        self::assertGreaterThan(0, $button->count(), 'No login button found');
1047
1048        $form = $crawler->selectButton(self::lang('LOGIN'))->form();
1049        if ($autologin)
1050        {
1051            $form['autologin']->tick();
1052        }
1053        $crawler = self::submit($form, array('username' => $username, 'password' => $username . $username));
1054        self::assertStringNotContainsString(self::lang('LOGIN'), $crawler->filter('.navbar')->text());
1055
1056        $cookies = self::$cookieJar->all();
1057
1058        // The session id is stored in a cookie that ends with _sid - we assume there is only one such cookie
1059        foreach ($cookies as $cookie)
1060        {
1061            if (substr($cookie->getName(), -4) == '_sid')
1062            {
1063                self::$session_id = $cookie->getValue();
1064            }
1065        }
1066    }
1067
1068    protected static function logout()
1069    {
1070        self::add_lang('ucp');
1071
1072        $crawler = self::request('GET', 'index.php');
1073        $logout_link = $crawler->filter('a[title="' . self::lang('LOGOUT') . '"]')->attr('href');
1074        self::request('GET', $logout_link);
1075
1076        $crawler = self::request('GET', $logout_link);
1077        self::assertStringContainsString(self::lang('REGISTER'), $crawler->filter('.navbar')->text());
1078        self::$session_id = null;
1079    }
1080
1081    /**
1082    * Login to the ACP
1083    * You must run login() before calling this.
1084    */
1085    protected static function admin_login($username = 'admin')
1086    {
1087        self::add_lang('acp/common');
1088
1089        // Requires login first!
1090        if (empty(self::$session_id))
1091        {
1092            self::fail('$this->sid is empty. Make sure you call login() before admin_login()');
1093            return;
1094        }
1095
1096        $crawler = self::request('GET', 'adm/index.php?sid=' . self::$session_id);
1097        self::assertStringContainsString(self::lang('LOGIN_ADMIN_CONFIRM'), $crawler->filter('html')->text());
1098
1099        $form = $crawler->selectButton(self::lang('LOGIN'))->form();
1100
1101        foreach ($form->getValues() as $field => $value)
1102        {
1103            if (strpos($field, 'password_') === 0)
1104            {
1105                $crawler = self::submit($form, array('username' => $username, $field => $username . $username));
1106                self::assertStringContainsString(self::lang('ADMIN_PANEL'), $crawler->filter('h1')->text());
1107
1108                $cookies = self::$cookieJar->all();
1109
1110                // The session id is stored in a cookie that ends with _sid - we assume there is only one such cookie
1111                foreach ($cookies as $cookie)
1112                {
1113                    if (substr($cookie->getName(), -4) == '_sid')
1114                    {
1115                        self::$session_id = $cookie->getValue();
1116                    }
1117                }
1118
1119                break;
1120            }
1121        }
1122    }
1123
1124    protected static function add_lang($lang_file)
1125    {
1126        if (is_array($lang_file))
1127        {
1128            foreach ($lang_file as $file)
1129            {
1130                self::add_lang($file);
1131            }
1132
1133            return;
1134        }
1135
1136        $lang_path = __DIR__ . "/../../phpBB/language/en/$lang_file.php";
1137
1138        $lang = [];
1139
1140        if (file_exists($lang_path))
1141        {
1142            include($lang_path);
1143        }
1144
1145        self::$lang_ary = array_merge(self::$lang_ary, $lang);
1146    }
1147
1148    protected static function add_lang_ext($ext_name, $lang_file)
1149    {
1150        if (is_array($lang_file))
1151        {
1152            foreach ($lang_file as $file)
1153            {
1154                self::add_lang_ext($ext_name, $file);
1155            }
1156
1157            return;
1158        }
1159
1160        $lang_path = __DIR__ . "/../../phpBB/ext/{$ext_name}/language/en/$lang_file.php";
1161
1162        $lang = [];
1163
1164        if (file_exists($lang_path))
1165        {
1166            include($lang_path);
1167        }
1168
1169        self::$lang_ary = array_merge(self::$lang_ary, $lang);
1170    }
1171
1172    protected static function lang()
1173    {
1174        $args = func_get_args();
1175        $key = $args[0];
1176
1177        if (empty(self::$lang_ary[$key]))
1178        {
1179            throw new RuntimeException('Language key "' . $key . '" could not be found.');
1180        }
1181
1182        $args[0] = self::$lang_ary[$key];
1183
1184        return call_user_func_array('sprintf', $args);
1185    }
1186
1187    /**
1188     * assertContains for language strings
1189     *
1190     * @param string $needle    Search string
1191     * @param string $haystack    Search this
1192     * @param string $message    Optional failure message
1193     */
1194    public static function assertContainsLang($needle, $haystack, $message = '')
1195    {
1196        self::assertStringContainsString(html_entity_decode(self::lang($needle), ENT_QUOTES), $haystack, $message);
1197    }
1198
1199    /**
1200    * assertNotContains for language strings
1201    *
1202    * @param string $needle        Search string
1203    * @param string $haystack    Search this
1204    * @param string $message    Optional failure message
1205    */
1206    public static function assertNotContainsLang($needle, $haystack, $message = '')
1207    {
1208        self::assertStringNotContainsString(html_entity_decode(self::lang($needle), ENT_QUOTES), $haystack, $message);
1209    }
1210
1211    /*
1212    * Perform some basic assertions for the page
1213    *
1214    * Checks for debug/error output before the actual page content and the status code
1215    *
1216    * @param mixed $status_code        Expected status code, false to disable check
1217    * @return null
1218    */
1219    public static function assert_response_html($status_code = 200)
1220    {
1221        // Any output before the doc type means there was an error
1222        $content = self::get_content();
1223        self::assertStringNotContainsString('[phpBB Debug]', $content);
1224        self::assertStringStartsWith('<!DOCTYPE', strtoupper(substr(trim($content), 0, 10)), $content);
1225
1226        if ($status_code !== false)
1227        {
1228            self::assert_response_status_code($status_code);
1229        }
1230    }
1231
1232    /*
1233    * Perform some basic assertions for an xml page
1234    *
1235    * Checks for debug/error output before the actual page content and the status code
1236    *
1237    * @param mixed $status_code        Expected status code, false to disable check
1238    * @return null
1239    */
1240    public static function assert_response_xml($status_code = 200)
1241    {
1242        // Any output before the xml opening means there was an error
1243        $content = self::get_content();
1244        self::assertStringNotContainsString('[phpBB Debug]', $content);
1245        self::assertStringStartsWith('<?xml', trim($content), 'Output found before XML specification.');
1246
1247        if ($status_code !== false)
1248        {
1249            self::assert_response_status_code($status_code);
1250        }
1251    }
1252
1253    /**
1254    * Heuristic function to check that the response is success.
1255    *
1256    * When php decides to die with a fatal error, it still sends 200 OK
1257    * status code. This assertion tries to catch that.
1258    *
1259    * @param int $status_code    Expected status code
1260    * @return void
1261    */
1262    public static function assert_response_status_code($status_code = 200)
1263    {
1264        if ($status_code != self::$client->getResponse()->getStatusCode() &&
1265            preg_match('/^5[0-9]{2}/', self::$client->getResponse()->getStatusCode()))
1266        {
1267            self::fail("Encountered unexpected server error:\n" . self::$client->getResponse()->getContent());
1268        }
1269        self::assertEquals($status_code, self::$client->getResponse()->getStatusCode(), 'HTTP status code does not match');
1270    }
1271
1272    public function assert_filter($crawler, $expr, $msg = null)
1273    {
1274        $nodes = $crawler->filter($expr);
1275        if ($msg)
1276        {
1277            $msg .= "\n";
1278        }
1279        else
1280        {
1281            $msg = '';
1282        }
1283        $msg .= "`$expr` not found in DOM.";
1284        $this->assertGreaterThan(0, count($nodes), $msg);
1285        return $nodes;
1286    }
1287
1288    /**
1289    * Asserts that exactly one checkbox with name $name exists within the scope
1290    * of $crawler and that the checkbox is checked.
1291    *
1292    * @param Symfony\Component\DomCrawler\Crawler $crawler
1293    * @param string $name
1294    * @param string $message
1295    *
1296    * @return void
1297    */
1298    public function assert_checkbox_is_checked($crawler, $name, $message = '')
1299    {
1300        $this->assertNotNull(
1301            $this->assert_find_one_checkbox($crawler, $name)->attr('checked'),
1302            $message ?: "Failed asserting that checkbox $name is checked."
1303        );
1304    }
1305
1306    /**
1307    * Asserts that exactly one checkbox with name $name exists within the scope
1308    * of $crawler and that the checkbox is unchecked.
1309    *
1310    * @param Symfony\Component\DomCrawler\Crawler $crawler
1311    * @param string $name
1312    * @param string $message
1313    *
1314    * @return void
1315    */
1316    public function assert_checkbox_is_unchecked($crawler, $name, $message = '')
1317    {
1318        $this->assertNull(
1319            $this->assert_find_one_checkbox($crawler, $name)->attr('checked'),
1320            $message ?: "Failed asserting that checkbox $name is unchecked."
1321        );
1322    }
1323
1324    /**
1325    * Searches for an input element of type checkbox with the name $name using
1326    * $crawler. Contains an assertion that only one such checkbox exists within
1327    * the scope of $crawler.
1328    *
1329    * @param Symfony\Component\DomCrawler\Crawler $crawler
1330    * @param string $name
1331    * @param string $message
1332    *
1333    * @return Symfony\Component\DomCrawler\Crawler
1334    */
1335    public function assert_find_one_checkbox($crawler, $name, $message = '')
1336    {
1337        $query = sprintf('//input[@type="checkbox" and @name="%s"]', $name);
1338        $result = $crawler->filterXPath($query);
1339
1340        $this->assertEquals(
1341            1,
1342            count($result),
1343            $message ?: 'Failed asserting that exactly one checkbox with name' .
1344                " $name exists in crawler scope."
1345        );
1346
1347        return $result;
1348    }
1349
1350    /**
1351    * Creates a topic
1352    *
1353    * Be sure to login before creating
1354    *
1355    * @param int $forum_id
1356    * @param string $subject
1357    * @param string $message
1358    * @param array $additional_form_data Any additional form data to be sent in the request
1359    * @param string $expected Lang var of expected message after posting
1360    * @return array|null post_id, topic_id if message is empty
1361    */
1362    public function create_topic($forum_id, $subject, $message, $additional_form_data = array(), $expected = '')
1363    {
1364        $posting_url = "posting.php?mode=post&f={$forum_id}&sid={$this->sid}";
1365
1366        $form_data = array_merge(array(
1367            'subject'        => $subject,
1368            'message'        => $message,
1369            'post'            => true,
1370        ), $additional_form_data);
1371
1372        return self::submit_post($posting_url, 'POST_TOPIC', $form_data, $expected);
1373    }
1374
1375    /**
1376    * Creates a post
1377    *
1378    * Be sure to login before creating
1379    *
1380    * @param int $forum_id
1381    * @param int $topic_id
1382    * @param string $subject
1383    * @param string $message
1384    * @param array $additional_form_data Any additional form data to be sent in the request
1385    * @param string $expected Lang var of expected message after posting
1386    * @return array|null post_id, topic_id if message is empty
1387    */
1388    public function create_post($forum_id, $topic_id, $subject, $message, $additional_form_data = array(), $expected = '')
1389    {
1390        $posting_url = "posting.php?mode=reply&t={$topic_id}&sid={$this->sid}";
1391
1392        $form_data = array_merge(array(
1393            'subject'        => $subject,
1394            'message'        => $message,
1395            'post'            => true,
1396            'topic_id'        => $topic_id,
1397        ), $additional_form_data);
1398
1399        return self::submit_post($posting_url, 'POST_REPLY', $form_data, $expected);
1400    }
1401
1402    /**
1403    * Helper for submitting posts
1404    *
1405    * @param string $posting_url
1406    * @param string $posting_contains
1407    * @param array $form_data
1408    * @param string $expected Lang var of expected message after posting
1409    * @return array|null post_id, topic_id if message is empty
1410    */
1411    protected function submit_post($posting_url, $posting_contains, $form_data, $expected = '')
1412    {
1413        $this->add_lang('posting');
1414
1415        $crawler = $this->submit_message($posting_url, $posting_contains, $form_data);
1416
1417        if ($expected !== '')
1418        {
1419            if (isset($this->lang[$expected]))
1420            {
1421                $this->assertContainsLang($expected, $crawler->filter('html')->text());
1422            }
1423            else
1424            {
1425                $this->assertStringContainsString($expected, $crawler->filter('html')->text());
1426            }
1427            return null;
1428        }
1429
1430        $post_link = $crawler->filter('.postbody a[title="Post"]')->last()->attr('href');
1431        $topic_link = $crawler->filter('h2[class="topic-title"] > a')->attr('href');
1432
1433        $post_id = $this->get_parameter_from_link($post_link, 'p');
1434        $topic_id = $this->get_parameter_from_link($topic_link, 't');
1435
1436        if (!$topic_id)
1437        {
1438            $topic_id = $form_data['topic_id'];
1439        }
1440
1441        return array(
1442            'topic_id'    => $topic_id,
1443            'post_id'    => $post_id,
1444        );
1445    }
1446
1447    /**
1448    * Creates a private message
1449    *
1450    * Be sure to login before creating
1451    *
1452    * @param string $subject
1453    * @param string $message
1454    * @param array $to
1455    * @param array $additional_form_data Any additional form data to be sent in the request
1456    * @return int private_message_id
1457    */
1458    public function create_private_message($subject, $message, $to, $additional_form_data = array())
1459    {
1460        $this->add_lang(array('ucp', 'posting'));
1461
1462        $posting_url = "ucp.php?i=pm&mode=compose&sid={$this->sid}";
1463
1464        $form_data = array_merge(array(
1465            'subject'        => $subject,
1466            'message'        => $message,
1467            'post'            => true,
1468        ), $additional_form_data);
1469
1470        foreach ($to as $user_id)
1471        {
1472            $form_data['address_list[u][' . $user_id . ']'] = 'to';
1473        }
1474
1475        $crawler = self::submit_message($posting_url, 'POST_NEW_PM', $form_data);
1476
1477        $this->assertStringContainsString($this->lang('MESSAGE_STORED'), $crawler->filter('html')->text());
1478        $url = $crawler->selectLink($this->lang('VIEW_PRIVATE_MESSAGE', '', ''))->link()->getUri();
1479
1480        return $this->get_parameter_from_link($url, 'p');
1481    }
1482
1483    /**
1484    * Helper for submitting a message (post or private message)
1485    *
1486    * @param string $posting_url
1487    * @param string $posting_contains
1488    * @param array $form_data
1489    * @return \Symfony\Component\DomCrawler\Crawler the crawler object
1490    */
1491    protected function submit_message($posting_url, $posting_contains, $form_data)
1492    {
1493        $crawler = self::request('GET', $posting_url);
1494        $this->assertStringContainsString($this->lang($posting_contains), $crawler->filter('html')->text());
1495
1496        if (!empty($form_data['upload_files']))
1497        {
1498            for ($i = 0; $i < $form_data['upload_files']; $i++)
1499            {
1500                $file = array(
1501                    'tmp_name'    => __DIR__ . '/../functional/fixtures/files/valid.jpg',
1502                    'name'        => 'valid.jpg',
1503                    'type'        => 'image/jpeg',
1504                    'size'        => filesize(__DIR__ . '/../functional/fixtures/files/valid.jpg'),
1505                    'error'        => UPLOAD_ERR_OK,
1506                );
1507
1508                $file_form_data = array_merge(['add_file' => $this->lang('ADD_FILE')], $this->get_hidden_fields($crawler, $posting_url));
1509
1510                $crawler = self::$client->request('POST', $posting_url, $file_form_data, array('fileupload' => $file));
1511            }
1512            unset($form_data['upload_files']);
1513        }
1514
1515        $form_data = array_merge($form_data, $this->get_hidden_fields($crawler, $posting_url));
1516
1517        // I use a request because the form submission method does not allow you to send data that is not
1518        // contained in one of the actual form fields that the browser sees (i.e. it ignores "hidden" inputs)
1519        // Instead, I send it as a request with the submit button "post" set to true.
1520        return self::request('POST', $posting_url, $form_data);
1521    }
1522
1523    /**
1524    * Deletes a topic
1525    *
1526    * Be sure to login before creating
1527    *
1528    * @param int $topic_id
1529    * @return null
1530    */
1531    public function delete_topic($topic_id)
1532    {
1533        $this->add_lang('posting');
1534        $crawler = $this->get_quickmod_page($topic_id, 'DELETE_TOPIC');
1535        $this->assertContainsLang('DELETE_PERMANENTLY', $crawler->text());
1536
1537        $this->add_lang('mcp');
1538        $form = $crawler->selectButton('Yes')->form();
1539        $form['delete_permanent'] = 1;
1540        $crawler = self::submit($form);
1541        $this->assertContainsLang('TOPIC_DELETED_SUCCESS', $crawler->text());
1542    }
1543
1544    /**
1545    * Deletes a post
1546    *
1547    * Be sure to login before creating
1548    *
1549    * @param int $forum_id
1550    * @param int $topic_id
1551    * @return null
1552    */
1553    public function delete_post($forum_id, $post_id)
1554    {
1555        $this->add_lang('posting');
1556        $crawler = self::request('GET', "posting.php?mode=delete&p={$post_id}&sid={$this->sid}");
1557        $this->assertContainsLang('DELETE_PERMANENTLY', $crawler->text());
1558
1559        $form = $crawler->selectButton('Yes')->form();
1560        $form['delete_permanent'] = 1;
1561        $crawler = self::submit($form);
1562        $this->assertContainsLang('POST_DELETED', $crawler->text());
1563    }
1564
1565    /**
1566    * Returns the requested parameter from a URL
1567    *
1568    * @param    string    $url
1569    * @param    string    $parameter
1570    * @return        string    Value of the parameter in the URL, null if not set
1571    */
1572    public function get_parameter_from_link($url, $parameter)
1573    {
1574        if (strpos($url, '?') === false)
1575        {
1576            return null;
1577        }
1578
1579        $url_parts = explode('?', $url);
1580        if (isset($url_parts[1]))
1581        {
1582            $url_parameters = $url_parts[1];
1583            if (strpos($url_parameters, '#') !== false)
1584            {
1585                $url_parameters = explode('#', $url_parameters);
1586                $url_parameters = $url_parameters[0];
1587            }
1588
1589            foreach (explode('&', $url_parameters) as $url_param)
1590            {
1591                list($param, $value) = explode('=', $url_param);
1592                if ($param == $parameter)
1593                {
1594                    return $value;
1595                }
1596            }
1597        }
1598        return null;
1599    }
1600
1601    /**
1602    * Return a passwords manager instance
1603    *
1604    * @return phpbb\passwords\manager
1605    */
1606    public function get_passwords_manager()
1607    {
1608        // Prepare dependencies for manager and driver
1609        $config = new \phpbb\config\config(array());
1610        $driver_helper = new \phpbb\passwords\driver\helper($config);
1611
1612        $passwords_drivers = array(
1613            'passwords.driver.bcrypt_2y'    => new \phpbb\passwords\driver\bcrypt_2y($config, $driver_helper),
1614            'passwords.driver.bcrypt'        => new \phpbb\passwords\driver\bcrypt($config, $driver_helper),
1615            'passwords.driver.salted_md5'    => new \phpbb\passwords\driver\salted_md5($config, $driver_helper),
1616            'passwords.driver.phpass'        => new \phpbb\passwords\driver\phpass($config, $driver_helper),
1617        );
1618
1619        $passwords_helper = new \phpbb\passwords\helper;
1620        // Set up passwords manager
1621        $manager = new \phpbb\passwords\manager($config, $passwords_drivers, $passwords_helper, array_keys($passwords_drivers));
1622
1623        return $manager;
1624    }
1625
1626    /**
1627    * Get quickmod page
1628    *
1629    * @param int $topic_id
1630    * @param string $action    Language key for the quickmod action
1631    * @param Symfony\Component\DomCrawler\Crawler Optional crawler object to use instead of creating new one.
1632    * @return Symfony\Component\DomCrawler\Crawler
1633    */
1634    public function get_quickmod_page($topic_id, $action, $crawler = false)
1635    {
1636        $this->add_lang('viewtopic');
1637
1638        if ($crawler === false)
1639        {
1640            $crawler = self::request('GET', "viewtopic.php?t={$topic_id}&sid={$this->sid}");
1641        }
1642        $link = $crawler->filter('#quickmod')->selectLink($this->lang($action))->link()->getUri();
1643
1644        return self::request('GET', substr($link, strpos($link, 'mcp.')) . "&sid={$this->sid}");
1645    }
1646
1647    /**
1648     * Get hidden fields for URL
1649     *
1650     * @param Symfony\Component\DomCrawler\Crawler|null $crawler Crawler instance or null
1651     * @param string $url Request URL
1652     *
1653     * @return array Hidden form fields array
1654     */
1655    protected function get_hidden_fields($crawler, $url)
1656    {
1657        if (!$crawler)
1658        {
1659            $crawler = self::$client->request('GET', $url);
1660        }
1661        $hidden_fields = [
1662            $crawler->filter('[type="hidden"]')->each(function ($node, $i) {
1663                return ['name' => $node->attr('name'), 'value' => $node->attr('value')];
1664            }),
1665        ];
1666
1667        $file_form_data = [];
1668
1669        foreach ($hidden_fields as $fields)
1670        {
1671            foreach($fields as $field)
1672            {
1673                $file_form_data[$field['name']] = $field['value'];
1674            }
1675        }
1676
1677        return $file_form_data;
1678    }
1679
1680    /**
1681     * Get username of currently logged in user
1682     *
1683     * @return string|bool username if logged in, false otherwise
1684     */
1685    protected static function get_logged_in_user()
1686    {
1687        $username_logged_in = false;
1688        $crawler = self::request('GET', 'index.php');
1689        $is_logged_in = strpos($crawler->filter('div[class="navbar"]')->text(), 'Login') === false;
1690        if ($is_logged_in)
1691        {
1692            $username_logged_in = $crawler->filter('li[id="username_logged_in"] > div > a > span:not(.avatar)')->text();
1693        }
1694        return $username_logged_in;
1695    }
1696
1697    /**
1698     * Posting flood control
1699     */
1700    protected function set_flood_interval($flood_interval)
1701    {
1702        $relogin_back = false;
1703        $logged_in_username = $this->get_logged_in_user();
1704        if ($logged_in_username && $logged_in_username !== 'admin')
1705        {
1706            $this->logout();
1707            $relogin_back = true;
1708        }
1709
1710        if (!$logged_in_username || $relogin_back)
1711        {
1712            $this->login();
1713            $this->admin_login();
1714        }
1715
1716        $this->add_lang('acp/common');
1717        $crawler = self::request('GET', 'adm/index.php?i=acp_board&mode=post&sid=' . $this->sid);
1718        $form = $crawler->selectButton('submit')->form([
1719            'config[flood_interval]'    => $flood_interval,
1720        ]);
1721        $crawler = self::submit($form);
1722        $this->assertContainsLang('CONFIG_UPDATED', $crawler->text());
1723
1724        // Get logged out back or get logged in in user back if needed
1725        if (!$logged_in_username)
1726        {
1727            $this->logout();
1728        }
1729
1730        if ($relogin_back)
1731        {
1732            $this->logout();
1733            $this->login($logged_in_username);
1734        }
1735    }
1736
1737    /**
1738    * Check if a user exists by username or user_id
1739    *
1740    * @param string $username The username to check or empty if user_id is used
1741    * @param int $user_id The user id to check or empty if username is used
1742    *
1743    * @return array Returns user_id => username array or empty array if user does not exist
1744    */
1745    protected function user_exists($username = '', $user_id = '')
1746    {
1747        global $db;
1748
1749        $db = $this->get_db();
1750
1751        if (!function_exists('utf_clean_string'))
1752        {
1753            require_once(__DIR__ . '/../../phpBB/includes/utf/utf_tools.php');
1754        }
1755        if (!function_exists('user_get_id_name'))
1756        {
1757            require_once(__DIR__ . '/../../phpBB/includes/functions_user.php');
1758        }
1759
1760        user_get_id_name($user_id, $username, false, true);
1761
1762        return $username;
1763    }
1764}