bootstrap(); self::$cookieJar = new CookieJar; self::$client = new Goutte\Client(array(), null, self::$cookieJar); // Reset the curl handle because it is 0 at this point and not a valid // resource self::$client->getClient()->getCurlMulti()->reset(true); // Clear the language array so that things // that were added in other tests are gone $this->lang = array(); $this->add_lang('common'); $this->purge_cache(); } /** * Perform a request to page * * @param string $method HTTP Method * @param string $path Page path, relative from phpBB root path * @param array $form_data An array of form field values * @param bool $assert_response_html Should we perform standard assertions for a normal html page * @return Symfony\Component\DomCrawler\Crawler */ static public function request($method, $path, $form_data = array(), $assert_response_html = true) { $crawler = self::$client->request($method, self::$root_url . $path, $form_data); if ($assert_response_html) { self::assert_response_html(); } return $crawler; } /** * Submits a form * * @param Symfony\Component\DomCrawler\Form $form A Form instance * @param array $values An array of form field values * @param bool $assert_response_html Should we perform standard assertions for a normal html page * @return Symfony\Component\DomCrawler\Crawler */ static public function submit(Symfony\Component\DomCrawler\Form $form, array $values = array(), $assert_response_html = true) { $crawler = self::$client->submit($form, $values); if ($assert_response_html) { self::assert_response_html(); } return $crawler; } /** * Get Client Content * * @return string HTML page */ static public function get_content() { return self::$client->getResponse()->getContent(); } // bootstrap, called after board is set up // once per test case class // test cases can override this protected function bootstrap() { } public function __construct($name = NULL, array $data = array(), $dataName = '') { parent::__construct($name, $data, $dataName); $this->backupStaticAttributesBlacklist += array( 'phpbb_functional_test_case' => array('config', 'already_installed'), ); } protected function get_db() { global $phpbb_root_path, $phpEx; // so we don't reopen an open connection if (!($this->db instanceof \phpbb\db\driver\driver)) { $dbms = self::$config['dbms']; $this->db = new $dbms(); $this->db->sql_connect(self::$config['dbhost'], self::$config['dbuser'], self::$config['dbpasswd'], self::$config['dbname'], self::$config['dbport']); } return $this->db; } protected function get_cache_driver() { if (!$this->cache) { $this->cache = new \phpbb\cache\driver\file; } return $this->cache; } protected function purge_cache() { $cache = $this->get_cache_driver(); $cache->purge(); $cache->unload(); $cache->load(); } protected function get_extension_manager() { global $phpbb_root_path, $phpEx; $config = new \phpbb\config\config(array()); $db = $this->get_db(); $db_tools = new \phpbb\db\tools($db); $migrator = new \phpbb\db\migrator( $config, $db, $db_tools, self::$config['table_prefix'] . 'migrations', $phpbb_root_path, $phpEx, self::$config['table_prefix'], array(), new \phpbb\db\migration\helper() ); $container = new phpbb_mock_container_builder(); $container->set('migrator', $migrator); $extension_manager = new \phpbb\extension\manager( $container, $db, $config, new phpbb\filesystem(), self::$config['table_prefix'] . 'ext', dirname(__FILE__) . '/', $phpEx, $this->get_cache_driver() ); return $extension_manager; } static protected function install_board() { global $phpbb_root_path, $phpEx; self::recreate_database(self::$config); $config_file = $phpbb_root_path . "config.$phpEx"; $config_file_dev = $phpbb_root_path . "config_dev.$phpEx"; $config_file_test = $phpbb_root_path . "config_test.$phpEx"; if (file_exists($config_file)) { if (!file_exists($config_file_dev)) { rename($config_file, $config_file_dev); } else { unlink($config_file); } } self::$cookieJar = new CookieJar; self::$client = new Goutte\Client(array(), null, self::$cookieJar); // Set client manually so we can increase the cURL timeout self::$client->setClient(new Guzzle\Http\Client('', array( Guzzle\Http\Client::DISABLE_REDIRECTS => true, 'curl.options' => array( CURLOPT_TIMEOUT => 120, ), ))); // Reset the curl handle because it is 0 at this point and not a valid // resource self::$client->getClient()->getCurlMulti()->reset(true); $parseURL = parse_url(self::$config['phpbb_functional_url']); $crawler = self::request('GET', 'install/index.php?mode=install'); self::assertContains('Welcome to Installation', $crawler->filter('#main')->text()); $form = $crawler->selectButton('submit')->form(); // install/index.php?mode=install&sub=requirements $crawler = self::submit($form); self::assertContains('Installation compatibility', $crawler->filter('#main')->text()); $form = $crawler->selectButton('submit')->form(); // install/index.php?mode=install&sub=database $crawler = self::submit($form); self::assertContains('Database configuration', $crawler->filter('#main')->text()); $form = $crawler->selectButton('submit')->form(array( // Installer uses 3.0-style dbms name 'dbms' => str_replace('phpbb\db\driver\\', '', self::$config['dbms']), 'dbhost' => self::$config['dbhost'], 'dbport' => self::$config['dbport'], 'dbname' => self::$config['dbname'], 'dbuser' => self::$config['dbuser'], 'dbpasswd' => self::$config['dbpasswd'], 'table_prefix' => self::$config['table_prefix'], )); // install/index.php?mode=install&sub=database $crawler = self::submit($form); self::assertContains('Successful connection', $crawler->filter('#main')->text()); $form = $crawler->selectButton('submit')->form(); // install/index.php?mode=install&sub=administrator $crawler = self::submit($form); self::assertContains('Administrator configuration', $crawler->filter('#main')->text()); $form = $crawler->selectButton('submit')->form(array( 'default_lang' => 'en', 'admin_name' => 'admin', 'admin_pass1' => 'adminadmin', 'admin_pass2' => 'adminadmin', 'board_email' => 'nobody@example.com', )); // install/index.php?mode=install&sub=administrator $crawler = self::submit($form); self::assertContains('Tests passed', $crawler->filter('#main')->text()); $form = $crawler->selectButton('submit')->form(); // We have to skip install/index.php?mode=install&sub=config_file // because that step will create a config.php file if phpBB has the // permission to do so. We have to create the config file on our own // in order to get the DEBUG constants defined. $config_php_data = phpbb_create_config_file_data(self::$config, self::$config['dbms'], true, true); $config_created = file_put_contents($config_file, $config_php_data) !== false; if (!$config_created) { self::markTestSkipped("Could not write $config_file file."); } // We also have to create a install lock that is normally created by // the installer. The file will be removed by the final step of the // installer. $install_lock_file = $phpbb_root_path . 'cache/install_lock'; $lock_created = file_put_contents($install_lock_file, '') !== false; if (!$lock_created) { self::markTestSkipped("Could not create $lock_created file."); } @chmod($install_lock_file, 0666); // install/index.php?mode=install&sub=advanced $form_data = $form->getValues(); unset($form_data['submit']); $crawler = self::request('POST', 'install/index.php?mode=install&sub=advanced', $form_data); self::assertContains('The settings on this page are only necessary to set if you know that you require something different from the default.', $crawler->filter('#main')->text()); $form = $crawler->selectButton('submit')->form(array( 'email_enable' => true, 'smtp_delivery' => true, 'smtp_host' => 'nxdomain.phpbb.com', 'smtp_auth' => 'PLAIN', 'smtp_user' => 'nxuser', 'smtp_pass' => 'nxpass', 'cookie_secure' => false, 'force_server_vars' => false, 'server_protocol' => $parseURL['scheme'] . '://', 'server_name' => 'localhost', 'server_port' => isset($parseURL['port']) ? (int) $parseURL['port'] : 80, 'script_path' => $parseURL['path'], )); // install/index.php?mode=install&sub=create_table $crawler = self::submit($form); self::assertContains('The database tables used by phpBB', $crawler->filter('#main')->text()); self::assertContains('have been created and populated with some initial data.', $crawler->filter('#main')->text()); $form = $crawler->selectButton('submit')->form(); // install/index.php?mode=install&sub=final $crawler = self::submit($form); self::assertContains('You have successfully installed', $crawler->text()); copy($config_file, $config_file_test); } static private function recreate_database($config) { $db_conn_mgr = new phpbb_database_test_connection_manager($config); $db_conn_mgr->recreate_db(); } /** * Creates a new style * * @param string $style_id Style ID * @param string $style_path Style directory * @param string $parent_style_id Parent style id. Default = 1 * @param string $parent_style_path Parent style directory. Default = 'prosilver' */ protected function add_style($style_id, $style_path, $parent_style_id = 1, $parent_style_path = 'prosilver') { global $phpbb_root_path; $db = $this->get_db(); if (version_compare(PHPBB_VERSION, '3.1.0-dev', '<')) { $sql = 'INSERT INTO ' . STYLES_TABLE . ' ' . $db->sql_build_array('INSERT', array( 'style_id' => $style_id, 'style_name' => $style_path, 'style_copyright' => '', 'style_active' => 1, 'template_id' => $style_id, 'theme_id' => $style_id, 'imageset_id' => $style_id, )); $db->sql_query($sql); $sql = 'INSERT INTO ' . STYLES_IMAGESET_TABLE . ' ' . $db->sql_build_array('INSERT', array( 'imageset_id' => $style_id, 'imageset_name' => $style_path, 'imageset_copyright' => '', 'imageset_path' => $style_path, )); $db->sql_query($sql); $sql = 'INSERT INTO ' . STYLES_TEMPLATE_TABLE . ' ' . $db->sql_build_array('INSERT', array( 'template_id' => $style_id, 'template_name' => $style_path, 'template_copyright' => '', 'template_path' => $style_path, 'bbcode_bitfield' => 'kNg=', 'template_inherits_id' => $parent_style_id, 'template_inherit_path' => $parent_style_path, )); $db->sql_query($sql); $sql = 'INSERT INTO ' . STYLES_THEME_TABLE . ' ' . $db->sql_build_array('INSERT', array( 'theme_id' => $style_id, 'theme_name' => $style_path, 'theme_copyright' => '', 'theme_path' => $style_path, 'theme_storedb' => 0, 'theme_mtime' => 0, 'theme_data' => '', )); $db->sql_query($sql); if ($style_path != 'prosilver' && $style_path != 'subsilver2') { @mkdir($phpbb_root_path . 'styles/' . $style_path, 0777); @mkdir($phpbb_root_path . 'styles/' . $style_path . '/template', 0777); } } else { $db->sql_multi_insert(STYLES_TABLE, array( 'style_id' => $style_id, 'style_name' => $style_path, 'style_copyright' => '', 'style_active' => 1, 'style_path' => $style_path, 'bbcode_bitfield' => 'kNg=', 'style_parent_id' => $parent_style_id, 'style_parent_tree' => $parent_style_path, )); } } /** * Remove temporary style created by add_style() * * @param string $style_id Style ID * @param string $style_path Style directory */ protected function delete_style($style_id, $style_path) { global $phpbb_root_path; $db = $this->get_db(); $db->sql_query('DELETE FROM ' . STYLES_TABLE . ' WHERE style_id = ' . $style_id); if (version_compare(PHPBB_VERSION, '3.1.0-dev', '<')) { $db->sql_query('DELETE FROM ' . STYLES_IMAGESET_TABLE . ' WHERE imageset_id = ' . $style_id); $db->sql_query('DELETE FROM ' . STYLES_TEMPLATE_TABLE . ' WHERE template_id = ' . $style_id); $db->sql_query('DELETE FROM ' . STYLES_THEME_TABLE . ' WHERE theme_id = ' . $style_id); if ($style_path != 'prosilver' && $style_path != 'subsilver2') { @rmdir($phpbb_root_path . 'styles/' . $style_path . '/template'); @rmdir($phpbb_root_path . 'styles/' . $style_path); } } } /** * Creates a new user with limited permissions * * @param string $username Also doubles up as the user's password * @return int ID of created user */ protected function create_user($username) { // Required by unique_id global $config; $config = new \phpbb\config\config(array()); /* * Add required config entries to the config array to prevent * set_config() sending an INSERT query for already existing entries, * resulting in a SQL error. * This is because set_config() first sends an UPDATE query, then checks * sql_affectedrows() which can be 0 (e.g. on MySQL) when the new * data is already there. */ $config['newest_user_colour'] = ''; $config['rand_seed'] = ''; $config['rand_seed_last_update'] = time() + 600; // Required by user_add global $db, $cache, $phpbb_dispatcher, $phpbb_container; $db = $this->get_db(); if (!function_exists('phpbb_mock_null_cache')) { require_once(__DIR__ . '/../mock/null_cache.php'); } $cache = new phpbb_mock_null_cache; $cache_driver = new \phpbb\cache\driver\null(); $phpbb_container = $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface'); $phpbb_container ->expects($this->any()) ->method('get') ->with('cache.driver') ->will($this->returnValue($cache_driver)); if (!function_exists('utf_clean_string')) { require_once(__DIR__ . '/../../phpBB/includes/utf/utf_tools.php'); } if (!function_exists('user_add')) { require_once(__DIR__ . '/../../phpBB/includes/functions_user.php'); } set_config(null, null, null, $config); set_config_count(null, null, null, $config); $phpbb_dispatcher = new phpbb_mock_event_dispatcher(); $passwords_manager = $this->get_passwords_manager(); $user_row = array( 'username' => $username, 'group_id' => 2, 'user_email' => 'nobody@example.com', 'user_type' => 0, 'user_lang' => 'en', 'user_timezone' => 0, 'user_dateformat' => '', 'user_password' => $passwords_manager->hash($username . $username), ); return user_add($user_row); } protected function remove_user_group($group_name, $usernames) { global $db, $cache, $auth, $config, $phpbb_dispatcher, $phpbb_log, $phpbb_container, $phpbb_root_path, $phpEx; $config = new \phpbb\config\config(array()); $config['coppa_enable'] = 0; $db = $this->get_db(); $phpbb_dispatcher = new phpbb_mock_event_dispatcher(); $user = $this->getMock('\phpbb\user'); $auth = $this->getMock('\phpbb\auth\auth'); $phpbb_log = new \phpbb\log\log($db, $user, $auth, $phpbb_dispatcher, $phpbb_root_path, 'adm/', $phpEx, LOG_TABLE); $cache = new phpbb_mock_null_cache; $cache_driver = new \phpbb\cache\driver\null(); $phpbb_container = new phpbb_mock_container_builder(); $phpbb_container->set('cache.driver', $cache_driver); $phpbb_container->set('notification_manager', new phpbb_mock_notification_manager()); if (!function_exists('utf_clean_string')) { require_once(__DIR__ . '/../../phpBB/includes/utf/utf_tools.php'); } if (!function_exists('group_user_del')) { require_once(__DIR__ . '/../../phpBB/includes/functions_user.php'); } $sql = 'SELECT group_id FROM ' . GROUPS_TABLE . " WHERE group_name = '" . $db->sql_escape($group_name) . "'"; $result = $db->sql_query($sql); $group_id = (int) $db->sql_fetchfield('group_id'); $db->sql_freeresult($result); return group_user_del($group_id, false, $usernames, $group_name); } protected function add_user_group($group_name, $usernames, $default = false, $leader = false) { global $db, $cache, $auth, $config, $phpbb_dispatcher, $phpbb_log, $phpbb_container, $phpbb_root_path, $phpEx; $config = new \phpbb\config\config(array()); $config['coppa_enable'] = 0; $db = $this->get_db(); $phpbb_dispatcher = new phpbb_mock_event_dispatcher(); $user = $this->getMock('\phpbb\user'); $auth = $this->getMock('\phpbb\auth\auth'); $phpbb_log = new \phpbb\log\log($db, $user, $auth, $phpbb_dispatcher, $phpbb_root_path, 'adm/', $phpEx, LOG_TABLE); $cache = new phpbb_mock_null_cache; $cache_driver = new \phpbb\cache\driver\null(); $phpbb_container = $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface'); $phpbb_container ->expects($this->any()) ->method('get') ->with('cache.driver') ->will($this->returnValue($cache_driver)); if (!function_exists('utf_clean_string')) { require_once(__DIR__ . '/../../phpBB/includes/utf/utf_tools.php'); } if (!function_exists('group_user_del')) { require_once(__DIR__ . '/../../phpBB/includes/functions_user.php'); } $sql = 'SELECT group_id FROM ' . GROUPS_TABLE . " WHERE group_name = '" . $db->sql_escape($group_name) . "'"; $result = $db->sql_query($sql); $group_id = (int) $db->sql_fetchfield('group_id'); $db->sql_freeresult($result); return group_user_add($group_id, false, $usernames, $group_name, $default, $leader); } protected function login($username = 'admin') { $this->add_lang('ucp'); $crawler = self::request('GET', 'ucp.php'); $this->assertContains($this->lang('LOGIN_EXPLAIN_UCP'), $crawler->filter('html')->text()); $form = $crawler->selectButton($this->lang('LOGIN'))->form(); $crawler = self::submit($form, array('username' => $username, 'password' => $username . $username)); $this->assertNotContains($this->lang('LOGIN'), $crawler->filter('.navbar')->text()); $cookies = self::$cookieJar->all(); // The session id is stored in a cookie that ends with _sid - we assume there is only one such cookie foreach ($cookies as $cookie); { if (substr($cookie->getName(), -4) == '_sid') { $this->sid = $cookie->getValue(); } } } protected function logout() { $this->add_lang('ucp'); $crawler = self::request('GET', 'ucp.php?sid=' . $this->sid . '&mode=logout'); $this->assertContains($this->lang('REGISTER'), $crawler->filter('.navbar')->text()); unset($this->sid); } /** * Login to the ACP * You must run login() before calling this. */ protected function admin_login($username = 'admin') { $this->add_lang('acp/common'); // Requires login first! if (empty($this->sid)) { $this->fail('$this->sid is empty. Make sure you call login() before admin_login()'); return; } $crawler = self::request('GET', 'adm/index.php?sid=' . $this->sid); $this->assertContains($this->lang('LOGIN_ADMIN_CONFIRM'), $crawler->filter('html')->text()); $form = $crawler->selectButton($this->lang('LOGIN'))->form(); foreach ($form->getValues() as $field => $value) { if (strpos($field, 'password_') === 0) { $crawler = self::submit($form, array('username' => $username, $field => $username . $username)); $this->assertContains($this->lang('ADMIN_PANEL'), $crawler->filter('h1')->text()); $cookies = self::$cookieJar->all(); // The session id is stored in a cookie that ends with _sid - we assume there is only one such cookie foreach ($cookies as $cookie); { if (substr($cookie->getName(), -4) == '_sid') { $this->sid = $cookie->getValue(); } } break; } } } protected function add_lang($lang_file) { if (is_array($lang_file)) { foreach ($lang_file as $file) { $this->add_lang($file); } } $lang_path = __DIR__ . "/../../phpBB/language/en/$lang_file.php"; $lang = array(); if (file_exists($lang_path)) { include($lang_path); } $this->lang = array_merge($this->lang, $lang); } protected function lang() { $args = func_get_args(); $key = $args[0]; if (empty($this->lang[$key])) { throw new RuntimeException('Language key "' . $key . '" could not be found.'); } $args[0] = $this->lang[$key]; return call_user_func_array('sprintf', $args); } /** * assertContains for language strings * * @param string $needle Search string * @param string $haystack Search this * @param string $message Optional failure message */ public function assertContainsLang($needle, $haystack, $message = null) { $this->assertContains(html_entity_decode($this->lang($needle), ENT_QUOTES), $haystack, $message); } /** * assertNotContains for language strings * * @param string $needle Search string * @param string $haystack Search this * @param string $message Optional failure message */ public function assertNotContainsLang($needle, $haystack, $message = null) { $this->assertNotContains(html_entity_decode($this->lang($needle), ENT_QUOTES), $haystack, $message); } /* * Perform some basic assertions for the page * * Checks for debug/error output before the actual page content and the status code * * @param mixed $status_code Expected status code, false to disable check * @return null */ static public function assert_response_html($status_code = 200) { if ($status_code !== false) { self::assert_response_status_code($status_code); } // Any output before the doc type means there was an error $content = self::$client->getResponse()->getContent(); self::assertNotContains('[phpBB Debug]', $content); self::assertStringStartsWith('getResponse()->getContent(); self::assertNotContains('[phpBB Debug]', $content); self::assertStringStartsWith('getResponse()->getStatus()); } public function assert_filter($crawler, $expr, $msg = null) { $nodes = $crawler->filter($expr); if ($msg) { $msg .= "\n"; } else { $msg = ''; } $msg .= "`$expr` not found in DOM."; $this->assertGreaterThan(0, count($nodes), $msg); return $nodes; } /** * Asserts that exactly one checkbox with name $name exists within the scope * of $crawler and that the checkbox is checked. * * @param Symfony\Component\DomCrawler\Crawler $crawler * @param string $name * @param string $message * * @return null */ public function assert_checkbox_is_checked($crawler, $name, $message = '') { $this->assertSame( 'checked', $this->assert_find_one_checkbox($crawler, $name)->attr('checked'), $message ?: "Failed asserting that checkbox $name is checked." ); } /** * Asserts that exactly one checkbox with name $name exists within the scope * of $crawler and that the checkbox is unchecked. * * @param Symfony\Component\DomCrawler\Crawler $crawler * @param string $name * @param string $message * * @return null */ public function assert_checkbox_is_unchecked($crawler, $name, $message = '') { $this->assertSame( '', $this->assert_find_one_checkbox($crawler, $name)->attr('checked'), $message ?: "Failed asserting that checkbox $name is unchecked." ); } /** * Searches for an input element of type checkbox with the name $name using * $crawler. Contains an assertion that only one such checkbox exists within * the scope of $crawler. * * @param Symfony\Component\DomCrawler\Crawler $crawler * @param string $name * @param string $message * * @return Symfony\Component\DomCrawler\Crawler */ public function assert_find_one_checkbox($crawler, $name, $message = '') { $query = sprintf('//input[@type="checkbox" and @name="%s"]', $name); $result = $crawler->filterXPath($query); $this->assertEquals( 1, sizeof($result), $message ?: 'Failed asserting that exactly one checkbox with name' . " $name exists in crawler scope." ); return $result; } /** * Creates a topic * * Be sure to login before creating * * @param int $forum_id * @param string $subject * @param string $message * @param array $additional_form_data Any additional form data to be sent in the request * @param string $expected Lang var of expected message after posting * @return array|null post_id, topic_id if message is empty */ public function create_topic($forum_id, $subject, $message, $additional_form_data = array(), $expected = '') { $posting_url = "posting.php?mode=post&f={$forum_id}&sid={$this->sid}"; $form_data = array_merge(array( 'subject' => $subject, 'message' => $message, 'post' => true, ), $additional_form_data); return self::submit_post($posting_url, 'POST_TOPIC', $form_data, $expected); } /** * Creates a post * * Be sure to login before creating * * @param int $forum_id * @param int $topic_id * @param string $subject * @param string $message * @param array $additional_form_data Any additional form data to be sent in the request * @param string $expected Lang var of expected message after posting * @return array|null post_id, topic_id if message is empty */ public function create_post($forum_id, $topic_id, $subject, $message, $additional_form_data = array(), $expected = '') { $posting_url = "posting.php?mode=reply&f={$forum_id}&t={$topic_id}&sid={$this->sid}"; $form_data = array_merge(array( 'subject' => $subject, 'message' => $message, 'post' => true, ), $additional_form_data); return self::submit_post($posting_url, 'POST_REPLY', $form_data, $expected); } /** * Helper for submitting posts * * @param string $posting_url * @param string $posting_contains * @param array $form_data * @param string $expected Lang var of expected message after posting * @return array|null post_id, topic_id if message is empty */ protected function submit_post($posting_url, $posting_contains, $form_data, $expected = '') { $this->add_lang('posting'); $crawler = self::request('GET', $posting_url); $this->assertContains($this->lang($posting_contains), $crawler->filter('html')->text()); if (!empty($form_data['upload_files'])) { for ($i = 0; $i < $form_data['upload_files']; $i++) { $file = array( 'tmp_name' => __DIR__ . '/../functional/fixtures/files/valid.jpg', 'name' => 'valid.jpg', 'type' => 'image/jpeg', 'size' => filesize(__DIR__ . '/../functional/fixtures/files/valid.jpg'), 'error' => UPLOAD_ERR_OK, ); $crawler = self::$client->request('POST', $posting_url, array('add_file' => $this->lang('ADD_FILE')), array('fileupload' => $file)); } unset($form_data['upload_files']); } $hidden_fields = array( $crawler->filter('[type="hidden"]')->each(function ($node, $i) { return array('name' => $node->attr('name'), 'value' => $node->attr('value')); }), ); foreach ($hidden_fields as $fields) { foreach($fields as $field) { $form_data[$field['name']] = $field['value']; } } // Bypass time restriction that said that if the lastclick time (i.e. time when the form was opened) // is not at least 2 seconds before submission, cancel the form $form_data['lastclick'] = 0; // I use a request because the form submission method does not allow you to send data that is not // contained in one of the actual form fields that the browser sees (i.e. it ignores "hidden" inputs) // Instead, I send it as a request with the submit button "post" set to true. $crawler = self::request('POST', $posting_url, $form_data); if ($expected !== '') { $this->assertContainsLang($expected, $crawler->filter('html')->text()); return null; } $url = $crawler->selectLink($form_data['subject'])->link()->getUri(); return array( 'topic_id' => $this->get_parameter_from_link($url, 't'), 'post_id' => $this->get_parameter_from_link($url, 'p'), ); } /** * Deletes a topic * * Be sure to login before creating * * @param int $topic_id * @return null */ public function delete_topic($topic_id) { $crawler = self::request('GET', "viewtopic.php?t={$topic_id}&sid={$this->sid}"); $this->add_lang('posting'); $form = $crawler->selectButton('Go')->eq(1)->form(); $form['action']->select('delete_topic'); $crawler = self::submit($form); $this->assertContainsLang('DELETE_PERMANENTLY', $crawler->text()); $this->add_lang('mcp'); $form = $crawler->selectButton('Yes')->form(); $form['delete_permanent'] = 1; $crawler = self::submit($form); $this->assertContainsLang('TOPIC_DELETED_SUCCESS', $crawler->text()); } /** * Deletes a post * * Be sure to login before creating * * @param int $forum_id * @param int $topic_id * @return null */ public function delete_post($forum_id, $post_id) { $this->add_lang('posting'); $crawler = self::request('GET', "posting.php?mode=delete&f={$forum_id}&p={$post_id}&sid={$this->sid}"); $this->assertContainsLang('DELETE_PERMANENTLY', $crawler->text()); $form = $crawler->selectButton('Yes')->form(); $form['delete_permanent'] = 1; $crawler = self::submit($form); $this->assertContainsLang('POST_DELETED', $crawler->text()); } /** * Returns the requested parameter from a URL * * @param string $url * @param string $parameter * @return string Value of the parameter in the URL, null if not set */ public function get_parameter_from_link($url, $parameter) { if (strpos($url, '?') === false) { return null; } $url_parts = explode('?', $url); if (isset($url_parts[1])) { $url_parameters = $url_parts[1]; if (strpos($url_parameters, '#') !== false) { $url_parameters = explode('#', $url_parameters); $url_parameters = $url_parameters[0]; } foreach (explode('&', $url_parameters) as $url_param) { list($param, $value) = explode('=', $url_param); if ($param == $parameter) { return $value; } } } return null; } /** * Return a passwords manager instance * * @return phpbb\passwords\manager */ public function get_passwords_manager() { // Prepare dependencies for manager and driver $config = new \phpbb\config\config(array()); $driver_helper = new \phpbb\passwords\driver\helper($config); $passwords_drivers = array( 'passwords.driver.bcrypt_2y' => new \phpbb\passwords\driver\bcrypt_2y($config, $driver_helper), 'passwords.driver.bcrypt' => new \phpbb\passwords\driver\bcrypt($config, $driver_helper), 'passwords.driver.salted_md5' => new \phpbb\passwords\driver\salted_md5($config, $driver_helper), 'passwords.driver.phpass' => new \phpbb\passwords\driver\phpass($config, $driver_helper), ); $passwords_helper = new \phpbb\passwords\helper; // Set up passwords manager $manager = new \phpbb\passwords\manager($config, $passwords_drivers, $passwords_helper, array_keys($passwords_drivers)); return $manager; } } Tպ b%p"U?Dk句|6/ڬ>k#MjCFc9er On@zrQj.:TW n] A'x\_](Y1ݮMb{B~.j˿nTVUB7Z08J8 |Ȗ?:o\3{ ~(}ƣ8a!9Vo ?iLqYdY&YUr2D?G'ɀ;`  ށ }dk8CƐQ3E\OO`Nr}PpbU/{QNyZKXY GI5޵CmǠ/\NYsDz*sC&aO6 DA{PEUA;oͻ 95uHBF_=+U;j3ho(VB=DR.^u0U )Ձ9Lx;kLHUrC9bAb0I1W~WyY] g3M&r&M#C+FTM8(^37Q9!1Zsfsbxr_F_xMH U7'*UPe֛:F99R b)r]l\RM]&JN&)R2*?%dکPN^|1lf,Dǽ-vs#nVkeЬ JP6t lOۂ! Ƶ~R 8g'ɘ[rb1_a3CCVTX;SQ)xe=?Q[EI*Xss_$1WYl}lT {w"RF|8kpx.tI֏9mC )G #d٩%?PP{'HEбZ9rB[dOa}*xhPÞBTvٖ6z#6 49[ރv9lH9#g㯯BId@QSIR&Z3dM~7 yT9v>Hv'Q9/J?5S⣚CT)vNT 5 ]1~D]zR5t͏+fK HXz᲻5nMV ~miC S&p]7vqQ~w /:`-{y A,q 1tJpAQ1#,L臂ZZMlNӻmS;a!Lx eхfY_sY-1ۑC0g_)W(SϡeԤ֧1`.K>V_&̳u{ۋS!L:'QV28h3HubCۥ^RҾ![r ])`C,XKU>T(Qvu2 fba;NNltq|VvG bZ^g[ ~;_8{1En^UCvO;F Y/9 3Xʛhk\'y:u+,NAI^5dq4MkrrھlRg/?}OwqÍ.GDB-qgsbIY; @hWԵ] /md=z ͆RA{%3WD>q #qg!^hLli"eP-Mƃ֡qL|y1,wv*b0jS!o";(UȂOA{a7ہ~gbRF00UqRʲ8emDMW=ceq<(V]IzqU9aH,Aexw6h, 976@k/B63np 4C!*bM-/ȎCfIbZ,7qJCoJUj3A2bes?R}0S/pe]%b}\d0Sf(&{Am>ns(C5 RT?b`ވ}7֖~YclFwv͕yj&čIsIOkh ><ƚURrpW0Ŧ9'P%i BO/?m\7#s๒J޾L :+kd[g/y$Ӱ2r ={7u4@׫59g$|$;69T͉|WcjE-:~u( P9ZUjW@FQ46úH:NQbuٗu!.--1 "l[e^nJ Xuz:{.+Q mǝMv%ynJBè!Rƿ˘o뿫dݏbVS ݭuT(#>(AkCA9|30 @PoS3H|p"w0Vm|uli6dakiZOP|‘{4jW18״⛴YB#Je͒Pe j _ P#9!=\,4^s &Kăs0uS\\E~E{b%_#T@ۆEp/(wsTbvKM02%V6|b?ja̞pi )r+-sZw} `-R6 \%N22-Z_ǡ?e%)8Sj0'&M(U]OΛ Gȑ?1|7旑\&!JLAΖyr?F75#(,T,Z,Q7$YBtAV"VN ~! $n82z\8Gx{_78Y2O{ɴg{"^B;,<p9|1>d; ~z#"%[N>̌z7xFXa!itTxF)DYv (AoZ]O֔ %[Je~nb4TȐWG3gcr&q\xaz}*&$d}NF ʻ x։LE߇_pGGZ#ckYn7ge%%<2HVKjqt)ݖg5eqA Uߧ? rV-Vq.ҒAx%zi+ķ I$K^k ]rOfBJ2ܱ^z0NY}oB/u'{y*9Mq(XPJj8 hyb .FR6T?Ǿujcp7^F^Lf)h~_ ,׋u&ff}A% W眒 jhS18tAI(oq? k5 |f<fB3ym7! *m"UNry F `>UJVA~qjG_s {D5 #(cA :1ջgJ-.rF\,Jܴ`p6;UP?X|ʂ?O%.g@@pX ljf/?5)=eh*T?%BGl/CeVP`|" 0PD=R((X5Yxa|GZIE5e4zyn>bBk`e=T:ԑ"޺=Z홈|@j>PYrY05|쮴g=t{az l /g<{ QMPhm~/$<> h>Vz k];Yr,!eИH%g~2Y72ӉVp@w?' D* M+? GilE6+q"F8@-s2==)̶+ zCk\o7xg W|c5z?,K.&}hm#{v a^Dއ][N ҼW}9˚6%8jU% Քk/f oD"<}|]t~0~QGd_ڬf&If>R@gIrQ\Kd"eu?jkxݮeб5eFz=*+q,,y`.)x {Elߴ̺MgS>I$AG~;3SgVfü{$N*t4rwH6 ˚lTWz`޵,\ϾTɡD)eN;*9۹";V qًUJfbV%<3/3CIMcf)ߤob6yl,j?K-QA)90]޾2FzN6xjmuq ݙ!t:\_YM((⃣aC[d#5*(`"X q`$wPsl Iv=S̻J"b^һo:D4sx!<ўxdۛjl =^Kc%U ,;͌Lّ&s{)āw"R*..z\A:5FT5S?埜zc-9)ogRgě1D:1zc )MHn9\<+kG|Fޣ$˽O*2jM E-PjmZ1x@"c\3-;c/{E[2G1aW#SEOTEnmt*GIGmTY tbxK6 !' I :D Ϫ/9uO==%y;0Lhl8|*ʵu)DWOSil.2$=IKrN+ To 9{Zrk+^ {$aE?!H,  .(7a'wW߁v 'Fn֑ Wl } s;jb!~ahIU,at"6d jrP@%򷶫ΗK!K^fӌ,7VF+딛~J=.J҉&V_N[z@B֤f4YjlLLXYʱ*mY4 d˟u=c4uG}3ǟ˚{;S&:ܐjN8^LGno~"-B06k,yA5Ɂ)ٛR*=Mեewa-s-FsK׷1N9v[ ֬rgC.C@?dpaQITaXvcvKHm,蝉(ʎN$h)+kSwHJ3SNLScKgOzH99eK1H&xG%ai? ؜'r#qՂ2)V F#I"TI,`(I,Kl* C [f%^aι̣Ljł R1_{Y ٯVxWFTQ޽ʘ묤0fY1>H.O~3:É^ T 54ɸ@_ІQ5~l{iՉla$8W@UY֍yŻ*jZ6t%9HKҩ - mhJmںڤb@@%Au\%>#ۊ6!BQ&2p5IAEW8#.\ģGK. /5ɳ#KDbK>As`m0pbU&bZ4(4~8Ek7}*(3a9,AHQ# J!eR B 9.Txc)ixԿ\ڦ8,E"ow>Z1y!v*VW(aMy]F 9yZ{\S#0U|_/;+WPx,+; P܃nDFɦmhAeHmwjU BqWm(q7+Q/`i]z?SKDj2ЉYOTWB5_IT;G&Z3ƥ(2m=5^nͰz2~Y! :%&„e({-mfI5̸(nS_EF=ç˭v{n+z$shCjSĒitߞ:r|) G` kUNգG36^x| /;cj }Yg8J= 4Wj-ڔT%O6a&]a1,F dHlh.? ْǐ:fWQKLwAbN$[Hy'=~'tg&7)X=P -*MJ~1P 2! h1cMk<}ʦzցc6$/$ߦۭ3\۴Maqz"HɎ oRc.Fn A4ݡ'3+ " :E114$ E-#f=T R ^SJy5 :s{E|!!jUPPĺ)L Ulj~UT.|ٷy8Y?o 3 BY(e80a8d*cW{V 7`O_IL3"2ePC/(t[ ,I2-¶T@dmNbkOk,)UEiaUvn)Q] Oznijb#)9K؂6C:v7!U^Bb~:1 ٟAmZ@zVzT } C<#z'P%Hfʻ0[z^9YÊ!sZ@sK5.n @nP}J#V[ts\c p|ٱ-!W4Oĭ8ە :Q\\F,KlX{BiޭO֤F2QYI~|/,{6/Pi7KVLsoO $wbۢr`, ԃO|"WRZ2So7#β#Ŵue ]@_a5@;^CVЬga]}0V+YJVY^[*Rc`jpF@=C֪}Hv@M_"/(=][Oڴp"5WaEЉh*;;uv>΃if"qj 4esuUzjaKy sp z\N^FL1tNJK;gtt▓G,Ŗ6,i(-Ѝrɗ0tի3Pm ǀ."zZt<ȢýD]pyLAum99*3(un'= s5PM\Iv%KJţ 0U=&rYZ7o3H'se]j?ާ سl5y.))X S=n3)67=M%zFsj`=EY6/:0P?ehŷ}vP-t@ N?hM7\DBUٖ\Pn.NrY ܉qbUɒ#ZRԕt)ts'oCa^O. V' *R;pER@-||ؘ0Nʢ櫧8{} Mx7b+L?NNpv'On`EϨ Xt5ژuXٔv`v -ܦ p*E5/aQw'.ֲf23ʂ^o8:\57CTK O[V3aJK.)\p8&PO {EedK#ia icQJn$~۽WJs{uKZ_n\)#X:<ȿc.!N2tlUZ3gLLZoghc2-fBS!pOġ2!9m,}z,NNQKg>Lb1q;(|GJ%qkVt`ۻ`?tydͰh)B8ҍ.}h>?Dl63TIa}K%5^B5{j\=l"F "EuSFq,QF?q<)#0j)v8ylVȑ 7eH%HAtp[PL|ip/f$@g֩ܫŏ;( !4Ĥ%Y82v,|-d-|>MXF(*VH>-z[(7FĮPEXS K~ DW43v0E{G4)`p &b?&I !##?˓r,ו4fI( wLmOV:mm3_c,axPuxmRdABJ$S-7 %*f.6Gn_w ,`H&etu$:&aߡm(2d>-;y!ԬڡHj]0sRLZA5IAu$(^NDkQYߟ,mAhҤVo Jz œ@'3!ir@^1X}-L&k#΄Z t" Ƴܵ ]bbZSZ╽Z.I[hzEl)2^ʆ" ֜ m'$ckN@C#饤V<$LVx}LSi/x=B3FEc4 m>NjVZ3wb) |UODe!x\-dˀ%'.7vyӻK_So׍t@EX3ne5OrD19i[˕go`S?ZPcOͷOOK{.픝yń\YvśrnY e^ hrRYn:1EfLN^\͵«}|`r.'r$f&cR9})We'H"YmÓҏujR{*f6JڔPgAK5=! T+Ky?cͤnO/ӂ+\&:GXK$~rxf0yV9wNr_nB;2!͟a͒Y9hN-7 |{VkXE(ơ Q{}'vzc pV*r5sS{68FbɊ_6$:-H$' H-^?kaW,Ր"5$Dnb-ػu <0>GךHO)A~pIiZi6r'(RՓOUM'_SXy)! %D. S6WO%/t*6I' Tz[8H'\Zi3z)~ך BrP6+?)á)L:aШ^t}/r,yDݗ d67`+bE19-m-OǃgzFx)a%+.Oޕt@Bfp1b`K{XVG9JiBYAn7BZÒ/tx#yBSg}m=Mws8#Txn1qhBOstRKM;7U\(umGۍ)/O11FإB="Ozr41\jn; rpVk呞=&1ٔ}aI20S9?IzBaz)+^x=\H$ЩTePLUʝ+VJM+\Bב9|8 &Xz:YbjYgKV kEΥYM\x:J)u'Nlx.y' ҙjͨ*NJ8ɏM35+.P@!e ^iaKOE7O/dOѬbU< R >8;F%TK#z"7/[b\kʹGlt#=uz JZlQ!^3mdrC"hsd`W[f=~!KT(73r5s2s(s+=Xz|ޟ*:P#>r!<;w(J@l-_]=WHUQ2J hVm!f+v17cI*GqcigrUh%/ ]~ce^/>F(7 *a:5lzʬ^#+*SWy}cq.tCq`{w,6oaxKꘈ%XmV1[]:M y%Ln"=,#Ym1"E~Ke-qGyEv`樻1!N?6PZڎSbzmMD{$uxsͬd9X\ϋԕ01Q bD8= db[ߦ |B( GZc8D&XޡpZ~LccI=IfۙO~EH/ O. tӠ@Mq':KRT_݋ RnK?q9:khSSU?msR9& -),hʑv ëvQdTM X=n&@,{;_\l9nD]eD%/<_ZHuJ"HDǦ=v`SMʊwщ]{4ƯN1O"m<"}~1CC-d<žS#Tdy\=QMvֺ#ЫWC|k;O+Fq{NAA3Nɹi|b\DVIFwE,x}Ns.Xq^̴:S 1*ccp\CDɈg$QAPNQ2YNID9US%x#/k)(~ G*-XDDft oRkPTk#5 sTZ{۠NTN߂ ;\B ch ?P#/ jш{Q,BsdQ,v@h41J^%q`":t: (P# xܭY?gkJPu/";WO:cPӞ2E~38_XhG&!0Fv[QP]Vo7&[jrm=9 bߛ̏gIބD\)gdE`X9 sbj+Qf>:4R*qd2v@[)5~DJ0`yT=!XsNgOB񮨲ƉF#az-xޱ/ᔹƶ mnP33Ms[sm'GsL?т+h{$ykv{wd:NUU 0% g>"]6uWpOr4v})Ο\G`=4}>^}!NDT{`XL_4 x+ce@?Uxq3u-|ʱ,8Vv)"ˇ;Vuj!'k16Dׁi?g^sav9~[epǝ`؅ibãWL]&yXGhFw+wj3!I)8/Yi)aBZF-)6 :AݠPA)6aCpٝBEn@A'ȠN2Opҭ&8-r}m GpuudS1TX/-;Y*Q}^%xXE³"YF}11J@;|4P`1;96g6ZxP%֊o'|}2ݣgfKh 'ZX3Fv jQv8N V{;ɂ@F9x3_O>r4 ?^˺Ga9է+R,<\.<Րw Ps2p@rX)3B*ߩY^Tc~\Bl Æ)𻴙DIy$.ORhS6A MIv,T&`h2i$b$ H:*%cA}RsVȰ=꽰4[Q*Xan E|;ŏC}xME2*rE蓐|1Gv[E'-qe{I˛_$C̺{2l|Ib1eQM=Yi594#*v4羉DMRH~PE\MTV9cK ,2NSf*x@q,)\I4]sy}Iz1pc'ݗ { [}XmJJ!L{E?;bS֜ȿyU6)]n@ꐛA|aH.h n{0\a8Ѽ/ؕ (wev?yG-AIS#.,ZjTd=ph3Zxm( XohV%w?L[YY%usR\WՁǪ5I1:s|O.-kNeXM ]?(Dh3qvҰ\ $T78e)n /hf!X]F\5Z@Z$tV\LPu2C'g*'uz[|oTMR NCk1! &t ($uAтJJAwlQ ]w~/\TDQ¤f~Fl")xlJ]bՋn*R Βkw;/1z@at;oxZkC^BO4-q 50>.G:,0Ү-VH҉Cu:[o~N~^:*X\a5}$XGc4K!l2_PZ X%!M& h?rIX}VL6*"JR3jD[g.RUJ;eJ؎*F Iݚvn @Rz FUVt%E8ʶΒ򲙒!j=bF+{h_pOvraQ+u^y/kJ4Sž27=D^FPklxvSIfiBR-q;膌.KO(*,@fn` rUbx͓uO}*m\@Y7,"PLEKT+pLI@a7\fFY(x*֢*}'؝q3T$ΆToRnU髕*ukHx2:EMq}>z[F؞"Tv+"F V@Vʮ؋rǧ6d0M (X2 xW[:6O#,m~Pl1w-[/1>Qv+xgb11A^y 3}^?u3V$"Jt4P-=Ѥ7En˝gqQ AG^N Q?lE. =x3+m\n` rg3:ڋlnݹWfJ62_Y+O@E\MvO%Ce/yA% JV':69uˉ'dv=,P!&ҭ^q;u tEP] CFTE=WAD\k]w'Cm"|`s){Z,o rx+8M.k"7h^b"]F"w62V4vOi3ٽE_XS\li!D7O\x`ﶲv2@s؄)34`jţW({_^xɞM7nf,=ZXZ;a{lc D.؉$7y~2AFhZSs4cl/YȽ^rT$ `Wͦ鿶QQ [?H ]k2?xGn rсYn |4:"+Q*bt&B  ⒜3x 2S "w|hXI^{e %ϓ(*`juKfkq9*9ǥ|M:Ne *9a 7l#kZᅦ Y]TAh-.p %iTe05jda[Hn `K}> m%V 7Fwp.xEX,d@g!,OuXk"qڴP1QƊHQ1Eh!!Ai#Mvz`${<b!`bSBn5E~~&P$ 0UĐ1W(1P;wu0՛%gT ׫ki`SlkY6)R?Q/x BfO|7+WHW+Z{*L!EGx= ,jxhM^ ;a^\ ^A~pr<H!S=4jښ$%][Or j(q2<3X*$C d4]G|An S kH{)p!n`IН4\tECUy{UI!c[[¤0] /=97$ |׭ X:KQb1!N&\iyw,7͂Cs0_~qN&|6Tw"AMxXDǠ(!0lP8uӞ|to#qWquEVjx1!qԶIJ;ձ^6uZϥמܽYf-4NvcG/k! 3ԟ2AB7/,U\"TgNwm M<7s.瘟nע:kb;f=we 1TɯXBWd qMfx)g|29jXd;71^1A:Z~6J`j0BDhe<tdY6E7n*:L0x9니^lGV<ЩR'~s ϨXzA toDM3Q=~GP7_Yɜb"k",OΙou[iV'2J)9{:f7ɳ)+Y8M խW6A+w, Ur)iJXbA!Ҩ"W=}4`%0N@Hr* GᏺkFQh+]|3=A_Q竞sK?2ސǝ(˜4ʷy7&2R_FxaNƺ]X'#Ʋ(P 84LtN]pxRL$HzѣC+'en&G8oLd'h8j0JCM4T+t?c,>u0TQ\>Ht4e-֎yO~B&*M=ukf(~]U3zp$(5< q ?7>I{J^Y2y=0}:|22Ry_JJf- XHdrŰXgU^i(%Àza!?\/l9ieOZe_"(i:fYpa r#wᨭ]sIRRvcH1 *2aӺ9|f͍֓5 _b 9q .ZŒk!?GblbU x.:z  z;|B"u`kם$LnK~_߬D LBʍ&Qp9}Ys=.{/^;\l`xcOZ¬|{:m6J ._/?XȖ9z; tzh kʀ+)sl?e5a(;|Uo:oF a|pžfU?߾áE[gAjN;DRބJ^A54[k0]'XAOE "(˶e JU, U,So3t&c7c*  Ch9޾&9qtePZv}ѿmM3ֱ7煼c ct'ܮw@R1=ϿᴅuJAJӳp|n55(,YJ*"Iy8֋Tm0S!NTVuS&?]OxRWQG#O$+y %txXrVI7*T\~Ӣ^_<&&CԖ^^̹WЇ^5֪ Hn `FZ΄ֶWr#d$y6g5dC<;/&!Gahi|.eG\>  zNYoVA`dX|!7%߬ ~vxLmTĺBy9N2:R}YX-6+%]_?HHulpm?lwz&(U'1%L-fbxi!8CIYq/|e&0:}BtӢq'CjF~v^V[$ `{,=\Umr^ݚA„U@ ˨HwQuVH LfXV(&z= Q$2t~ie\m$>ws;eU}3aO.7~) (B/,@{])Dgwdwr!b#>s0 :d)IލP:|dP'C:nbteW}Bu _J7xqW1oF-C}.GxtŹ{؆Ƴ* 2p@􁌡 [mxQ+\K0wuEv4YWӃrB|vEMatBswInw$y`P 9UVtQ/q\ސu~4WYa 0ɿ,V1}4QԫVk'oᨆ4)ϣJ676q}nb uK ?yp]YVa7#N]ᮁ3%,ˬ2)0)/XR)L##ӂ 0~2JV?{a. Bfzf,YĊ0h ?GNӣu.3t@֧ |p(fA }kSkGܐ,DSbxzJi S(dSɢk:z#Ok @53e"^$Kɴ~.4N}KS?Tܘ-wZ|ǙMZJ 4{/WxZ+o3z?2+'&eԧ~JE/3|mˏ_vbt tڄC7fXʿ!*BŖԄVֆ-OnCLɵucO2@&;"Nh4i } dt}P0m 1%0p5M̚@ lR؇ /Qn h(w}UrK/:#N8bH: 6lʔZVfQLk3eK!%pE:NbTB'Ka'D\<+MX: m@^Na_O!X@u+ _1':cLyJ446AD ReC S5Mi1P}zKˈ p_ 4Q a|#waO3v/b,qWf&Xzi֝7_:_ D7wb%P[1\Yvo#^ -;9ZeSD~,5EI(]:]1-<<6v :)Hחn䀺$yoJjUBpㅇ8A3]a<rdEYQ3ݖҥqo@|m"ͼys=mjQmP%:vsn('ۋm+Za,657I:[Rpӌ]͎Xa&Iʹ`¬idy7;T+8s2R,+UTf.ř:R'8Jz@dX{Ҳ҂Hw!`ƆXGJX񶂤]z[ ǘt$?[t⧌~^$qIuMP oiv;g1IuR"l4GOݲ^w|8umu 6J5{^GCބ@[ M=Aeʗw"I tC{Z9t6I_-|ܫ Nct(eڷI&9ٹ'ƆjF\Co ;!n(͉n}+s̐ɍv1W%=Y18+{=UBVhE'l7pdޙRK"sl|Rsv*t'D֠ =c*1ȒrtKT} `%gvi>p[lo[5wV({jHLIΗb,ӟRn#5_G5UJY49CZf7A;2氐x2Ȼ[B 4Éc\Rk W&M&iz#@ju03Tވ8n fGp4xI=}Ox?ҀJ]WBK[O{! /a39tX/˔~{t bv2o&# p="ettpjJOj-[}C ""@29򈇌@')RT2̗O<ǓX$<,e^4e7%|ORm_˺KΑrs6`<ǭؘm,|tFĀ~ 8Uu{8:61=T➿=lm?6Nҁs&YGS X6ϦjhS${P1+ހJV. a۶k$ip|Td̥rvFk9<.2>=Iv8Ձ}&v>U9#RyMذ"ݳJ7Wu|>B[L v%?(5zNXƨfWbpZ):},8Fz`|>>IT^K.2VHI\Ezj SCWwhu0c%׾mږ1dQ7rQK:7SmR2d,]0=U'H9va]IwH؃*bxS顖 z.vek]h"3]$֩H4׆GG|3-T%/ fD(QgRI0q eޘ *Vn@% !u_AuJǶiRkJtsV:}$fs$LM 02n$x tT-L~$G9ϔ-$b!$2 3q !sТ~b4T^H6taNzFWB.Ɂ4ȷ. =JB.'K 20'{[P3-ڳq'8fU@e4m;#KtׅѠ5kBCHmhA .y 諆S?3'{ #HX ෘnj?OZԟ2A Piw3`>bl([x 8-.?{NMvS=%!zC@`0 _Kߋ򌇛\ uPЯҒSR0L+u_l;~ \f/iٙ';/ZPG{(lT;8YY~maZˌĚntBUDrF%@|| {-Q-S%aMҳOHEϙJjεׁ}P3uNUQ؆#^̓Zê4!T3HœeGEE鑫qNiG̪ϘwQA|1Rp掂Na߳Ǥ(]j/:;늅G_᨟ͳmpCE`^@He,ƦHx k8f hn6݃OUncSy?lKx!WѧE\BkҤ\ w&!$ &a?IBftQ/K궔|)0Ge'r̥^5^BYT? Y` @drz .&lֱ/KMzD3œܽo,!z6lUu><:իu?7==ꄲE4sk>p @jJtI᭙0˗ +ˋOG<;08Lڽ=a!$q{c]Wɢp^?`?/*s/ Q]C:cI2` |(T8 5k2L^zo̟CYDӅ+Gt%&I˘ӱ% tf7k<@zXa#bIAe`@#)*{2'f['kvF|eĿ3|ׅ;vDR+hS$^Ґ]p%Fʎ\?Zh?(d~bU50/*?Gua e}ƟBt5"C EK/%/`FHs43?zOAim}4_ޏr ?.ʌj,]=(mdSU@1Uat%т$$X{%"8h%+\wo>/I46c@$?yW\L,A(ڊwq,[_1 EʏVê6ePVlQO6vCHh&tu)ɘgKVmj`H.8#)jSyM+w 9U`5[PRFl1FKL,n]7r$s74<e˜jAekCS샘b]PhFj<`ܻ}5Y^ ژzPykq%/7DEq* =f5c"6$C[VnrP{:SwwȥoC,.jf}TƄNNG8|r zKuT-ύO+ՎH Qe+)bwAȮkAĸ1 "0F}4u(hh07Aր"JmRųwP-ߦԝ);DWhմo΁Vgau) ?2='=/Ӱ,EJ Za4mȒl/QES~-,;+28F Y6юeL@ ɘ߇8,6eH3RtB8mlEB!t3@620 qIuD&RDD>ٔ%W Ռ>V#XaLzˑ&snxo!RжLk0j ,'Jy3SDڟulύY x<9@8BMv:`Mԏ#3x?߭'*WYmrZ^cR ygۑdOFezxF3[ux$-򗳪#iVqÎYoLk Fնu{~q({gWs&S XқUD:{O+H,y-/ !ӿSg)( t2R'h:(A3Ki{k,!&9(+.hUcTo%T=vp!->J 1!=HոJ{B2tqN3}2zùDMa-UCtp7JA^a۞R"toY{<+z[-&6+ d(h~0O E)WINKbE%R-VuK 5ON"F F  #a y¦j2Lv"=&{w-Cȸ "=Ncjo\LT^eБ-L %Ǻ:$x,;E¢ä)R]$g6Uz&b|6cְ&mXָk!nGJ 3hh/R&xZ'՜q\< sRp4`k0 tߧTBEJg#,aX k%!O ¡YpmO2ZL ]^mW]ɛ9UU͊M̻>3k"!A-{a7I _އ'.7~ t=w棠UBH;TO=~Iq:" GT!τ8~l6#UTMYê3#K0MS2s߹wc Icev4Nd;-le],]%? `1tTSw5_" \|r,x%f̜q;y"R*6q{8)Ղq?hy9NJ CEK'{bYz$: %\t /xetԌFT y+9,1olY-%Z_sU=S yhIb#`gW;>@a}RӚ#@QS?0H p2R2N&2>n$!v%$'(SJe_GmѸ{\x)VI@xI{gGGd#QGUY\ӇG':UZx^ͷEJըk(jU#Xo(v@ek  hۖٞm?hZDw}ޠg,qsGo7cU‹y[^嚾Ap;*9`2DUU--'r>Gp TʛMKNoTia_TE?d͟@li#Sفcoy2zS͘ΥNՌӲ_[Hj0B*d!lP-vr1ˣ1ΟdvIZ8B:[";p~ޢNqS>5bBZD9qI AwWFR@r < ,3-Ԏi$H2%l/BnBSҸprq}082aG̀e3J]%~5Hv!)ys}L<1۫]n^H~i'E:U3{V[{fxYg+L~Ia </#4Huҷi: vF1l/Q6t+^ͷ&k_PV>Oe.N["1JtdxLJ#ז|֌x|L†_+7y˃D %V(y|=)[=k/ bqU22]=, Y x7[is ̉x*NTN*̀WY[9)p̆F L5RJ~[Kc>Qr/B-nF*vk}EYnF v[o~2eKf}&|7L|J2~iy?WVE rXZN\-BP:tVXᕏ]$He*y1T&/OCQ:/_l:-r| \%ޯK*Td<ŲAcMIVHqYMXOPY B7:F5q5c4ZG%U2v#-9V`SoTx݆wvkƯfH̷3r:*2Xۨj@9iru.P9FJG[J ˔%:f!oh?qLqa]8. ߁_@,Y"9>m }{46`jY 晭U)8Xy@kap.jaeߦ5y-adMjXp^܁"1ifr.wK8^nNso-@=+ hFUOBXru"z`i[z)@hDu|s]7Vaw5:E-T3J;#e6d*be΃4RkΛb{bl2g^={> ucçsD]VL^zNY$2UxT1$‹OIhr=>iӽkIh/z bC-[@=43)Z$*>'noܔAn 1ߠU9G\oE0}聖vH:x]/1L\4錉y QDwH:_d9 Ebs~,`]G%.hS z&gG/Bw~>ikZqx7jZұs!lI=5:ȡT5ӗ%!mג+DAfPci_+WoY}F?l!%Faxz#8Ԗh(@_](Sƺ%?*GYTdbsTud=P)kĥg){5FR T&_$k NnO,<~NV0sFF_ZN,YNR$Dvo2P8ϼtn@n֭SEg]t"f>j>êS+|TtLbI~_[xJiI~be"L 0q8E.*u'60T.l2&h:mbrLbLzE݅FMg&:Ҁ+qN1MkhtwƵA䌎(p s\>=&+Q_5^cX9B&v1 1}}ɺEc 7A% ies J%P??NMvt~(Nw 6yCu Z6f p{ɣ&-u'# ^%#^C8Չ'jFȠV n-jnIk[E^oR^J= -uUlu_bV7aKʪ+{&II΅q(ʹD'ٌN L\L}Wvh:V uzծF}kng>#+H{\ʻدGhE׃y7a;Il nrUðk3d r&kJdnDs=˄#?/jeUDU/+m9pNky|gW]<]za?tvnmeVxUpu2)L”<-lcn1)*HĢdhp߳"0}:mӪ2U#M倄] (xD?4&Ri[8kLuyWCS$p^6uˮ1' DbZj\Cs+٢єJVT+&Ad g/ Stߨ57A׻95a$jB Yl/JYPIGKbYճ.i m;pqebTe&0==eV&z>〈1+2;0{Idv]. XgfL :a15:+,1QNs;"NsJ!E+EʖmG{XȁS v3҇jTڻUxʪ]"+T{+; U}ű?F2 ,Қ0!j_v`;(@s$ZmV6ƿA[ٛJbܞ gM9'%7>ԑښLu]7e]˳:#GkQts2 W||,O Ww #/ !LY^J̳@J P({ j] mbt 杴 l(^˚ qhSd;UI| 66pUQ֠, [[*{»Uyi 7Y10,Zlw#O10(tJѓ).zJ-M2 kMI߄)Yl cX6d*]hf&"g(8[ɬI<)?myJ6 M'|.m.g?LD9^emZQ#] (UfdžأMuaH֕ķ<g'i:Q̻_$Io`ON$ĕa/@Ȯ3o rkc)^K)h@Owkg4(WrbBDr,De2;p"v׃S o) 7i ԥgc,!1X*P_+CٺO&պ{W[~gh2C3lB%}MM1jy_9qA}@,4!}5QhGHR51DnVdMzI0\LE@/csTO.tu>#FQ8KQh?a(A8Qt]xs^ء&x^BNOuuP!.EL}% k4G,ݸu#k$Y B|cI -L<+JxH]TieC(M+:bJXQQ98_sm C&h50l n$`ԋb<)wG@ufUm'g._kH!Gڈ:@k&j$o$VzAD쳧'6bKljxVR0Q=_cr TQ&|a=ӽF¯ ZoeV V+M9uZ< XR_sP1iIH]j!M-칕-28Lf\*h·kL. fkډ=I^$\wP(1h2dʴk d~R?iXDln9ߺm Tf#߂{(0ovD'zgx=0Gݹt,-22pu=+<+sc\o+cΚdb@FjpT@mex+` m٬q3¤ol'J̮dFqtYbƔ8\;Qj[aʩDU2C!ϳ 29 lį鐛T02|}ǫ(Bs1`zK% GzmiKd.bDVACQZ1.؊* p] cRS ZS;IYZw"Bɦ+8@*Ui d]ؕX:j<.Wj|À4<̪T7  -0׉נߌ@D3!uR,L2bv<]v('~aupPghZȰOd1Rc.p9:U z6‚+8LjH^]Nnۨîu ,%֩ ZαgP (@bܔJΤ^DqɆM[]~[.daJFl ?W,:\jūYcc<Zz01n+j7 LϮD,jPaлzc8:ѻN,eY DC3 p/h#~l4Msjyipfw  *-[؛ BDݼ<AUe2^[Zzv͌c,&k9~-(eP6$%KX(F~u-=u# dMw+*|'vZ3\l=-d'ݢ3I v};NGe.+OGPAZBf"]C8U K9-6־}|k\G˵̨A$GS3'=޹ XfĄ^YޅUiGjR937c8-hT $$Y%S)i'R' :*~jfI(@.xkXYr%*֝ukJ{19fVɒmT:ujٱp/D7W'ao%UY͹>vzf|*_pn)v_CUo3nhFg: eEnlģ6F0 `S;+a&Iʫ$s`~ԂL"0<Ϡr= h 5 {N5>ܱ֎W (cH޺D[::lMXdܥ ՘ ^?_=!W(6BRҁIC,SQb 0v^t7.~ Dj~(TGI܁TպդFuAkasTU,JVη[,"EWTBzQB ;{V( {c%s`eB`ͭ'&t b6zџ(-TOJ$!&1< wh2mCQܮ`Qķ zĝI+bퟲ#SO{< /4T YWXab,aJ,s_{ylM .. VW&j- ?<bW<.ƬNޯTJ5iA*hw}y :n}m̒9t~ң~OPՓP)(؝0^۱oH[85k5;rgڅMm~ {J@?Qg/VNkf T IH⹼¦y (^@mT0(n46,7%H?OJT3Q`DJ`KtfH9㧗 09> gXWo pNG w1Nr:i5H@%-k@p0.=\1E*ee1^֦}u ,9hl="GfVAǪgp+y6u JP颿mktgiTZ HndT1 d<㍫t\Hm{&C8ԺW(-8VcK "S~l!?d;:k)@>7ɘUJh|ݧj}>SErqD@{dM*c@V ^~_[ MPAnmSb8Aa1(H⺨z&\` [sb7񝆪zI(U22JmCxKbř. I|4U;]TZsV+kA$Cсq(KɑqAFƞ]So8A`d\%lmϹr5D"O-wlw`ln&:b#oLB$7 *f ۊhLr~z`kLz^j+B$NFЀ=œ!]7n,( YXE-@y^'ս*^hj=X[JÂٷ?*4gqK=. hmt/.5<-TI!|ho:4ZZ5c$SNGWMj!}(0s~f!PHa?KQҏREM.ֺAb7& }hep0"%:x:mtlTv.wUVڡTp$/,߽PWGv͞|;1b.*a+;^A~4$/j?W2Ŏ ,{@Cu/ԚtֹA3fTF0ضXFmlqC ఱwfqVߊqj`jp/P˷GQC&$iƂ ~/MOg"p'DoN^?BkXܠK?ŗoNé{s#m)oSUvSuAˣcjur,*Y|foVnAmd4ӎEW^M-=m6»171Ug܏*g,/'YRo&|(~?'G@kӝ QVf^zBJxgx@;[BXc5g֙J-'.e}LgOFL@)vkpܺ8+fˤ3 8i)[m-:zT^ gnxB0,Ck^\n0J\~!f䳺ʔx6. )z+&\]Gc~@G 8Lоu f7]g;Ęrn]@@T4+ROְ]ƱqhylrK;-u~C 桀fls~ 4]@Vw:5n}=FLG918DC,T['맣_n|Q@ |p+IxqE6JUCṊ(d ӣ]cA5U.-$mܸudk/?YӪ̔uwesk6Jwq.λzng$[og'G1)GUAF^u}O]>5o-Φj'ׁvk1=o ~)/>AȾ"m9io0yp RW0l"]Z6+KrT[ao!#`G.KWk+4 ;! *%D8J>BV56sik<%z`H&(uad|o(.gLRĴ![7FmsxA#uN̤if$PÀ\NlVAy4(}a:iIOyv?ӘI2YKF&#:/z# z vme,/;q;2"w/sXy twu"= "??m=S|t2+0Le.њ/ QXFXPGg`VЮESzN?Mu"R}NkyoCB5b&1Yi=k̸wH.YDSHZ "=L̵ 0K-:Ğ䱪/}rlj$Ne\NaƲ>ٞlow9 Zq֞ ]wY<'' ~fa +(E@k`meBv@{Pg6R^n$^ΒˤO|'֌X:[tĶ$ ^cNѰHNj +Fƹ^vŧ@kƕu՟'~f/ƭ?u*YH/s c uiҜ{Lo.N>\Ɯs-Xڈ*2vKC7B6-W-v!gdtD36&Z{8䧍4V;)/3 x>l,Su NU%!$ -%jvC N" Wx˞W#E~OIs(#QտD'l73CpKP),KVcFヲK1ť,Tux;] U|,)/"?*ļFyƵi)5%, S!]Ulւ.9u*{%"7Gi0,OV7C. .c?hk2pw!KNi yƨskx [xȇV%1sIJ{Qty__SXIgKJ8Cf/݃3R'1HMe07Sktd#rݭl6 Ń oe%KFtO ig`X`ե5sC n Q$@VH~ $Vv.)/~ıC"LP˧AZH$u+g[-y+V'i/FrwCW9$*)r8'#JU Ut }-62{75&#`/&wfsxqX@PC;xrbGѼL=59jpw [ <>'`DCkW:l3$(!97Z+4{O+{@r9}bCJi&T6Uj)_p/#g$ fM2= rx-Mcn&ۚB4 g`-=H 'toTx *;BDӟ|Hu  Ϩ*lem ]p {?

@MyE%O~uZq:$--ʠ"G )|v:A> %0MgS`!`]7/(|A\g5}SJᵞ{g ?;}dsSq6ˀ+oa{u5([ Ok]2J6=PU9vCխj)c6c:*stx{vEU3pakW'4ǒk0D@ɋG4{aȸt"؍D\e˜b>l.Rd =}kIMt9vz>;?#>7W)nzWhft@'snn'\qAteઐ'ʒkk_/.,豅'昒yR>2AvDXa_'?D7¡Efs5㐰0`aufrV7u.]/ƣV~zxvhVG:2dZT|C GhB)ͬNG&2ȴ8yh,d_,E?Jb8 ZJR yMv Qks: ?ԏiXK eY~`TaWBFM4yZΌmTr.K9kXs~fEרZH+5}ί50Qq%_Z~fzfq󧧞]0It"Ir#Jxbt@MDIBյ ˧䌐IF͚Ah`0=?D*6%=8\QS-Jު;IK1'}x#ѨuLU=ДAA8/I}(tV&xzRtn~Jt)`Cy$@c-!xxp#QϝL?8R f "d+~;6YgDwo"*mv9tO&ڼ–$UNj(g"_'o}#kRBg[ub/uE﨧>"_e76bMo.#@Hu q"=34V3o<8et.?7ZUI#|Q,{rU#P= ϾLC cMӇ0%r2x,ۍgA ώC\}ωTm 8q)6`=fSvr$f-ae_7*39/'9WiUyx d=eG< eVvj, W/y5NGpWa(8Cp[ӗ܋=-(<s'ⱻ3 hnЎba qJYuNC,;5+eyBXPkǠt .qO?b튤M7䞳*o[%ӯ_:r0NĶ;bR;s!ҫQ:U#ۢMu3[¾ Ac\ jgC8ΛBomi$kj3*19#rh;g64^dl"e!H r8#äL䍻zwk ;uMJ)9'doJ9qV(b^bQO6\oJ"ۼ<*ل^'i+]-#,4Gjtyu#b\BgrT&P6H"1w@5zc8gN|ҮΌvQ 64ҳ\ ȉNJ D LAx;t[gz~OWY^EG뗚dE)HE'du,dٞl;Qm7.~d8[jR'-"gWUm,/Wm[ECe,R^5ae譗vB2a^$"HԪ,' {PޮUlq `pTc.Q{YMjj,'a0:qau߄XRxa+cl:p揊lb*ZX>56T .uɅ|s\'`xPh0x2Tnzpl6@Dz^} ߤkzعmJh3GYAexO(;wl˨^,c j9l?de<=\TEG=L;7&ؑƑfwhϣ{ pg+U ij`,.r<|6P){*gfwTZvջr3j>Gϴe)XSTN=d$-uQs:J_8NO코bfG̋Z-+W $ AjU'wl1\7'%5h3VoW`p7OɚaOMG= tcY=bL a_ߟa?IҋHtZ۪)$:Hsiwqq>;Gq:b6_fʆ΂f܂ Q;Ib%0o&)DZIZ\aLJ-;)(/B:|CV(J& +vp3(E7@7j:zU .{W(S>!9l&8%$2 i4sˌxzl.@kakӔ|U8S x? IQ@Q>F.LS#CC䙜QX@NdJVy*x-˞ᤈ֤Xœe~xɹG>!ǬFex44/MLṭd*@2?#uR l@O2[o䔜e/u]O) O98r_y)}:'tT^Ȓ:81kv)Eз.ҍW2m_,ě[nβ]Dk7V59+2(I -*Ij[,nK M[a;@Ym*@G "|ne']#S$Lq &o%|2͢z{‹|AGL٫Gx٩В%߮3*Pa0Aں`_:A(J TnQ8d"˄@!/f ,ldG"V|'I<) bUEU>uQ$%H js< 3j2oA,sݏbN2bV.NCVU_IX4V$b0? !6Wk N0`pJklPe@xN^ZT"]ljwK1Wx1>A& 4fwݲʢ\X(-u7Sw`w"WMw=kj4leRwٌ%aQpw Nh'C'{? F{?n<"s+DgDj~PkdD'`Ftm1gM-,[@NC>appZ#*H5Iip?ն:n=ޫրu,5MXrQ2'6Xwb/0r /( MQv!ȷ-89A(YS&2ҳɝ'PeAfS1ԇ0vvV 3y~M{8xxt[s1űM0ꍿ{-&DƂ!- ÿOȲ'jmK0;Wzak v }#9B ;9Q%Otztqk=T)zLNfd6`)捐o)ysqgu %* [t89tGٌ GL6fRl߂lA,)Œw*wj=[mTjNʫ؜`0XSdesV[opہA6 +vB E&: MAhQZ>3)f'a4Ta54K\{<{ߵHT`ɣsAh~R^ $/2,sM6_mխ+x4y~$SAɡӟSہP"Pe9ͯa_y8 n2~H *aRHxT^sSGD,9[,kMtU>u*Z urͮT)y츐V)~d:CO V}8T-JuG}: Sa#":FxMkS 3L-d7#<9SE[)W.vgH,< $WFs]A6UF؛Uk",ak.dxA*bYhnuQ` 3Tm@Uj…WoKBu\蟨Gl C1w-tQ[&S,/ Fa"̤h@XÍcK*$Buk[?-=uA] $O3 W#1Bs`TWƖ:Oi#\ X8R2*>Pj?yXH:tsTub1_Xc]ϛ~*VXb-+)rb1Zdi\$8bqh`)!~!`PꙤ\;AE䄤 {t :3ޣ`Q_dOj,s 5e|*"W7h=Iy '*R1&Wۄv֏Ķ+I,0R8\6zuir1@$lӺ'/{7EyudU4erFKJ@l3b N4ʸ?։䒼Mh}&O,dʧ{4#_Fl nnsΐLod<+)cL|ƇyA<Wr;wEERR>8.QHR{C*ⲊrMͬwF8;N_&q.a!^Tc #*|8+Q PԭOН"K~)yye'Q$`ߊ7Aᅖ  g#ݑ 5=w\zDjOAar!]R9\ǥ3P>j̏?gӨCzZn(եogK^&?\^mXhVameKXdJ⠠?\tߴb>%#vh.Y81KYU6\ (ۈ ]/u>}WuK}\=Bs/\e=/֥JF>9R e IPs̀JX^ XfÛ TzkT*aS";/K W&}bq Bvg ))O(FE'\H-Niy8E;E&f[)cϓXƆ&WnydMboTlc F6>kʯ寵Dߺ<_C -e-(h0T?ǴqN'܊g՛Uen^G4#d؋{|Cj4XeK'?pS,P =:xU7{)dJCٶL3hue8^so:^wT's *`๗к<=#p :8DI-Ker7ޡq> K}Uv\_`/ VcTcb#N갽Hƣ)3;7d:ڼr C`Glv?{X 1Fy&~Opqx|,t-c 5Y*n C_NUkxw&t뼵;懝#Łd<-˔-0IN?H,0LK`Q)f+ dFhw{+ίڑ+VG3 'E6/HMM|NY pwjHtH!^ ]>[xÝ"rUǼh<c<{Wi8'6&u1TFHSEq\SҎ oKGd GA|yWN1^]9w)۩/tjulaZ7G\TOM; q.D/KWo|ZxcFUadsKC+Bk=e +k<^u NI'?^0^+E ncS%jgNWݐ|}5 "xuHsiL[ a"4>P˅tw':$ơ`S_}`)Z0[5CխI3G3~KπR4H",)|'y[zO$Efo6йٴ|l'nNZipu X͐ dTt^>Q(5 =e< HlTQ]" ̈8&w e0=rILB9)EJTH 64jVpO)" Γ=Gc,;Xo60y(jёE_3R'L wc9tKu5p5(sJ!eZ8cKF)uA}dBGS@ȕ4S1yE`/-p" 竓sEUu_<Ԅٸv(Ϸ"E_.QdLe+b)>__p48=ReB%h*OHR,U墏B g: 1؛܅< S[ar-{?F$Krְ̹걡O#\OuǢ=L8vDЬ_sͮ\}sMX{e-}ԃ=57Ϝ[a?f5F"xU@r4w@JtkTty)m˺,U@Cxq6(p)W$5j2 4` %ڏ.'d&<8vTsg M^qǣ \xLCAN:k|;w{s(9`m`Ƞ5D`b4MlqN+ W!oj$Brl-V(+4|;9қ>kbe#d4=TƷLɖ}~<}!@e'V!M))%L1oh]PVwdiI5\@EneLAEs zG7c}q D/5}(YK~i3윍b Fu[9;8>SŦ4ԠPR߬0(-(]?`CR"*K1؁ƾHEvg +#D[0aRlT~))J_q|yZ(BQ.2tEVNIw%(g>NԢMJ(0\xk )Z#GHّeleLwIn` qĢZLե4GP yGS뤂"L_ h0OD$Azfp#:m\*{|?qou"hXv0kNB v!%\E6"(̅jq {5N;ZL  !q0rupTSt6 L1.M/}C9ESKRhP ӽ}d:2Q$6渣47.Wh]\{yFyYr=J!KabuP[}=?u-}%,Ehh)T j!_*8)ynTq fA@9P*&R׎!\<џ=}vNh5. >YH}"KK<*CX׃#cw=ߡRuҶ%\@0mBYw\4YcyS8I S+Ӂ}e(% Bl \'v6.XǗ3e{3]d1u<7sD1HB 9e^3KCh9% 5僣%_Cm/KXx(ץQl*SDM}F! Cנ۲ohH|dP0|n\&vU2z0sJƒt_/ AlwCQ09g-3|و\q̆YVPo VD#qtYfm4Y\˲;~764~((3چ[3N]4OhI_ݒ^Uczҷ$?9զNs;S,a]|Z v( =@+Y)&mnQ[x4)q{&qN2<>oޘq޶DbJmz 9xn:lД펕6jcS< nIo<.-ޘ ̦Nh!}8lT`ZCMl3 /]mT۽E}-pP25]%JQ_on0Fmɋ3\ݴm~ hupzx ZĕD(rn>5ⷥSQP'duzÈ.v&I{3hA+J?ޚAa0Du1~in"*IP ?hyOC6XS}4|IyPT^YTDERGW:Bq!YwFy9~pC/uƼѫXW7~<_f*ȋ2.3xQu%t]a* ޒ~֌~1^\w kw^gxL8F (af\&[ V)l|O7M/D:Aۙsz|Ml^3C>#P8aҰmZ1/\Ťda帣Alf^Ą,S0!01u=NV#i2jw La6va=ܯȦ'&8ٖO+EmC>6CYy̌ox?Ca\yT۔҆_x{3p-MU~[ʋ7/LӚf@zy#z Zn˭nbF) a3gCO{$76nHLw2T#h 1̭-0Yo46OWpb*ځ M $k̓ۦ[S…A=EV~\zcnBNrZ jc?rzj2џ/|?2 d@64&Qn5g,=3?I1/%?&H,1‹v=TNB[o+|08象h]}-\ Ka59t*rr\b|FS~Q)_w 8J^y]^W J@ȇ2) .aDfD|/dM ۧ:נ3xɯp9|xF29YS G{4;_bmɥ?^i86+ebW45GbCިߨc8krڬ%q1C Р|޸"TncJƕC^'ihs,E GVSJYWBkzRMɆCasa#(f[ju+NbZw!Sǂ('沜h\㤣VKhzϦRcG66I PQ4#Lvj`|rIG՞r E#ȅ~D Qq~59ݘDbcܲ33G'nAV-P] N]!iq 7K#3+?n",b|ϱἡ]CK|c+$zꍳPŪ1weDev-/5:M%xx7Umq+wBAV~+`䉈2`1+lCo3¢ Ilm"k-yA"^K)i\DGsUksi23)I]+~_%#ǭ"=1MO+CGlrs1A$+ L{a>F6ಡ.ss86ҳaRx كv0X\e–''<W7\@,(jF6ta[bfVo:<[P?}SƂhO6R!oSj(E\7v H"zqmVjFw X.ד,hZu1CDU͹<#SX`ʂCӒkep/H kynE9VAbiRgWo#R='_$JA<(Lź qq2̚mp~oo2fm]v 40RVbRw5#"{RF"c/e39fE&m*Yv4ibqTߜ,8SZ>Ma*H(9?/B U˵!1qR iGW[Cbfnʹ+Y_2cv?*?)Lޏ#d]`x?֘}lD{2{E3 \yGڽ";keD61[C 2b䕀Z6^(mgO8CE3>]ٴ\a݀/+MzC/,W9xoaJ${ %dQDaS`8ۣM#]~AIC{W&RV9@RaI})7#AlH(/OYґgodIkjN?gY?4Kl zmb.r2!9b1W6)=V`P9rj]/C~P1 :BԊm8Uy=z%o`78䮉Jqx9=ʎi gx{r3yqY~ԅqѴ(EMtdkYS:_j Fsz$H@˧?[b8 K׺e;lvD;f dJ=krgQF6+cEHF *k i\M9mgcD8@g\9 -"F1^|$NfPUwzLY`M5i&l:NH%T,B3օU/bb9 4b(HG?C JO@(OQu]_#IyXgPXj#ņ;ݩW@ I@MaٍUCy橎tbS&鉿s(fm;;jj;\ F]ۘ'ld֛wOZBrqV~]4~r[Z6=s`$uD$ c3{`TP<à\5SE֦õ d&71CIj{XS֓7ğB }Bԃd1#\yjYeoCQVTl2\rh76b~JR=&`[~n D"յv}Tu3$OK6@śuHۭ\Ig lGԬE3Boэ ֹv*(,YԲM ֖2bKh؈[%1{O0_' L >vWa ıc-X6"4j,A8gwft 4…ZnE}%!;Scx}^ Hzxi tRvWL!Ik`JoYa@ЀMu1`'{7MBɈ3[ 0>Yt5NX*AU0Y %78_P.+ن'ΆKa86e+0&g.J \QЭXg:ZJHv+;a,Ț>&zB>XV*6Zz46C-}'U3ǞɛG]A.dh +%ʝ_LB]VpG`kgyW-NץrLv< 5WBEӿsZ/B+ֆ*iJvZmJ^>\5N'@GVik]gAe}[/aV̚-0z5N>ȾԽ= ,NC}a\P:"X[}fp|KI,%q*k& _[ _50ltԝeK<=kBBaYݴ͐avѲ]ZHD) qĪ[Kp9ѯp,e Q󧩠8v6BZ^.+7H& H2=*bAϏܗN)l?=ɖW#wfgݮUdS!oLܷIb[^j}OwMhڶ p֋|7h 4q "ˊB8]qPCzӱnw`=g$I3Jby4S l)suУ!lWSᵪtJ0_J4f.+(7 %~ۺ'z0v%v%6 gsD6&]i;.=v(1Y'LVg}/$ X;T&)MD8+NzB=!uܘS?=G)}%|7kdp߮{ _ $hlK3ro cF͗"sno_FY,E+8ptAH1u gd;ѝ4{_*Ȃ'1C zv{Pͬ pgnz|ÎuB_QMYXoe'lHE!pXAr)}ɆXPa^kM2o['{ RcaOc1$g =Ƞd/eu *nn[,sn Z b%;ʙL",#E)qH־6AH.^T2 _0)V76YMe(H}$Ad k =. ?G ̪{D[2 ąZluB琧ln>~x{N! G.^1|ߕ(n02x&#TYʡ={_MA}Uf@%:N%4lfX^gGk$OT4+?|],`UO $~G nyRﯵL&gP'v*<YB_%j]$V uZCkBEn]ՅJ?4:έ֠cE*S ?9c2 6# gC/أ N`}i~0&M>5O]UvL.$q?$n̋`Bu=׏rB ,b/o_q|tWQywt1dz˂͚&z Y !h)5K[D/d&{AM)Ta+"z[&""nTP4| 3N3lзt vSyX>R rsnCWr6XRl6[7Mٜ,tiDf`E&sۇ;6IIL@v L_tщǤU镋@vGk\t#}r߳QN#JW͏ xaKqV|"PgZ4jΪkiqBZ(fQ|j %^ŊcF@(Ϭ>bQa}>Rc,ei#ef cF{ƷEQD;kz)'K(lSq 3)é>5e@F@&#gݮIG ߏ*zr2r^/g5;ZhӷOG kjq`XQ{2YT{;K wp$+Z!O,/r-I|C"gH˓_ g gLD)=\|H7''[(tJqT˞#<6ZPM32&Mb%!W̊!ҁn}'=Xh;VN ]oHp/9B q ;{r/ $vk=$\[ôMhx~z'U4*N+'ݠKA/E\KFxбEwrd yӡ Ín|fwހ?^rQDʁ<#Fpؗl[*eQg?5,>lӹP43EF 3D}uroEgh w?`'(՝Y|eN)iS"8OJ\m],CL`[\!`1r4x_MUEir)N| AZ.[v/.@k7M(A<ٰcN/ۭZ\TH?苹孁qpE|^qߦwf򚌻xegPZv+@G ,czQhNv̜ܨȗ8nn]@v-dqkW\!ԪKs!}`tL$֡ +r ϳ] Iq%Twewۮ(8JTs?q2ĺ!KK\z8}Ci2*!΀/Յ^Tsil H`?B>ZjE]Otr!*ܶ$eu@γA݁GrspDɅ6/CN)h3'Q ەY{:ɪ אȇ<*F*LVT{Y&v.ljx2I 㩱9ZAS.QHP/pTE@3;CBODʼP2u]aavb=֥ENu}<%SA @aڍDͻ/}URA u_~X|q cK炓u"ɶ JOr,{mY"L*ähH8㷵 MR}d 9OJ|WGX 58LNβ`YS 'bHnU4Q,AhXT}82w=5ln sa^jR24S UG`Ɛ9@g #'py'Ȣ8^8М8 01Rm؋2KzUL,}bi# 7ۘz(St=oֆm0X1 ).6JXaM=MqFXaH)DV*ݒol^lǾF_8"P}Q"^X!)voGLv= }UQ~9~ GQ|4SyݛpFq|CRj@uj/Zz4CΧeLeٛm .C!*adM:k28y&q&8Vzx %\o*`8 ˜{ח,D.JS S- som?+'&p翄H8bF.-㲷K"}94+!)SqUB54(¸E5??B#3)8>BT[Ohyrx1C޹wA" ~QWbƖkpcS;b>ދR>Iu "f`74%2)(qC:XO H>4`AaPw;Q7niS,WI }py5O5m!$k13--^. ,yKb)qsN|4fxͶ&!>^S5Dԉ49{4LUiP+<3g1T3y`Vٯ M,ߦ5uh*e39CpwD@"Dƛyg Wݶ6vwcTguVJZu?|3.!86vm:;B[?1ϟn6u u JBtͼR'ɑq=4 vtаU+ѣ@4A\-6)( *u1 BES%sBFW ;ؕiy6Z 䪦w{xD۬7\yUXpMFu4\ROt xg;{3><\ԜٕH鹌,*vSk*ʸi7-8)SbJ}fإ&R+ ʖ_>7 ,= a5"UIZkD2)TsSbGX 65^ڑYIZsf+iGp"g'|)2JGUfe]v% )s#,|#(߯rSB#R dтe9^ԪG"LJ2.,ǕR|Fi~JC\|,+A{8Syd +Zq ζH~*K(% Oq])6٧at2MFzW3$Uŧxk ϟ+bUy_MXtxΊs vRO,Ҵ9?FJUtH CA}ŋE] ^UKlFF}^ij9gCsPk ũD`2T*7{-6)#o2=飼gthrE-;%RT0Rp/ 7)a2R5vw*XQ"h9ʑLIPYЍ1s;$R@̺r8漜R mlK7D\(k곹>"H]\5L6W (HT%x=O e*TM؋ASֿ9'. H?J,iC1I]آ/_}+:=p׺ʽ(ق) `nPprD:o2ǟTTAO.䢪1Ub Y:)F~V9uwGMܻ( i!$/2fVFKa5;}v$N*)eo~&) Sָa_EѸc3ǜTf_Q6jIbMymxvjo4)۪8| 1~X]J++s]Qf+!O:=u3( p1d=":axj[=`@Z2+MyG>E^B M RHTQN/Ob\mTVfSHTv9vyk&*~@(/#N_E*KQ wכb-Qstvuaz}g12J M>Y(ʾ|)RaZ⠠)*5%TUb`%(L?ZBb3v7cgNf⽨U-+5*y>9j#z8/AkdoMfL$ #]Y\L}!6-!B7Y ٛz- I `_ =Yo!w\-s X9D7&tk;$u~|?2)Y+N&ba򌑉7p WK2 )GHI@TV諩DI:e@:oP꘎dYU|]KVmsBqB ,ʏkrJyEO? Rf>#GD"^Ӌ!E?=4(uϪ>G2@ J6U,, ¼]8n}!m?WC[7 1=z)}jo'/s7wY5I;".R \ia#Vlw{#|WrH$DNʚ$4B1+77r"6Cwgp{B퀘^>a (QʶjBn29R>OeI܎vjw<9,nmHXv~8EDX0u Kj[kc艈ʲ覩Rx4>P!LW=,ެ8Õ*lr6rY L/w6~WVJ\ o>$H;'qolcl !)?kFh _NObEJ!j+{-[ jcڛ!:l>$Vs()5CYJLvP& ٕ#bn,n[ Npe#Fx ISM=q2R" ӧKሱVÿ$./G7ig]sRS\-gqÊU`aW{gv3 ^XK35E'.!"*2W HmY,K_%C;IY= hHH6Kw\"(ݟ(` -8E^j(#C2BǪvllnZ5ѢS<`P3OzCWՈ+) њJK4FAػj齩LsxՎzQSj Nʛ(xeɯY$|fAlXAӮ1FlxFPbY~/Oєkx^[u-'NkF lgsOQzY}-NNM_fury4F;22 @, +hD]a:0ҕ O0v=\#>nYl/`u"2lu{tL%&D"O:u&O@ zRN;z-`" TЕ_{ʚgAvirjgOYZ+[QmK0AH1]rKdE'ݠԆE5ߖȞF&\g>OB]BBb$3B!<.1FfYRgZne:

C\r?'B8lI S'W$SW^(    q@f^/mk܍+7ؔz)T9 0ٽ>|yCi9hr"W= Jņ8`7,6Y(K4&R[&,+H}_{5?2.VȦL~Ƣ&y?p\"YIo_>qX Myv0 xRQ@0xIohj0yiquաZ =W<-i{rKn Bn|x^nIcOHa@Vѷ A%۽n믤qR u*a7[0=+Ý3t#1ƣWz ,xWoZj[|).I9) B9=JBUh~}W`w#|K*ٗ Yl*^}W4DOQ5go8 +l7l?ηc+bYܹ'.tL;E /;kln>g87)nV#K:n^$=,)| br<1|?)GbK&$m徱& J x. bƬ>J ^@)nSPh9k6l &B,!&vT vWx&KƔS`SF4cXi8w8BЗ4g7Q0g33zmq])t÷mtT&7>_-^ąyȳE`=0~w(Ox '}N TWρrcV;ԩPMHNfzeP+NU9 j:=V Zvc3f &5rd_ Pݔ(ɹ5~[іe\Mii\!rskZm6cQ ӄ3S -0։[e mKΊ#هo m-l+:'3/@{tSތc ,b5 p߇`a+Glߵھ4gT;!@~f*:xK,#n`T?]'T*LJxnr S/r 7lc*K8OoB^(O=c-<=+AM)ƍ?Umf7΁7Vzy>!փX)~PSU#E;&NG/]͗yJ:դnI[ذM)3`l8ʴƦn"=! <67KYv.C1;[b[..eyZER)nUx9`\WO4m&=E2 ,]^.1>ӈ }I49<, =4iwV+ct!}5|W^h0s.bC9}1hr}/4wuEr? Iq$H3_>#nq+gdʱk܇uNKȆ)VҞH/J'I8LyewIi[߇u)5۶$mM Gl'7%mvz]╅W0i!ύ>ɘ~ц1 S1so>CΤӕKT  uLI"8}VDZ`8Lܼ A{*:撙;#\q_ ^nB{.#CIN7֝FI2D_ڑ# QrY|n/!Njo< SZrV{_96v(Y} 4>AAW>SbM U av#YEC쟖IJk@Ĉ#sjmMhzqDD7lOʁTgL#!<<7DBT;c7*ìvì l/3iRJj 7|~EU"@s9riLHv), *DjYgR\6@`hצhsMXs7/c&ݛ*KbE]B8BKi[޸tEg"T_IYih3]+'|uK3CTPЮ)[eBfDE Qܻ(B $Wy b DLU[ X,j5!é Ma=XuiZ!γ sZ5KBKH`I 2=z_.P& fyѬ[@2u5h=ǵ|bRPNqh,a~\fH&g)c?CʬKHXv3zICc&l()ѱo]+>ekSXu,$x"CӞJkxaO4f 7u1s'Gf~0ZtVd?Dn !d ㏷tF'p7CHFm( %jf Z!)Yz$Rg`_%xӳdniFVj.ȿM?'EԁLml}J8ժth #cTP]Ӈ֐zhा!B, 5U{/H X %"5; Zw-:$aF^shjg^8Y \j JGdT6bSM]Pc Iji쀊uT`55PoC+M;SnkXᕓP2vѩb_Bè\.5d8?n]U*e]M!48udKr&)p8;1{X I@xqvul 5doAMqՑjVAEbDZzp?!GUZax3w[9D(v[7W V;%bU GH RSiKG"o̟TH,[v@KMۈu&Cnr5QK/ӾJp[7lo)ŕ&;uaI=`ț {mIK[E~ƾ}TBY5xIl\x!^ Q>Edbo@l_xkZѦה4Ҵ#Jo])% T}k<ҡƞGBDO:]4CFVlw31P;+pˊLT>Јquv)| +(=!Bv&o;'b!9|w|{<M^Ԣ,wfQ>LU$Iux uFn?Z/*f&l}rj;wW>$?tWeͬ{™>j/4m}C͏=9AHmCԪ >P.=wm'W0Ϧ娯јz&3xt)-tkC譙;NPg:i_Ll//h~~w٧V Tg/;I?l>{wXN<^V8xFvHğ\tԙ}9]&=}irWn#xuP_ 7FB%3D[}L~l\ IVPM@ƙX]ማڧ8e GefKn]o&]Iaf 5= "lT)X2 nL[)s0wb*Fո*fmPEMM:%|2ѵ4kѸ|jaN| nrȒ525WPUdnT!";fH׿t]B=q|vuz|$2zгԽ= /Mvz=M=¿:S{LO{vRk* VJd#ºvm<ʩٖ:bZ BJl TGi*,}0#Y$Wá>jehffHrN]O[p/CWk<:UTI.MLG6/M~yf'L"4-Wv@^˖U])M;x.s Z' `-6\"JxqƑ f.^$so㜅_w(]?𭓉IXVQA,i?E{(NTN&%O8[DiB}綺+CKGdV) g'|]c!v=-d@µlMu"?ɦl8RB-JŃʘ|3sH!qSqkWmz}1- Ckƒ%x0m@i9 ]b\$M/YVu:iNlqd"w(Poιzcnj\`wG̃Nt@7o]%h(ӞaK!5l-@zhYGB7 uxY@'@%Бn53Ha"I@K`Y1‡̇HG7%Ύg+Z?2eIjQ&ZNC ʱc[35K@]"K=wQ6k]$̱ـXH ](Dׄ'_PŹ=g-A#UE.be&JhFd| s6[6Kթ91S2G +s3&.fO6K*ZrIਬ]͖\-ׄ|51}/ LP1[G X"D[mLS*\P{"jT-K}gC@7:Rhgqt^x/{k9 ] NZM6H SǛ:NQfbXi<~+CXZ*cwdwGڬ F?s ]~<`~j0(۲,OՒ!0?/{Znr}hR6aҘ!CIZdo#w%Eo_eCvM̱mVv{oXrO?wjѧyqSk=fINEںhCbjVڟ#M<f눫90CؑHM #HȩM8c>XaCm|Yit4Ħ<圕SL&s Z N+%7^I{0j,c#v )}!c!%)n"Ne:&zHnn"R<- g"WcK"llyzRx%GFXK=CZO;mw*(ˊ]HRexkO[{/VZԐiHxsIC|2na˔4\P ~-;>zMC/J[MHR$Rzz _V.3wJ?/D|&J;kz*B}gpd݃;V:C &nչFƲ_K_yAhvPk|~I+j,'Yk$dJWJmcr ^ `iӢ+۹Zj GJms`2aOorC*uc/Hb\_Zr&?XΈk9͹$lZR< 3 m=f=ef݋" []ʩnxr jZFҦ#c|szAf{.Xk7C!PmXa7?TIYk+1IQW#S>Q_IX*;⮈I(馨˛ڲI 8j[ΓXj8Ǘ+-2?rnQ3)%ɨU y9f07|܍|dL|xxƓ!2(9nGb)HlTq2&<"R-:5#ƨvHN55@\ue\VyCp{=0̣94$[WG ǭ1w%alC,zHHM 4wpO.H7t[n-%|moA=z<=M|9'|JV\MfnU?t*yW/$iDNSY!@:fj(Uhyiܥxq*oZm䢐Dk fQ4UɽHkiڠS\Θ"XLM%g$67-5 _ Kk ~-s,9\6v M :xPmcgi )mHK۳D'sijAj|@Ojw~f^ǹRϑmBk*9D+ϖ m /AG;*ՉsS 3\ ܲBsP|k_ `@`#O=X ]|=)ERGCn}kCKs)( KG}P&fc8[1|XPpW>' XYOL5HpkTe=p%>6NL+mE=%w|~˥4ybHWƹDN3Ja yCd%;a?ըaWw:oKp l|m?~hE4St -W!$WQF8u}%^bQ4=tNB܃ÿ7F|27h s/;!hw/5-mÞu_BbKK+c*Bq@J9O򲵹-U9 R 1f̷:b]=I M;q}{ W ۑoI[By27kroxr>i ?,[>L627UB~KI#G#խ >{nH5`]?b,;nC.v3}p uvhUPߑcG`_ks*xh'l"g~6p;eU Uxm:z :8q/# I!QE h ߭hn!d{v<sŒN" sZSeľHSӀ E6t 334_н0IJēw ⃠D C As6{r,bt8oEΎtK&kVn(BV[t}e+ \;OA'@-k]# +t;gEy5Z vտP)HNx̺ C{X^r˓wxRt}4~m3ȜjU5K2ယ`2  hA\}B6Ƽa.J9t`{}-Z\ǀq)wt-[<%wP @kX9Wjݢ6bTy`:Mf1%C8*' ۽Usކ%'Ki!g8nO:9 @, b~,ZWM*h$gO4_5bVV-vHWa0톤OQ>%v2- p:ju \kҎgBGKc}a2F#ތwǍ::S7^c&zc{YDuIkS5@4yD Va` J3u0uBleT׿` <l/Qm+ǹMU]\oh