Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 799
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 / 799
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 / 24
0.00% covered (danger)
0.00%
0 / 1
12
 add_user_group
0.00% covered (danger)
0.00%
0 / 27
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', array(), array(
972            new \phpbb\language\language(new \phpbb\language\language_file_loader($phpbb_root_path, $phpEx)),
973            '\phpbb\datetime'
974        ));
975        $user->data['user_id'] = 2; // admin
976        $user->ip = '';
977
978        $auth = $this->createMock('\phpbb\auth\auth');
979
980        $phpbb_log = new \phpbb\log\log($db, $user, $auth, $phpbb_dispatcher, $phpbb_root_path, 'adm/', $phpEx, LOG_TABLE);
981        $cache = new phpbb_mock_null_cache;
982
983        $cache_driver = new \phpbb\cache\driver\dummy();
984        $phpbb_container = new phpbb_mock_container_builder();
985        $phpbb_container->set('cache.driver', $cache_driver);
986        $phpbb_container->set('notification_manager', new phpbb_mock_notification_manager());
987
988        if (!function_exists('utf_clean_string'))
989        {
990            require_once(__DIR__ . '/../../phpBB/includes/utf/utf_tools.php');
991        }
992        if (!function_exists('group_user_del'))
993        {
994            require_once(__DIR__ . '/../../phpBB/includes/functions_user.php');
995        }
996
997        $group_id = $this->get_group_id($group_name);
998
999        return group_user_del($group_id, false, $usernames, $group_name);
1000    }
1001
1002    protected function add_user_group($group_name, $usernames, $default = false, $leader = false)
1003    {
1004        global $db, $cache, $auth, $config, $phpbb_dispatcher, $phpbb_log, $phpbb_container, $user, $phpbb_root_path, $phpEx;
1005
1006        $config = new \phpbb\config\config(array());
1007        $config['coppa_enable'] = 0;
1008
1009        $db = $this->get_db();
1010        $phpbb_dispatcher = new phpbb_mock_event_dispatcher();
1011
1012        $user = $this->createMock('\phpbb\user', array(), array(
1013            new \phpbb\language\language(new \phpbb\language\language_file_loader($phpbb_root_path, $phpEx)),
1014            '\phpbb\datetime'
1015        ));
1016        $user->data['user_id'] = 2; // admin
1017        $user->ip = '';
1018
1019        $auth = $this->createMock('\phpbb\auth\auth');
1020
1021        $phpbb_log = new \phpbb\log\log($db, $user, $auth, $phpbb_dispatcher, $phpbb_root_path, 'adm/', $phpEx, LOG_TABLE);
1022        $cache = new phpbb_mock_null_cache;
1023
1024        $cache_driver = new \phpbb\cache\driver\dummy();
1025        $phpbb_container = $this->createMock('Symfony\Component\DependencyInjection\ContainerInterface');
1026        $phpbb_container
1027            ->expects($this->any())
1028            ->method('get')
1029            ->with('cache.driver')
1030            ->will($this->returnValue($cache_driver));
1031
1032        if (!function_exists('utf_clean_string'))
1033        {
1034            require_once(__DIR__ . '/../../phpBB/includes/utf/utf_tools.php');
1035        }
1036        if (!function_exists('group_user_del'))
1037        {
1038            require_once(__DIR__ . '/../../phpBB/includes/functions_user.php');
1039        }
1040
1041        $group_id = $this->get_group_id($group_name);
1042
1043        return group_user_add($group_id, false, $usernames, $group_name, $default, $leader);
1044    }
1045
1046    protected static function login($username = 'admin', $autologin = false)
1047    {
1048        self::add_lang('ucp');
1049
1050        $crawler = self::request('GET', 'ucp.php?mode=login');
1051        $button = $crawler->selectButton(self::lang('LOGIN'));
1052        self::assertGreaterThan(0, $button->count(), 'No login button found');
1053
1054        $form = $crawler->selectButton(self::lang('LOGIN'))->form();
1055        if ($autologin)
1056        {
1057            $form['autologin']->tick();
1058        }
1059        $crawler = self::submit($form, array('username' => $username, 'password' => $username . $username));
1060        self::assertStringNotContainsString(self::lang('LOGIN'), $crawler->filter('.navbar')->text());
1061
1062        $cookies = self::$cookieJar->all();
1063
1064        // The session id is stored in a cookie that ends with _sid - we assume there is only one such cookie
1065        foreach ($cookies as $cookie)
1066        {
1067            if (substr($cookie->getName(), -4) == '_sid')
1068            {
1069                self::$session_id = $cookie->getValue();
1070            }
1071        }
1072    }
1073
1074    protected static function logout()
1075    {
1076        self::add_lang('ucp');
1077
1078        $crawler = self::request('GET', 'index.php');
1079        $logout_link = $crawler->filter('a[title="' . self::lang('LOGOUT') . '"]')->attr('href');
1080        self::request('GET', $logout_link);
1081
1082        $crawler = self::request('GET', $logout_link);
1083        self::assertStringContainsString(self::lang('REGISTER'), $crawler->filter('.navbar')->text());
1084        self::$session_id = null;
1085    }
1086
1087    /**
1088    * Login to the ACP
1089    * You must run login() before calling this.
1090    */
1091    protected static function admin_login($username = 'admin')
1092    {
1093        self::add_lang('acp/common');
1094
1095        // Requires login first!
1096        if (empty(self::$session_id))
1097        {
1098            self::fail('$this->sid is empty. Make sure you call login() before admin_login()');
1099            return;
1100        }
1101
1102        $crawler = self::request('GET', 'adm/index.php?sid=' . self::$session_id);
1103        self::assertStringContainsString(self::lang('LOGIN_ADMIN_CONFIRM'), $crawler->filter('html')->text());
1104
1105        $form = $crawler->selectButton(self::lang('LOGIN'))->form();
1106
1107        foreach ($form->getValues() as $field => $value)
1108        {
1109            if (strpos($field, 'password_') === 0)
1110            {
1111                $crawler = self::submit($form, array('username' => $username, $field => $username . $username));
1112                self::assertStringContainsString(self::lang('ADMIN_PANEL'), $crawler->filter('h1')->text());
1113
1114                $cookies = self::$cookieJar->all();
1115
1116                // The session id is stored in a cookie that ends with _sid - we assume there is only one such cookie
1117                foreach ($cookies as $cookie)
1118                {
1119                    if (substr($cookie->getName(), -4) == '_sid')
1120                    {
1121                        self::$session_id = $cookie->getValue();
1122                    }
1123                }
1124
1125                break;
1126            }
1127        }
1128    }
1129
1130    protected static function add_lang($lang_file)
1131    {
1132        if (is_array($lang_file))
1133        {
1134            foreach ($lang_file as $file)
1135            {
1136                self::add_lang($file);
1137            }
1138
1139            return;
1140        }
1141
1142        $lang_path = __DIR__ . "/../../phpBB/language/en/$lang_file.php";
1143
1144        $lang = [];
1145
1146        if (file_exists($lang_path))
1147        {
1148            include($lang_path);
1149        }
1150
1151        self::$lang_ary = array_merge(self::$lang_ary, $lang);
1152    }
1153
1154    protected static function add_lang_ext($ext_name, $lang_file)
1155    {
1156        if (is_array($lang_file))
1157        {
1158            foreach ($lang_file as $file)
1159            {
1160                self::add_lang_ext($ext_name, $file);
1161            }
1162
1163            return;
1164        }
1165
1166        $lang_path = __DIR__ . "/../../phpBB/ext/{$ext_name}/language/en/$lang_file.php";
1167
1168        $lang = [];
1169
1170        if (file_exists($lang_path))
1171        {
1172            include($lang_path);
1173        }
1174
1175        self::$lang_ary = array_merge(self::$lang_ary, $lang);
1176    }
1177
1178    protected static function lang()
1179    {
1180        $args = func_get_args();
1181        $key = $args[0];
1182
1183        if (empty(self::$lang_ary[$key]))
1184        {
1185            throw new RuntimeException('Language key "' . $key . '" could not be found.');
1186        }
1187
1188        $args[0] = self::$lang_ary[$key];
1189
1190        return call_user_func_array('sprintf', $args);
1191    }
1192
1193    /**
1194     * assertContains for language strings
1195     *
1196     * @param string $needle    Search string
1197     * @param string $haystack    Search this
1198     * @param string $message    Optional failure message
1199     */
1200    public static function assertContainsLang($needle, $haystack, $message = '')
1201    {
1202        self::assertStringContainsString(html_entity_decode(self::lang($needle), ENT_QUOTES), $haystack, $message);
1203    }
1204
1205    /**
1206    * assertNotContains for language strings
1207    *
1208    * @param string $needle        Search string
1209    * @param string $haystack    Search this
1210    * @param string $message    Optional failure message
1211    */
1212    public static function assertNotContainsLang($needle, $haystack, $message = '')
1213    {
1214        self::assertStringNotContainsString(html_entity_decode(self::lang($needle), ENT_QUOTES), $haystack, $message);
1215    }
1216
1217    /*
1218    * Perform some basic assertions for the page
1219    *
1220    * Checks for debug/error output before the actual page content and the status code
1221    *
1222    * @param mixed $status_code        Expected status code, false to disable check
1223    * @return null
1224    */
1225    public static function assert_response_html($status_code = 200)
1226    {
1227        // Any output before the doc type means there was an error
1228        $content = self::get_content();
1229        self::assertStringNotContainsString('[phpBB Debug]', $content);
1230        self::assertStringStartsWith('<!DOCTYPE', strtoupper(substr(trim($content), 0, 10)), $content);
1231
1232        if ($status_code !== false)
1233        {
1234            self::assert_response_status_code($status_code);
1235        }
1236    }
1237
1238    /*
1239    * Perform some basic assertions for an xml page
1240    *
1241    * Checks for debug/error output before the actual page content and the status code
1242    *
1243    * @param mixed $status_code        Expected status code, false to disable check
1244    * @return null
1245    */
1246    public static function assert_response_xml($status_code = 200)
1247    {
1248        // Any output before the xml opening means there was an error
1249        $content = self::get_content();
1250        self::assertStringNotContainsString('[phpBB Debug]', $content);
1251        self::assertStringStartsWith('<?xml', trim($content), 'Output found before XML specification.');
1252
1253        if ($status_code !== false)
1254        {
1255            self::assert_response_status_code($status_code);
1256        }
1257    }
1258
1259    /**
1260    * Heuristic function to check that the response is success.
1261    *
1262    * When php decides to die with a fatal error, it still sends 200 OK
1263    * status code. This assertion tries to catch that.
1264    *
1265    * @param int $status_code    Expected status code
1266    * @return void
1267    */
1268    public static function assert_response_status_code($status_code = 200)
1269    {
1270        if ($status_code != self::$client->getResponse()->getStatusCode() &&
1271            preg_match('/^5[0-9]{2}/', self::$client->getResponse()->getStatusCode()))
1272        {
1273            self::fail("Encountered unexpected server error:\n" . self::$client->getResponse()->getContent());
1274        }
1275        self::assertEquals($status_code, self::$client->getResponse()->getStatusCode(), 'HTTP status code does not match');
1276    }
1277
1278    public function assert_filter($crawler, $expr, $msg = null)
1279    {
1280        $nodes = $crawler->filter($expr);
1281        if ($msg)
1282        {
1283            $msg .= "\n";
1284        }
1285        else
1286        {
1287            $msg = '';
1288        }
1289        $msg .= "`$expr` not found in DOM.";
1290        $this->assertGreaterThan(0, count($nodes), $msg);
1291        return $nodes;
1292    }
1293
1294    /**
1295    * Asserts that exactly one checkbox with name $name exists within the scope
1296    * of $crawler and that the checkbox is checked.
1297    *
1298    * @param Symfony\Component\DomCrawler\Crawler $crawler
1299    * @param string $name
1300    * @param string $message
1301    *
1302    * @return void
1303    */
1304    public function assert_checkbox_is_checked($crawler, $name, $message = '')
1305    {
1306        $this->assertNotNull(
1307            $this->assert_find_one_checkbox($crawler, $name)->attr('checked'),
1308            $message ?: "Failed asserting that checkbox $name is checked."
1309        );
1310    }
1311
1312    /**
1313    * Asserts that exactly one checkbox with name $name exists within the scope
1314    * of $crawler and that the checkbox is unchecked.
1315    *
1316    * @param Symfony\Component\DomCrawler\Crawler $crawler
1317    * @param string $name
1318    * @param string $message
1319    *
1320    * @return void
1321    */
1322    public function assert_checkbox_is_unchecked($crawler, $name, $message = '')
1323    {
1324        $this->assertNull(
1325            $this->assert_find_one_checkbox($crawler, $name)->attr('checked'),
1326            $message ?: "Failed asserting that checkbox $name is unchecked."
1327        );
1328    }
1329
1330    /**
1331    * Searches for an input element of type checkbox with the name $name using
1332    * $crawler. Contains an assertion that only one such checkbox exists within
1333    * the scope of $crawler.
1334    *
1335    * @param Symfony\Component\DomCrawler\Crawler $crawler
1336    * @param string $name
1337    * @param string $message
1338    *
1339    * @return Symfony\Component\DomCrawler\Crawler
1340    */
1341    public function assert_find_one_checkbox($crawler, $name, $message = '')
1342    {
1343        $query = sprintf('//input[@type="checkbox" and @name="%s"]', $name);
1344        $result = $crawler->filterXPath($query);
1345
1346        $this->assertEquals(
1347            1,
1348            count($result),
1349            $message ?: 'Failed asserting that exactly one checkbox with name' .
1350                " $name exists in crawler scope."
1351        );
1352
1353        return $result;
1354    }
1355
1356    /**
1357    * Creates a topic
1358    *
1359    * Be sure to login before creating
1360    *
1361    * @param int $forum_id
1362    * @param string $subject
1363    * @param string $message
1364    * @param array $additional_form_data Any additional form data to be sent in the request
1365    * @param string $expected Lang var of expected message after posting
1366    * @return array|null post_id, topic_id if message is empty
1367    */
1368    public function create_topic($forum_id, $subject, $message, $additional_form_data = array(), $expected = '')
1369    {
1370        $posting_url = "posting.php?mode=post&f={$forum_id}&sid={$this->sid}";
1371
1372        $form_data = array_merge(array(
1373            'subject'        => $subject,
1374            'message'        => $message,
1375            'post'            => true,
1376        ), $additional_form_data);
1377
1378        return self::submit_post($posting_url, 'POST_TOPIC', $form_data, $expected);
1379    }
1380
1381    /**
1382    * Creates a post
1383    *
1384    * Be sure to login before creating
1385    *
1386    * @param int $forum_id
1387    * @param int $topic_id
1388    * @param string $subject
1389    * @param string $message
1390    * @param array $additional_form_data Any additional form data to be sent in the request
1391    * @param string $expected Lang var of expected message after posting
1392    * @return array|null post_id, topic_id if message is empty
1393    */
1394    public function create_post($forum_id, $topic_id, $subject, $message, $additional_form_data = array(), $expected = '')
1395    {
1396        $posting_url = "posting.php?mode=reply&t={$topic_id}&sid={$this->sid}";
1397
1398        $form_data = array_merge(array(
1399            'subject'        => $subject,
1400            'message'        => $message,
1401            'post'            => true,
1402            'topic_id'        => $topic_id,
1403        ), $additional_form_data);
1404
1405        return self::submit_post($posting_url, 'POST_REPLY', $form_data, $expected);
1406    }
1407
1408    /**
1409    * Helper for submitting posts
1410    *
1411    * @param string $posting_url
1412    * @param string $posting_contains
1413    * @param array $form_data
1414    * @param string $expected Lang var of expected message after posting
1415    * @return array|null post_id, topic_id if message is empty
1416    */
1417    protected function submit_post($posting_url, $posting_contains, $form_data, $expected = '')
1418    {
1419        $this->add_lang('posting');
1420
1421        $crawler = $this->submit_message($posting_url, $posting_contains, $form_data);
1422
1423        if ($expected !== '')
1424        {
1425            if (isset($this->lang[$expected]))
1426            {
1427                $this->assertContainsLang($expected, $crawler->filter('html')->text());
1428            }
1429            else
1430            {
1431                $this->assertStringContainsString($expected, $crawler->filter('html')->text());
1432            }
1433            return null;
1434        }
1435
1436        $post_link = $crawler->filter('.postbody a[title="Post"]')->last()->attr('href');
1437        $topic_link = $crawler->filter('h2[class="topic-title"] > a')->attr('href');
1438
1439        $post_id = $this->get_parameter_from_link($post_link, 'p');
1440        $topic_id = $this->get_parameter_from_link($topic_link, 't');
1441
1442        if (!$topic_id)
1443        {
1444            $topic_id = $form_data['topic_id'];
1445        }
1446
1447        return array(
1448            'topic_id'    => $topic_id,
1449            'post_id'    => $post_id,
1450        );
1451    }
1452
1453    /**
1454    * Creates a private message
1455    *
1456    * Be sure to login before creating
1457    *
1458    * @param string $subject
1459    * @param string $message
1460    * @param array $to
1461    * @param array $additional_form_data Any additional form data to be sent in the request
1462    * @return int private_message_id
1463    */
1464    public function create_private_message($subject, $message, $to, $additional_form_data = array())
1465    {
1466        $this->add_lang(array('ucp', 'posting'));
1467
1468        $posting_url = "ucp.php?i=pm&mode=compose&sid={$this->sid}";
1469
1470        $form_data = array_merge(array(
1471            'subject'        => $subject,
1472            'message'        => $message,
1473            'post'            => true,
1474        ), $additional_form_data);
1475
1476        foreach ($to as $user_id)
1477        {
1478            $form_data['address_list[u][' . $user_id . ']'] = 'to';
1479        }
1480
1481        $crawler = self::submit_message($posting_url, 'POST_NEW_PM', $form_data);
1482
1483        $this->assertStringContainsString($this->lang('MESSAGE_STORED'), $crawler->filter('html')->text());
1484        $url = $crawler->selectLink($this->lang('VIEW_PRIVATE_MESSAGE', '', ''))->link()->getUri();
1485
1486        return $this->get_parameter_from_link($url, 'p');
1487    }
1488
1489    /**
1490    * Helper for submitting a message (post or private message)
1491    *
1492    * @param string $posting_url
1493    * @param string $posting_contains
1494    * @param array $form_data
1495    * @return \Symfony\Component\DomCrawler\Crawler the crawler object
1496    */
1497    protected function submit_message($posting_url, $posting_contains, $form_data)
1498    {
1499        $crawler = self::request('GET', $posting_url);
1500        $this->assertStringContainsString($this->lang($posting_contains), $crawler->filter('html')->text());
1501
1502        if (!empty($form_data['upload_files']))
1503        {
1504            for ($i = 0; $i < $form_data['upload_files']; $i++)
1505            {
1506                $file = array(
1507                    'tmp_name'    => __DIR__ . '/../functional/fixtures/files/valid.jpg',
1508                    'name'        => 'valid.jpg',
1509                    'type'        => 'image/jpeg',
1510                    'size'        => filesize(__DIR__ . '/../functional/fixtures/files/valid.jpg'),
1511                    'error'        => UPLOAD_ERR_OK,
1512                );
1513
1514                $file_form_data = array_merge(['add_file' => $this->lang('ADD_FILE')], $this->get_hidden_fields($crawler, $posting_url));
1515
1516                $crawler = self::$client->request('POST', $posting_url, $file_form_data, array('fileupload' => $file));
1517            }
1518            unset($form_data['upload_files']);
1519        }
1520
1521        $form_data = array_merge($form_data, $this->get_hidden_fields($crawler, $posting_url));
1522
1523        // I use a request because the form submission method does not allow you to send data that is not
1524        // contained in one of the actual form fields that the browser sees (i.e. it ignores "hidden" inputs)
1525        // Instead, I send it as a request with the submit button "post" set to true.
1526        return self::request('POST', $posting_url, $form_data);
1527    }
1528
1529    /**
1530    * Deletes a topic
1531    *
1532    * Be sure to login before creating
1533    *
1534    * @param int $topic_id
1535    * @return null
1536    */
1537    public function delete_topic($topic_id)
1538    {
1539        $this->add_lang('posting');
1540        $crawler = $this->get_quickmod_page($topic_id, 'DELETE_TOPIC');
1541        $this->assertContainsLang('DELETE_PERMANENTLY', $crawler->text());
1542
1543        $this->add_lang('mcp');
1544        $form = $crawler->selectButton('Yes')->form();
1545        $form['delete_permanent'] = 1;
1546        $crawler = self::submit($form);
1547        $this->assertContainsLang('TOPIC_DELETED_SUCCESS', $crawler->text());
1548    }
1549
1550    /**
1551    * Deletes a post
1552    *
1553    * Be sure to login before creating
1554    *
1555    * @param int $forum_id
1556    * @param int $topic_id
1557    * @return null
1558    */
1559    public function delete_post($forum_id, $post_id)
1560    {
1561        $this->add_lang('posting');
1562        $crawler = self::request('GET', "posting.php?mode=delete&p={$post_id}&sid={$this->sid}");
1563        $this->assertContainsLang('DELETE_PERMANENTLY', $crawler->text());
1564
1565        $form = $crawler->selectButton('Yes')->form();
1566        $form['delete_permanent'] = 1;
1567        $crawler = self::submit($form);
1568        $this->assertContainsLang('POST_DELETED', $crawler->text());
1569    }
1570
1571    /**
1572    * Returns the requested parameter from a URL
1573    *
1574    * @param    string    $url
1575    * @param    string    $parameter
1576    * @return        string    Value of the parameter in the URL, null if not set
1577    */
1578    public function get_parameter_from_link($url, $parameter)
1579    {
1580        if (strpos($url, '?') === false)
1581        {
1582            return null;
1583        }
1584
1585        $url_parts = explode('?', $url);
1586        if (isset($url_parts[1]))
1587        {
1588            $url_parameters = $url_parts[1];
1589            if (strpos($url_parameters, '#') !== false)
1590            {
1591                $url_parameters = explode('#', $url_parameters);
1592                $url_parameters = $url_parameters[0];
1593            }
1594
1595            foreach (explode('&', $url_parameters) as $url_param)
1596            {
1597                list($param, $value) = explode('=', $url_param);
1598                if ($param == $parameter)
1599                {
1600                    return $value;
1601                }
1602            }
1603        }
1604        return null;
1605    }
1606
1607    /**
1608    * Return a passwords manager instance
1609    *
1610    * @return phpbb\passwords\manager
1611    */
1612    public function get_passwords_manager()
1613    {
1614        // Prepare dependencies for manager and driver
1615        $config = new \phpbb\config\config(array());
1616        $driver_helper = new \phpbb\passwords\driver\helper($config);
1617
1618        $passwords_drivers = array(
1619            'passwords.driver.bcrypt_2y'    => new \phpbb\passwords\driver\bcrypt_2y($config, $driver_helper),
1620            'passwords.driver.bcrypt'        => new \phpbb\passwords\driver\bcrypt($config, $driver_helper),
1621            'passwords.driver.salted_md5'    => new \phpbb\passwords\driver\salted_md5($config, $driver_helper),
1622            'passwords.driver.phpass'        => new \phpbb\passwords\driver\phpass($config, $driver_helper),
1623        );
1624
1625        $passwords_helper = new \phpbb\passwords\helper;
1626        // Set up passwords manager
1627        $manager = new \phpbb\passwords\manager($config, $passwords_drivers, $passwords_helper, array_keys($passwords_drivers));
1628
1629        return $manager;
1630    }
1631
1632    /**
1633    * Get quickmod page
1634    *
1635    * @param int $topic_id
1636    * @param string $action    Language key for the quickmod action
1637    * @param Symfony\Component\DomCrawler\Crawler Optional crawler object to use instead of creating new one.
1638    * @return Symfony\Component\DomCrawler\Crawler
1639    */
1640    public function get_quickmod_page($topic_id, $action, $crawler = false)
1641    {
1642        $this->add_lang('viewtopic');
1643
1644        if ($crawler === false)
1645        {
1646            $crawler = self::request('GET', "viewtopic.php?t={$topic_id}&sid={$this->sid}");
1647        }
1648        $link = $crawler->filter('#quickmod')->selectLink($this->lang($action))->link()->getUri();
1649
1650        return self::request('GET', substr($link, strpos($link, 'mcp.')) . "&sid={$this->sid}");
1651    }
1652
1653    /**
1654     * Get hidden fields for URL
1655     *
1656     * @param Symfony\Component\DomCrawler\Crawler|null $crawler Crawler instance or null
1657     * @param string $url Request URL
1658     *
1659     * @return array Hidden form fields array
1660     */
1661    protected function get_hidden_fields($crawler, $url)
1662    {
1663        if (!$crawler)
1664        {
1665            $crawler = self::$client->request('GET', $url);
1666        }
1667        $hidden_fields = [
1668            $crawler->filter('[type="hidden"]')->each(function ($node, $i) {
1669                return ['name' => $node->attr('name'), 'value' => $node->attr('value')];
1670            }),
1671        ];
1672
1673        $file_form_data = [];
1674
1675        foreach ($hidden_fields as $fields)
1676        {
1677            foreach($fields as $field)
1678            {
1679                $file_form_data[$field['name']] = $field['value'];
1680            }
1681        }
1682
1683        return $file_form_data;
1684    }
1685
1686    /**
1687     * Get username of currently logged in user
1688     *
1689     * @return string|bool username if logged in, false otherwise
1690     */
1691    protected static function get_logged_in_user()
1692    {
1693        $username_logged_in = false;
1694        $crawler = self::request('GET', 'index.php');
1695        $is_logged_in = strpos($crawler->filter('div[class="navbar"]')->text(), 'Login') === false;
1696        if ($is_logged_in)
1697        {
1698            $username_logged_in = $crawler->filter('li[id="username_logged_in"] > div > a > span:not(.avatar)')->text();
1699        }
1700        return $username_logged_in;
1701    }
1702
1703    /**
1704     * Posting flood control
1705     */
1706    protected function set_flood_interval($flood_interval)
1707    {
1708        $relogin_back = false;
1709        $logged_in_username = $this->get_logged_in_user();
1710        if ($logged_in_username && $logged_in_username !== 'admin')
1711        {
1712            $this->logout();
1713            $relogin_back = true;
1714        }
1715
1716        if (!$logged_in_username || $relogin_back)
1717        {
1718            $this->login();
1719            $this->admin_login();
1720        }
1721
1722        $this->add_lang('acp/common');
1723        $crawler = self::request('GET', 'adm/index.php?i=acp_board&mode=post&sid=' . $this->sid);
1724        $form = $crawler->selectButton('submit')->form([
1725            'config[flood_interval]'    => $flood_interval,
1726        ]);
1727        $crawler = self::submit($form);
1728        $this->assertContainsLang('CONFIG_UPDATED', $crawler->text());
1729
1730        // Get logged out back or get logged in in user back if needed
1731        if (!$logged_in_username)
1732        {
1733            $this->logout();
1734        }
1735
1736        if ($relogin_back)
1737        {
1738            $this->logout();
1739            $this->login($logged_in_username);
1740        }
1741    }
1742
1743    /**
1744    * Check if a user exists by username or user_id
1745    *
1746    * @param string $username The username to check or empty if user_id is used
1747    * @param int $user_id The user id to check or empty if username is used
1748    *
1749    * @return array Returns user_id => username array or empty array if user does not exist
1750    */
1751    protected function user_exists($username = '', $user_id = '')
1752    {
1753        global $db;
1754
1755        $db = $this->get_db();
1756
1757        if (!function_exists('utf_clean_string'))
1758        {
1759            require_once(__DIR__ . '/../../phpBB/includes/utf/utf_tools.php');
1760        }
1761        if (!function_exists('user_get_id_name'))
1762        {
1763            require_once(__DIR__ . '/../../phpBB/includes/functions_user.php');
1764        }
1765
1766        user_get_id_name($user_id, $username, false, true);
1767
1768        return $username;
1769    }
1770}