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