aboutsummaryrefslogtreecommitdiffstats
path: root/phpBB/phpbb
diff options
context:
space:
mode:
Diffstat (limited to 'phpBB/phpbb')
-rw-r--r--phpBB/phpbb/auth/auth.php1
-rw-r--r--phpBB/phpbb/auth/provider/db.php1
-rw-r--r--phpBB/phpbb/avatar/driver/driver.php7
-rw-r--r--phpBB/phpbb/avatar/driver/gravatar.php12
-rw-r--r--phpBB/phpbb/avatar/driver/local.php8
-rw-r--r--phpBB/phpbb/avatar/driver/remote.php33
-rw-r--r--phpBB/phpbb/avatar/driver/upload.php15
-rw-r--r--phpBB/phpbb/cache/driver/base.php15
-rw-r--r--phpBB/phpbb/cache/driver/dummy.php (renamed from phpBB/phpbb/cache/driver/null.php)8
-rw-r--r--phpBB/phpbb/cache/driver/eaccelerator.php4
-rw-r--r--phpBB/phpbb/cache/driver/file.php43
-rw-r--r--phpBB/phpbb/cache/driver/memory.php12
-rw-r--r--phpBB/phpbb/captcha/plugins/captcha_abstract.php18
-rw-r--r--phpBB/phpbb/captcha/plugins/gd.php16
-rw-r--r--phpBB/phpbb/captcha/plugins/qa.php46
-rw-r--r--phpBB/phpbb/captcha/plugins/recaptcha.php18
-rw-r--r--phpBB/phpbb/composer.json2
-rw-r--r--phpBB/phpbb/console/command/db/migrate.php8
-rw-r--r--phpBB/phpbb/controller/exception.php2
-rw-r--r--phpBB/phpbb/controller/helper.php37
-rw-r--r--phpBB/phpbb/controller/provider.php92
-rw-r--r--phpBB/phpbb/controller/resolver.php18
-rw-r--r--phpBB/phpbb/cron/task/core/prune_forum.php2
-rw-r--r--phpBB/phpbb/cron/task/core/prune_shadow_topics.php2
-rw-r--r--phpBB/phpbb/cron/task/core/tidy_plupload.php8
-rw-r--r--phpBB/phpbb/db/driver/driver.php16
-rw-r--r--phpBB/phpbb/db/driver/mssql.php56
-rw-r--r--phpBB/phpbb/db/driver/mssql_odbc.php32
-rw-r--r--phpBB/phpbb/db/driver/mssqlnative.php34
-rw-r--r--phpBB/phpbb/db/driver/mysql.php56
-rw-r--r--phpBB/phpbb/db/driver/mysqli.php49
-rw-r--r--phpBB/phpbb/db/driver/oracle.php59
-rw-r--r--phpBB/phpbb/db/driver/postgres.php45
-rw-r--r--phpBB/phpbb/db/driver/sqlite.php34
-rw-r--r--phpBB/phpbb/db/driver/sqlite3.php12
-rw-r--r--phpBB/phpbb/db/extractor/base_extractor.php252
-rw-r--r--phpBB/phpbb/db/extractor/exception/extractor_not_initialized_exception.php24
-rw-r--r--phpBB/phpbb/db/extractor/exception/invalid_format_exception.php22
-rw-r--r--phpBB/phpbb/db/extractor/extractor_interface.php80
-rw-r--r--phpBB/phpbb/db/extractor/factory.php79
-rw-r--r--phpBB/phpbb/db/extractor/mssql_extractor.php524
-rw-r--r--phpBB/phpbb/db/extractor/mysql_extractor.php403
-rw-r--r--phpBB/phpbb/db/extractor/oracle_extractor.php265
-rw-r--r--phpBB/phpbb/db/extractor/postgres_extractor.php339
-rw-r--r--phpBB/phpbb/db/extractor/sqlite3_extractor.php151
-rw-r--r--phpBB/phpbb/db/extractor/sqlite_extractor.php149
-rw-r--r--phpBB/phpbb/db/log_wrapper_migrator_output_handler.php11
-rw-r--r--phpBB/phpbb/db/migration/data/v30x/release_3_0_5_rc1.php2
-rw-r--r--phpBB/phpbb/db/migration/data/v30x/release_3_0_9_rc1.php2
-rw-r--r--phpBB/phpbb/db/migration/migration.php6
-rw-r--r--phpBB/phpbb/db/migration/profilefield_base_migration.php1
-rw-r--r--phpBB/phpbb/db/migration/schema_generator.php4
-rw-r--r--phpBB/phpbb/db/migration/tool/module.php4
-rw-r--r--phpBB/phpbb/db/migrator.php4
-rw-r--r--phpBB/phpbb/db/tools/factory.php43
-rw-r--r--phpBB/phpbb/db/tools/mssql.php793
-rw-r--r--phpBB/phpbb/db/tools/postgres.php613
-rw-r--r--phpBB/phpbb/db/tools/tools.php (renamed from phpBB/phpbb/db/tools.php)1009
-rw-r--r--phpBB/phpbb/db/tools/tools_interface.php202
-rw-r--r--phpBB/phpbb/di/container_builder.php160
-rw-r--r--phpBB/phpbb/di/extension/container_configuration.php46
-rw-r--r--phpBB/phpbb/di/extension/core.php98
-rw-r--r--phpBB/phpbb/di/extension/ext.php67
-rw-r--r--phpBB/phpbb/event/kernel_exception_subscriber.php2
-rw-r--r--phpBB/phpbb/event/kernel_request_subscriber.php82
-rw-r--r--phpBB/phpbb/event/kernel_terminate_subscriber.php2
-rw-r--r--phpBB/phpbb/event/md_exporter.php11
-rw-r--r--phpBB/phpbb/extension/di/extension_base.php138
-rw-r--r--phpBB/phpbb/extension/exception.php6
-rw-r--r--phpBB/phpbb/extension/manager.php26
-rw-r--r--phpBB/phpbb/extension/metadata_manager.php26
-rw-r--r--phpBB/phpbb/filesystem.php35
-rw-r--r--phpBB/phpbb/filesystem/exception/filesystem_exception.php42
-rw-r--r--phpBB/phpbb/filesystem/filesystem.php916
-rw-r--r--phpBB/phpbb/filesystem/filesystem_interface.php284
-rw-r--r--phpBB/phpbb/finder.php4
-rw-r--r--phpBB/phpbb/help/controller/help.php160
-rw-r--r--phpBB/phpbb/language/exception/invalid_plural_rule_exception.php22
-rw-r--r--phpBB/phpbb/language/exception/language_exception.php22
-rw-r--r--phpBB/phpbb/language/exception/language_file_not_found.php22
-rw-r--r--phpBB/phpbb/language/language.php571
-rw-r--r--phpBB/phpbb/language/language_file_helper.php71
-rw-r--r--phpBB/phpbb/language/language_file_loader.php212
-rw-r--r--phpBB/phpbb/log/dummy.php (renamed from phpBB/phpbb/log/null.php)4
-rw-r--r--phpBB/phpbb/log/log.php8
-rw-r--r--phpBB/phpbb/log/log_interface.php16
-rw-r--r--phpBB/phpbb/notification/exception.php6
-rw-r--r--phpBB/phpbb/notification/manager.php2
-rw-r--r--phpBB/phpbb/notification/type/admin_activate_user.php6
-rw-r--r--phpBB/phpbb/notification/type/approve_post.php2
-rw-r--r--phpBB/phpbb/notification/type/approve_topic.php2
-rw-r--r--phpBB/phpbb/notification/type/base.php2
-rw-r--r--phpBB/phpbb/notification/type/bookmark.php2
-rw-r--r--phpBB/phpbb/notification/type/disapprove_post.php2
-rw-r--r--phpBB/phpbb/notification/type/disapprove_topic.php2
-rw-r--r--phpBB/phpbb/notification/type/group_request.php6
-rw-r--r--phpBB/phpbb/notification/type/group_request_approved.php4
-rw-r--r--phpBB/phpbb/notification/type/pm.php6
-rw-r--r--phpBB/phpbb/notification/type/post.php6
-rw-r--r--phpBB/phpbb/notification/type/post_in_queue.php2
-rw-r--r--phpBB/phpbb/notification/type/quote.php33
-rw-r--r--phpBB/phpbb/notification/type/report_pm.php4
-rw-r--r--phpBB/phpbb/notification/type/report_post.php2
-rw-r--r--phpBB/phpbb/notification/type/topic.php6
-rw-r--r--phpBB/phpbb/notification/type/topic_in_queue.php2
-rw-r--r--phpBB/phpbb/notification/type/type_interface.php4
-rw-r--r--phpBB/phpbb/path_helper.php6
-rw-r--r--phpBB/phpbb/profilefields/type/type_date.php2
-rw-r--r--phpBB/phpbb/report/controller/report.php319
-rw-r--r--phpBB/phpbb/report/exception/already_reported_exception.php19
-rw-r--r--phpBB/phpbb/report/exception/empty_report_exception.php22
-rw-r--r--phpBB/phpbb/report/exception/entity_not_found_exception.php19
-rw-r--r--phpBB/phpbb/report/exception/factory_invalid_argument_exception.php21
-rw-r--r--phpBB/phpbb/report/exception/invalid_report_exception.php21
-rw-r--r--phpBB/phpbb/report/exception/pm_reporting_disabled_exception.php22
-rw-r--r--phpBB/phpbb/report/exception/report_permission_denied_exception.php19
-rw-r--r--phpBB/phpbb/report/handler_factory.php56
-rw-r--r--phpBB/phpbb/report/report_handler.php104
-rw-r--r--phpBB/phpbb/report/report_handler_interface.php43
-rw-r--r--phpBB/phpbb/report/report_handler_pm.php137
-rw-r--r--phpBB/phpbb/report/report_handler_post.php175
-rw-r--r--phpBB/phpbb/report/report_reason_list_provider.php78
-rw-r--r--phpBB/phpbb/request/deactivated_super_global.php2
-rw-r--r--phpBB/phpbb/routing/router.php343
-rw-r--r--phpBB/phpbb/search/fulltext_mysql.php6
-rw-r--r--phpBB/phpbb/search/fulltext_native.php40
-rw-r--r--phpBB/phpbb/search/fulltext_postgres.php2
-rw-r--r--phpBB/phpbb/search/fulltext_sphinx.php17
-rw-r--r--phpBB/phpbb/session.php37
-rw-r--r--phpBB/phpbb/template/asset.php26
-rw-r--r--phpBB/phpbb/template/exception/user_object_not_available.php22
-rw-r--r--phpBB/phpbb/template/twig/environment.php53
-rw-r--r--phpBB/phpbb/template/twig/extension.php15
-rw-r--r--phpBB/phpbb/template/twig/lexer.php11
-rw-r--r--phpBB/phpbb/template/twig/loader.php22
-rw-r--r--phpBB/phpbb/template/twig/node/includeasset.php2
-rw-r--r--phpBB/phpbb/template/twig/twig.php63
-rw-r--r--phpBB/phpbb/textformatter/cache_interface.php31
-rw-r--r--phpBB/phpbb/textformatter/data_access.php228
-rw-r--r--phpBB/phpbb/textformatter/parser_interface.php112
-rw-r--r--phpBB/phpbb/textformatter/renderer_interface.php92
-rw-r--r--phpBB/phpbb/textformatter/s9e/factory.php545
-rw-r--r--phpBB/phpbb/textformatter/s9e/parser.php390
-rw-r--r--phpBB/phpbb/textformatter/s9e/renderer.php338
-rw-r--r--phpBB/phpbb/textformatter/s9e/utils.php85
-rw-r--r--phpBB/phpbb/textformatter/utils_interface.php56
-rw-r--r--phpBB/phpbb/user.php336
-rw-r--r--phpBB/phpbb/user_loader.php14
-rw-r--r--phpBB/phpbb/viewonline_helper.php6
149 files changed, 10919 insertions, 1995 deletions
diff --git a/phpBB/phpbb/auth/auth.php b/phpBB/phpbb/auth/auth.php
index b59f0e60ec..92c19fd5f7 100644
--- a/phpBB/phpbb/auth/auth.php
+++ b/phpBB/phpbb/auth/auth.php
@@ -929,6 +929,7 @@ class auth
{
global $db, $user, $phpbb_root_path, $phpEx, $phpbb_container;
+ /* @var $provider_collection \phpbb\auth\provider_collection */
$provider_collection = $phpbb_container->get('auth.provider_collection');
$provider = $provider_collection->get_provider();
diff --git a/phpBB/phpbb/auth/provider/db.php b/phpBB/phpbb/auth/provider/db.php
index d8c5fb72de..1adf85ee05 100644
--- a/phpBB/phpbb/auth/provider/db.php
+++ b/phpBB/phpbb/auth/provider/db.php
@@ -155,6 +155,7 @@ class db extends \phpbb\auth\provider\base
// Every auth module is able to define what to do by itself...
if ($show_captcha)
{
+ /* @var $captcha_factory \phpbb\captcha\factory */
$captcha_factory = $this->phpbb_container->get('captcha.factory');
$captcha = $captcha_factory->get_instance($this->config['captcha_plugin']);
$captcha->init(CONFIRM_LOGIN);
diff --git a/phpBB/phpbb/avatar/driver/driver.php b/phpBB/phpbb/avatar/driver/driver.php
index b3ced7edf7..b6fd380bda 100644
--- a/phpBB/phpbb/avatar/driver/driver.php
+++ b/phpBB/phpbb/avatar/driver/driver.php
@@ -30,6 +30,9 @@ abstract class driver implements \phpbb\avatar\driver\driver_interface
*/
protected $config;
+ /** @var \fastImageSize\fastImageSize */
+ protected $imagesize;
+
/**
* Current $phpbb_root_path
* @var string
@@ -73,14 +76,16 @@ abstract class driver implements \phpbb\avatar\driver\driver_interface
* Construct a driver object
*
* @param \phpbb\config\config $config phpBB configuration
+ * @param \fastImageSize\fastImageSize $imagesize fastImageSize class
* @param string $phpbb_root_path Path to the phpBB root
* @param string $php_ext PHP file extension
* @param \phpbb\path_helper $path_helper phpBB path helper
* @param \phpbb\cache\driver\driver_interface $cache Cache driver
*/
- public function __construct(\phpbb\config\config $config, $phpbb_root_path, $php_ext, \phpbb\path_helper $path_helper, \phpbb\cache\driver\driver_interface $cache = null)
+ public function __construct(\phpbb\config\config $config, \fastImageSize\fastImageSize $imagesize, $phpbb_root_path, $php_ext, \phpbb\path_helper $path_helper, \phpbb\cache\driver\driver_interface $cache = null)
{
$this->config = $config;
+ $this->imagesize = $imagesize;
$this->phpbb_root_path = $phpbb_root_path;
$this->php_ext = $php_ext;
$this->path_helper = $path_helper;
diff --git a/phpBB/phpbb/avatar/driver/gravatar.php b/phpBB/phpbb/avatar/driver/gravatar.php
index 2082e0fd02..badbd9421d 100644
--- a/phpBB/phpbb/avatar/driver/gravatar.php
+++ b/phpBB/phpbb/avatar/driver/gravatar.php
@@ -98,8 +98,8 @@ class gravatar extends \phpbb\avatar\driver\driver
return false;
}
- // Make sure getimagesize works...
- if (function_exists('getimagesize') && ($row['avatar_width'] <= 0 || $row['avatar_height'] <= 0))
+ // Get image dimensions if they are not set
+ if ($row['avatar_width'] <= 0 || $row['avatar_height'] <= 0)
{
/**
* default to the minimum of the maximum allowed avatar size if the size
@@ -108,20 +108,20 @@ class gravatar extends \phpbb\avatar\driver\driver
$row['avatar_width'] = $row['avatar_height'] = min($this->config['avatar_max_width'], $this->config['avatar_max_height']);
$url = $this->get_gravatar_url($row);
- if (($row['avatar_width'] <= 0 || $row['avatar_height'] <= 0) && (($image_data = getimagesize($url)) === false))
+ if (($row['avatar_width'] <= 0 || $row['avatar_height'] <= 0) && (($image_data = $this->imagesize->getImageSize($url)) === false))
{
$error[] = 'UNABLE_GET_IMAGE_SIZE';
return false;
}
- if (!empty($image_data) && ($image_data[0] <= 0 || $image_data[1] <= 0))
+ if (!empty($image_data) && ($image_data['width'] <= 0 || $image_data['width'] <= 0))
{
$error[] = 'AVATAR_NO_SIZE';
return false;
}
- $row['avatar_width'] = ($row['avatar_width'] && $row['avatar_height']) ? $row['avatar_width'] : $image_data[0];
- $row['avatar_height'] = ($row['avatar_width'] && $row['avatar_height']) ? $row['avatar_height'] : $image_data[1];
+ $row['avatar_width'] = ($row['avatar_width'] && $row['avatar_height']) ? $row['avatar_width'] : $image_data['width'];
+ $row['avatar_height'] = ($row['avatar_width'] && $row['avatar_height']) ? $row['avatar_height'] : $image_data['height'];
}
if ($row['avatar_width'] <= 0 || $row['avatar_height'] <= 0)
diff --git a/phpBB/phpbb/avatar/driver/local.php b/phpBB/phpbb/avatar/driver/local.php
index 36087f8ba0..88a139f81e 100644
--- a/phpBB/phpbb/avatar/driver/local.php
+++ b/phpBB/phpbb/avatar/driver/local.php
@@ -172,13 +172,15 @@ class local extends \phpbb\avatar\driver\driver
// Match all images in the gallery folder
if (preg_match('#^[^&\'"<>]+\.(?:' . implode('|', $this->allowed_extensions) . ')$#i', $image) && is_file($file_path . '/' . $image))
{
- if (function_exists('getimagesize'))
+ $dims = $this->imagesize->getImageSize($file_path . '/' . $image);
+
+ if ($dims === false)
{
- $dims = getimagesize($file_path . '/' . $image);
+ $dims = array(0, 0);
}
else
{
- $dims = array(0, 0);
+ $dims = array($dims['width'], $dims['height']);
}
$cat = ($path == $file_path) ? $user->lang['NO_AVATAR_CATEGORY'] : str_replace("$path/", '', $file_path);
$avatar_list[$cat][$image] = array(
diff --git a/phpBB/phpbb/avatar/driver/remote.php b/phpBB/phpbb/avatar/driver/remote.php
index 4b0ee3f06f..90443c9b4e 100644
--- a/phpBB/phpbb/avatar/driver/remote.php
+++ b/phpBB/phpbb/avatar/driver/remote.php
@@ -92,25 +92,22 @@ class remote extends \phpbb\avatar\driver\driver
return false;
}
- // Make sure getimagesize works...
- if (function_exists('getimagesize'))
+ // Get image dimensions
+ if (($width <= 0 || $height <= 0) && (($image_data = $this->imagesize->getImageSize($url)) === false))
{
- if (($width <= 0 || $height <= 0) && (($image_data = @getimagesize($url)) === false))
- {
- $error[] = 'UNABLE_GET_IMAGE_SIZE';
- return false;
- }
-
- if (!empty($image_data) && ($image_data[0] <= 0 || $image_data[1] <= 0))
- {
- $error[] = 'AVATAR_NO_SIZE';
- return false;
- }
+ $error[] = 'UNABLE_GET_IMAGE_SIZE';
+ return false;
+ }
- $width = ($width && $height) ? $width : $image_data[0];
- $height = ($width && $height) ? $height : $image_data[1];
+ if (!empty($image_data) && ($image_data['width'] <= 0 || $image_data['height'] <= 0))
+ {
+ $error[] = 'AVATAR_NO_SIZE';
+ return false;
}
+ $width = ($width && $height) ? $width : $image_data['width'];
+ $height = ($width && $height) ? $height : $image_data['height'];
+
if ($width <= 0 || $height <= 0)
{
$error[] = 'AVATAR_NO_SIZE';
@@ -172,15 +169,15 @@ class remote extends \phpbb\avatar\driver\driver
return false;
}
- if (!empty($image_data) && (!isset($types[$image_data[2]]) || !in_array($extension, $types[$image_data[2]])))
+ if (!empty($image_data) && (!isset($types[$image_data['type']]) || !in_array($extension, $types[$image_data['type']])))
{
- if (!isset($types[$image_data[2]]))
+ if (!isset($types[$image_data['type']]))
{
$error[] = 'UNABLE_GET_IMAGE_SIZE';
}
else
{
- $error[] = array('IMAGE_FILETYPE_MISMATCH', $types[$image_data[2]][0], $extension);
+ $error[] = array('IMAGE_FILETYPE_MISMATCH', $types[$image_data['type']][0], $extension);
}
return false;
diff --git a/phpBB/phpbb/avatar/driver/upload.php b/phpBB/phpbb/avatar/driver/upload.php
index ee36243844..4fdaee9561 100644
--- a/phpBB/phpbb/avatar/driver/upload.php
+++ b/phpBB/phpbb/avatar/driver/upload.php
@@ -19,6 +19,11 @@ namespace phpbb\avatar\driver;
class upload extends \phpbb\avatar\driver\driver
{
/**
+ * @var \phpbb\filesystem\filesystem_interface
+ */
+ protected $filesystem;
+
+ /**
* @var \phpbb\mimetype\guesser
*/
protected $mimetype_guesser;
@@ -29,15 +34,17 @@ class upload extends \phpbb\avatar\driver\driver
* @param \phpbb\config\config $config phpBB configuration
* @param string $phpbb_root_path Path to the phpBB root
* @param string $php_ext PHP file extension
- * @param \phpbb_path_helper $path_helper phpBB path helper
+ * @param \phpbb\filesystem\filesystem_interface phpBB filesystem helper
+ * @param \phpbb\path_helper $path_helper phpBB path helper
* @param \phpbb\mimetype\guesser $mimetype_guesser Mimetype guesser
* @param \phpbb\cache\driver\driver_interface $cache Cache driver
*/
- public function __construct(\phpbb\config\config $config, $phpbb_root_path, $php_ext, \phpbb\path_helper $path_helper, \phpbb\mimetype\guesser $mimetype_guesser, \phpbb\cache\driver\driver_interface $cache = null)
+ public function __construct(\phpbb\config\config $config, $phpbb_root_path, $php_ext, \phpbb\filesystem\filesystem_interface $filesystem, \phpbb\path_helper $path_helper, \phpbb\mimetype\guesser $mimetype_guesser, \phpbb\cache\driver\driver_interface $cache = null)
{
$this->config = $config;
$this->phpbb_root_path = $phpbb_root_path;
$this->php_ext = $php_ext;
+ $this->filesystem = $filesystem;
$this->path_helper = $path_helper;
$this->mimetype_guesser = $mimetype_guesser;
$this->cache = $cache;
@@ -90,7 +97,7 @@ class upload extends \phpbb\avatar\driver\driver
include($this->phpbb_root_path . 'includes/functions_upload.' . $this->php_ext);
}
- $upload = new \fileupload('AVATAR_', $this->allowed_extensions, $this->config['avatar_filesize'], $this->config['avatar_min_width'], $this->config['avatar_min_height'], $this->config['avatar_max_width'], $this->config['avatar_max_height'], (isset($this->config['mime_triggers']) ? explode('|', $this->config['mime_triggers']) : false));
+ $upload = new \fileupload($this->filesystem, 'AVATAR_', $this->allowed_extensions, $this->config['avatar_filesize'], $this->config['avatar_min_width'], $this->config['avatar_min_height'], $this->config['avatar_max_width'], $this->config['avatar_max_height'], (isset($this->config['mime_triggers']) ? explode('|', $this->config['mime_triggers']) : false));
$url = $request->variable('avatar_upload_url', '');
$upload_file = $request->file('avatar_upload_file');
@@ -211,6 +218,6 @@ class upload extends \phpbb\avatar\driver\driver
*/
protected function can_upload()
{
- return (file_exists($this->phpbb_root_path . $this->config['avatar_path']) && phpbb_is_writable($this->phpbb_root_path . $this->config['avatar_path']) && (@ini_get('file_uploads') || strtolower(@ini_get('file_uploads')) == 'on'));
+ return (file_exists($this->phpbb_root_path . $this->config['avatar_path']) && $this->filesystem->is_writable($this->phpbb_root_path . $this->config['avatar_path']) && (@ini_get('file_uploads') || strtolower(@ini_get('file_uploads')) == 'on'));
}
}
diff --git a/phpBB/phpbb/cache/driver/base.php b/phpBB/phpbb/cache/driver/base.php
index 4c20ad916d..55cd4668de 100644
--- a/phpBB/phpbb/cache/driver/base.php
+++ b/phpBB/phpbb/cache/driver/base.php
@@ -50,6 +50,7 @@ abstract class base implements \phpbb\cache\driver\driver_interface
}
else if (strpos($filename, 'container_') === 0 ||
strpos($filename, 'url_matcher') === 0 ||
+ strpos($filename, 'url_generator') === 0 ||
strpos($filename, 'sql_') === 0 ||
strpos($filename, 'data_') === 0)
{
@@ -90,14 +91,14 @@ abstract class base implements \phpbb\cache\driver\driver_interface
{
// Remove extra spaces and tabs
$query = preg_replace('/[\n\r\s\t]+/', ' ', $query);
+ $query_id = md5($query);
- if (($rowset = $this->_read('sql_' . md5($query))) === false)
+ if (($result = $this->_read('sql_' . $query_id)) === false)
{
return false;
}
- $query_id = sizeof($this->sql_rowset);
- $this->sql_rowset[$query_id] = $rowset;
+ $this->sql_rowset[$query_id] = $result;
$this->sql_row_pointer[$query_id] = 0;
return $query_id;
@@ -176,13 +177,9 @@ abstract class base implements \phpbb\cache\driver\driver_interface
*/
function remove_file($filename, $check = false)
{
- if (!function_exists('phpbb_is_writable'))
- {
- global $phpbb_root_path, $phpEx;
- include($phpbb_root_path . 'includes/functions.' . $phpEx);
- }
+ global $phpbb_filesystem;
- if ($check && !phpbb_is_writable($this->cache_dir))
+ if ($check && !$phpbb_filesystem->is_writable($this->cache_dir))
{
// E_USER_ERROR - not using language entry - intended.
trigger_error('Unable to remove files within ' . $this->cache_dir . '. Please check directory permissions.', E_USER_ERROR);
diff --git a/phpBB/phpbb/cache/driver/null.php b/phpBB/phpbb/cache/driver/dummy.php
index a45cf97862..1f74f6dd77 100644
--- a/phpBB/phpbb/cache/driver/null.php
+++ b/phpBB/phpbb/cache/driver/dummy.php
@@ -14,9 +14,9 @@
namespace phpbb\cache\driver;
/**
-* ACM Null Caching
+* ACM dummy Caching
*/
-class null extends \phpbb\cache\driver\base
+class dummy extends \phpbb\cache\driver\base
{
/**
* Set cache path
@@ -52,8 +52,10 @@ class null extends \phpbb\cache\driver\base
*/
function tidy()
{
+ global $config;
+
// This cache always has a tidy room.
- set_config('cache_last_gc', time(), true);
+ $config->set('cache_last_gc', time(), false);
}
/**
diff --git a/phpBB/phpbb/cache/driver/eaccelerator.php b/phpBB/phpbb/cache/driver/eaccelerator.php
index 1697758acc..740855144f 100644
--- a/phpBB/phpbb/cache/driver/eaccelerator.php
+++ b/phpBB/phpbb/cache/driver/eaccelerator.php
@@ -44,9 +44,11 @@ class eaccelerator extends \phpbb\cache\driver\memory
*/
function tidy()
{
+ global $config;
+
eaccelerator_gc();
- set_config('cache_last_gc', time(), true);
+ $config->set('cache_last_gc', time(), false);
}
/**
diff --git a/phpBB/phpbb/cache/driver/file.php b/phpBB/phpbb/cache/driver/file.php
index 9a7c4aec7f..bb055d3acf 100644
--- a/phpBB/phpbb/cache/driver/file.php
+++ b/phpBB/phpbb/cache/driver/file.php
@@ -21,14 +21,26 @@ class file extends \phpbb\cache\driver\base
var $var_expires = array();
/**
+ * @var \phpbb\filesystem\filesystem_interface
+ */
+ protected $filesystem;
+
+ /**
* Set cache path
*
* @param string $cache_dir Define the path to the cache directory (default: $phpbb_root_path . 'cache/')
*/
function __construct($cache_dir = null)
{
- global $phpbb_root_path;
- $this->cache_dir = !is_null($cache_dir) ? $cache_dir : $phpbb_root_path . 'cache/';
+ global $phpbb_root_path, $phpbb_container;
+
+ $this->cache_dir = !is_null($cache_dir) ? $cache_dir : $phpbb_root_path . 'cache/' . $phpbb_container->getParameter('core.environment') . '/';
+ $this->filesystem = new \phpbb\filesystem\filesystem();
+
+ if (!is_dir($this->cache_dir))
+ {
+ @mkdir($this->cache_dir, 0777, true);
+ }
}
/**
@@ -63,14 +75,8 @@ class file extends \phpbb\cache\driver\base
if (!$this->_write('data_global'))
{
- if (!function_exists('phpbb_is_writable'))
- {
- global $phpbb_root_path;
- include($phpbb_root_path . 'includes/functions.' . $phpEx);
- }
-
// Now, this occurred how often? ... phew, just tell the user then...
- if (!phpbb_is_writable($this->cache_dir))
+ if (!$this->filesystem->is_writable($this->cache_dir))
{
// We need to use die() here, because else we may encounter an infinite loop (the message handler calls $cache->unload())
die('Fatal: ' . $this->cache_dir . ' is NOT writable.');
@@ -89,7 +95,7 @@ class file extends \phpbb\cache\driver\base
*/
function tidy()
{
- global $phpEx;
+ global $config, $phpEx;
$dir = @opendir($this->cache_dir);
@@ -143,7 +149,7 @@ class file extends \phpbb\cache\driver\base
}
}
- set_config('cache_last_gc', time(), true);
+ $config->set('cache_last_gc', time(), false);
}
/**
@@ -306,7 +312,7 @@ class file extends \phpbb\cache\driver\base
// Remove extra spaces and tabs
$query = preg_replace('/[\n\r\s\t]+/', ' ', $query);
- $query_id = sizeof($this->sql_rowset);
+ $query_id = md5($query);
$this->sql_rowset[$query_id] = array();
$this->sql_row_pointer[$query_id] = 0;
@@ -316,7 +322,7 @@ class file extends \phpbb\cache\driver\base
}
$db->sql_freeresult($query_result);
- if ($this->_write('sql_' . md5($query), $this->sql_rowset[$query_id], $ttl + time(), $query))
+ if ($this->_write('sql_' . $query_id, $this->sql_rowset[$query_id], $ttl + time(), $query))
{
return $query_id;
}
@@ -568,13 +574,14 @@ class file extends \phpbb\cache\driver\base
fclose($handle);
- if (!function_exists('phpbb_chmod'))
+ try
{
- global $phpbb_root_path;
- include($phpbb_root_path . 'includes/functions.' . $phpEx);
+ $this->filesystem->phpbb_chmod($file, CHMOD_READ | CHMOD_WRITE);
+ }
+ catch (\phpbb\filesystem\exception\filesystem_exception $e)
+ {
+ // Do nothing
}
-
- phpbb_chmod($file, CHMOD_READ | CHMOD_WRITE);
$return_value = true;
}
diff --git a/phpBB/phpbb/cache/driver/memory.php b/phpBB/phpbb/cache/driver/memory.php
index 0b0e323e3d..baae22d809 100644
--- a/phpBB/phpbb/cache/driver/memory.php
+++ b/phpBB/phpbb/cache/driver/memory.php
@@ -81,9 +81,10 @@ abstract class memory extends \phpbb\cache\driver\base
*/
function tidy()
{
- // cache has auto GC, no need to have any code here :)
+ global $config;
- set_config('cache_last_gc', time(), true);
+ // cache has auto GC, no need to have any code here :)
+ $config->set('cache_last_gc', time(), false);
}
/**
@@ -203,7 +204,7 @@ abstract class memory extends \phpbb\cache\driver\base
{
// Remove extra spaces and tabs
$query = preg_replace('/[\n\r\s\t]+/', ' ', $query);
- $hash = md5($query);
+ $query_id = md5($query);
// determine which tables this query belongs to
// Some queries use backticks, namely the get_database_size() query
@@ -244,14 +245,13 @@ abstract class memory extends \phpbb\cache\driver\base
$temp = array();
}
- $temp[$hash] = true;
+ $temp[$query_id] = true;
// This must never expire
$this->_write('sql_' . $table_name, $temp, 0);
}
// store them in the right place
- $query_id = sizeof($this->sql_rowset);
$this->sql_rowset[$query_id] = array();
$this->sql_row_pointer[$query_id] = 0;
@@ -261,7 +261,7 @@ abstract class memory extends \phpbb\cache\driver\base
}
$db->sql_freeresult($query_result);
- $this->_write('sql_' . $hash, $this->sql_rowset[$query_id], $ttl);
+ $this->_write('sql_' . $query_id, $this->sql_rowset[$query_id], $ttl);
return $query_id;
}
diff --git a/phpBB/phpbb/captcha/plugins/captcha_abstract.php b/phpBB/phpbb/captcha/plugins/captcha_abstract.php
index 24ed7f939d..b29f144f97 100644
--- a/phpBB/phpbb/captcha/plugins/captcha_abstract.php
+++ b/phpBB/phpbb/captcha/plugins/captcha_abstract.php
@@ -34,12 +34,12 @@ abstract class captcha_abstract
function init($type)
{
- global $config, $db, $user;
+ global $config, $db, $user, $request;
// read input
- $this->confirm_id = request_var('confirm_id', '');
- $this->confirm_code = request_var('confirm_code', '');
- $refresh = request_var('refresh_vc', false) && $config['confirm_refresh'];
+ $this->confirm_id = $request->variable('confirm_id', '');
+ $this->confirm_code = $request->variable('confirm_code', '');
+ $refresh = $request->variable('refresh_vc', false) && $config['confirm_refresh'];
$this->type = (int) $type;
@@ -117,7 +117,7 @@ abstract class captcha_abstract
function get_demo_template($id)
{
- global $config, $user, $template, $phpbb_admin_path, $phpEx;
+ global $config, $user, $template, $request, $phpbb_admin_path, $phpEx;
$variables = '';
@@ -125,7 +125,7 @@ abstract class captcha_abstract
{
foreach ($this->captcha_vars as $captcha_var => $template_var)
{
- $variables .= '&amp;' . rawurlencode($captcha_var) . '=' . request_var($captcha_var, (int) $config[$captcha_var]);
+ $variables .= '&amp;' . rawurlencode($captcha_var) . '=' . $request->variable($captcha_var, (int) $config[$captcha_var]);
}
}
@@ -195,7 +195,7 @@ abstract class captcha_abstract
{
global $config, $db, $user;
- if (empty($user->lang))
+ if (!$user->is_setup())
{
$user->setup();
}
@@ -350,7 +350,9 @@ abstract class captcha_abstract
function is_solved()
{
- if (request_var('confirm_code', false) && $this->solved === 0)
+ global $request;
+
+ if ($request->variable('confirm_code', false) && $this->solved === 0)
{
$this->validate();
}
diff --git a/phpBB/phpbb/captcha/plugins/gd.php b/phpBB/phpbb/captcha/plugins/gd.php
index f6200b5b2f..1727dcc1bb 100644
--- a/phpBB/phpbb/captcha/plugins/gd.php
+++ b/phpBB/phpbb/captcha/plugins/gd.php
@@ -53,7 +53,7 @@ class gd extends captcha_abstract
function acp_page($id, &$module)
{
- global $db, $user, $auth, $template;
+ global $db, $user, $auth, $template, $phpbb_log, $request;
global $config, $phpbb_root_path, $phpbb_admin_path, $phpEx;
$user->add_lang('acp/board');
@@ -70,21 +70,21 @@ class gd extends captcha_abstract
$form_key = 'acp_captcha';
add_form_key($form_key);
- $submit = request_var('submit', '');
+ $submit = $request->variable('submit', '');
if ($submit && check_form_key($form_key))
{
$captcha_vars = array_keys($this->captcha_vars);
foreach ($captcha_vars as $captcha_var)
{
- $value = request_var($captcha_var, 0);
+ $value = $request->variable($captcha_var, 0);
if ($value >= 0)
{
- set_config($captcha_var, $value);
+ $config->set($captcha_var, $value);
}
}
- add_log('admin', 'LOG_CONFIG_VISUAL');
+ $phpbb_log->add('admin', $user->data['user_id'], $user->ip, 'LOG_CONFIG_VISUAL');
trigger_error($user->lang['CONFIG_UPDATED'] . adm_back_link($module->u_action));
}
else if ($submit)
@@ -95,7 +95,7 @@ class gd extends captcha_abstract
{
foreach ($this->captcha_vars as $captcha_var => $template_var)
{
- $var = (isset($_REQUEST[$captcha_var])) ? request_var($captcha_var, 0) : $config[$captcha_var];
+ $var = (isset($_REQUEST[$captcha_var])) ? $request->variable($captcha_var, 0) : $config[$captcha_var];
$template->assign_var($template_var, $var);
}
@@ -109,7 +109,7 @@ class gd extends captcha_abstract
function execute_demo()
{
- global $config;
+ global $config, $request;
$config_old = $config;
@@ -121,7 +121,7 @@ class gd extends captcha_abstract
foreach ($this->captcha_vars as $captcha_var => $template_var)
{
- $config->set($captcha_var, request_var($captcha_var, (int) $config[$captcha_var]));
+ $config->set($captcha_var, $request->variable($captcha_var, (int) $config[$captcha_var]));
}
parent::execute_demo();
$config = $config_old;
diff --git a/phpBB/phpbb/captcha/plugins/qa.php b/phpBB/phpbb/captcha/plugins/qa.php
index 04052b3406..4df8a86432 100644
--- a/phpBB/phpbb/captcha/plugins/qa.php
+++ b/phpBB/phpbb/captcha/plugins/qa.php
@@ -58,14 +58,14 @@ class qa
*/
function init($type)
{
- global $config, $db, $user;
+ global $config, $db, $user, $request;
// load our language file
$user->add_lang('captcha_qa');
// read input
- $this->confirm_id = request_var('qa_confirm_id', '');
- $this->answer = utf8_normalize_nfc(request_var('qa_answer', '', true));
+ $this->confirm_id = $request->variable('qa_confirm_id', '');
+ $this->answer = $request->variable('qa_answer', '', true);
$this->type = (int) $type;
$this->question_lang = $user->lang_name;
@@ -113,9 +113,9 @@ class qa
*/
public function is_installed()
{
- global $db;
+ global $phpbb_container;
- $db_tool = new \phpbb\db\tools($db);
+ $db_tool = $phpbb_container->get('dbal.tools');
return $db_tool->sql_table_exists($this->table_captcha_questions);
}
@@ -306,10 +306,9 @@ class qa
*/
function install()
{
- global $db;
-
- $db_tool = new \phpbb\db\tools($db);
+ global $phpbb_container;
+ $db_tool = $phpbb_container->get('dbal.tools');
$schemas = array(
$this->table_captcha_questions => array (
'COLUMNS' => array(
@@ -350,7 +349,7 @@ class qa
),
);
- foreach($schemas as $table => $schema)
+ foreach ($schemas as $table => $schema)
{
if (!$db_tool->sql_table_exists($table))
{
@@ -542,9 +541,9 @@ class qa
*/
function check_answer()
{
- global $db;
+ global $db, $request;
- $answer = ($this->question_strict) ? utf8_normalize_nfc(request_var('qa_answer', '', true)) : utf8_clean_string(utf8_normalize_nfc(request_var('qa_answer', '', true)));
+ $answer = ($this->question_strict) ? $request->variable('qa_answer', '', true) : utf8_clean_string($request->variable('qa_answer', '', true));
$sql = 'SELECT answer_text
FROM ' . $this->table_captcha_answers . '
@@ -596,7 +595,9 @@ class qa
*/
function is_solved()
{
- if (request_var('qa_answer', false) && $this->solved === 0)
+ global $request;
+
+ if ($request->variable('qa_answer', false) && $this->solved === 0)
{
$this->validate();
}
@@ -609,8 +610,7 @@ class qa
*/
function acp_page($id, &$module)
{
- global $user, $template;
- global $config;
+ global $config, $request, $phpbb_log, $template, $user;
$user->add_lang('acp/board');
$user->add_lang('captcha_qa');
@@ -625,9 +625,9 @@ class qa
$form_key = 'acp_captcha';
add_form_key($form_key);
- $submit = request_var('submit', false);
- $question_id = request_var('question_id', 0);
- $action = request_var('action', '');
+ $submit = $request->variable('submit', false);
+ $question_id = $request->variable('question_id', 0);
+ $action = $request->variable('action', '');
// we have two pages, so users might want to navigate from one to the other
$list_url = $module->u_action . "&amp;configure=1&amp;select_captcha=" . $this->get_service_name();
@@ -732,7 +732,7 @@ class qa
$this->acp_add_question($question_input);
}
- add_log('admin', 'LOG_CONFIG_VISUAL');
+ $phpbb_log->add('admin', $user->data['user_id'], $user->ip, 'LOG_CONFIG_VISUAL');
trigger_error($user->lang['CONFIG_UPDATED'] . adm_back_link($list_url));
}
}
@@ -818,7 +818,9 @@ class qa
*/
function acp_get_question_input()
{
- $answers = utf8_normalize_nfc(request_var('answers', '', true));
+ global $request;
+
+ $answers = $request->variable('answers', '', true);
// Convert answers into array and filter if answers are set
if (strlen($answers))
@@ -829,9 +831,9 @@ class qa
}
$question = array(
- 'question_text' => request_var('question_text', '', true),
- 'strict' => request_var('strict', false),
- 'lang_iso' => request_var('lang_iso', ''),
+ 'question_text' => $request->variable('question_text', '', true),
+ 'strict' => $request->variable('strict', false),
+ 'lang_iso' => $request->variable('lang_iso', ''),
'answers' => $answers,
);
return $question;
diff --git a/phpBB/phpbb/captcha/plugins/recaptcha.php b/phpBB/phpbb/captcha/plugins/recaptcha.php
index 584f3afec1..98132ab47d 100644
--- a/phpBB/phpbb/captcha/plugins/recaptcha.php
+++ b/phpBB/phpbb/captcha/plugins/recaptcha.php
@@ -37,12 +37,12 @@ class recaptcha extends captcha_abstract
function init($type)
{
- global $config, $db, $user;
+ global $config, $db, $user, $request;
$user->add_lang('captcha_recaptcha');
parent::init($type);
- $this->challenge = request_var('recaptcha_challenge_field', '');
- $this->response = request_var('recaptcha_response_field', '');
+ $this->challenge = $request->variable('recaptcha_challenge_field', '');
+ $this->response = $request->variable('recaptcha_response_field', '');
}
public function is_available()
@@ -75,7 +75,7 @@ class recaptcha extends captcha_abstract
function acp_page($id, &$module)
{
- global $config, $db, $template, $user;
+ global $config, $db, $template, $user, $phpbb_log, $request;
$captcha_vars = array(
'recaptcha_pubkey' => 'RECAPTCHA_PUBKEY',
@@ -87,21 +87,21 @@ class recaptcha extends captcha_abstract
$form_key = 'acp_captcha';
add_form_key($form_key);
- $submit = request_var('submit', '');
+ $submit = $request->variable('submit', '');
if ($submit && check_form_key($form_key))
{
$captcha_vars = array_keys($captcha_vars);
foreach ($captcha_vars as $captcha_var)
{
- $value = request_var($captcha_var, '');
+ $value = $request->variable($captcha_var, '');
if ($value)
{
- set_config($captcha_var, $value);
+ $config->set($captcha_var, $value);
}
}
- add_log('admin', 'LOG_CONFIG_VISUAL');
+ $phpbb_log->add('admin', $user->data['user_id'], $user->ip, 'LOG_CONFIG_VISUAL');
trigger_error($user->lang['CONFIG_UPDATED'] . adm_back_link($module->u_action));
}
else if ($submit)
@@ -112,7 +112,7 @@ class recaptcha extends captcha_abstract
{
foreach ($captcha_vars as $captcha_var => $template_var)
{
- $var = (isset($_REQUEST[$captcha_var])) ? request_var($captcha_var, '') : ((isset($config[$captcha_var])) ? $config[$captcha_var] : '');
+ $var = (isset($_REQUEST[$captcha_var])) ? $request->variable($captcha_var, '') : ((isset($config[$captcha_var])) ? $config[$captcha_var] : '');
$template->assign_var($template_var, $var);
}
diff --git a/phpBB/phpbb/composer.json b/phpBB/phpbb/composer.json
index 513d7e4559..175be4b0ab 100644
--- a/phpBB/phpbb/composer.json
+++ b/phpBB/phpbb/composer.json
@@ -22,6 +22,6 @@
"classmap": [""]
},
"require": {
- "php": ">=5.3.3"
+ "php": ">=5.3.9"
}
}
diff --git a/phpBB/phpbb/console/command/db/migrate.php b/phpBB/phpbb/console/command/db/migrate.php
index 87c2a057d1..2490bf1310 100644
--- a/phpBB/phpbb/console/command/db/migrate.php
+++ b/phpBB/phpbb/console/command/db/migrate.php
@@ -35,13 +35,17 @@ class migrate extends \phpbb\console\command\command
/** @var string phpBB root path */
protected $phpbb_root_path;
- function __construct(\phpbb\user $user, \phpbb\db\migrator $migrator, \phpbb\extension\manager $extension_manager, \phpbb\config\config $config, \phpbb\cache\service $cache, \phpbb\log\log $log, $phpbb_root_path)
+ /** @var \phpbb\filesystem\filesystem_interface */
+ protected $filesystem;
+
+ function __construct(\phpbb\user $user, \phpbb\db\migrator $migrator, \phpbb\extension\manager $extension_manager, \phpbb\config\config $config, \phpbb\cache\service $cache, \phpbb\log\log $log, \phpbb\filesystem\filesystem_interface $filesystem, $phpbb_root_path)
{
$this->migrator = $migrator;
$this->extension_manager = $extension_manager;
$this->config = $config;
$this->cache = $cache;
$this->log = $log;
+ $this->filesystem = $filesystem;
$this->phpbb_root_path = $phpbb_root_path;
parent::__construct($user);
$this->user->add_lang(array('common', 'install', 'migrator'));
@@ -57,7 +61,7 @@ class migrate extends \phpbb\console\command\command
protected function execute(InputInterface $input, OutputInterface $output)
{
- $this->migrator->set_output_handler(new \phpbb\db\log_wrapper_migrator_output_handler($this->user, new console_migrator_output_handler($this->user, $output), $this->phpbb_root_path . 'store/migrations_' . time() . '.log'));
+ $this->migrator->set_output_handler(new \phpbb\db\log_wrapper_migrator_output_handler($this->user, new console_migrator_output_handler($this->user, $output), $this->phpbb_root_path . 'store/migrations_' . time() . '.log', $this->filesystem));
$this->migrator->create_migrations_table();
diff --git a/phpBB/phpbb/controller/exception.php b/phpBB/phpbb/controller/exception.php
index 437558b06a..e227c7c37b 100644
--- a/phpBB/phpbb/controller/exception.php
+++ b/phpBB/phpbb/controller/exception.php
@@ -16,6 +16,6 @@ namespace phpbb\controller;
/**
* Controller exception class
*/
-class exception extends \RuntimeException
+class exception extends \phpbb\exception\runtime_exception
{
}
diff --git a/phpBB/phpbb/controller/helper.php b/phpBB/phpbb/controller/helper.php
index a07a396e73..3782512fa4 100644
--- a/phpBB/phpbb/controller/helper.php
+++ b/phpBB/phpbb/controller/helper.php
@@ -15,7 +15,6 @@ namespace phpbb\controller;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
-use Symfony\Component\Routing\Generator\UrlGenerator;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Routing\RequestContext;
@@ -42,6 +41,12 @@ class helper
*/
protected $config;
+ /**
+ * phpBB router
+ * @var \phpbb\routing\router
+ */
+ protected $router;
+
/* @var \phpbb\symfony_request */
protected $symfony_request;
@@ -49,7 +54,7 @@ class helper
protected $request;
/**
- * @var \phpbb\filesystem The filesystem object
+ * @var \phpbb\filesystem\filesystem_interface The filesystem object
*/
protected $filesystem;
@@ -71,26 +76,24 @@ class helper
* @param \phpbb\template\template $template Template object
* @param \phpbb\user $user User object
* @param \phpbb\config\config $config Config object
- * @param \phpbb\controller\provider $provider Path provider
- * @param \phpbb\extension\manager $manager Extension manager object
+ * @param \phpbb\routing\router $router phpBB router
* @param \phpbb\symfony_request $symfony_request Symfony Request object
* @param \phpbb\request\request_interface $request phpBB request object
- * @param \phpbb\filesystem $filesystem The filesystem object
+ * @param \phpbb\filesystem\filesystem_interface $filesystem The filesystem object
* @param string $phpbb_root_path phpBB root path
* @param string $php_ext PHP file extension
*/
- public function __construct(\phpbb\template\template $template, \phpbb\user $user, \phpbb\config\config $config, \phpbb\controller\provider $provider, \phpbb\extension\manager $manager, \phpbb\symfony_request $symfony_request, \phpbb\request\request_interface $request, \phpbb\filesystem $filesystem, $phpbb_root_path, $php_ext)
+ public function __construct(\phpbb\template\template $template, \phpbb\user $user, \phpbb\config\config $config, \phpbb\routing\router $router, \phpbb\symfony_request $symfony_request, \phpbb\request\request_interface $request, \phpbb\filesystem\filesystem_interface $filesystem, $phpbb_root_path, $php_ext)
{
$this->template = $template;
$this->user = $user;
$this->config = $config;
+ $this->router = $router;
$this->symfony_request = $symfony_request;
$this->request = $request;
$this->filesystem = $filesystem;
$this->phpbb_root_path = $phpbb_root_path;
$this->php_ext = $php_ext;
- $provider->find_routing_files($manager->get_finder());
- $this->route_collection = $provider->find($phpbb_root_path)->get_routes();
}
/**
@@ -169,8 +172,8 @@ class helper
$context->setBaseUrl($base_url);
- $url_generator = new UrlGenerator($this->route_collection, $context);
- $route_url = $url_generator->generate($route, $params, $reference_type);
+ $this->router->setContext($context);
+ $route_url = $this->router->generate($route, $params, $reference_type);
if ($is_amp)
{
@@ -241,6 +244,20 @@ class helper
}
/**
+ * Assigns automatic refresh time meta tag in template
+ *
+ * @param int $time time in seconds, when redirection should occur
+ * @param string $url the URL where the user should be redirected
+ * @return null
+ */
+ public function assign_meta_refresh_var($time, $url)
+ {
+ $this->template->assign_vars(array(
+ 'META' => '<meta http-equiv="refresh" content="' . $time . '; url=' . $url . '" />',
+ ));
+ }
+
+ /**
* Return the current url
*
* @return string
diff --git a/phpBB/phpbb/controller/provider.php b/phpBB/phpbb/controller/provider.php
deleted file mode 100644
index 7e26848290..0000000000
--- a/phpBB/phpbb/controller/provider.php
+++ /dev/null
@@ -1,92 +0,0 @@
-<?php
-/**
-*
-* This file is part of the phpBB Forum Software package.
-*
-* @copyright (c) phpBB Limited <https://www.phpbb.com>
-* @license GNU General Public License, version 2 (GPL-2.0)
-*
-* For full copyright and license information, please see
-* the docs/CREDITS.txt file.
-*
-*/
-
-namespace phpbb\controller;
-
-use Symfony\Component\Routing\RouteCollection;
-use Symfony\Component\Routing\Loader\YamlFileLoader;
-use Symfony\Component\Config\FileLocator;
-
-/**
-* Controller interface
-*/
-class provider
-{
- /**
- * YAML file(s) containing route information
- * @var array
- */
- protected $routing_files;
-
- /**
- * Collection of the routes in phpBB and all found extensions
- * @var RouteCollection
- */
- protected $routes;
-
- /**
- * Construct method
- *
- * @param array $routing_files Array of strings containing paths
- * to YAML files holding route information
- */
- public function __construct($routing_files = array())
- {
- $this->routing_files = $routing_files;
- }
-
- /**
- * Find the list of routing files
- *
- * @param \phpbb\finder $finder
- * @return null
- */
- public function find_routing_files(\phpbb\finder $finder)
- {
- // We hardcode the path to the core config directory
- // because the finder cannot find it
- $this->routing_files = array_merge($this->routing_files, array('config/routing.yml'), array_keys($finder
- ->directory('/config')
- ->suffix('routing.yml')
- ->find()
- ));
- }
-
- /**
- * Find a list of controllers
- *
- * @param string $base_path Base path to prepend to file paths
- * @return provider
- */
- public function find($base_path = '')
- {
- $this->routes = new RouteCollection;
- foreach ($this->routing_files as $file_path)
- {
- $loader = new YamlFileLoader(new FileLocator(phpbb_realpath($base_path)));
- $this->routes->addCollection($loader->load($file_path));
- }
-
- return $this;
- }
-
- /**
- * Get the list of routes
- *
- * @return RouteCollection Get the route collection
- */
- public function get_routes()
- {
- return $this->routes;
- }
-}
diff --git a/phpBB/phpbb/controller/resolver.php b/phpBB/phpbb/controller/resolver.php
index 948a6a218c..4f432c3323 100644
--- a/phpBB/phpbb/controller/resolver.php
+++ b/phpBB/phpbb/controller/resolver.php
@@ -23,12 +23,6 @@ use Symfony\Component\HttpFoundation\Request;
class resolver implements ControllerResolverInterface
{
/**
- * User object
- * @var \phpbb\user
- */
- protected $user;
-
- /**
* ContainerInterface object
* @var ContainerInterface
*/
@@ -55,14 +49,12 @@ class resolver implements ControllerResolverInterface
/**
* Construct method
*
- * @param \phpbb\user $user User Object
* @param ContainerInterface $container ContainerInterface object
* @param string $phpbb_root_path Relative path to phpBB root
* @param \phpbb\template\template $template
*/
- public function __construct(\phpbb\user $user, ContainerInterface $container, $phpbb_root_path, \phpbb\template\template $template = null)
+ public function __construct(ContainerInterface $container, $phpbb_root_path, \phpbb\template\template $template = null)
{
- $this->user = $user;
$this->container = $container;
$this->template = $template;
$this->type_cast_helper = new \phpbb\request\type_cast_helper();
@@ -82,20 +74,20 @@ class resolver implements ControllerResolverInterface
if (!$controller)
{
- throw new \phpbb\controller\exception($this->user->lang['CONTROLLER_NOT_SPECIFIED']);
+ throw new \phpbb\controller\exception('CONTROLLER_NOT_SPECIFIED');
}
// Require a method name along with the service name
if (stripos($controller, ':') === false)
{
- throw new \phpbb\controller\exception($this->user->lang['CONTROLLER_METHOD_NOT_SPECIFIED']);
+ throw new \phpbb\controller\exception('CONTROLLER_METHOD_NOT_SPECIFIED');
}
list($service, $method) = explode(':', $controller);
if (!$this->container->has($service))
{
- throw new \phpbb\controller\exception($this->user->lang('CONTROLLER_SERVICE_UNDEFINED', $service));
+ throw new \phpbb\controller\exception('CONTROLLER_SERVICE_UNDEFINED', array($service));
}
$controller_object = $this->container->get($service);
@@ -166,7 +158,7 @@ class resolver implements ControllerResolverInterface
}
else
{
- throw new \phpbb\controller\exception($this->user->lang('CONTROLLER_ARGUMENT_VALUE_MISSING', $param->getPosition() + 1, get_class($object) . ':' . $method, $param->name));
+ throw new \phpbb\controller\exception('CONTROLLER_ARGUMENT_VALUE_MISSING', array($param->getPosition() + 1, get_class($object) . ':' . $method, $param->name));
}
}
diff --git a/phpBB/phpbb/cron/task/core/prune_forum.php b/phpBB/phpbb/cron/task/core/prune_forum.php
index ba68565197..abf91aee19 100644
--- a/phpBB/phpbb/cron/task/core/prune_forum.php
+++ b/phpBB/phpbb/cron/task/core/prune_forum.php
@@ -31,7 +31,7 @@ class prune_forum extends \phpbb\cron\task\base implements \phpbb\cron\task\para
* If $forum_data is given, it is assumed to contain necessary information
* about a single forum that is to be pruned.
*
- * If $forum_data is not given, forum id will be retrieved via request_var
+ * If $forum_data is not given, forum id will be retrieved via $request->variable()
* and a database query will be performed to load the necessary information
* about the forum.
*/
diff --git a/phpBB/phpbb/cron/task/core/prune_shadow_topics.php b/phpBB/phpbb/cron/task/core/prune_shadow_topics.php
index 97a4b0ea86..0ab59f9ed5 100644
--- a/phpBB/phpbb/cron/task/core/prune_shadow_topics.php
+++ b/phpBB/phpbb/cron/task/core/prune_shadow_topics.php
@@ -33,7 +33,7 @@ class prune_shadow_topics extends \phpbb\cron\task\base implements \phpbb\cron\t
* If $forum_data is given, it is assumed to contain necessary information
* about a single forum that is to be pruned.
*
- * If $forum_data is not given, forum id will be retrieved via request_var
+ * If $forum_data is not given, forum id will be retrieved via $request->variable()
* and a database query will be performed to load the necessary information
* about the forum.
*/
diff --git a/phpBB/phpbb/cron/task/core/tidy_plupload.php b/phpBB/phpbb/cron/task/core/tidy_plupload.php
index b6aeecf4b4..d7364374af 100644
--- a/phpBB/phpbb/cron/task/core/tidy_plupload.php
+++ b/phpBB/phpbb/cron/task/core/tidy_plupload.php
@@ -67,6 +67,8 @@ class tidy_plupload extends \phpbb\cron\task\base
*/
public function run()
{
+ global $user, $phpbb_log;
+
// Remove old temporary file (perhaps failed uploads?)
$last_valid_timestamp = time() - $this->max_file_age;
try
@@ -88,13 +90,11 @@ class tidy_plupload extends \phpbb\cron\task\base
}
catch (\UnexpectedValueException $e)
{
- add_log(
- 'critical',
- 'LOG_PLUPLOAD_TIDY_FAILED',
+ $phpbb_log->add('critical', $user->data['user_id'], $user->ip, 'LOG_PLUPLOAD_TIDY_FAILED', false, array(
$this->plupload_upload_path,
$e->getMessage(),
$e->getTraceAsString()
- );
+ ));
}
$this->config->set('plupload_last_gc', time(), true);
diff --git a/phpBB/phpbb/db/driver/driver.php b/phpBB/phpbb/db/driver/driver.php
index 9fc04d47a1..8d360fc3e2 100644
--- a/phpBB/phpbb/db/driver/driver.php
+++ b/phpBB/phpbb/db/driver/driver.php
@@ -271,7 +271,7 @@ abstract class driver implements driver_interface
$query_id = $this->query_result;
}
- if ($query_id !== false)
+ if ($query_id)
{
$result = array();
while ($row = $this->sql_fetchrow($query_id))
@@ -302,7 +302,7 @@ abstract class driver implements driver_interface
return $cache->sql_rowseek($rownum, $query_id);
}
- if ($query_id === false)
+ if (!$query_id)
{
return false;
}
@@ -310,7 +310,7 @@ abstract class driver implements driver_interface
$this->sql_freeresult($query_id);
$query_id = $this->sql_query($this->last_query_text);
- if ($query_id === false)
+ if (!$query_id)
{
return false;
}
@@ -339,7 +339,7 @@ abstract class driver implements driver_interface
$query_id = $this->query_result;
}
- if ($query_id !== false)
+ if ($query_id)
{
if ($rownum !== false)
{
@@ -363,8 +363,8 @@ abstract class driver implements driver_interface
*/
function sql_like_expression($expression)
{
- $expression = utf8_str_replace(array('_', '%'), array("\_", "\%"), $expression);
- $expression = utf8_str_replace(array(chr(0) . "\_", chr(0) . "\%"), array('_', '%'), $expression);
+ $expression = str_replace(array('_', '%'), array("\_", "\%"), $expression);
+ $expression = str_replace(array(chr(0) . "\_", chr(0) . "\%"), array('_', '%'), $expression);
return $this->_sql_like_expression('LIKE \'' . $this->sql_escape($expression) . '\'');
}
@@ -374,8 +374,8 @@ abstract class driver implements driver_interface
*/
function sql_not_like_expression($expression)
{
- $expression = utf8_str_replace(array('_', '%'), array("\_", "\%"), $expression);
- $expression = utf8_str_replace(array(chr(0) . "\_", chr(0) . "\%"), array('_', '%'), $expression);
+ $expression = str_replace(array('_', '%'), array("\_", "\%"), $expression);
+ $expression = str_replace(array(chr(0) . "\_", chr(0) . "\%"), array('_', '%'), $expression);
return $this->_sql_not_like_expression('NOT LIKE \'' . $this->sql_escape($expression) . '\'');
}
diff --git a/phpBB/phpbb/db/driver/mssql.php b/phpBB/phpbb/db/driver/mssql.php
index f9ea884ce2..dfdbfe15e6 100644
--- a/phpBB/phpbb/db/driver/mssql.php
+++ b/phpBB/phpbb/db/driver/mssql.php
@@ -71,8 +71,8 @@ class mssql extends \phpbb\db\driver\driver
$row = false;
if ($result_id)
{
- $row = @mssql_fetch_assoc($result_id);
- @mssql_free_result($result_id);
+ $row = mssql_fetch_assoc($result_id);
+ mssql_free_result($result_id);
}
$this->sql_server_version = ($row) ? trim(implode(' ', $row)) : 0;
@@ -161,12 +161,17 @@ class mssql extends \phpbb\db\driver\driver
$this->sql_time += microtime(true) - $this->curtime;
}
+ if (!$this->query_result)
+ {
+ return false;
+ }
+
if ($cache && $cache_ttl)
{
$this->open_queries[(int) $this->query_result] = $this->query_result;
$this->query_result = $cache->sql_save($this, $query, $this->query_result, $cache_ttl);
}
- else if (strpos($query, 'SELECT') === 0 && $this->query_result)
+ else if (strpos($query, 'SELECT') === 0 && $this->query_result !== true)
{
$this->open_queries[(int) $this->query_result] = $this->query_result;
}
@@ -241,12 +246,12 @@ class mssql extends \phpbb\db\driver\driver
return $cache->sql_fetchrow($query_id);
}
- if ($query_id === false)
+ if (!$query_id || $query_id === true)
{
return false;
}
- $row = @mssql_fetch_assoc($query_id);
+ $row = mssql_fetch_assoc($query_id);
// I hope i am able to remove this later... hopefully only a PHP or MSSQL bug
if ($row)
@@ -272,12 +277,17 @@ class mssql extends \phpbb\db\driver\driver
$query_id = $this->query_result;
}
+ if ($query_id === true)
+ {
+ return false;
+ }
+
if ($cache && $cache->sql_exists($query_id))
{
return $cache->sql_rowseek($rownum, $query_id);
}
- return ($query_id !== false) ? @mssql_data_seek($query_id, $rownum) : false;
+ return ($query_id) ? @mssql_data_seek($query_id, $rownum) : false;
}
/**
@@ -288,12 +298,12 @@ class mssql extends \phpbb\db\driver\driver
$result_id = @mssql_query('SELECT SCOPE_IDENTITY()', $this->db_connect_id);
if ($result_id)
{
- if ($row = @mssql_fetch_assoc($result_id))
+ if ($row = mssql_fetch_assoc($result_id))
{
- @mssql_free_result($result_id);
+ mssql_free_result($result_id);
return $row['computed'];
}
- @mssql_free_result($result_id);
+ mssql_free_result($result_id);
}
return false;
@@ -311,6 +321,11 @@ class mssql extends \phpbb\db\driver\driver
$query_id = $this->query_result;
}
+ if ($query_id === true)
+ {
+ return false;
+ }
+
if ($cache && !is_object($query_id) && $cache->sql_exists($query_id))
{
return $cache->sql_freeresult($query_id);
@@ -319,7 +334,7 @@ class mssql extends \phpbb\db\driver\driver
if (isset($this->open_queries[(int) $query_id]))
{
unset($this->open_queries[(int) $query_id]);
- return @mssql_free_result($query_id);
+ return mssql_free_result($query_id);
}
return false;
@@ -376,9 +391,9 @@ class mssql extends \phpbb\db\driver\driver
$result_id = @mssql_query('SELECT @@ERROR as code', $this->db_connect_id);
if ($result_id)
{
- $row = @mssql_fetch_assoc($result_id);
+ $row = mssql_fetch_assoc($result_id);
$error['code'] = $row['code'];
- @mssql_free_result($result_id);
+ mssql_free_result($result_id);
}
// Get full error message if possible
@@ -389,12 +404,12 @@ class mssql extends \phpbb\db\driver\driver
if ($result_id)
{
- $row = @mssql_fetch_assoc($result_id);
+ $row = mssql_fetch_assoc($result_id);
if (!empty($row['message']))
{
$error['message'] .= '<br />' . $row['message'];
}
- @mssql_free_result($result_id);
+ mssql_free_result($result_id);
}
}
else
@@ -440,13 +455,13 @@ class mssql extends \phpbb\db\driver\driver
if ($result = @mssql_query($query, $this->db_connect_id))
{
@mssql_next_result($result);
- while ($row = @mssql_fetch_row($result))
+ while ($row = mssql_fetch_row($result))
{
$html_table = $this->sql_report('add_select_row', $query, $html_table, $row);
}
}
@mssql_query('SET SHOWPLAN_TEXT OFF;', $this->db_connect_id);
- @mssql_free_result($result);
+ mssql_free_result($result);
if ($html_table)
{
@@ -459,11 +474,14 @@ class mssql extends \phpbb\db\driver\driver
$endtime = $endtime[0] + $endtime[1];
$result = @mssql_query($query, $this->db_connect_id);
- while ($void = @mssql_fetch_assoc($result))
+ if ($result)
{
- // Take the time spent on parsing rows into account
+ while ($void = mssql_fetch_assoc($result))
+ {
+ // Take the time spent on parsing rows into account
+ }
+ mssql_free_result($result);
}
- @mssql_free_result($result);
$splittime = explode(' ', microtime());
$splittime = $splittime[0] + $splittime[1];
diff --git a/phpBB/phpbb/db/driver/mssql_odbc.php b/phpBB/phpbb/db/driver/mssql_odbc.php
index 8e5d4c7a4c..9d9ad603e0 100644
--- a/phpBB/phpbb/db/driver/mssql_odbc.php
+++ b/phpBB/phpbb/db/driver/mssql_odbc.php
@@ -98,8 +98,8 @@ class mssql_odbc extends \phpbb\db\driver\mssql_base
$row = false;
if ($result_id)
{
- $row = @odbc_fetch_array($result_id);
- @odbc_free_result($result_id);
+ $row = odbc_fetch_array($result_id);
+ odbc_free_result($result_id);
}
$this->sql_server_version = ($row) ? trim(implode(' ', $row)) : 0;
@@ -181,12 +181,17 @@ class mssql_odbc extends \phpbb\db\driver\mssql_base
$this->sql_time += microtime(true) - $this->curtime;
}
+ if (!$this->query_result)
+ {
+ return false;
+ }
+
if ($cache && $cache_ttl)
{
$this->open_queries[(int) $this->query_result] = $this->query_result;
$this->query_result = $cache->sql_save($this, $query, $this->query_result, $cache_ttl);
}
- else if (strpos($query, 'SELECT') === 0 && $this->query_result)
+ else if (strpos($query, 'SELECT') === 0)
{
$this->open_queries[(int) $this->query_result] = $this->query_result;
}
@@ -261,7 +266,7 @@ class mssql_odbc extends \phpbb\db\driver\mssql_base
return $cache->sql_fetchrow($query_id);
}
- return ($query_id !== false) ? @odbc_fetch_array($query_id) : false;
+ return ($query_id) ? odbc_fetch_array($query_id) : false;
}
/**
@@ -273,13 +278,13 @@ class mssql_odbc extends \phpbb\db\driver\mssql_base
if ($result_id)
{
- if (@odbc_fetch_array($result_id))
+ if (odbc_fetch_array($result_id))
{
- $id = @odbc_result($result_id, 1);
- @odbc_free_result($result_id);
+ $id = odbc_result($result_id, 1);
+ odbc_free_result($result_id);
return $id;
}
- @odbc_free_result($result_id);
+ odbc_free_result($result_id);
}
return false;
@@ -305,7 +310,7 @@ class mssql_odbc extends \phpbb\db\driver\mssql_base
if (isset($this->open_queries[(int) $query_id]))
{
unset($this->open_queries[(int) $query_id]);
- return @odbc_free_result($query_id);
+ return odbc_free_result($query_id);
}
return false;
@@ -360,11 +365,14 @@ class mssql_odbc extends \phpbb\db\driver\mssql_base
$endtime = $endtime[0] + $endtime[1];
$result = @odbc_exec($this->db_connect_id, $query);
- while ($void = @odbc_fetch_array($result))
+ if ($result)
{
- // Take the time spent on parsing rows into account
+ while ($void = odbc_fetch_array($result))
+ {
+ // Take the time spent on parsing rows into account
+ }
+ odbc_free_result($result);
}
- @odbc_free_result($result);
$splittime = explode(' ', microtime());
$splittime = $splittime[0] + $splittime[1];
diff --git a/phpBB/phpbb/db/driver/mssqlnative.php b/phpBB/phpbb/db/driver/mssqlnative.php
index 46a9b3a477..50dce35baa 100644
--- a/phpBB/phpbb/db/driver/mssqlnative.php
+++ b/phpBB/phpbb/db/driver/mssqlnative.php
@@ -154,12 +154,17 @@ class mssqlnative extends \phpbb\db\driver\mssql_base
$this->sql_time += microtime(true) - $this->curtime;
}
+ if (!$this->query_result)
+ {
+ return false;
+ }
+
if ($cache && $cache_ttl)
{
$this->open_queries[(int) $this->query_result] = $this->query_result;
$this->query_result = $cache->sql_save($this, $query, $this->query_result, $cache_ttl);
}
- else if (strpos($query, 'SELECT') === 0 && $this->query_result)
+ else if (strpos($query, 'SELECT') === 0)
{
$this->open_queries[(int) $this->query_result] = $this->query_result;
}
@@ -242,12 +247,12 @@ class mssqlnative extends \phpbb\db\driver\mssql_base
return $cache->sql_fetchrow($query_id);
}
- if ($query_id === false)
+ if (!$query_id)
{
return false;
}
- $row = @sqlsrv_fetch_array($query_id, SQLSRV_FETCH_ASSOC);
+ $row = sqlsrv_fetch_array($query_id, SQLSRV_FETCH_ASSOC);
if ($row)
{
@@ -272,11 +277,11 @@ class mssqlnative extends \phpbb\db\driver\mssql_base
{
$result_id = @sqlsrv_query($this->db_connect_id, 'SELECT @@IDENTITY');
- if ($result_id !== false)
+ if ($result_id)
{
- $row = @sqlsrv_fetch_array($result_id);
+ $row = sqlsrv_fetch_array($result_id);
$id = $row[0];
- @sqlsrv_free_stmt($result_id);
+ sqlsrv_free_stmt($result_id);
return $id;
}
else
@@ -305,7 +310,7 @@ class mssqlnative extends \phpbb\db\driver\mssql_base
if (isset($this->open_queries[(int) $query_id]))
{
unset($this->open_queries[(int) $query_id]);
- return @sqlsrv_free_stmt($query_id);
+ return sqlsrv_free_stmt($query_id);
}
return false;
@@ -378,14 +383,14 @@ class mssqlnative extends \phpbb\db\driver\mssql_base
@sqlsrv_query($this->db_connect_id, 'SET SHOWPLAN_TEXT ON;');
if ($result = @sqlsrv_query($this->db_connect_id, $query))
{
- @sqlsrv_next_result($result);
- while ($row = @sqlsrv_fetch_array($result))
+ sqlsrv_next_result($result);
+ while ($row = sqlsrv_fetch_array($result))
{
$html_table = $this->sql_report('add_select_row', $query, $html_table, $row);
}
+ sqlsrv_free_stmt($result);
}
@sqlsrv_query($this->db_connect_id, 'SET SHOWPLAN_TEXT OFF;');
- @sqlsrv_free_stmt($result);
if ($html_table)
{
@@ -398,11 +403,14 @@ class mssqlnative extends \phpbb\db\driver\mssql_base
$endtime = $endtime[0] + $endtime[1];
$result = @sqlsrv_query($this->db_connect_id, $query);
- while ($void = @sqlsrv_fetch_array($result))
+ if ($result)
{
- // Take the time spent on parsing rows into account
+ while ($void = sqlsrv_fetch_array($result))
+ {
+ // Take the time spent on parsing rows into account
+ }
+ sqlsrv_free_stmt($result);
}
- @sqlsrv_free_stmt($result);
$splittime = explode(' ', microtime());
$splittime = $splittime[0] + $splittime[1];
diff --git a/phpBB/phpbb/db/driver/mysql.php b/phpBB/phpbb/db/driver/mysql.php
index e93c7239e8..a94e88b331 100644
--- a/phpBB/phpbb/db/driver/mysql.php
+++ b/phpBB/phpbb/db/driver/mysql.php
@@ -70,9 +70,16 @@ class mysql extends \phpbb\db\driver\mysql_base
if (version_compare($this->sql_server_info(true), '5.0.2', '>='))
{
$result = @mysql_query('SELECT @@session.sql_mode AS sql_mode', $this->db_connect_id);
- $row = @mysql_fetch_assoc($result);
- @mysql_free_result($result);
- $modes = array_map('trim', explode(',', $row['sql_mode']));
+ if ($result)
+ {
+ $row = mysql_fetch_assoc($result);
+ mysql_free_result($result);
+ $modes = array_map('trim', explode(',', $row['sql_mode']));
+ }
+ else
+ {
+ $modes = array();
+ }
// TRADITIONAL includes STRICT_ALL_TABLES and STRICT_TRANS_TABLES
if (!in_array('TRADITIONAL', $modes))
@@ -114,14 +121,17 @@ class mysql extends \phpbb\db\driver\mysql_base
if (!$use_cache || empty($cache) || ($this->sql_server_version = $cache->get('mysql_version')) === false)
{
$result = @mysql_query('SELECT VERSION() AS version', $this->db_connect_id);
- $row = @mysql_fetch_assoc($result);
- @mysql_free_result($result);
+ if ($result)
+ {
+ $row = mysql_fetch_assoc($result);
+ mysql_free_result($result);
- $this->sql_server_version = $row['version'];
+ $this->sql_server_version = $row['version'];
- if (!empty($cache) && $use_cache)
- {
- $cache->put('mysql_version', $this->sql_server_version);
+ if (!empty($cache) && $use_cache)
+ {
+ $cache->put('mysql_version', $this->sql_server_version);
+ }
}
}
@@ -190,12 +200,17 @@ class mysql extends \phpbb\db\driver\mysql_base
$this->sql_time += microtime(true) - $this->curtime;
}
+ if (!$this->query_result)
+ {
+ return false;
+ }
+
if ($cache && $cache_ttl)
{
$this->open_queries[(int) $this->query_result] = $this->query_result;
$this->query_result = $cache->sql_save($this, $query, $this->query_result, $cache_ttl);
}
- else if (strpos($query, 'SELECT') === 0 && $this->query_result)
+ else if (strpos($query, 'SELECT') === 0)
{
$this->open_queries[(int) $this->query_result] = $this->query_result;
}
@@ -257,7 +272,7 @@ class mysql extends \phpbb\db\driver\mysql_base
return $cache->sql_fetchrow($query_id);
}
- return ($query_id !== false) ? @mysql_fetch_assoc($query_id) : false;
+ return ($query_id) ? mysql_fetch_assoc($query_id) : false;
}
/**
@@ -308,7 +323,7 @@ class mysql extends \phpbb\db\driver\mysql_base
if (isset($this->open_queries[(int) $query_id]))
{
unset($this->open_queries[(int) $query_id]);
- return @mysql_free_result($query_id);
+ return mysql_free_result($query_id);
}
return false;
@@ -411,12 +426,12 @@ class mysql extends \phpbb\db\driver\mysql_base
if ($result = @mysql_query("EXPLAIN $explain_query", $this->db_connect_id))
{
- while ($row = @mysql_fetch_assoc($result))
+ while ($row = mysql_fetch_assoc($result))
{
$html_table = $this->sql_report('add_select_row', $query, $html_table, $row);
}
+ mysql_free_result($result);
}
- @mysql_free_result($result);
if ($html_table)
{
@@ -431,7 +446,7 @@ class mysql extends \phpbb\db\driver\mysql_base
if ($result = @mysql_query('SHOW PROFILE ALL;', $this->db_connect_id))
{
$this->html_hold .= '<br />';
- while ($row = @mysql_fetch_assoc($result))
+ while ($row = mysql_fetch_assoc($result))
{
// make <unknown> HTML safe
if (!empty($row['Source_function']))
@@ -449,8 +464,8 @@ class mysql extends \phpbb\db\driver\mysql_base
}
$html_table = $this->sql_report('add_select_row', $query, $html_table, $row);
}
+ mysql_free_result($result);
}
- @mysql_free_result($result);
if ($html_table)
{
@@ -468,11 +483,14 @@ class mysql extends \phpbb\db\driver\mysql_base
$endtime = $endtime[0] + $endtime[1];
$result = @mysql_query($query, $this->db_connect_id);
- while ($void = @mysql_fetch_assoc($result))
+ if ($result)
{
- // Take the time spent on parsing rows into account
+ while ($void = mysql_fetch_assoc($result))
+ {
+ // Take the time spent on parsing rows into account
+ }
+ mysql_free_result($result);
}
- @mysql_free_result($result);
$splittime = explode(' ', microtime());
$splittime = $splittime[0] + $splittime[1];
diff --git a/phpBB/phpbb/db/driver/mysqli.php b/phpBB/phpbb/db/driver/mysqli.php
index c0ddfbf76c..d43e201526 100644
--- a/phpBB/phpbb/db/driver/mysqli.php
+++ b/phpBB/phpbb/db/driver/mysqli.php
@@ -74,9 +74,10 @@ class mysqli extends \phpbb\db\driver\mysql_base
if (version_compare($this->sql_server_info(true), '5.0.2', '>='))
{
$result = @mysqli_query($this->db_connect_id, 'SELECT @@session.sql_mode AS sql_mode');
- if ($result !== null)
+ if ($result)
{
- $row = @mysqli_fetch_assoc($result);
+ $row = mysqli_fetch_assoc($result);
+ mysqli_free_result($result);
$modes = array_map('trim', explode(',', $row['sql_mode']));
}
@@ -84,7 +85,6 @@ class mysqli extends \phpbb\db\driver\mysql_base
{
$modes = array();
}
- @mysqli_free_result($result);
// TRADITIONAL includes STRICT_ALL_TABLES and STRICT_TRANS_TABLES
if (!in_array('TRADITIONAL', $modes))
@@ -119,9 +119,10 @@ class mysqli extends \phpbb\db\driver\mysql_base
if (!$use_cache || empty($cache) || ($this->sql_server_version = $cache->get('mysqli_version')) === false)
{
$result = @mysqli_query($this->db_connect_id, 'SELECT VERSION() AS version');
- if ($result !== null)
+ if ($result)
{
- $row = @mysqli_fetch_assoc($result);
+ $row = mysqli_fetch_assoc($result);
+ mysqli_free_result($result);
$this->sql_server_version = $row['version'];
@@ -130,7 +131,6 @@ class mysqli extends \phpbb\db\driver\mysql_base
$cache->put('mysqli_version', $this->sql_server_version);
}
}
- @mysqli_free_result($result);
}
return ($raw) ? $this->sql_server_version : 'MySQL(i) ' . $this->sql_server_version;
@@ -202,6 +202,11 @@ class mysqli extends \phpbb\db\driver\mysql_base
$this->sql_time += microtime(true) - $this->curtime;
}
+ if (!$this->query_result)
+ {
+ return false;
+ }
+
if ($cache && $cache_ttl)
{
$this->query_result = $cache->sql_save($this, $query, $this->query_result, $cache_ttl);
@@ -245,9 +250,9 @@ class mysqli extends \phpbb\db\driver\mysql_base
return $cache->sql_fetchrow($query_id);
}
- if ($query_id !== false && $query_id !== null)
+ if ($query_id)
{
- $result = @mysqli_fetch_assoc($query_id);
+ $result = mysqli_fetch_assoc($query_id);
return $result !== null ? $result : false;
}
@@ -271,7 +276,7 @@ class mysqli extends \phpbb\db\driver\mysql_base
return $cache->sql_rowseek($rownum, $query_id);
}
- return ($query_id !== false) ? @mysqli_data_seek($query_id, $rownum) : false;
+ return ($query_id) ? @mysqli_data_seek($query_id, $rownum) : false;
}
/**
@@ -299,7 +304,17 @@ class mysqli extends \phpbb\db\driver\mysql_base
return $cache->sql_freeresult($query_id);
}
- return @mysqli_free_result($query_id);
+ if (!$query_id)
+ {
+ return false;
+ }
+
+ if ($query_id === true)
+ {
+ return true;
+ }
+
+ return mysqli_free_result($query_id);
}
/**
@@ -398,12 +413,12 @@ class mysqli extends \phpbb\db\driver\mysql_base
if ($result = @mysqli_query($this->db_connect_id, "EXPLAIN $explain_query"))
{
- while ($row = @mysqli_fetch_assoc($result))
+ while ($row = mysqli_fetch_assoc($result))
{
$html_table = $this->sql_report('add_select_row', $query, $html_table, $row);
}
+ mysqli_free_result($result);
}
- @mysqli_free_result($result);
if ($html_table)
{
@@ -418,7 +433,7 @@ class mysqli extends \phpbb\db\driver\mysql_base
if ($result = @mysqli_query($this->db_connect_id, 'SHOW PROFILE ALL;'))
{
$this->html_hold .= '<br />';
- while ($row = @mysqli_fetch_assoc($result))
+ while ($row = mysqli_fetch_assoc($result))
{
// make <unknown> HTML safe
if (!empty($row['Source_function']))
@@ -436,8 +451,8 @@ class mysqli extends \phpbb\db\driver\mysql_base
}
$html_table = $this->sql_report('add_select_row', $query, $html_table, $row);
}
+ mysqli_free_result($result);
}
- @mysqli_free_result($result);
if ($html_table)
{
@@ -455,14 +470,14 @@ class mysqli extends \phpbb\db\driver\mysql_base
$endtime = $endtime[0] + $endtime[1];
$result = @mysqli_query($this->db_connect_id, $query);
- if ($result !== null)
+ if ($result)
{
- while ($void = @mysqli_fetch_assoc($result))
+ while ($void = mysqli_fetch_assoc($result))
{
// Take the time spent on parsing rows into account
}
+ mysqli_free_result($result);
}
- @mysqli_free_result($result);
$splittime = explode(' ', microtime());
$splittime = $splittime[0] + $splittime[1];
diff --git a/phpBB/phpbb/db/driver/oracle.php b/phpBB/phpbb/db/driver/oracle.php
index 6dcab5dd7d..89e1b68aac 100644
--- a/phpBB/phpbb/db/driver/oracle.php
+++ b/phpBB/phpbb/db/driver/oracle.php
@@ -439,12 +439,17 @@ class oracle extends \phpbb\db\driver\driver
$this->sql_time += microtime(true) - $this->curtime;
}
+ if (!$this->query_result)
+ {
+ return false;
+ }
+
if ($cache && $cache_ttl)
{
$this->open_queries[(int) $this->query_result] = $this->query_result;
$this->query_result = $cache->sql_save($this, $query, $this->query_result, $cache_ttl);
}
- else if (strpos($query, 'SELECT') === 0 && $this->query_result)
+ else if (strpos($query, 'SELECT') === 0)
{
$this->open_queries[(int) $this->query_result] = $this->query_result;
}
@@ -499,10 +504,10 @@ class oracle extends \phpbb\db\driver\driver
return $cache->sql_fetchrow($query_id);
}
- if ($query_id !== false)
+ if ($query_id)
{
$row = array();
- $result = @ocifetchinto($query_id, $row, OCI_ASSOC + OCI_RETURN_NULLS);
+ $result = ocifetchinto($query_id, $row, OCI_ASSOC + OCI_RETURN_NULLS);
if (!$result || !$row)
{
@@ -550,7 +555,7 @@ class oracle extends \phpbb\db\driver\driver
return $cache->sql_rowseek($rownum, $query_id);
}
- if ($query_id === false)
+ if (!$query_id)
{
return false;
}
@@ -583,18 +588,24 @@ class oracle extends \phpbb\db\driver\driver
{
$query = 'SELECT ' . $tablename[1] . '_seq.currval FROM DUAL';
$stmt = @ociparse($this->db_connect_id, $query);
- @ociexecute($stmt, OCI_DEFAULT);
+ if ($stmt)
+ {
+ $success = @ociexecute($stmt, OCI_DEFAULT);
- $temp_result = @ocifetchinto($stmt, $temp_array, OCI_ASSOC + OCI_RETURN_NULLS);
- @ocifreestatement($stmt);
+ if ($success)
+ {
+ $temp_result = ocifetchinto($stmt, $temp_array, OCI_ASSOC + OCI_RETURN_NULLS);
+ ocifreestatement($stmt);
- if ($temp_result)
- {
- return $temp_array['CURRVAL'];
- }
- else
- {
- return false;
+ if ($temp_result)
+ {
+ return $temp_array['CURRVAL'];
+ }
+ else
+ {
+ return false;
+ }
+ }
}
}
}
@@ -622,7 +633,7 @@ class oracle extends \phpbb\db\driver\driver
if (isset($this->open_queries[(int) $query_id]))
{
unset($this->open_queries[(int) $query_id]);
- return @ocifreestatement($query_id);
+ return ocifreestatement($query_id);
}
return false;
@@ -787,14 +798,20 @@ class oracle extends \phpbb\db\driver\driver
$endtime = $endtime[0] + $endtime[1];
$result = @ociparse($this->db_connect_id, $query);
- $success = @ociexecute($result, OCI_DEFAULT);
- $row = array();
-
- while (@ocifetchinto($result, $row, OCI_ASSOC + OCI_RETURN_NULLS))
+ if ($result)
{
- // Take the time spent on parsing rows into account
+ $success = @ociexecute($result, OCI_DEFAULT);
+ if ($success)
+ {
+ $row = array();
+
+ while (ocifetchinto($result, $row, OCI_ASSOC + OCI_RETURN_NULLS))
+ {
+ // Take the time spent on parsing rows into account
+ }
+ @ocifreestatement($result);
+ }
}
- @ocifreestatement($result);
$splittime = explode(' ', microtime());
$splittime = $splittime[0] + $splittime[1];
diff --git a/phpBB/phpbb/db/driver/postgres.php b/phpBB/phpbb/db/driver/postgres.php
index a3b9aa4c6b..44476612c3 100644
--- a/phpBB/phpbb/db/driver/postgres.php
+++ b/phpBB/phpbb/db/driver/postgres.php
@@ -123,14 +123,17 @@ class postgres extends \phpbb\db\driver\driver
if (!$use_cache || empty($cache) || ($this->sql_server_version = $cache->get('pgsql_version')) === false)
{
$query_id = @pg_query($this->db_connect_id, 'SELECT VERSION() AS version');
- $row = @pg_fetch_assoc($query_id, null);
- @pg_free_result($query_id);
+ if ($query_id)
+ {
+ $row = pg_fetch_assoc($query_id, null);
+ pg_free_result($query_id);
- $this->sql_server_version = (!empty($row['version'])) ? trim(substr($row['version'], 10)) : 0;
+ $this->sql_server_version = (!empty($row['version'])) ? trim(substr($row['version'], 10)) : 0;
- if (!empty($cache) && $use_cache)
- {
- $cache->put('pgsql_version', $this->sql_server_version);
+ if (!empty($cache) && $use_cache)
+ {
+ $cache->put('pgsql_version', $this->sql_server_version);
+ }
}
}
@@ -200,12 +203,17 @@ class postgres extends \phpbb\db\driver\driver
$this->sql_time += microtime(true) - $this->curtime;
}
+ if (!$this->query_result)
+ {
+ return false;
+ }
+
if ($cache && $cache_ttl)
{
$this->open_queries[(int) $this->query_result] = $this->query_result;
$this->query_result = $cache->sql_save($this, $query, $this->query_result, $cache_ttl);
}
- else if (strpos($query, 'SELECT') === 0 && $this->query_result)
+ else if (strpos($query, 'SELECT') === 0)
{
$this->open_queries[(int) $this->query_result] = $this->query_result;
}
@@ -275,7 +283,7 @@ class postgres extends \phpbb\db\driver\driver
return $cache->sql_fetchrow($query_id);
}
- return ($query_id !== false) ? @pg_fetch_assoc($query_id, null) : false;
+ return ($query_id) ? pg_fetch_assoc($query_id, null) : false;
}
/**
@@ -295,7 +303,7 @@ class postgres extends \phpbb\db\driver\driver
return $cache->sql_rowseek($rownum, $query_id);
}
- return ($query_id !== false) ? @pg_result_seek($query_id, $rownum) : false;
+ return ($query_id) ? @pg_result_seek($query_id, $rownum) : false;
}
/**
@@ -317,8 +325,8 @@ class postgres extends \phpbb\db\driver\driver
return false;
}
- $temp_result = @pg_fetch_assoc($temp_q_id, null);
- @pg_free_result($query_id);
+ $temp_result = pg_fetch_assoc($temp_q_id, null);
+ pg_free_result($query_id);
return ($temp_result) ? $temp_result['last_value'] : false;
}
@@ -347,7 +355,7 @@ class postgres extends \phpbb\db\driver\driver
if (isset($this->open_queries[(int) $query_id]))
{
unset($this->open_queries[(int) $query_id]);
- return @pg_free_result($query_id);
+ return pg_free_result($query_id);
}
return false;
@@ -453,12 +461,12 @@ class postgres extends \phpbb\db\driver\driver
if ($result = @pg_query($this->db_connect_id, "EXPLAIN $explain_query"))
{
- while ($row = @pg_fetch_assoc($result, null))
+ while ($row = pg_fetch_assoc($result, null))
{
$html_table = $this->sql_report('add_select_row', $query, $html_table, $row);
}
+ pg_free_result($result);
}
- @pg_free_result($result);
if ($html_table)
{
@@ -473,11 +481,14 @@ class postgres extends \phpbb\db\driver\driver
$endtime = $endtime[0] + $endtime[1];
$result = @pg_query($this->db_connect_id, $query);
- while ($void = @pg_fetch_assoc($result, null))
+ if ($result)
{
- // Take the time spent on parsing rows into account
+ while ($void = pg_fetch_assoc($result, null))
+ {
+ // Take the time spent on parsing rows into account
+ }
+ pg_free_result($result);
}
- @pg_free_result($result);
$splittime = explode(' ', microtime());
$splittime = $splittime[0] + $splittime[1];
diff --git a/phpBB/phpbb/db/driver/sqlite.php b/phpBB/phpbb/db/driver/sqlite.php
index d5da0e2438..8e205ebb81 100644
--- a/phpBB/phpbb/db/driver/sqlite.php
+++ b/phpBB/phpbb/db/driver/sqlite.php
@@ -70,13 +70,16 @@ class sqlite extends \phpbb\db\driver\driver
if (!$use_cache || empty($cache) || ($this->sql_server_version = $cache->get('sqlite_version')) === false)
{
$result = @sqlite_query('SELECT sqlite_version() AS version', $this->db_connect_id);
- $row = @sqlite_fetch_array($result, SQLITE_ASSOC);
+ if ($result)
+ {
+ $row = sqlite_fetch_array($result, SQLITE_ASSOC);
- $this->sql_server_version = (!empty($row['version'])) ? $row['version'] : 0;
+ $this->sql_server_version = (!empty($row['version'])) ? $row['version'] : 0;
- if (!empty($cache) && $use_cache)
- {
- $cache->put('sqlite_version', $this->sql_server_version);
+ if (!empty($cache) && $use_cache)
+ {
+ $cache->put('sqlite_version', $this->sql_server_version);
+ }
}
}
@@ -145,14 +148,14 @@ class sqlite extends \phpbb\db\driver\driver
$this->sql_time += microtime(true) - $this->curtime;
}
- if ($cache && $cache_ttl)
+ if (!$this->query_result)
{
- $this->open_queries[(int) $this->query_result] = $this->query_result;
- $this->query_result = $cache->sql_save($this, $query, $this->query_result, $cache_ttl);
+ return false;
}
- else if (strpos($query, 'SELECT') === 0 && $this->query_result)
+
+ if ($cache && $cache_ttl)
{
- $this->open_queries[(int) $this->query_result] = $this->query_result;
+ $this->query_result = $cache->sql_save($this, $query, $this->query_result, $cache_ttl);
}
}
else if (defined('DEBUG'))
@@ -211,7 +214,7 @@ class sqlite extends \phpbb\db\driver\driver
return $cache->sql_fetchrow($query_id);
}
- return ($query_id !== false) ? @sqlite_fetch_array($query_id, SQLITE_ASSOC) : false;
+ return ($query_id) ? sqlite_fetch_array($query_id, SQLITE_ASSOC) : false;
}
/**
@@ -231,7 +234,7 @@ class sqlite extends \phpbb\db\driver\driver
return $cache->sql_rowseek($rownum, $query_id);
}
- return ($query_id !== false) ? @sqlite_seek($query_id, $rownum) : false;
+ return ($query_id) ? @sqlite_seek($query_id, $rownum) : false;
}
/**
@@ -362,9 +365,12 @@ class sqlite extends \phpbb\db\driver\driver
$endtime = $endtime[0] + $endtime[1];
$result = @sqlite_query($query, $this->db_connect_id);
- while ($void = @sqlite_fetch_array($result, SQLITE_ASSOC))
+ if ($result)
{
- // Take the time spent on parsing rows into account
+ while ($void = sqlite_fetch_array($result, SQLITE_ASSOC))
+ {
+ // Take the time spent on parsing rows into account
+ }
}
$splittime = explode(' ', microtime());
diff --git a/phpBB/phpbb/db/driver/sqlite3.php b/phpBB/phpbb/db/driver/sqlite3.php
index 4e3e0d3329..f5c2dd225b 100644
--- a/phpBB/phpbb/db/driver/sqlite3.php
+++ b/phpBB/phpbb/db/driver/sqlite3.php
@@ -147,6 +147,11 @@ class sqlite3 extends \phpbb\db\driver\driver
$this->sql_time += microtime(true) - $this->curtime;
}
+ if (!$this->query_result)
+ {
+ return false;
+ }
+
if ($cache && $cache_ttl)
{
$this->query_result = $cache->sql_save($this, $query, $this->query_result, $cache_ttl);
@@ -388,9 +393,12 @@ class sqlite3 extends \phpbb\db\driver\driver
$endtime = $endtime[0] + $endtime[1];
$result = $this->dbo->query($query);
- while ($void = $result->fetchArray(SQLITE3_ASSOC))
+ if ($result)
{
- // Take the time spent on parsing rows into account
+ while ($void = $result->fetchArray(SQLITE3_ASSOC))
+ {
+ // Take the time spent on parsing rows into account
+ }
}
$splittime = explode(' ', microtime());
diff --git a/phpBB/phpbb/db/extractor/base_extractor.php b/phpBB/phpbb/db/extractor/base_extractor.php
new file mode 100644
index 0000000000..547c85f066
--- /dev/null
+++ b/phpBB/phpbb/db/extractor/base_extractor.php
@@ -0,0 +1,252 @@
+<?php
+/**
+*
+* This file is part of the phpBB Forum Software package.
+*
+* @copyright (c) phpBB Limited <https://www.phpbb.com>
+* @license GNU General Public License, version 2 (GPL-2.0)
+*
+* For full copyright and license information, please see
+* the docs/CREDITS.txt file.
+*
+*/
+
+namespace phpbb\db\extractor;
+
+use phpbb\db\extractor\exception\invalid_format_exception;
+use phpbb\db\extractor\exception\extractor_not_initialized_exception;
+
+/**
+ * Abstract base class for database extraction
+ */
+abstract class base_extractor implements extractor_interface
+{
+ /**
+ * @var string phpBB root path
+ */
+ protected $phpbb_root_path;
+
+ /**
+ * @var \phpbb\request\request_interface
+ */
+ protected $request;
+
+ /**
+ * @var \phpbb\db\driver\driver_interface
+ */
+ protected $db;
+
+ /**
+ * @var bool
+ */
+ protected $download;
+
+ /**
+ * @var bool
+ */
+ protected $store;
+
+ /**
+ * @var int
+ */
+ protected $time;
+
+ /**
+ * @var string
+ */
+ protected $format;
+
+ /**
+ * @var resource
+ */
+ protected $fp;
+
+ /**
+ * @var string
+ */
+ protected $write;
+
+ /**
+ * @var string
+ */
+ protected $close;
+
+ /**
+ * @var bool
+ */
+ protected $run_comp;
+
+ /**
+ * @var bool
+ */
+ protected $is_initialized;
+
+ /**
+ * Constructor
+ *
+ * @param string $phpbb_root_path
+ * @param \phpbb\request\request_interface $request
+ * @param \phpbb\db\driver\driver_interface $db
+ */
+ public function __construct($phpbb_root_path, \phpbb\request\request_interface $request, \phpbb\db\driver\driver_interface $db)
+ {
+ $this->phpbb_root_path = $phpbb_root_path;
+ $this->request = $request;
+ $this->db = $db;
+ $this->fp = null;
+
+ $this->is_initialized = false;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function init_extractor($format, $filename, $time, $download = false, $store = false)
+ {
+ $this->download = $download;
+ $this->store = $store;
+ $this->time = $time;
+ $this->format = $format;
+
+ switch ($format)
+ {
+ case 'text':
+ $ext = '.sql';
+ $open = 'fopen';
+ $this->write = 'fwrite';
+ $this->close = 'fclose';
+ $mimetype = 'text/x-sql';
+ break;
+ case 'bzip2':
+ $ext = '.sql.bz2';
+ $open = 'bzopen';
+ $this->write = 'bzwrite';
+ $this->close = 'bzclose';
+ $mimetype = 'application/x-bzip2';
+ break;
+ case 'gzip':
+ $ext = '.sql.gz';
+ $open = 'gzopen';
+ $this->write = 'gzwrite';
+ $this->close = 'gzclose';
+ $mimetype = 'application/x-gzip';
+ break;
+ default:
+ throw new invalid_format_exception();
+ break;
+ }
+
+ if ($download === true)
+ {
+ $name = $filename . $ext;
+ header('Cache-Control: private, no-cache');
+ header("Content-Type: $mimetype; name=\"$name\"");
+ header("Content-disposition: attachment; filename=$name");
+
+ switch ($format)
+ {
+ case 'bzip2':
+ ob_start();
+ break;
+
+ case 'gzip':
+ if (strpos($this->request->header('Accept-Encoding'), 'gzip') !== false && strpos(strtolower($this->request->header('User-Agent')), 'msie') === false)
+ {
+ ob_start('ob_gzhandler');
+ }
+ else
+ {
+ $this->run_comp = true;
+ }
+ break;
+ }
+ }
+
+ if ($store === true)
+ {
+ $file = $this->phpbb_root_path . 'store/' . $filename . $ext;
+
+ $this->fp = $open($file, 'w');
+
+ if (!$this->fp)
+ {
+ trigger_error('FILE_WRITE_FAIL', E_USER_ERROR);
+ }
+ }
+
+ $this->is_initialized = true;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function write_end()
+ {
+ static $close;
+
+ if (!$this->is_initialized)
+ {
+ throw new extractor_not_initialized_exception();
+ }
+
+ if ($this->store)
+ {
+ if ($close === null)
+ {
+ $close = $this->close;
+ }
+ $close($this->fp);
+ }
+
+ // bzip2 must be written all the way at the end
+ if ($this->download && $this->format === 'bzip2')
+ {
+ $c = ob_get_clean();
+ echo bzcompress($c);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function flush($data)
+ {
+ static $write;
+
+ if (!$this->is_initialized)
+ {
+ throw new extractor_not_initialized_exception();
+ }
+
+ if ($this->store === true)
+ {
+ if ($write === null)
+ {
+ $write = $this->write;
+ }
+ $write($this->fp, $data);
+ }
+
+ if ($this->download === true)
+ {
+ if ($this->format === 'bzip2' || $this->format === 'text' || ($this->format === 'gzip' && !$this->run_comp))
+ {
+ echo $data;
+ }
+
+ // we can write the gzip data as soon as we get it
+ if ($this->format === 'gzip')
+ {
+ if ($this->run_comp)
+ {
+ echo gzencode($data);
+ }
+ else
+ {
+ ob_flush();
+ flush();
+ }
+ }
+ }
+ }
+}
diff --git a/phpBB/phpbb/db/extractor/exception/extractor_not_initialized_exception.php b/phpBB/phpbb/db/extractor/exception/extractor_not_initialized_exception.php
new file mode 100644
index 0000000000..62eb434be1
--- /dev/null
+++ b/phpBB/phpbb/db/extractor/exception/extractor_not_initialized_exception.php
@@ -0,0 +1,24 @@
+<?php
+/**
+*
+* This file is part of the phpBB Forum Software package.
+*
+* @copyright (c) phpBB Limited <https://www.phpbb.com>
+* @license GNU General Public License, version 2 (GPL-2.0)
+*
+* For full copyright and license information, please see
+* the docs/CREDITS.txt file.
+*
+*/
+
+namespace phpbb\db\extractor\exception;
+
+use phpbb\exception\runtime_exception;
+
+/**
+* This exception is thrown when invalid format is given to the extractor
+*/
+class extractor_not_initialized_exception extends runtime_exception
+{
+
+}
diff --git a/phpBB/phpbb/db/extractor/exception/invalid_format_exception.php b/phpBB/phpbb/db/extractor/exception/invalid_format_exception.php
new file mode 100644
index 0000000000..6be24cb5dc
--- /dev/null
+++ b/phpBB/phpbb/db/extractor/exception/invalid_format_exception.php
@@ -0,0 +1,22 @@
+<?php
+/**
+*
+* This file is part of the phpBB Forum Software package.
+*
+* @copyright (c) phpBB Limited <https://www.phpbb.com>
+* @license GNU General Public License, version 2 (GPL-2.0)
+*
+* For full copyright and license information, please see
+* the docs/CREDITS.txt file.
+*
+*/
+
+namespace phpbb\db\extractor\exception;
+
+/**
+* This exception is thrown when invalid format is given to the extractor
+*/
+class invalid_format_exception extends \InvalidArgumentException
+{
+
+}
diff --git a/phpBB/phpbb/db/extractor/extractor_interface.php b/phpBB/phpbb/db/extractor/extractor_interface.php
new file mode 100644
index 0000000000..ff45df9bb7
--- /dev/null
+++ b/phpBB/phpbb/db/extractor/extractor_interface.php
@@ -0,0 +1,80 @@
+<?php
+/**
+*
+* This file is part of the phpBB Forum Software package.
+*
+* @copyright (c) phpBB Limited <https://www.phpbb.com>
+* @license GNU General Public License, version 2 (GPL-2.0)
+*
+* For full copyright and license information, please see
+* the docs/CREDITS.txt file.
+*
+*/
+
+namespace phpbb\db\extractor;
+
+/**
+* Database extractor interface
+*/
+interface extractor_interface
+{
+ /**
+ * Start the extraction of the database
+ *
+ * This function initialize the database extraction. It is required to call this
+ * function before calling any other extractor functions.
+ *
+ * @param string $format
+ * @param string $filename
+ * @param int $time
+ * @param bool $download
+ * @param bool $store
+ * @return null
+ * @throws \phpbb\db\extractor\exception\invalid_format_exception when $format is invalid
+ */
+ public function init_extractor($format, $filename, $time, $download = false, $store = false);
+
+ /**
+ * Writes header comments to the database backup
+ *
+ * @param string $table_prefix prefix of phpBB database tables
+ * @return null
+ * @throws \phpbb\db\extractor\exception\extractor_not_initialized_exception when calling this function before init_extractor()
+ */
+ public function write_start($table_prefix);
+
+ /**
+ * Closes file and/or dumps download data
+ *
+ * @return null
+ * @throws \phpbb\db\extractor\exception\extractor_not_initialized_exception when calling this function before init_extractor()
+ */
+ public function write_end();
+
+ /**
+ * Extracts database table structure
+ *
+ * @param string $table_name name of the database table
+ * @return null
+ * @throws \phpbb\db\extractor\exception\extractor_not_initialized_exception when calling this function before init_extractor()
+ */
+ public function write_table($table_name);
+
+ /**
+ * Extracts data from database table
+ *
+ * @param string $table_name name of the database table
+ * @return null
+ * @throws \phpbb\db\extractor\exception\extractor_not_initialized_exception when calling this function before init_extractor()
+ */
+ public function write_data($table_name);
+
+ /**
+ * Writes data to file/download content
+ *
+ * @param string $data
+ * @return null
+ * @throws \phpbb\db\extractor\exception\extractor_not_initialized_exception when calling this function before init_extractor()
+ */
+ public function flush($data);
+}
diff --git a/phpBB/phpbb/db/extractor/factory.php b/phpBB/phpbb/db/extractor/factory.php
new file mode 100644
index 0000000000..a1ffb65595
--- /dev/null
+++ b/phpBB/phpbb/db/extractor/factory.php
@@ -0,0 +1,79 @@
+<?php
+/**
+*
+* This file is part of the phpBB Forum Software package.
+*
+* @copyright (c) phpBB Limited <https://www.phpbb.com>
+* @license GNU General Public License, version 2 (GPL-2.0)
+*
+* For full copyright and license information, please see
+* the docs/CREDITS.txt file.
+*
+*/
+
+namespace phpbb\db\extractor;
+
+/**
+* A factory which serves the suitable extractor instance for the given dbal
+*/
+class factory
+{
+ /**
+ * @var \phpbb\db\driver\driver_interface
+ */
+ protected $db;
+
+ /**
+ * @var \Symfony\Component\DependencyInjection\ContainerInterface
+ */
+ protected $container;
+
+ /**
+ * Extractor factory constructor
+ *
+ * @param \phpbb\db\driver\driver_interface $db
+ * @param \Symfony\Component\DependencyInjection\ContainerInterface $container
+ */
+ public function __construct(\phpbb\db\driver\driver_interface $db, \Symfony\Component\DependencyInjection\ContainerInterface $container)
+ {
+ $this->db = $db;
+ $this->container = $container;
+ }
+
+ /**
+ * DB extractor factory getter
+ *
+ * @return \phpbb\db\extractor\extractor_interface an appropriate instance of the database extractor for the used database driver
+ * @throws \InvalidArgumentException when the database driver is unknown
+ */
+ public function get()
+ {
+ // Return the appropriate DB extractor
+ if ($this->db instanceof \phpbb\db\driver\mssql || $this->db instanceof \phpbb\db\driver\mssql_base)
+ {
+ return $this->container->get('dbal.extractor.extractors.mssql_extractor');
+ }
+ else if ($this->db instanceof \phpbb\db\driver\mysql_base)
+ {
+ return $this->container->get('dbal.extractor.extractors.mysql_extractor');
+ }
+ else if ($this->db instanceof \phpbb\db\driver\oracle)
+ {
+ return $this->container->get('dbal.extractor.extractors.oracle_extractor');
+ }
+ else if ($this->db instanceof \phpbb\db\driver\postgres)
+ {
+ return $this->container->get('dbal.extractor.extractors.postgres_extractor');
+ }
+ else if ($this->db instanceof \phpbb\db\driver\sqlite)
+ {
+ return $this->container->get('dbal.extractor.extractors.sqlite_extractor');
+ }
+ else if ($this->db instanceof \phpbb\db\driver\sqlite3)
+ {
+ return $this->container->get('dbal.extractor.extractors.sqlite3_extractor');
+ }
+
+ throw new \InvalidArgumentException('Invalid database driver given');
+ }
+}
diff --git a/phpBB/phpbb/db/extractor/mssql_extractor.php b/phpBB/phpbb/db/extractor/mssql_extractor.php
new file mode 100644
index 0000000000..d0aa78f1f5
--- /dev/null
+++ b/phpBB/phpbb/db/extractor/mssql_extractor.php
@@ -0,0 +1,524 @@
+<?php
+/**
+*
+* This file is part of the phpBB Forum Software package.
+*
+* @copyright (c) phpBB Limited <https://www.phpbb.com>
+* @license GNU General Public License, version 2 (GPL-2.0)
+*
+* For full copyright and license information, please see
+* the docs/CREDITS.txt file.
+*
+*/
+
+namespace phpbb\db\extractor;
+
+use phpbb\db\extractor\exception\extractor_not_initialized_exception;
+
+class mssql_extractor extends base_extractor
+{
+ /**
+ * Writes closing line(s) to database backup
+ *
+ * @return null
+ * @throws \phpbb\db\extractor\exception\extractor_not_initialized_exception when calling this function before init_extractor()
+ */
+ public function write_end()
+ {
+ if (!$this->is_initialized)
+ {
+ throw new extractor_not_initialized_exception();
+ }
+
+ $this->flush("COMMIT\nGO\n");
+ parent::write_end();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function write_start($table_prefix)
+ {
+ if (!$this->is_initialized)
+ {
+ throw new extractor_not_initialized_exception();
+ }
+
+ $sql_data = "--\n";
+ $sql_data .= "-- phpBB Backup Script\n";
+ $sql_data .= "-- Dump of tables for $table_prefix\n";
+ $sql_data .= "-- DATE : " . gmdate("d-m-Y H:i:s", $this->time) . " GMT\n";
+ $sql_data .= "--\n";
+ $sql_data .= "BEGIN TRANSACTION\n";
+ $sql_data .= "GO\n";
+ $this->flush($sql_data);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function write_table($table_name)
+ {
+ if (!$this->is_initialized)
+ {
+ throw new extractor_not_initialized_exception();
+ }
+
+ $sql_data = '-- Table: ' . $table_name . "\n";
+ $sql_data .= "IF OBJECT_ID(N'$table_name', N'U') IS NOT NULL\n";
+ $sql_data .= "DROP TABLE $table_name;\n";
+ $sql_data .= "GO\n";
+ $sql_data .= "\nCREATE TABLE [$table_name] (\n";
+ $rows = array();
+
+ $text_flag = false;
+
+ $sql = "SELECT COLUMN_NAME, COLUMN_DEFAULT, IS_NULLABLE, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, COLUMNPROPERTY(object_id(TABLE_NAME), COLUMN_NAME, 'IsIdentity') as IS_IDENTITY
+ FROM INFORMATION_SCHEMA.COLUMNS
+ WHERE TABLE_NAME = '$table_name'";
+ $result = $this->db->sql_query($sql);
+
+ while ($row = $this->db->sql_fetchrow($result))
+ {
+ $line = "\t[{$row['COLUMN_NAME']}] [{$row['DATA_TYPE']}]";
+
+ if ($row['DATA_TYPE'] == 'text')
+ {
+ $text_flag = true;
+ }
+
+ if ($row['IS_IDENTITY'])
+ {
+ $line .= ' IDENTITY (1 , 1)';
+ }
+
+ if ($row['CHARACTER_MAXIMUM_LENGTH'] && $row['DATA_TYPE'] !== 'text')
+ {
+ $line .= ' (' . $row['CHARACTER_MAXIMUM_LENGTH'] . ')';
+ }
+
+ if ($row['IS_NULLABLE'] == 'YES')
+ {
+ $line .= ' NULL';
+ }
+ else
+ {
+ $line .= ' NOT NULL';
+ }
+
+ if ($row['COLUMN_DEFAULT'])
+ {
+ $line .= ' DEFAULT ' . $row['COLUMN_DEFAULT'];
+ }
+
+ $rows[] = $line;
+ }
+ $this->db->sql_freeresult($result);
+
+ $sql_data .= implode(",\n", $rows);
+ $sql_data .= "\n) ON [PRIMARY]";
+
+ if ($text_flag)
+ {
+ $sql_data .= " TEXTIMAGE_ON [PRIMARY]";
+ }
+
+ $sql_data .= "\nGO\n\n";
+ $rows = array();
+
+ $sql = "SELECT CONSTRAINT_NAME, COLUMN_NAME
+ FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
+ WHERE TABLE_NAME = '$table_name'";
+ $result = $this->db->sql_query($sql);
+ while ($row = $this->db->sql_fetchrow($result))
+ {
+ if (!sizeof($rows))
+ {
+ $sql_data .= "ALTER TABLE [$table_name] WITH NOCHECK ADD\n";
+ $sql_data .= "\tCONSTRAINT [{$row['CONSTRAINT_NAME']}] PRIMARY KEY CLUSTERED \n\t(\n";
+ }
+ $rows[] = "\t\t[{$row['COLUMN_NAME']}]";
+ }
+ if (sizeof($rows))
+ {
+ $sql_data .= implode(",\n", $rows);
+ $sql_data .= "\n\t) ON [PRIMARY] \nGO\n";
+ }
+ $this->db->sql_freeresult($result);
+
+ $index = array();
+ $sql = "EXEC sp_statistics '$table_name'";
+ $result = $this->db->sql_query($sql);
+ while ($row = $this->db->sql_fetchrow($result))
+ {
+ if ($row['TYPE'] == 3)
+ {
+ $index[$row['INDEX_NAME']][] = '[' . $row['COLUMN_NAME'] . ']';
+ }
+ }
+ $this->db->sql_freeresult($result);
+
+ foreach ($index as $index_name => $column_name)
+ {
+ $index[$index_name] = implode(', ', $column_name);
+ }
+
+ foreach ($index as $index_name => $columns)
+ {
+ $sql_data .= "\nCREATE INDEX [$index_name] ON [$table_name]($columns) ON [PRIMARY]\nGO\n";
+ }
+ $this->flush($sql_data);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function write_data($table_name)
+ {
+ if (!$this->is_initialized)
+ {
+ throw new extractor_not_initialized_exception();
+ }
+
+ if ($this->db->get_sql_layer() === 'mssql')
+ {
+ $this->write_data_mssql($table_name);
+ }
+ else if($this->db->get_sql_layer() === 'mssqlnative')
+ {
+ $this->write_data_mssqlnative($table_name);
+ }
+ else
+ {
+ $this->write_data_odbc($table_name);
+ }
+ }
+
+ /**
+ * Extracts data from database table (for MSSQL driver)
+ *
+ * @param string $table_name name of the database table
+ * @return null
+ * @throws \phpbb\db\extractor\exception\extractor_not_initialized_exception when calling this function before init_extractor()
+ */
+ protected function write_data_mssql($table_name)
+ {
+ if (!$this->is_initialized)
+ {
+ throw new extractor_not_initialized_exception();
+ }
+
+ $ary_type = $ary_name = array();
+ $ident_set = false;
+ $sql_data = '';
+
+ // Grab all of the data from current table.
+ $sql = "SELECT *
+ FROM $table_name";
+ $result = $this->db->sql_query($sql);
+
+ $retrieved_data = mssql_num_rows($result);
+
+ $i_num_fields = mssql_num_fields($result);
+
+ for ($i = 0; $i < $i_num_fields; $i++)
+ {
+ $ary_type[$i] = mssql_field_type($result, $i);
+ $ary_name[$i] = mssql_field_name($result, $i);
+ }
+
+ if ($retrieved_data)
+ {
+ $sql = "SELECT 1 as has_identity
+ FROM INFORMATION_SCHEMA.COLUMNS
+ WHERE COLUMNPROPERTY(object_id('$table_name'), COLUMN_NAME, 'IsIdentity') = 1";
+ $result2 = $this->db->sql_query($sql);
+ $row2 = $this->db->sql_fetchrow($result2);
+ if (!empty($row2['has_identity']))
+ {
+ $sql_data .= "\nSET IDENTITY_INSERT $table_name ON\nGO\n";
+ $ident_set = true;
+ }
+ $this->db->sql_freeresult($result2);
+ }
+
+ while ($row = $this->db->sql_fetchrow($result))
+ {
+ $schema_vals = $schema_fields = array();
+
+ // Build the SQL statement to recreate the data.
+ for ($i = 0; $i < $i_num_fields; $i++)
+ {
+ $str_val = $row[$ary_name[$i]];
+
+ if (preg_match('#char|text|bool|varbinary#i', $ary_type[$i]))
+ {
+ $str_quote = '';
+ $str_empty = "''";
+ $str_val = sanitize_data_mssql(str_replace("'", "''", $str_val));
+ }
+ else if (preg_match('#date|timestamp#i', $ary_type[$i]))
+ {
+ if (empty($str_val))
+ {
+ $str_quote = '';
+ }
+ else
+ {
+ $str_quote = "'";
+ }
+ }
+ else
+ {
+ $str_quote = '';
+ $str_empty = 'NULL';
+ }
+
+ if (empty($str_val) && $str_val !== '0' && !(is_int($str_val) || is_float($str_val)))
+ {
+ $str_val = $str_empty;
+ }
+
+ $schema_vals[$i] = $str_quote . $str_val . $str_quote;
+ $schema_fields[$i] = $ary_name[$i];
+ }
+
+ // Take the ordered fields and their associated data and build it
+ // into a valid sql statement to recreate that field in the data.
+ $sql_data .= "INSERT INTO $table_name (" . implode(', ', $schema_fields) . ') VALUES (' . implode(', ', $schema_vals) . ");\nGO\n";
+
+ $this->flush($sql_data);
+ $sql_data = '';
+ }
+ $this->db->sql_freeresult($result);
+
+ if ($retrieved_data && $ident_set)
+ {
+ $sql_data .= "\nSET IDENTITY_INSERT $table_name OFF\nGO\n";
+ }
+ $this->flush($sql_data);
+ }
+
+ /**
+ * Extracts data from database table (for MSSQL Native driver)
+ *
+ * @param string $table_name name of the database table
+ * @return null
+ * @throws \phpbb\db\extractor\exception\extractor_not_initialized_exception when calling this function before init_extractor()
+ */
+ protected function write_data_mssqlnative($table_name)
+ {
+ if (!$this->is_initialized)
+ {
+ throw new extractor_not_initialized_exception();
+ }
+
+ $ary_type = $ary_name = array();
+ $ident_set = false;
+ $sql_data = '';
+
+ // Grab all of the data from current table.
+ $sql = "SELECT * FROM $table_name";
+ $this->db->mssqlnative_set_query_options(array('Scrollable' => SQLSRV_CURSOR_STATIC));
+ $result = $this->db->sql_query($sql);
+
+ $retrieved_data = $this->db->mssqlnative_num_rows($result);
+
+ if (!$retrieved_data)
+ {
+ $this->db->sql_freeresult($result);
+ return;
+ }
+
+ $sql = "SELECT COLUMN_NAME, DATA_TYPE
+ FROM INFORMATION_SCHEMA.COLUMNS
+ WHERE INFORMATION_SCHEMA.COLUMNS.TABLE_NAME = '" . $this->db->sql_escape($table_name) . "'";
+ $result_fields = $this->db->sql_query($sql);
+
+ $i_num_fields = 0;
+ while ($row = $this->db->sql_fetchrow($result_fields))
+ {
+ $ary_type[$i_num_fields] = $row['DATA_TYPE'];
+ $ary_name[$i_num_fields] = $row['COLUMN_NAME'];
+ $i_num_fields++;
+ }
+ $this->db->sql_freeresult($result_fields);
+
+ $sql = "SELECT 1 as has_identity
+ FROM INFORMATION_SCHEMA.COLUMNS
+ WHERE COLUMNPROPERTY(object_id('$table_name'), COLUMN_NAME, 'IsIdentity') = 1";
+ $result2 = $this->db->sql_query($sql);
+ $row2 = $this->db->sql_fetchrow($result2);
+
+ if (!empty($row2['has_identity']))
+ {
+ $sql_data .= "\nSET IDENTITY_INSERT $table_name ON\nGO\n";
+ $ident_set = true;
+ }
+ $this->db->sql_freeresult($result2);
+
+ while ($row = $this->db->sql_fetchrow($result))
+ {
+ $schema_vals = $schema_fields = array();
+
+ // Build the SQL statement to recreate the data.
+ for ($i = 0; $i < $i_num_fields; $i++)
+ {
+ $str_val = $row[$ary_name[$i]];
+
+ // defaults to type number - better quote just to be safe, so check for is_int too
+ if (is_int($ary_type[$i]) || preg_match('#char|text|bool|varbinary#i', $ary_type[$i]))
+ {
+ $str_quote = '';
+ $str_empty = "''";
+ $str_val = sanitize_data_mssql(str_replace("'", "''", $str_val));
+ }
+ else if (preg_match('#date|timestamp#i', $ary_type[$i]))
+ {
+ if (empty($str_val))
+ {
+ $str_quote = '';
+ }
+ else
+ {
+ $str_quote = "'";
+ }
+ }
+ else
+ {
+ $str_quote = '';
+ $str_empty = 'NULL';
+ }
+
+ if (empty($str_val) && $str_val !== '0' && !(is_int($str_val) || is_float($str_val)))
+ {
+ $str_val = $str_empty;
+ }
+
+ $schema_vals[$i] = $str_quote . $str_val . $str_quote;
+ $schema_fields[$i] = $ary_name[$i];
+ }
+
+ // Take the ordered fields and their associated data and build it
+ // into a valid sql statement to recreate that field in the data.
+ $sql_data .= "INSERT INTO $table_name (" . implode(', ', $schema_fields) . ') VALUES (' . implode(', ', $schema_vals) . ");\nGO\n";
+
+ $this->flush($sql_data);
+ $sql_data = '';
+ }
+ $this->db->sql_freeresult($result);
+
+ if ($ident_set)
+ {
+ $sql_data .= "\nSET IDENTITY_INSERT $table_name OFF\nGO\n";
+ }
+ $this->flush($sql_data);
+ }
+
+ /**
+ * Extracts data from database table (for ODBC driver)
+ *
+ * @param string $table_name name of the database table
+ * @return null
+ * @throws \phpbb\db\extractor\exception\extractor_not_initialized_exception when calling this function before init_extractor()
+ */
+ protected function write_data_odbc($table_name)
+ {
+ if (!$this->is_initialized)
+ {
+ throw new extractor_not_initialized_exception();
+ }
+
+ $ary_type = $ary_name = array();
+ $ident_set = false;
+ $sql_data = '';
+
+ // Grab all of the data from current table.
+ $sql = "SELECT *
+ FROM $table_name";
+ $result = $this->db->sql_query($sql);
+
+ $retrieved_data = odbc_num_rows($result);
+
+ if ($retrieved_data)
+ {
+ $sql = "SELECT 1 as has_identity
+ FROM INFORMATION_SCHEMA.COLUMNS
+ WHERE COLUMNPROPERTY(object_id('$table_name'), COLUMN_NAME, 'IsIdentity') = 1";
+ $result2 = $this->db->sql_query($sql);
+ $row2 = $this->db->sql_fetchrow($result2);
+ if (!empty($row2['has_identity']))
+ {
+ $sql_data .= "\nSET IDENTITY_INSERT $table_name ON\nGO\n";
+ $ident_set = true;
+ }
+ $this->db->sql_freeresult($result2);
+ }
+
+ $i_num_fields = odbc_num_fields($result);
+
+ for ($i = 0; $i < $i_num_fields; $i++)
+ {
+ $ary_type[$i] = odbc_field_type($result, $i + 1);
+ $ary_name[$i] = odbc_field_name($result, $i + 1);
+ }
+
+ while ($row = $this->db->sql_fetchrow($result))
+ {
+ $schema_vals = $schema_fields = array();
+
+ // Build the SQL statement to recreate the data.
+ for ($i = 0; $i < $i_num_fields; $i++)
+ {
+ $str_val = $row[$ary_name[$i]];
+
+ if (preg_match('#char|text|bool|varbinary#i', $ary_type[$i]))
+ {
+ $str_quote = '';
+ $str_empty = "''";
+ $str_val = sanitize_data_mssql(str_replace("'", "''", $str_val));
+ }
+ else if (preg_match('#date|timestamp#i', $ary_type[$i]))
+ {
+ if (empty($str_val))
+ {
+ $str_quote = '';
+ }
+ else
+ {
+ $str_quote = "'";
+ }
+ }
+ else
+ {
+ $str_quote = '';
+ $str_empty = 'NULL';
+ }
+
+ if (empty($str_val) && $str_val !== '0' && !(is_int($str_val) || is_float($str_val)))
+ {
+ $str_val = $str_empty;
+ }
+
+ $schema_vals[$i] = $str_quote . $str_val . $str_quote;
+ $schema_fields[$i] = $ary_name[$i];
+ }
+
+ // Take the ordered fields and their associated data and build it
+ // into a valid sql statement to recreate that field in the data.
+ $sql_data .= "INSERT INTO $table_name (" . implode(', ', $schema_fields) . ') VALUES (' . implode(', ', $schema_vals) . ");\nGO\n";
+
+ $this->flush($sql_data);
+
+ $sql_data = '';
+
+ }
+ $this->db->sql_freeresult($result);
+
+ if ($retrieved_data && $ident_set)
+ {
+ $sql_data .= "\nSET IDENTITY_INSERT $table_name OFF\nGO\n";
+ }
+ $this->flush($sql_data);
+ }
+}
diff --git a/phpBB/phpbb/db/extractor/mysql_extractor.php b/phpBB/phpbb/db/extractor/mysql_extractor.php
new file mode 100644
index 0000000000..34e309c19e
--- /dev/null
+++ b/phpBB/phpbb/db/extractor/mysql_extractor.php
@@ -0,0 +1,403 @@
+<?php
+/**
+*
+* This file is part of the phpBB Forum Software package.
+*
+* @copyright (c) phpBB Limited <https://www.phpbb.com>
+* @license GNU General Public License, version 2 (GPL-2.0)
+*
+* For full copyright and license information, please see
+* the docs/CREDITS.txt file.
+*
+*/
+
+namespace phpbb\db\extractor;
+
+use phpbb\db\extractor\exception\extractor_not_initialized_exception;
+
+class mysql_extractor extends base_extractor
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function write_start($table_prefix)
+ {
+ if (!$this->is_initialized)
+ {
+ throw new extractor_not_initialized_exception();
+ }
+
+ $sql_data = "#\n";
+ $sql_data .= "# phpBB Backup Script\n";
+ $sql_data .= "# Dump of tables for $table_prefix\n";
+ $sql_data .= "# DATE : " . gmdate("d-m-Y H:i:s", $this->time) . " GMT\n";
+ $sql_data .= "#\n";
+ $this->flush($sql_data);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function write_table($table_name)
+ {
+ static $new_extract;
+
+ if (!$this->is_initialized)
+ {
+ throw new extractor_not_initialized_exception();
+ }
+
+ if ($new_extract === null)
+ {
+ if ($this->db->get_sql_layer() === 'mysqli' || version_compare($this->db->sql_server_info(true), '3.23.20', '>='))
+ {
+ $new_extract = true;
+ }
+ else
+ {
+ $new_extract = false;
+ }
+ }
+
+ if ($new_extract)
+ {
+ $this->new_write_table($table_name);
+ }
+ else
+ {
+ $this->old_write_table($table_name);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function write_data($table_name)
+ {
+ if (!$this->is_initialized)
+ {
+ throw new extractor_not_initialized_exception();
+ }
+
+ if ($this->db->get_sql_layer() === 'mysqli')
+ {
+ $this->write_data_mysqli($table_name);
+ }
+ else
+ {
+ $this->write_data_mysql($table_name);
+ }
+ }
+
+ /**
+ * Extracts data from database table (for MySQLi driver)
+ *
+ * @param string $table_name name of the database table
+ * @return null
+ * @throws \phpbb\db\extractor\exception\extractor_not_initialized_exception when calling this function before init_extractor()
+ */
+ protected function write_data_mysqli($table_name)
+ {
+ if (!$this->is_initialized)
+ {
+ throw new extractor_not_initialized_exception();
+ }
+
+ $sql = "SELECT *
+ FROM $table_name";
+ $result = mysqli_query($this->db->get_db_connect_id(), $sql, MYSQLI_USE_RESULT);
+ if ($result != false)
+ {
+ $fields_cnt = mysqli_num_fields($result);
+
+ // Get field information
+ $field = mysqli_fetch_fields($result);
+ $field_set = array();
+
+ for ($j = 0; $j < $fields_cnt; $j++)
+ {
+ $field_set[] = $field[$j]->name;
+ }
+
+ $search = array("\\", "'", "\x00", "\x0a", "\x0d", "\x1a", '"');
+ $replace = array("\\\\", "\\'", '\0', '\n', '\r', '\Z', '\\"');
+ $fields = implode(', ', $field_set);
+ $sql_data = 'INSERT INTO ' . $table_name . ' (' . $fields . ') VALUES ';
+ $first_set = true;
+ $query_len = 0;
+ $max_len = get_usable_memory();
+
+ while ($row = mysqli_fetch_row($result))
+ {
+ $values = array();
+ if ($first_set)
+ {
+ $query = $sql_data . '(';
+ }
+ else
+ {
+ $query .= ',(';
+ }
+
+ for ($j = 0; $j < $fields_cnt; $j++)
+ {
+ if (!isset($row[$j]) || is_null($row[$j]))
+ {
+ $values[$j] = 'NULL';
+ }
+ else if (($field[$j]->flags & 32768) && !($field[$j]->flags & 1024))
+ {
+ $values[$j] = $row[$j];
+ }
+ else
+ {
+ $values[$j] = "'" . str_replace($search, $replace, $row[$j]) . "'";
+ }
+ }
+ $query .= implode(', ', $values) . ')';
+
+ $query_len += strlen($query);
+ if ($query_len > $max_len)
+ {
+ $this->flush($query . ";\n\n");
+ $query = '';
+ $query_len = 0;
+ $first_set = true;
+ }
+ else
+ {
+ $first_set = false;
+ }
+ }
+ mysqli_free_result($result);
+
+ // check to make sure we have nothing left to flush
+ if (!$first_set && $query)
+ {
+ $this->flush($query . ";\n\n");
+ }
+ }
+ }
+
+ /**
+ * Extracts data from database table (for MySQL driver)
+ *
+ * @param string $table_name name of the database table
+ * @return null
+ * @throws \phpbb\db\extractor\exception\extractor_not_initialized_exception when calling this function before init_extractor()
+ */
+ protected function write_data_mysql($table_name)
+ {
+ if (!$this->is_initialized)
+ {
+ throw new extractor_not_initialized_exception();
+ }
+
+ $sql = "SELECT *
+ FROM $table_name";
+ $result = mysql_unbuffered_query($sql, $this->db->get_db_connect_id());
+
+ if ($result != false)
+ {
+ $fields_cnt = mysql_num_fields($result);
+
+ // Get field information
+ $field = array();
+ for ($i = 0; $i < $fields_cnt; $i++)
+ {
+ $field[] = mysql_fetch_field($result, $i);
+ }
+ $field_set = array();
+
+ for ($j = 0; $j < $fields_cnt; $j++)
+ {
+ $field_set[] = $field[$j]->name;
+ }
+
+ $search = array("\\", "'", "\x00", "\x0a", "\x0d", "\x1a", '"');
+ $replace = array("\\\\", "\\'", '\0', '\n', '\r', '\Z', '\\"');
+ $fields = implode(', ', $field_set);
+ $sql_data = 'INSERT INTO ' . $table_name . ' (' . $fields . ') VALUES ';
+ $first_set = true;
+ $query_len = 0;
+ $max_len = get_usable_memory();
+
+ while ($row = mysql_fetch_row($result))
+ {
+ $values = array();
+ if ($first_set)
+ {
+ $query = $sql_data . '(';
+ }
+ else
+ {
+ $query .= ',(';
+ }
+
+ for ($j = 0; $j < $fields_cnt; $j++)
+ {
+ if (!isset($row[$j]) || is_null($row[$j]))
+ {
+ $values[$j] = 'NULL';
+ }
+ else if ($field[$j]->numeric && ($field[$j]->type !== 'timestamp'))
+ {
+ $values[$j] = $row[$j];
+ }
+ else
+ {
+ $values[$j] = "'" . str_replace($search, $replace, $row[$j]) . "'";
+ }
+ }
+ $query .= implode(', ', $values) . ')';
+
+ $query_len += strlen($query);
+ if ($query_len > $max_len)
+ {
+ $this->flush($query . ";\n\n");
+ $query = '';
+ $query_len = 0;
+ $first_set = true;
+ }
+ else
+ {
+ $first_set = false;
+ }
+ }
+ mysql_free_result($result);
+
+ // check to make sure we have nothing left to flush
+ if (!$first_set && $query)
+ {
+ $this->flush($query . ";\n\n");
+ }
+ }
+ }
+
+ /**
+ * Extracts database table structure (for MySQLi or MySQL 3.23.20+)
+ *
+ * @param string $table_name name of the database table
+ * @return null
+ * @throws \phpbb\db\extractor\exception\extractor_not_initialized_exception when calling this function before init_extractor()
+ */
+ protected function new_write_table($table_name)
+ {
+ if (!$this->is_initialized)
+ {
+ throw new extractor_not_initialized_exception();
+ }
+
+ $sql = 'SHOW CREATE TABLE ' . $table_name;
+ $result = $this->db->sql_query($sql);
+ $row = $this->db->sql_fetchrow($result);
+
+ $sql_data = '# Table: ' . $table_name . "\n";
+ $sql_data .= "DROP TABLE IF EXISTS $table_name;\n";
+ $this->flush($sql_data . $row['Create Table'] . ";\n\n");
+
+ $this->db->sql_freeresult($result);
+ }
+
+ /**
+ * Extracts database table structure (for MySQL verisons older than 3.23.20)
+ *
+ * @param string $table_name name of the database table
+ * @return null
+ * @throws \phpbb\db\extractor\exception\extractor_not_initialized_exception when calling this function before init_extractor()
+ */
+ protected function old_write_table($table_name)
+ {
+ if (!$this->is_initialized)
+ {
+ throw new extractor_not_initialized_exception();
+ }
+
+ $sql_data = '# Table: ' . $table_name . "\n";
+ $sql_data .= "DROP TABLE IF EXISTS $table_name;\n";
+ $sql_data .= "CREATE TABLE $table_name(\n";
+ $rows = array();
+
+ $sql = "SHOW FIELDS
+ FROM $table_name";
+ $result = $this->db->sql_query($sql);
+
+ while ($row = $this->db->sql_fetchrow($result))
+ {
+ $line = ' ' . $row['Field'] . ' ' . $row['Type'];
+
+ if (!is_null($row['Default']))
+ {
+ $line .= " DEFAULT '{$row['Default']}'";
+ }
+
+ if ($row['Null'] != 'YES')
+ {
+ $line .= ' NOT NULL';
+ }
+
+ if ($row['Extra'] != '')
+ {
+ $line .= ' ' . $row['Extra'];
+ }
+
+ $rows[] = $line;
+ }
+ $this->db->sql_freeresult($result);
+
+ $sql = "SHOW KEYS
+ FROM $table_name";
+
+ $result = $this->db->sql_query($sql);
+
+ $index = array();
+ while ($row = $this->db->sql_fetchrow($result))
+ {
+ $kname = $row['Key_name'];
+
+ if ($kname != 'PRIMARY')
+ {
+ if ($row['Non_unique'] == 0)
+ {
+ $kname = "UNIQUE|$kname";
+ }
+ }
+
+ if ($row['Sub_part'])
+ {
+ $row['Column_name'] .= '(' . $row['Sub_part'] . ')';
+ }
+ $index[$kname][] = $row['Column_name'];
+ }
+ $this->db->sql_freeresult($result);
+
+ foreach ($index as $key => $columns)
+ {
+ $line = ' ';
+
+ if ($key == 'PRIMARY')
+ {
+ $line .= 'PRIMARY KEY (' . implode(', ', $columns) . ')';
+ }
+ else if (strpos($key, 'UNIQUE') === 0)
+ {
+ $line .= 'UNIQUE ' . substr($key, 7) . ' (' . implode(', ', $columns) . ')';
+ }
+ else if (strpos($key, 'FULLTEXT') === 0)
+ {
+ $line .= 'FULLTEXT ' . substr($key, 9) . ' (' . implode(', ', $columns) . ')';
+ }
+ else
+ {
+ $line .= "KEY $key (" . implode(', ', $columns) . ')';
+ }
+
+ $rows[] = $line;
+ }
+
+ $sql_data .= implode(",\n", $rows);
+ $sql_data .= "\n);\n\n";
+
+ $this->flush($sql_data);
+ }
+}
diff --git a/phpBB/phpbb/db/extractor/oracle_extractor.php b/phpBB/phpbb/db/extractor/oracle_extractor.php
new file mode 100644
index 0000000000..05f7b8ac95
--- /dev/null
+++ b/phpBB/phpbb/db/extractor/oracle_extractor.php
@@ -0,0 +1,265 @@
+<?php
+/**
+*
+* This file is part of the phpBB Forum Software package.
+*
+* @copyright (c) phpBB Limited <https://www.phpbb.com>
+* @license GNU General Public License, version 2 (GPL-2.0)
+*
+* For full copyright and license information, please see
+* the docs/CREDITS.txt file.
+*
+*/
+
+namespace phpbb\db\extractor;
+
+use phpbb\db\extractor\exception\extractor_not_initialized_exception;
+
+class oracle_extractor extends base_extractor
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function write_table($table_name)
+ {
+ if (!$this->is_initialized)
+ {
+ throw new extractor_not_initialized_exception();
+ }
+
+ $sql_data = '-- Table: ' . $table_name . "\n";
+ $sql_data .= "DROP TABLE $table_name\n/\n";
+ $sql_data .= "\nCREATE TABLE $table_name (\n";
+
+ $sql = "SELECT COLUMN_NAME, DATA_TYPE, DATA_PRECISION, DATA_LENGTH, NULLABLE, DATA_DEFAULT
+ FROM ALL_TAB_COLS
+ WHERE table_name = '{$table_name}'";
+ $result = $this->db->sql_query($sql);
+
+ $rows = array();
+ while ($row = $this->db->sql_fetchrow($result))
+ {
+ $line = ' "' . $row['column_name'] . '" ' . $row['data_type'];
+
+ if ($row['data_type'] !== 'CLOB')
+ {
+ if ($row['data_type'] !== 'VARCHAR2' && $row['data_type'] !== 'CHAR')
+ {
+ $line .= '(' . $row['data_precision'] . ')';
+ }
+ else
+ {
+ $line .= '(' . $row['data_length'] . ')';
+ }
+ }
+
+ if (!empty($row['data_default']))
+ {
+ $line .= ' DEFAULT ' . $row['data_default'];
+ }
+
+ if ($row['nullable'] == 'N')
+ {
+ $line .= ' NOT NULL';
+ }
+ $rows[] = $line;
+ }
+ $this->db->sql_freeresult($result);
+
+ $sql = "SELECT A.CONSTRAINT_NAME, A.COLUMN_NAME
+ FROM USER_CONS_COLUMNS A, USER_CONSTRAINTS B
+ WHERE A.CONSTRAINT_NAME = B.CONSTRAINT_NAME
+ AND B.CONSTRAINT_TYPE = 'P'
+ AND A.TABLE_NAME = '{$table_name}'";
+ $result = $this->db->sql_query($sql);
+
+ $primary_key = array();
+ $constraint_name = '';
+ while ($row = $this->db->sql_fetchrow($result))
+ {
+ $constraint_name = '"' . $row['constraint_name'] . '"';
+ $primary_key[] = '"' . $row['column_name'] . '"';
+ }
+ $this->db->sql_freeresult($result);
+
+ if (sizeof($primary_key))
+ {
+ $rows[] = " CONSTRAINT {$constraint_name} PRIMARY KEY (" . implode(', ', $primary_key) . ')';
+ }
+
+ $sql = "SELECT A.CONSTRAINT_NAME, A.COLUMN_NAME
+ FROM USER_CONS_COLUMNS A, USER_CONSTRAINTS B
+ WHERE A.CONSTRAINT_NAME = B.CONSTRAINT_NAME
+ AND B.CONSTRAINT_TYPE = 'U'
+ AND A.TABLE_NAME = '{$table_name}'";
+ $result = $this->db->sql_query($sql);
+
+ $unique = array();
+ $constraint_name = '';
+ while ($row = $this->db->sql_fetchrow($result))
+ {
+ $constraint_name = '"' . $row['constraint_name'] . '"';
+ $unique[] = '"' . $row['column_name'] . '"';
+ }
+ $this->db->sql_freeresult($result);
+
+ if (sizeof($unique))
+ {
+ $rows[] = " CONSTRAINT {$constraint_name} UNIQUE (" . implode(', ', $unique) . ')';
+ }
+
+ $sql_data .= implode(",\n", $rows);
+ $sql_data .= "\n)\n/\n";
+
+ $sql = "SELECT A.REFERENCED_NAME, C.*
+ FROM USER_DEPENDENCIES A, USER_TRIGGERS B, USER_SEQUENCES C
+ WHERE A.REFERENCED_TYPE = 'SEQUENCE'
+ AND A.NAME = B.TRIGGER_NAME
+ AND B.TABLE_NAME = '{$table_name}'
+ AND C.SEQUENCE_NAME = A.REFERENCED_NAME";
+ $result = $this->db->sql_query($sql);
+
+ $type = $this->request->variable('type', '');
+
+ while ($row = $this->db->sql_fetchrow($result))
+ {
+ $sql_data .= "\nDROP SEQUENCE \"{$row['referenced_name']}\"\n/\n";
+ $sql_data .= "\nCREATE SEQUENCE \"{$row['referenced_name']}\"";
+
+ if ($type == 'full')
+ {
+ $sql_data .= ' START WITH ' . $row['last_number'];
+ }
+
+ $sql_data .= "\n/\n";
+ }
+ $this->db->sql_freeresult($result);
+
+ $sql = "SELECT DESCRIPTION, WHEN_CLAUSE, TRIGGER_BODY
+ FROM USER_TRIGGERS
+ WHERE TABLE_NAME = '{$table_name}'";
+ $result = $this->db->sql_query($sql);
+ while ($row = $this->db->sql_fetchrow($result))
+ {
+ $sql_data .= "\nCREATE OR REPLACE TRIGGER {$row['description']}WHEN ({$row['when_clause']})\n{$row['trigger_body']}\n/\n";
+ }
+ $this->db->sql_freeresult($result);
+
+ $sql = "SELECT A.INDEX_NAME, B.COLUMN_NAME
+ FROM USER_INDEXES A, USER_IND_COLUMNS B
+ WHERE A.UNIQUENESS = 'NONUNIQUE'
+ AND A.INDEX_NAME = B.INDEX_NAME
+ AND B.TABLE_NAME = '{$table_name}'";
+ $result = $this->db->sql_query($sql);
+
+ $index = array();
+
+ while ($row = $this->db->sql_fetchrow($result))
+ {
+ $index[$row['index_name']][] = $row['column_name'];
+ }
+
+ foreach ($index as $index_name => $column_names)
+ {
+ $sql_data .= "\nCREATE INDEX $index_name ON $table_name(" . implode(', ', $column_names) . ")\n/\n";
+ }
+ $this->db->sql_freeresult($result);
+ $this->flush($sql_data);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function write_data($table_name)
+ {
+ if (!$this->is_initialized)
+ {
+ throw new extractor_not_initialized_exception();
+ }
+
+ $ary_type = $ary_name = array();
+
+ // Grab all of the data from current table.
+ $sql = "SELECT *
+ FROM $table_name";
+ $result = $this->db->sql_query($sql);
+
+ $i_num_fields = ocinumcols($result);
+
+ for ($i = 0; $i < $i_num_fields; $i++)
+ {
+ $ary_type[$i] = ocicolumntype($result, $i + 1);
+ $ary_name[$i] = ocicolumnname($result, $i + 1);
+ }
+
+ $sql_data = '';
+
+ while ($row = $this->db->sql_fetchrow($result))
+ {
+ $schema_vals = $schema_fields = array();
+
+ // Build the SQL statement to recreate the data.
+ for ($i = 0; $i < $i_num_fields; $i++)
+ {
+ // Oracle uses uppercase - we use lowercase
+ $str_val = $row[strtolower($ary_name[$i])];
+
+ if (preg_match('#char|text|bool|raw|clob#i', $ary_type[$i]))
+ {
+ $str_quote = '';
+ $str_empty = "''";
+ $str_val = sanitize_data_oracle($str_val);
+ }
+ else if (preg_match('#date|timestamp#i', $ary_type[$i]))
+ {
+ if (empty($str_val))
+ {
+ $str_quote = '';
+ }
+ else
+ {
+ $str_quote = "'";
+ }
+ }
+ else
+ {
+ $str_quote = '';
+ $str_empty = 'NULL';
+ }
+
+ if (empty($str_val) && $str_val !== '0')
+ {
+ $str_val = $str_empty;
+ }
+
+ $schema_vals[$i] = $str_quote . $str_val . $str_quote;
+ $schema_fields[$i] = '"' . $ary_name[$i] . '"';
+ }
+
+ // Take the ordered fields and their associated data and build it
+ // into a valid sql statement to recreate that field in the data.
+ $sql_data = "INSERT INTO $table_name (" . implode(', ', $schema_fields) . ') VALUES (' . implode(', ', $schema_vals) . ")\n/\n";
+
+ $this->flush($sql_data);
+ }
+ $this->db->sql_freeresult($result);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function write_start($table_prefix)
+ {
+ if (!$this->is_initialized)
+ {
+ throw new extractor_not_initialized_exception();
+ }
+
+ $sql_data = "--\n";
+ $sql_data .= "-- phpBB Backup Script\n";
+ $sql_data .= "-- Dump of tables for $table_prefix\n";
+ $sql_data .= "-- DATE : " . gmdate("d-m-Y H:i:s", $this->time) . " GMT\n";
+ $sql_data .= "--\n";
+ $this->flush($sql_data);
+ }
+}
diff --git a/phpBB/phpbb/db/extractor/postgres_extractor.php b/phpBB/phpbb/db/extractor/postgres_extractor.php
new file mode 100644
index 0000000000..a98e39621c
--- /dev/null
+++ b/phpBB/phpbb/db/extractor/postgres_extractor.php
@@ -0,0 +1,339 @@
+<?php
+/**
+*
+* This file is part of the phpBB Forum Software package.
+*
+* @copyright (c) phpBB Limited <https://www.phpbb.com>
+* @license GNU General Public License, version 2 (GPL-2.0)
+*
+* For full copyright and license information, please see
+* the docs/CREDITS.txt file.
+*
+*/
+
+namespace phpbb\db\extractor;
+
+use phpbb\db\extractor\exception\extractor_not_initialized_exception;
+
+class postgres_extractor extends base_extractor
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function write_start($table_prefix)
+ {
+ if (!$this->is_initialized)
+ {
+ throw new extractor_not_initialized_exception();
+ }
+
+ $sql_data = "--\n";
+ $sql_data .= "-- phpBB Backup Script\n";
+ $sql_data .= "-- Dump of tables for $table_prefix\n";
+ $sql_data .= "-- DATE : " . gmdate("d-m-Y H:i:s", $this->time) . " GMT\n";
+ $sql_data .= "--\n";
+ $sql_data .= "BEGIN TRANSACTION;\n";
+ $this->flush($sql_data);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function write_table($table_name)
+ {
+ static $domains_created = array();
+
+ if (!$this->is_initialized)
+ {
+ throw new extractor_not_initialized_exception();
+ }
+
+ $sql = "SELECT a.domain_name, a.data_type, a.character_maximum_length, a.domain_default
+ FROM INFORMATION_SCHEMA.domains a, INFORMATION_SCHEMA.column_domain_usage b
+ WHERE a.domain_name = b.domain_name
+ AND b.table_name = '{$table_name}'";
+ $result = $this->db->sql_query($sql);
+ while ($row = $this->db->sql_fetchrow($result))
+ {
+ if (empty($domains_created[$row['domain_name']]))
+ {
+ $domains_created[$row['domain_name']] = true;
+ //$sql_data = "DROP DOMAIN {$row['domain_name']};\n";
+ $sql_data = "CREATE DOMAIN {$row['domain_name']} as {$row['data_type']}";
+ if (!empty($row['character_maximum_length']))
+ {
+ $sql_data .= '(' . $row['character_maximum_length'] . ')';
+ }
+ $sql_data .= ' NOT NULL';
+ if (!empty($row['domain_default']))
+ {
+ $sql_data .= ' DEFAULT ' . $row['domain_default'];
+ }
+ $this->flush($sql_data . ";\n");
+ }
+ }
+ $this->db->sql_freeresult($result);
+
+ $sql_data = '-- Table: ' . $table_name . "\n";
+ $sql_data .= "DROP TABLE $table_name;\n";
+ // PGSQL does not "tightly" bind sequences and tables, we must guess...
+ $sql = "SELECT relname
+ FROM pg_class
+ WHERE relkind = 'S'
+ AND relname = '{$table_name}_seq'";
+ $result = $this->db->sql_query($sql);
+ // We don't even care about storing the results. We already know the answer if we get rows back.
+ if ($this->db->sql_fetchrow($result))
+ {
+ $sql_data .= "DROP SEQUENCE {$table_name}_seq;\n";
+ $sql_data .= "CREATE SEQUENCE {$table_name}_seq;\n";
+ }
+ $this->db->sql_freeresult($result);
+
+ $field_query = "SELECT a.attnum, a.attname as field, t.typname as type, a.attlen as length, a.atttypmod as lengthvar, a.attnotnull as notnull
+ FROM pg_class c, pg_attribute a, pg_type t
+ WHERE c.relname = '" . $this->db->sql_escape($table_name) . "'
+ AND a.attnum > 0
+ AND a.attrelid = c.oid
+ AND a.atttypid = t.oid
+ ORDER BY a.attnum";
+ $result = $this->db->sql_query($field_query);
+
+ $sql_data .= "CREATE TABLE $table_name(\n";
+ $lines = array();
+ while ($row = $this->db->sql_fetchrow($result))
+ {
+ // Get the data from the table
+ $sql_get_default = "SELECT pg_get_expr(d.adbin, d.adrelid) as rowdefault
+ FROM pg_attrdef d, pg_class c
+ WHERE (c.relname = '" . $this->db->sql_escape($table_name) . "')
+ AND (c.oid = d.adrelid)
+ AND d.adnum = " . $row['attnum'];
+ $def_res = $this->db->sql_query($sql_get_default);
+ $def_row = $this->db->sql_fetchrow($def_res);
+ $this->db->sql_freeresult($def_res);
+
+ if (empty($def_row))
+ {
+ unset($row['rowdefault']);
+ }
+ else
+ {
+ $row['rowdefault'] = $def_row['rowdefault'];
+ }
+
+ if ($row['type'] == 'bpchar')
+ {
+ // Internally stored as bpchar, but isn't accepted in a CREATE TABLE statement.
+ $row['type'] = 'char';
+ }
+
+ $line = ' ' . $row['field'] . ' ' . $row['type'];
+
+ if (strpos($row['type'], 'char') !== false)
+ {
+ if ($row['lengthvar'] > 0)
+ {
+ $line .= '(' . ($row['lengthvar'] - 4) . ')';
+ }
+ }
+
+ if (strpos($row['type'], 'numeric') !== false)
+ {
+ $line .= '(';
+ $line .= sprintf("%s,%s", (($row['lengthvar'] >> 16) & 0xffff), (($row['lengthvar'] - 4) & 0xffff));
+ $line .= ')';
+ }
+
+ if (isset($row['rowdefault']))
+ {
+ $line .= ' DEFAULT ' . $row['rowdefault'];
+ }
+
+ if ($row['notnull'] == 't')
+ {
+ $line .= ' NOT NULL';
+ }
+
+ $lines[] = $line;
+ }
+ $this->db->sql_freeresult($result);
+
+ // Get the listing of primary keys.
+ $sql_pri_keys = "SELECT ic.relname as index_name, bc.relname as tab_name, ta.attname as column_name, i.indisunique as unique_key, i.indisprimary as primary_key
+ FROM pg_class bc, pg_class ic, pg_index i, pg_attribute ta, pg_attribute ia
+ WHERE (bc.oid = i.indrelid)
+ AND (ic.oid = i.indexrelid)
+ AND (ia.attrelid = i.indexrelid)
+ AND (ta.attrelid = bc.oid)
+ AND (bc.relname = '" . $this->db->sql_escape($table_name) . "')
+ AND (ta.attrelid = i.indrelid)
+ AND (ta.attnum = i.indkey[ia.attnum-1])
+ ORDER BY index_name, tab_name, column_name";
+
+ $result = $this->db->sql_query($sql_pri_keys);
+
+ $index_create = $index_rows = $primary_key = array();
+
+ // We do this in two steps. It makes placing the comma easier
+ while ($row = $this->db->sql_fetchrow($result))
+ {
+ if ($row['primary_key'] == 't')
+ {
+ $primary_key[] = $row['column_name'];
+ $primary_key_name = $row['index_name'];
+ }
+ else
+ {
+ // We have to store this all this info because it is possible to have a multi-column key...
+ // we can loop through it again and build the statement
+ $index_rows[$row['index_name']]['table'] = $table_name;
+ $index_rows[$row['index_name']]['unique'] = ($row['unique_key'] == 't') ? true : false;
+ $index_rows[$row['index_name']]['column_names'][] = $row['column_name'];
+ }
+ }
+ $this->db->sql_freeresult($result);
+
+ if (!empty($index_rows))
+ {
+ foreach ($index_rows as $idx_name => $props)
+ {
+ $index_create[] = 'CREATE ' . ($props['unique'] ? 'UNIQUE ' : '') . "INDEX $idx_name ON $table_name (" . implode(', ', $props['column_names']) . ");";
+ }
+ }
+
+ if (!empty($primary_key))
+ {
+ $lines[] = " CONSTRAINT $primary_key_name PRIMARY KEY (" . implode(', ', $primary_key) . ")";
+ }
+
+ // Generate constraint clauses for CHECK constraints
+ $sql_checks = "SELECT conname as index_name, consrc
+ FROM pg_constraint, pg_class bc
+ WHERE conrelid = bc.oid
+ AND bc.relname = '" . $this->db->sql_escape($table_name) . "'
+ AND NOT EXISTS (
+ SELECT *
+ FROM pg_constraint as c, pg_inherits as i
+ WHERE i.inhrelid = pg_constraint.conrelid
+ AND c.conname = pg_constraint.conname
+ AND c.consrc = pg_constraint.consrc
+ AND c.conrelid = i.inhparent
+ )";
+ $result = $this->db->sql_query($sql_checks);
+
+ // Add the constraints to the sql file.
+ while ($row = $this->db->sql_fetchrow($result))
+ {
+ if (!is_null($row['consrc']))
+ {
+ $lines[] = ' CONSTRAINT ' . $row['index_name'] . ' CHECK ' . $row['consrc'];
+ }
+ }
+ $this->db->sql_freeresult($result);
+
+ $sql_data .= implode(", \n", $lines);
+ $sql_data .= "\n);\n";
+
+ if (!empty($index_create))
+ {
+ $sql_data .= implode("\n", $index_create) . "\n\n";
+ }
+ $this->flush($sql_data);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function write_data($table_name)
+ {
+ if (!$this->is_initialized)
+ {
+ throw new extractor_not_initialized_exception();
+ }
+
+ // Grab all of the data from current table.
+ $sql = "SELECT *
+ FROM $table_name";
+ $result = $this->db->sql_query($sql);
+
+ $i_num_fields = pg_num_fields($result);
+ $seq = '';
+
+ for ($i = 0; $i < $i_num_fields; $i++)
+ {
+ $ary_type[] = pg_field_type($result, $i);
+ $ary_name[] = pg_field_name($result, $i);
+
+ $sql = "SELECT pg_get_expr(d.adbin, d.adrelid) as rowdefault
+ FROM pg_attrdef d, pg_class c
+ WHERE (c.relname = '{$table_name}')
+ AND (c.oid = d.adrelid)
+ AND d.adnum = " . strval($i + 1);
+ $result2 = $this->db->sql_query($sql);
+ if ($row = $this->db->sql_fetchrow($result2))
+ {
+ // Determine if we must reset the sequences
+ if (strpos($row['rowdefault'], "nextval('") === 0)
+ {
+ $seq .= "SELECT SETVAL('{$table_name}_seq',(select case when max({$ary_name[$i]})>0 then max({$ary_name[$i]})+1 else 1 end FROM {$table_name}));\n";
+ }
+ }
+ }
+
+ $this->flush("COPY $table_name (" . implode(', ', $ary_name) . ') FROM stdin;' . "\n");
+ while ($row = $this->db->sql_fetchrow($result))
+ {
+ $schema_vals = array();
+
+ // Build the SQL statement to recreate the data.
+ for ($i = 0; $i < $i_num_fields; $i++)
+ {
+ $str_val = $row[$ary_name[$i]];
+
+ if (preg_match('#char|text|bool|bytea#i', $ary_type[$i]))
+ {
+ $str_val = str_replace(array("\n", "\t", "\r", "\b", "\f", "\v"), array('\n', '\t', '\r', '\b', '\f', '\v'), addslashes($str_val));
+ $str_empty = '';
+ }
+ else
+ {
+ $str_empty = '\N';
+ }
+
+ if (empty($str_val) && $str_val !== '0')
+ {
+ $str_val = $str_empty;
+ }
+
+ $schema_vals[] = $str_val;
+ }
+
+ // Take the ordered fields and their associated data and build it
+ // into a valid sql statement to recreate that field in the data.
+ $this->flush(implode("\t", $schema_vals) . "\n");
+ }
+ $this->db->sql_freeresult($result);
+ $this->flush("\\.\n");
+
+ // Write out the sequence statements
+ $this->flush($seq);
+ }
+
+ /**
+ * Writes closing line(s) to database backup
+ *
+ * @return null
+ * @throws \phpbb\db\extractor\exception\extractor_not_initialized_exception when calling this function before init_extractor()
+ */
+ public function write_end()
+ {
+ if (!$this->is_initialized)
+ {
+ throw new extractor_not_initialized_exception();
+ }
+
+ $this->flush("COMMIT;\n");
+ parent::write_end();
+ }
+}
diff --git a/phpBB/phpbb/db/extractor/sqlite3_extractor.php b/phpBB/phpbb/db/extractor/sqlite3_extractor.php
new file mode 100644
index 0000000000..ce8da6a652
--- /dev/null
+++ b/phpBB/phpbb/db/extractor/sqlite3_extractor.php
@@ -0,0 +1,151 @@
+<?php
+/**
+*
+* This file is part of the phpBB Forum Software package.
+*
+* @copyright (c) phpBB Limited <https://www.phpbb.com>
+* @license GNU General Public License, version 2 (GPL-2.0)
+*
+* For full copyright and license information, please see
+* the docs/CREDITS.txt file.
+*
+*/
+
+namespace phpbb\db\extractor;
+
+use phpbb\db\extractor\exception\extractor_not_initialized_exception;
+
+class sqlite3_extractor extends base_extractor
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function write_start($table_prefix)
+ {
+ if (!$this->is_initialized)
+ {
+ throw new extractor_not_initialized_exception();
+ }
+
+ $sql_data = "--\n";
+ $sql_data .= "-- phpBB Backup Script\n";
+ $sql_data .= "-- Dump of tables for $table_prefix\n";
+ $sql_data .= "-- DATE : " . gmdate("d-m-Y H:i:s", $this->time) . " GMT\n";
+ $sql_data .= "--\n";
+ $sql_data .= "BEGIN TRANSACTION;\n";
+ $this->flush($sql_data);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function write_table($table_name)
+ {
+ if (!$this->is_initialized)
+ {
+ throw new extractor_not_initialized_exception();
+ }
+
+ $sql_data = '-- Table: ' . $table_name . "\n";
+ $sql_data .= "DROP TABLE $table_name;\n";
+
+ $sql = "SELECT sql
+ FROM sqlite_master
+ WHERE type = 'table'
+ AND name = '" . $this->db->sql_escape($table_name) . "'
+ ORDER BY name ASC;";
+ $result = $this->db->sql_query($sql);
+ $row = $this->db->sql_fetchrow($result);
+ $this->db->sql_freeresult($result);
+
+ // Create Table
+ $sql_data .= $row['sql'] . ";\n";
+
+ $result = $this->db->sql_query("PRAGMA index_list('" . $this->db->sql_escape($table_name) . "');");
+
+ while ($row = $this->db->sql_fetchrow($result))
+ {
+ if (strpos($row['name'], 'autoindex') !== false)
+ {
+ continue;
+ }
+
+ $result2 = $this->db->sql_query("PRAGMA index_info('" . $this->db->sql_escape($row['name']) . "');");
+
+ $fields = array();
+ while ($row2 = $this->db->sql_fetchrow($result2))
+ {
+ $fields[] = $row2['name'];
+ }
+ $this->db->sql_freeresult($result2);
+
+ $sql_data .= 'CREATE ' . ($row['unique'] ? 'UNIQUE ' : '') . 'INDEX ' . $row['name'] . ' ON ' . $table_name . ' (' . implode(', ', $fields) . ");\n";
+ }
+ $this->db->sql_freeresult($result);
+
+ $this->flush($sql_data . "\n");
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function write_data($table_name)
+ {
+ if (!$this->is_initialized)
+ {
+ throw new extractor_not_initialized_exception();
+ }
+
+ $result = $this->db->sql_query("PRAGMA table_info('" . $this->db->sql_escape($table_name) . "');");
+
+ $col_types = array();
+ while ($row = $this->db->sql_fetchrow($result))
+ {
+ $col_types[$row['name']] = $row['type'];
+ }
+ $this->db->sql_freeresult($result);
+
+ $sql_insert = 'INSERT INTO ' . $table_name . ' (' . implode(', ', array_keys($col_types)) . ') VALUES (';
+
+ $sql = "SELECT *
+ FROM $table_name";
+ $result = $this->db->sql_query($sql);
+
+ while ($row = $this->db->sql_fetchrow($result))
+ {
+ foreach ($row as $column_name => $column_data)
+ {
+ if (is_null($column_data))
+ {
+ $row[$column_name] = 'NULL';
+ }
+ else if ($column_data === '')
+ {
+ $row[$column_name] = "''";
+ }
+ else if (stripos($col_types[$column_name], 'text') !== false || stripos($col_types[$column_name], 'char') !== false || stripos($col_types[$column_name], 'blob') !== false)
+ {
+ $row[$column_name] = sanitize_data_generic(str_replace("'", "''", $column_data));
+ }
+ }
+ $this->flush($sql_insert . implode(', ', $row) . ");\n");
+ }
+ }
+
+ /**
+ * Writes closing line(s) to database backup
+ *
+ * @return null
+ * @throws \phpbb\db\extractor\exception\extractor_not_initialized_exception when calling this function before init_extractor()
+ */
+ public function write_end()
+ {
+ if (!$this->is_initialized)
+ {
+ throw new extractor_not_initialized_exception();
+ }
+
+ $this->flush("COMMIT;\n");
+ parent::write_end();
+ }
+}
diff --git a/phpBB/phpbb/db/extractor/sqlite_extractor.php b/phpBB/phpbb/db/extractor/sqlite_extractor.php
new file mode 100644
index 0000000000..2734e23235
--- /dev/null
+++ b/phpBB/phpbb/db/extractor/sqlite_extractor.php
@@ -0,0 +1,149 @@
+<?php
+/**
+*
+* This file is part of the phpBB Forum Software package.
+*
+* @copyright (c) phpBB Limited <https://www.phpbb.com>
+* @license GNU General Public License, version 2 (GPL-2.0)
+*
+* For full copyright and license information, please see
+* the docs/CREDITS.txt file.
+*
+*/
+
+namespace phpbb\db\extractor;
+
+use phpbb\db\extractor\exception\extractor_not_initialized_exception;
+
+class sqlite_extractor extends base_extractor
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function write_start($table_prefix)
+ {
+ if (!$this->is_initialized)
+ {
+ throw new extractor_not_initialized_exception();
+ }
+
+ $sql_data = "--\n";
+ $sql_data .= "-- phpBB Backup Script\n";
+ $sql_data .= "-- Dump of tables for $table_prefix\n";
+ $sql_data .= "-- DATE : " . gmdate("d-m-Y H:i:s", $this->time) . " GMT\n";
+ $sql_data .= "--\n";
+ $sql_data .= "BEGIN TRANSACTION;\n";
+ $this->flush($sql_data);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function write_table($table_name)
+ {
+ if (!$this->is_initialized)
+ {
+ throw new extractor_not_initialized_exception();
+ }
+
+ $sql_data = '-- Table: ' . $table_name . "\n";
+ $sql_data .= "DROP TABLE $table_name;\n";
+
+ $sql = "SELECT sql
+ FROM sqlite_master
+ WHERE type = 'table'
+ AND name = '" . $this->db->sql_escape($table_name) . "'
+ ORDER BY type DESC, name;";
+ $result = $this->db->sql_query($sql);
+ $row = $this->db->sql_fetchrow($result);
+ $this->db->sql_freeresult($result);
+
+ // Create Table
+ $sql_data .= $row['sql'] . ";\n";
+
+ $result = $this->db->sql_query("PRAGMA index_list('" . $this->db->sql_escape($table_name) . "');");
+
+ $ar = array();
+ while ($row = $this->db->sql_fetchrow($result))
+ {
+ $ar[] = $row;
+ }
+ $this->db->sql_freeresult($result);
+
+ foreach ($ar as $value)
+ {
+ if (strpos($value['name'], 'autoindex') !== false)
+ {
+ continue;
+ }
+
+ $result = $this->db->sql_query("PRAGMA index_info('" . $this->db->sql_escape($value['name']) . "');");
+
+ $fields = array();
+ while ($row = $this->db->sql_fetchrow($result))
+ {
+ $fields[] = $row['name'];
+ }
+ $this->db->sql_freeresult($result);
+
+ $sql_data .= 'CREATE ' . ($value['unique'] ? 'UNIQUE ' : '') . 'INDEX ' . $value['name'] . ' on ' . $table_name . ' (' . implode(', ', $fields) . ");\n";
+ }
+
+ $this->flush($sql_data . "\n");
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function write_data($table_name)
+ {
+ if (!$this->is_initialized)
+ {
+ throw new extractor_not_initialized_exception();
+ }
+
+ $col_types = sqlite_fetch_column_types($this->db->get_db_connect_id(), $table_name);
+
+ $sql = "SELECT *
+ FROM $table_name";
+ $result = sqlite_unbuffered_query($this->db->get_db_connect_id(), $sql);
+ $rows = sqlite_fetch_all($result, SQLITE_ASSOC);
+ $sql_insert = 'INSERT INTO ' . $table_name . ' (' . implode(', ', array_keys($col_types)) . ') VALUES (';
+ foreach ($rows as $row)
+ {
+ foreach ($row as $column_name => $column_data)
+ {
+ if (is_null($column_data))
+ {
+ $row[$column_name] = 'NULL';
+ }
+ else if ($column_data == '')
+ {
+ $row[$column_name] = "''";
+ }
+ else if (strpos($col_types[$column_name], 'text') !== false || strpos($col_types[$column_name], 'char') !== false || strpos($col_types[$column_name], 'blob') !== false)
+ {
+ $row[$column_name] = sanitize_data_generic(str_replace("'", "''", $column_data));
+ }
+ }
+ $this->flush($sql_insert . implode(', ', $row) . ");\n");
+ }
+ }
+
+ /**
+ * Writes closing line(s) to database backup
+ *
+ * @return null
+ * @throws \phpbb\db\extractor\exception\extractor_not_initialized_exception when calling this function before init_extractor()
+ */
+ public function write_end()
+ {
+ if (!$this->is_initialized)
+ {
+ throw new extractor_not_initialized_exception();
+ }
+
+ $this->flush("COMMIT;\n");
+ parent::write_end();
+ }
+}
diff --git a/phpBB/phpbb/db/log_wrapper_migrator_output_handler.php b/phpBB/phpbb/db/log_wrapper_migrator_output_handler.php
index 94c293dc45..4c85bf4d67 100644
--- a/phpBB/phpbb/db/log_wrapper_migrator_output_handler.php
+++ b/phpBB/phpbb/db/log_wrapper_migrator_output_handler.php
@@ -38,16 +38,23 @@ class log_wrapper_migrator_output_handler implements migrator_output_handler_int
protected $file_handle = false;
/**
+ * @var \phpbb\filesystem\filesystem_interface
+ */
+ protected $filesystem;
+
+ /**
* Constructor
*
* @param user $user User object
* @param migrator_output_handler_interface $migrator Migrator output handler
* @param string $log_file File to log to
+ * @param \phpbb\filesystem\filesystem_interface phpBB filesystem object
*/
- public function __construct(user $user, migrator_output_handler_interface $migrator, $log_file)
+ public function __construct(user $user, migrator_output_handler_interface $migrator, $log_file, \phpbb\filesystem\filesystem_interface $filesystem)
{
$this->user = $user;
$this->migrator = $migrator;
+ $this->filesystem = $filesystem;
$this->file_open($log_file);
}
@@ -58,7 +65,7 @@ class log_wrapper_migrator_output_handler implements migrator_output_handler_int
*/
protected function file_open($file)
{
- if (phpbb_is_writable(dirname($file)))
+ if ($this->filesystem->is_writable(dirname($file)))
{
$this->file_handle = fopen($file, 'w');
}
diff --git a/phpBB/phpbb/db/migration/data/v30x/release_3_0_5_rc1.php b/phpBB/phpbb/db/migration/data/v30x/release_3_0_5_rc1.php
index 003ccf8f18..9f6e3efb91 100644
--- a/phpBB/phpbb/db/migration/data/v30x/release_3_0_5_rc1.php
+++ b/phpBB/phpbb/db/migration/data/v30x/release_3_0_5_rc1.php
@@ -57,7 +57,9 @@ class release_3_0_5_rc1 extends container_aware_migration
public function hash_old_passwords()
{
+ /* @var $passwords_manager \phpbb\passwords\manager */
$passwords_manager = $this->container->get('passwords.manager');
+
$sql = 'SELECT user_id, user_password
FROM ' . $this->table_prefix . 'users
WHERE user_pass_convert = 1';
diff --git a/phpBB/phpbb/db/migration/data/v30x/release_3_0_9_rc1.php b/phpBB/phpbb/db/migration/data/v30x/release_3_0_9_rc1.php
index 06e46d522f..5f928df47c 100644
--- a/phpBB/phpbb/db/migration/data/v30x/release_3_0_9_rc1.php
+++ b/phpBB/phpbb/db/migration/data/v30x/release_3_0_9_rc1.php
@@ -34,7 +34,7 @@ class release_3_0_9_rc1 extends \phpbb\db\migration\migration
// this column was removed from the database updater
// after 3.0.9-RC3 was released. It might still exist
// in 3.0.9-RCX installations and has to be dropped as
- // soon as the db_tools class is capable of properly
+ // soon as the \phpbb\db\tools\tools class is capable of properly
// removing a primary key.
// 'attempt_id' => array('UINT', NULL, 'auto_increment'),
'attempt_ip' => array('VCHAR:40', ''),
diff --git a/phpBB/phpbb/db/migration/migration.php b/phpBB/phpbb/db/migration/migration.php
index 5f120333e1..2304c8e44c 100644
--- a/phpBB/phpbb/db/migration/migration.php
+++ b/phpBB/phpbb/db/migration/migration.php
@@ -28,7 +28,7 @@ abstract class migration
/** @var \phpbb\db\driver\driver_interface */
protected $db;
- /** @var \phpbb\db\tools */
+ /** @var \phpbb\db\tools\tools_interface */
protected $db_tools;
/** @var string */
@@ -51,12 +51,12 @@ abstract class migration
*
* @param \phpbb\config\config $config
* @param \phpbb\db\driver\driver_interface $db
- * @param \phpbb\db\tools $db_tools
+ * @param \phpbb\db\tools\tools_interface $db_tools
* @param string $phpbb_root_path
* @param string $php_ext
* @param string $table_prefix
*/
- public function __construct(\phpbb\config\config $config, \phpbb\db\driver\driver_interface $db, \phpbb\db\tools $db_tools, $phpbb_root_path, $php_ext, $table_prefix)
+ public function __construct(\phpbb\config\config $config, \phpbb\db\driver\driver_interface $db, \phpbb\db\tools\tools_interface $db_tools, $phpbb_root_path, $php_ext, $table_prefix)
{
$this->config = $config;
$this->db = $db;
diff --git a/phpBB/phpbb/db/migration/profilefield_base_migration.php b/phpBB/phpbb/db/migration/profilefield_base_migration.php
index da1a38e2fa..3f26a4998c 100644
--- a/phpBB/phpbb/db/migration/profilefield_base_migration.php
+++ b/phpBB/phpbb/db/migration/profilefield_base_migration.php
@@ -237,6 +237,7 @@ abstract class profilefield_base_migration extends container_aware_migration
if ($profile_row === null)
{
+ /* @var $manager \phpbb\profilefields\manager */
$manager = $this->container->get('profilefields.manager');
$profile_row = $manager->build_insert_sql_array(array());
}
diff --git a/phpBB/phpbb/db/migration/schema_generator.php b/phpBB/phpbb/db/migration/schema_generator.php
index 91d8307d91..7003844bc4 100644
--- a/phpBB/phpbb/db/migration/schema_generator.php
+++ b/phpBB/phpbb/db/migration/schema_generator.php
@@ -24,7 +24,7 @@ class schema_generator
/** @var \phpbb\db\driver\driver_interface */
protected $db;
- /** @var \phpbb\db\tools */
+ /** @var \phpbb\db\tools\tools_interface */
protected $db_tools;
/** @var array */
@@ -48,7 +48,7 @@ class schema_generator
/**
* Constructor
*/
- public function __construct(array $class_names, \phpbb\config\config $config, \phpbb\db\driver\driver_interface $db, \phpbb\db\tools $db_tools, $phpbb_root_path, $php_ext, $table_prefix)
+ public function __construct(array $class_names, \phpbb\config\config $config, \phpbb\db\driver\driver_interface $db, \phpbb\db\tools\tools_interface $db_tools, $phpbb_root_path, $php_ext, $table_prefix)
{
$this->config = $config;
$this->db = $db;
diff --git a/phpBB/phpbb/db/migration/tool/module.php b/phpBB/phpbb/db/migration/tool/module.php
index 035625b095..b6f0372181 100644
--- a/phpBB/phpbb/db/migration/tool/module.php
+++ b/phpBB/phpbb/db/migration/tool/module.php
@@ -171,6 +171,8 @@ class module implements \phpbb\db\migration\tool\tool_interface
*/
public function add($class, $parent = 0, $data = array())
{
+ global $user, $phpbb_log;
+
// Allows '' to be sent as 0
$parent = $parent ?: 0;
@@ -266,7 +268,7 @@ class module implements \phpbb\db\migration\tool\tool_interface
{
// Success
$module_log_name = ((isset($this->user->lang[$data['module_langname']])) ? $this->user->lang[$data['module_langname']] : $data['module_langname']);
- add_log('admin', 'LOG_MODULE_ADD', $module_log_name);
+ $phpbb_log->add('admin', $user->data['user_id'], $user->ip, 'LOG_MODULE_ADD', false, array($module_log_name));
// Move the module if requested above/below an existing one
if (isset($data['before']) && $data['before'])
diff --git a/phpBB/phpbb/db/migrator.php b/phpBB/phpbb/db/migrator.php
index 7fc3e787e2..6902913c64 100644
--- a/phpBB/phpbb/db/migrator.php
+++ b/phpBB/phpbb/db/migrator.php
@@ -32,7 +32,7 @@ class migrator
/** @var \phpbb\db\driver\driver_interface */
protected $db;
- /** @var \phpbb\db\tools */
+ /** @var \phpbb\db\tools\tools_interface */
protected $db_tools;
/** @var \phpbb\db\migration\helper */
@@ -92,7 +92,7 @@ class migrator
/**
* Constructor of the database migrator
*/
- public function __construct(ContainerInterface $container, \phpbb\config\config $config, \phpbb\db\driver\driver_interface $db, \phpbb\db\tools $db_tools, $migrations_table, $phpbb_root_path, $php_ext, $table_prefix, $tools, \phpbb\db\migration\helper $helper)
+ public function __construct(ContainerInterface $container, \phpbb\config\config $config, \phpbb\db\driver\driver_interface $db, \phpbb\db\tools\tools_interface $db_tools, $migrations_table, $phpbb_root_path, $php_ext, $table_prefix, $tools, \phpbb\db\migration\helper $helper)
{
$this->container = $container;
$this->config = $config;
diff --git a/phpBB/phpbb/db/tools/factory.php b/phpBB/phpbb/db/tools/factory.php
new file mode 100644
index 0000000000..d204451a63
--- /dev/null
+++ b/phpBB/phpbb/db/tools/factory.php
@@ -0,0 +1,43 @@
+<?php
+/**
+*
+* This file is part of the phpBB Forum Software package.
+*
+* @copyright (c) phpBB Limited <https://www.phpbb.com>
+* @license GNU General Public License, version 2 (GPL-2.0)
+*
+* For full copyright and license information, please see
+* the docs/CREDITS.txt file.
+*
+*/
+
+namespace phpbb\db\tools;
+
+/**
+ * A factory which serves the suitable tools instance for the given dbal
+ */
+class factory
+{
+ /**
+ * @param mixed $db_driver
+ * @param bool $return_statements
+ * @return \phpbb\db\tools\tools_interface
+ */
+ public function get($db_driver, $return_statements = false)
+ {
+ if ($db_driver instanceof \phpbb\db\driver\mssql || $db_driver instanceof \phpbb\db\driver\mssql_base)
+ {
+ return new \phpbb\db\tools\mssql($db_driver, $return_statements);
+ }
+ else if ($db_driver instanceof \phpbb\db\driver\postgres)
+ {
+ return new \phpbb\db\tools\postgres($db_driver, $return_statements);
+ }
+ else if ($db_driver instanceof \phpbb\db\driver\driver_interface)
+ {
+ return new \phpbb\db\tools\tools($db_driver, $return_statements);
+ }
+
+ throw new \InvalidArgumentException('Invalid database driver given');
+ }
+}
diff --git a/phpBB/phpbb/db/tools/mssql.php b/phpBB/phpbb/db/tools/mssql.php
new file mode 100644
index 0000000000..6e58171040
--- /dev/null
+++ b/phpBB/phpbb/db/tools/mssql.php
@@ -0,0 +1,793 @@
+<?php
+/**
+ *
+ * This file is part of the phpBB Forum Software package.
+ *
+ * @copyright (c) phpBB Limited <https://www.phpbb.com>
+ * @license GNU General Public License, version 2 (GPL-2.0)
+ *
+ * For full copyright and license information, please see
+ * the docs/CREDITS.txt file.
+ *
+ */
+
+namespace phpbb\db\tools;
+
+/**
+ * Database Tools for handling cross-db actions such as altering columns, etc.
+ * Currently not supported is returning SQL for creating tables.
+ */
+class mssql extends tools
+{
+ /**
+ * Is the used MS SQL Server a SQL Server 2000?
+ * @var bool
+ */
+ protected $is_sql_server_2000;
+
+ /**
+ * Get the column types for mssql based databases
+ *
+ * @return array
+ */
+ public static function get_dbms_type_map()
+ {
+ return array(
+ 'mssql' => array(
+ 'INT:' => '[int]',
+ 'BINT' => '[float]',
+ 'UINT' => '[int]',
+ 'UINT:' => '[int]',
+ 'TINT:' => '[int]',
+ 'USINT' => '[int]',
+ 'BOOL' => '[int]',
+ 'VCHAR' => '[varchar] (255)',
+ 'VCHAR:' => '[varchar] (%d)',
+ 'CHAR:' => '[char] (%d)',
+ 'XSTEXT' => '[varchar] (1000)',
+ 'STEXT' => '[varchar] (3000)',
+ 'TEXT' => '[varchar] (8000)',
+ 'MTEXT' => '[text]',
+ 'XSTEXT_UNI'=> '[varchar] (100)',
+ 'STEXT_UNI' => '[varchar] (255)',
+ 'TEXT_UNI' => '[varchar] (4000)',
+ 'MTEXT_UNI' => '[text]',
+ 'TIMESTAMP' => '[int]',
+ 'DECIMAL' => '[float]',
+ 'DECIMAL:' => '[float]',
+ 'PDECIMAL' => '[float]',
+ 'PDECIMAL:' => '[float]',
+ 'VCHAR_UNI' => '[varchar] (255)',
+ 'VCHAR_UNI:'=> '[varchar] (%d)',
+ 'VCHAR_CI' => '[varchar] (255)',
+ 'VARBINARY' => '[varchar] (255)',
+ ),
+
+ 'mssqlnative' => array(
+ 'INT:' => '[int]',
+ 'BINT' => '[float]',
+ 'UINT' => '[int]',
+ 'UINT:' => '[int]',
+ 'TINT:' => '[int]',
+ 'USINT' => '[int]',
+ 'BOOL' => '[int]',
+ 'VCHAR' => '[varchar] (255)',
+ 'VCHAR:' => '[varchar] (%d)',
+ 'CHAR:' => '[char] (%d)',
+ 'XSTEXT' => '[varchar] (1000)',
+ 'STEXT' => '[varchar] (3000)',
+ 'TEXT' => '[varchar] (8000)',
+ 'MTEXT' => '[text]',
+ 'XSTEXT_UNI'=> '[varchar] (100)',
+ 'STEXT_UNI' => '[varchar] (255)',
+ 'TEXT_UNI' => '[varchar] (4000)',
+ 'MTEXT_UNI' => '[text]',
+ 'TIMESTAMP' => '[int]',
+ 'DECIMAL' => '[float]',
+ 'DECIMAL:' => '[float]',
+ 'PDECIMAL' => '[float]',
+ 'PDECIMAL:' => '[float]',
+ 'VCHAR_UNI' => '[varchar] (255)',
+ 'VCHAR_UNI:'=> '[varchar] (%d)',
+ 'VCHAR_CI' => '[varchar] (255)',
+ 'VARBINARY' => '[varchar] (255)',
+ ),
+ );
+ }
+
+ /**
+ * Constructor. Set DB Object and set {@link $return_statements return_statements}.
+ *
+ * @param \phpbb\db\driver\driver_interface $db Database connection
+ * @param bool $return_statements True if only statements should be returned and no SQL being executed
+ */
+ public function __construct(\phpbb\db\driver\driver_interface $db, $return_statements = false)
+ {
+ parent::__construct($db, $return_statements);
+
+ // Determine mapping database type
+ switch ($this->db->get_sql_layer())
+ {
+ case 'mssql':
+ case 'mssql_odbc':
+ $this->sql_layer = 'mssql';
+ break;
+
+ case 'mssqlnative':
+ $this->sql_layer = 'mssqlnative';
+ break;
+ }
+
+ $this->dbms_type_map = self::get_dbms_type_map();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ function sql_list_tables()
+ {
+ $sql = "SELECT name
+ FROM sysobjects
+ WHERE type='U'";
+ $result = $this->db->sql_query($sql);
+
+ $tables = array();
+ while ($row = $this->db->sql_fetchrow($result))
+ {
+ $name = current($row);
+ $tables[$name] = $name;
+ }
+ $this->db->sql_freeresult($result);
+
+ return $tables;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ function sql_create_table($table_name, $table_data)
+ {
+ // holds the DDL for a column
+ $columns = $statements = array();
+
+ if ($this->sql_table_exists($table_name))
+ {
+ return $this->_sql_run_sql($statements);
+ }
+
+ // Begin transaction
+ $statements[] = 'begin';
+
+ // Determine if we have created a PRIMARY KEY in the earliest
+ $primary_key_gen = false;
+
+ // Determine if the table requires a sequence
+ $create_sequence = false;
+
+ // Begin table sql statement
+ $table_sql = 'CREATE TABLE [' . $table_name . '] (' . "\n";
+
+ if (!isset($table_data['PRIMARY_KEY']))
+ {
+ $table_data['COLUMNS']['mssqlindex'] = array('UINT', null, 'auto_increment');
+ $table_data['PRIMARY_KEY'] = 'mssqlindex';
+ }
+
+ // Iterate through the columns to create a table
+ foreach ($table_data['COLUMNS'] as $column_name => $column_data)
+ {
+ // here lies an array, filled with information compiled on the column's data
+ $prepared_column = $this->sql_prepare_column_data($table_name, $column_name, $column_data);
+
+ if (isset($prepared_column['auto_increment']) && $prepared_column['auto_increment'] && strlen($column_name) > 26) // "${column_name}_gen"
+ {
+ trigger_error("Index name '${column_name}_gen' on table '$table_name' is too long. The maximum auto increment column length is 26 characters.", E_USER_ERROR);
+ }
+
+ // here we add the definition of the new column to the list of columns
+ $columns[] = "\t [{$column_name}] " . $prepared_column['column_type_sql_default'];
+
+ // see if we have found a primary key set due to a column definition if we have found it, we can stop looking
+ if (!$primary_key_gen)
+ {
+ $primary_key_gen = isset($prepared_column['primary_key_set']) && $prepared_column['primary_key_set'];
+ }
+
+ // create sequence DDL based off of the existance of auto incrementing columns
+ if (!$create_sequence && isset($prepared_column['auto_increment']) && $prepared_column['auto_increment'])
+ {
+ $create_sequence = $column_name;
+ }
+ }
+
+ // this makes up all the columns in the create table statement
+ $table_sql .= implode(",\n", $columns);
+
+ // Close the table for two DBMS and add to the statements
+ $table_sql .= "\n);";
+ $statements[] = $table_sql;
+
+ // we have yet to create a primary key for this table,
+ // this means that we can add the one we really wanted instead
+ if (!$primary_key_gen)
+ {
+ // Write primary key
+ if (isset($table_data['PRIMARY_KEY']))
+ {
+ if (!is_array($table_data['PRIMARY_KEY']))
+ {
+ $table_data['PRIMARY_KEY'] = array($table_data['PRIMARY_KEY']);
+ }
+
+ // We need the data here
+ $old_return_statements = $this->return_statements;
+ $this->return_statements = true;
+
+ $primary_key_stmts = $this->sql_create_primary_key($table_name, $table_data['PRIMARY_KEY']);
+ foreach ($primary_key_stmts as $pk_stmt)
+ {
+ $statements[] = $pk_stmt;
+ }
+
+ $this->return_statements = $old_return_statements;
+ }
+ }
+
+ // Write Keys
+ if (isset($table_data['KEYS']))
+ {
+ foreach ($table_data['KEYS'] as $key_name => $key_data)
+ {
+ if (!is_array($key_data[1]))
+ {
+ $key_data[1] = array($key_data[1]);
+ }
+
+ $old_return_statements = $this->return_statements;
+ $this->return_statements = true;
+
+ $key_stmts = ($key_data[0] == 'UNIQUE') ? $this->sql_create_unique_index($table_name, $key_name, $key_data[1]) : $this->sql_create_index($table_name, $key_name, $key_data[1]);
+
+ foreach ($key_stmts as $key_stmt)
+ {
+ $statements[] = $key_stmt;
+ }
+
+ $this->return_statements = $old_return_statements;
+ }
+ }
+
+ // Commit Transaction
+ $statements[] = 'commit';
+
+ return $this->_sql_run_sql($statements);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ function sql_list_columns($table_name)
+ {
+ $columns = array();
+
+ $sql = "SELECT c.name
+ FROM syscolumns c
+ LEFT JOIN sysobjects o ON c.id = o.id
+ WHERE o.name = '{$table_name}'";
+ $result = $this->db->sql_query($sql);
+
+ while ($row = $this->db->sql_fetchrow($result))
+ {
+ $column = strtolower(current($row));
+ $columns[$column] = $column;
+ }
+ $this->db->sql_freeresult($result);
+
+ return $columns;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ function sql_index_exists($table_name, $index_name)
+ {
+ $sql = "EXEC sp_statistics '$table_name'";
+ $result = $this->db->sql_query($sql);
+
+ while ($row = $this->db->sql_fetchrow($result))
+ {
+ if ($row['TYPE'] == 3)
+ {
+ if (strtolower($row['INDEX_NAME']) == strtolower($index_name))
+ {
+ $this->db->sql_freeresult($result);
+ return true;
+ }
+ }
+ }
+ $this->db->sql_freeresult($result);
+
+ return false;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ function sql_unique_index_exists($table_name, $index_name)
+ {
+ $sql = "EXEC sp_statistics '$table_name'";
+ $result = $this->db->sql_query($sql);
+
+ while ($row = $this->db->sql_fetchrow($result))
+ {
+ // Usually NON_UNIQUE is the column we want to check, but we allow for both
+ if ($row['TYPE'] == 3)
+ {
+ if (strtolower($row['INDEX_NAME']) == strtolower($index_name))
+ {
+ $this->db->sql_freeresult($result);
+ return true;
+ }
+ }
+ }
+ $this->db->sql_freeresult($result);
+
+ return false;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ function sql_prepare_column_data($table_name, $column_name, $column_data)
+ {
+ if (strlen($column_name) > 30)
+ {
+ trigger_error("Column name '$column_name' on table '$table_name' is too long. The maximum is 30 characters.", E_USER_ERROR);
+ }
+
+ // Get type
+ list($column_type, ) = $this->get_column_type($column_data[0]);
+
+ // Adjust default value if db-dependent specified
+ if (is_array($column_data[1]))
+ {
+ $column_data[1] = (isset($column_data[1][$this->sql_layer])) ? $column_data[1][$this->sql_layer] : $column_data[1]['default'];
+ }
+
+ $sql = '';
+
+ $return_array = array();
+
+ $sql .= " {$column_type} ";
+ $sql_default = " {$column_type} ";
+
+ // For adding columns we need the default definition
+ if (!is_null($column_data[1]))
+ {
+ // For hexadecimal values do not use single quotes
+ if (strpos($column_data[1], '0x') === 0)
+ {
+ $return_array['default'] = 'DEFAULT (' . $column_data[1] . ') ';
+ $sql_default .= $return_array['default'];
+ }
+ else
+ {
+ $return_array['default'] = 'DEFAULT (' . ((is_numeric($column_data[1])) ? $column_data[1] : "'{$column_data[1]}'") . ') ';
+ $sql_default .= $return_array['default'];
+ }
+ }
+
+ if (isset($column_data[2]) && $column_data[2] == 'auto_increment')
+ {
+ // $sql .= 'IDENTITY (1, 1) ';
+ $sql_default .= 'IDENTITY (1, 1) ';
+ }
+
+ $return_array['textimage'] = $column_type === '[text]';
+
+ if (!is_null($column_data[1]) || (isset($column_data[2]) && $column_data[2] == 'auto_increment'))
+ {
+ $sql .= 'NOT NULL';
+ $sql_default .= 'NOT NULL';
+ }
+ else
+ {
+ $sql .= 'NULL';
+ $sql_default .= 'NULL';
+ }
+
+ $return_array['column_type_sql_default'] = $sql_default;
+
+ $return_array['column_type_sql'] = $sql;
+
+ return $return_array;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ function sql_column_add($table_name, $column_name, $column_data, $inline = false)
+ {
+ $column_data = $this->sql_prepare_column_data($table_name, $column_name, $column_data);
+ $statements = array();
+
+ // Does not support AFTER, only through temporary table
+ $statements[] = 'ALTER TABLE [' . $table_name . '] ADD [' . $column_name . '] ' . $column_data['column_type_sql_default'];
+
+ return $this->_sql_run_sql($statements);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ function sql_column_remove($table_name, $column_name, $inline = false)
+ {
+ $statements = array();
+
+ // We need the data here
+ $old_return_statements = $this->return_statements;
+ $this->return_statements = true;
+
+ $indexes = $this->get_existing_indexes($table_name, $column_name);
+ $indexes = array_merge($indexes, $this->get_existing_indexes($table_name, $column_name, true));
+
+ // Drop any indexes
+ $recreate_indexes = array();
+ if (!empty($indexes))
+ {
+ foreach ($indexes as $index_name => $index_data)
+ {
+ $result = $this->sql_index_drop($table_name, $index_name);
+ $statements = array_merge($statements, $result);
+ if (sizeof($index_data) > 1)
+ {
+ // Remove this column from the index and recreate it
+ $recreate_indexes[$index_name] = array_diff($index_data, array($column_name));
+ }
+ }
+ }
+
+ // Drop default value constraint
+ $result = $this->mssql_get_drop_default_constraints_queries($table_name, $column_name);
+ $statements = array_merge($statements, $result);
+
+ // Remove the column
+ $statements[] = 'ALTER TABLE [' . $table_name . '] DROP COLUMN [' . $column_name . ']';
+
+ if (!empty($recreate_indexes))
+ {
+ // Recreate indexes after we removed the column
+ foreach ($recreate_indexes as $index_name => $index_data)
+ {
+ $result = $this->sql_create_index($table_name, $index_name, $index_data);
+ $statements = array_merge($statements, $result);
+ }
+ }
+
+ $this->return_statements = $old_return_statements;
+
+ return $this->_sql_run_sql($statements);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ function sql_index_drop($table_name, $index_name)
+ {
+ $statements = array();
+
+ $statements[] = 'DROP INDEX ' . $table_name . '.' . $index_name;
+
+ return $this->_sql_run_sql($statements);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ function sql_table_drop($table_name)
+ {
+ $statements = array();
+
+ if (!$this->sql_table_exists($table_name))
+ {
+ return $this->_sql_run_sql($statements);
+ }
+
+ // the most basic operation, get rid of the table
+ $statements[] = 'DROP TABLE ' . $table_name;
+
+ return $this->_sql_run_sql($statements);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ function sql_create_primary_key($table_name, $column, $inline = false)
+ {
+ $statements = array();
+
+ $sql = "ALTER TABLE [{$table_name}] WITH NOCHECK ADD ";
+ $sql .= "CONSTRAINT [PK_{$table_name}] PRIMARY KEY CLUSTERED (";
+ $sql .= '[' . implode("],\n\t\t[", $column) . ']';
+ $sql .= ')';
+
+ $statements[] = $sql;
+
+ return $this->_sql_run_sql($statements);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ function sql_create_unique_index($table_name, $index_name, $column)
+ {
+ $statements = array();
+
+ $this->check_index_name_length($table_name, $index_name);
+
+ $statements[] = 'CREATE UNIQUE INDEX [' . $index_name . '] ON [' . $table_name . ']([' . implode('], [', $column) . '])';
+
+ return $this->_sql_run_sql($statements);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ function sql_create_index($table_name, $index_name, $column)
+ {
+ $statements = array();
+
+ $this->check_index_name_length($table_name, $index_name);
+
+ // remove index length
+ $column = preg_replace('#:.*$#', '', $column);
+
+ $statements[] = 'CREATE INDEX [' . $index_name . '] ON [' . $table_name . ']([' . implode('], [', $column) . '])';
+
+ return $this->_sql_run_sql($statements);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ function sql_list_index($table_name)
+ {
+ $index_array = array();
+ $sql = "EXEC sp_statistics '$table_name'";
+ $result = $this->db->sql_query($sql);
+ while ($row = $this->db->sql_fetchrow($result))
+ {
+ if ($row['TYPE'] == 3)
+ {
+ $index_array[] = strtolower($row['INDEX_NAME']);
+ }
+ }
+ $this->db->sql_freeresult($result);
+
+ return $index_array;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ function sql_column_change($table_name, $column_name, $column_data, $inline = false)
+ {
+ $column_data = $this->sql_prepare_column_data($table_name, $column_name, $column_data);
+ $statements = array();
+
+ // We need the data here
+ $old_return_statements = $this->return_statements;
+ $this->return_statements = true;
+
+ $indexes = $this->get_existing_indexes($table_name, $column_name);
+ $unique_indexes = $this->get_existing_indexes($table_name, $column_name, true);
+
+ // Drop any indexes
+ if (!empty($indexes) || !empty($unique_indexes))
+ {
+ $drop_indexes = array_merge(array_keys($indexes), array_keys($unique_indexes));
+ foreach ($drop_indexes as $index_name)
+ {
+ $result = $this->sql_index_drop($table_name, $index_name);
+ $statements = array_merge($statements, $result);
+ }
+ }
+
+ // Drop default value constraint
+ $result = $this->mssql_get_drop_default_constraints_queries($table_name, $column_name);
+ $statements = array_merge($statements, $result);
+
+ // Change the column
+ $statements[] = 'ALTER TABLE [' . $table_name . '] ALTER COLUMN [' . $column_name . '] ' . $column_data['column_type_sql'];
+
+ if (!empty($column_data['default']))
+ {
+ // Add new default value constraint
+ $statements[] = 'ALTER TABLE [' . $table_name . '] ADD CONSTRAINT [DF_' . $table_name . '_' . $column_name . '_1] ' . $this->db->sql_escape($column_data['default']) . ' FOR [' . $column_name . ']';
+ }
+
+ if (!empty($indexes))
+ {
+ // Recreate indexes after we changed the column
+ foreach ($indexes as $index_name => $index_data)
+ {
+ $result = $this->sql_create_index($table_name, $index_name, $index_data);
+ $statements = array_merge($statements, $result);
+ }
+ }
+
+ if (!empty($unique_indexes))
+ {
+ // Recreate unique indexes after we changed the column
+ foreach ($unique_indexes as $index_name => $index_data)
+ {
+ $result = $this->sql_create_unique_index($table_name, $index_name, $index_data);
+ $statements = array_merge($statements, $result);
+ }
+ }
+
+ $this->return_statements = $old_return_statements;
+
+ return $this->_sql_run_sql($statements);
+ }
+
+ /**
+ * Get queries to drop the default constraints of a column
+ *
+ * We need to drop the default constraints of a column,
+ * before being able to change their type or deleting them.
+ *
+ * @param string $table_name
+ * @param string $column_name
+ * @return array Array with SQL statements
+ */
+ protected function mssql_get_drop_default_constraints_queries($table_name, $column_name)
+ {
+ $statements = array();
+ if ($this->mssql_is_sql_server_2000())
+ {
+ // http://msdn.microsoft.com/en-us/library/aa175912%28v=sql.80%29.aspx
+ // Deprecated in SQL Server 2005
+ $sql = "SELECT so.name AS def_name
+ FROM sysobjects so
+ JOIN sysconstraints sc ON so.id = sc.constid
+ WHERE object_name(so.parent_obj) = '{$table_name}'
+ AND so.xtype = 'D'
+ AND sc.colid = (SELECT colid FROM syscolumns
+ WHERE id = object_id('{$table_name}')
+ AND name = '{$column_name}')";
+ }
+ else
+ {
+ $sql = "SELECT dobj.name AS def_name
+ FROM sys.columns col
+ LEFT OUTER JOIN sys.objects dobj ON (dobj.object_id = col.default_object_id AND dobj.type = 'D')
+ WHERE col.object_id = object_id('{$table_name}')
+ AND col.name = '{$column_name}'
+ AND dobj.name IS NOT NULL";
+ }
+
+ $result = $this->db->sql_query($sql);
+ while ($row = $this->db->sql_fetchrow($result))
+ {
+ $statements[] = 'ALTER TABLE [' . $table_name . '] DROP CONSTRAINT [' . $row['def_name'] . ']';
+ }
+ $this->db->sql_freeresult($result);
+
+ return $statements;
+ }
+
+ /**
+ * Get a list with existing indexes for the column
+ *
+ * @param string $table_name
+ * @param string $column_name
+ * @param bool $unique Should we get unique indexes or normal ones
+ * @return array Array with Index name => columns
+ */
+ public function get_existing_indexes($table_name, $column_name, $unique = false)
+ {
+ $existing_indexes = array();
+ if ($this->mssql_is_sql_server_2000())
+ {
+ // http://msdn.microsoft.com/en-us/library/aa175912%28v=sql.80%29.aspx
+ // Deprecated in SQL Server 2005
+ $sql = "SELECT DISTINCT ix.name AS phpbb_index_name
+ FROM sysindexes ix
+ INNER JOIN sysindexkeys ixc
+ ON ixc.id = ix.id
+ AND ixc.indid = ix.indid
+ INNER JOIN syscolumns cols
+ ON cols.colid = ixc.colid
+ AND cols.id = ix.id
+ WHERE ix.id = object_id('{$table_name}')
+ AND cols.name = '{$column_name}'
+ AND INDEXPROPERTY(ix.id, ix.name, 'IsUnique') = " . ($unique ? '1' : '0');
+ }
+ else
+ {
+ $sql = "SELECT DISTINCT ix.name AS phpbb_index_name
+ FROM sys.indexes ix
+ INNER JOIN sys.index_columns ixc
+ ON ixc.object_id = ix.object_id
+ AND ixc.index_id = ix.index_id
+ INNER JOIN sys.columns cols
+ ON cols.column_id = ixc.column_id
+ AND cols.object_id = ix.object_id
+ WHERE ix.object_id = object_id('{$table_name}')
+ AND cols.name = '{$column_name}'
+ AND ix.is_unique = " . ($unique ? '1' : '0');
+ }
+
+ $result = $this->db->sql_query($sql);
+ while ($row = $this->db->sql_fetchrow($result))
+ {
+ if (!isset($row['is_unique']) || ($unique && $row['is_unique'] == 'UNIQUE') || (!$unique && $row['is_unique'] == 'NONUNIQUE'))
+ {
+ $existing_indexes[$row['phpbb_index_name']] = array();
+ }
+ }
+ $this->db->sql_freeresult($result);
+
+ if (empty($existing_indexes))
+ {
+ return array();
+ }
+
+ if ($this->mssql_is_sql_server_2000())
+ {
+ $sql = "SELECT DISTINCT ix.name AS phpbb_index_name, cols.name AS phpbb_column_name
+ FROM sysindexes ix
+ INNER JOIN sysindexkeys ixc
+ ON ixc.id = ix.id
+ AND ixc.indid = ix.indid
+ INNER JOIN syscolumns cols
+ ON cols.colid = ixc.colid
+ AND cols.id = ix.id
+ WHERE ix.id = object_id('{$table_name}')
+ AND " . $this->db->sql_in_set('ix.name', array_keys($existing_indexes));
+ }
+ else
+ {
+ $sql = "SELECT DISTINCT ix.name AS phpbb_index_name, cols.name AS phpbb_column_name
+ FROM sys.indexes ix
+ INNER JOIN sys.index_columns ixc
+ ON ixc.object_id = ix.object_id
+ AND ixc.index_id = ix.index_id
+ INNER JOIN sys.columns cols
+ ON cols.column_id = ixc.column_id
+ AND cols.object_id = ix.object_id
+ WHERE ix.object_id = object_id('{$table_name}')
+ AND " . $this->db->sql_in_set('ix.name', array_keys($existing_indexes));
+ }
+
+ $result = $this->db->sql_query($sql);
+ while ($row = $this->db->sql_fetchrow($result))
+ {
+ $existing_indexes[$row['phpbb_index_name']][] = $row['phpbb_column_name'];
+ }
+ $this->db->sql_freeresult($result);
+
+ return $existing_indexes;
+ }
+
+ /**
+ * Is the used MS SQL Server a SQL Server 2000?
+ *
+ * @return bool
+ */
+ protected function mssql_is_sql_server_2000()
+ {
+ if ($this->is_sql_server_2000 === null)
+ {
+ $sql = "SELECT CAST(SERVERPROPERTY('productversion') AS VARCHAR(25)) AS mssql_version";
+ $result = $this->db->sql_query($sql);
+ $properties = $this->db->sql_fetchrow($result);
+ $this->db->sql_freeresult($result);
+ $this->is_sql_server_2000 = $properties['mssql_version'][0] == '8';
+ }
+
+ return $this->is_sql_server_2000;
+ }
+
+}
diff --git a/phpBB/phpbb/db/tools/postgres.php b/phpBB/phpbb/db/tools/postgres.php
new file mode 100644
index 0000000000..8b61625c3c
--- /dev/null
+++ b/phpBB/phpbb/db/tools/postgres.php
@@ -0,0 +1,613 @@
+<?php
+/**
+ *
+ * This file is part of the phpBB Forum Software package.
+ *
+ * @copyright (c) phpBB Limited <https://www.phpbb.com>
+ * @license GNU General Public License, version 2 (GPL-2.0)
+ *
+ * For full copyright and license information, please see
+ * the docs/CREDITS.txt file.
+ *
+ */
+
+namespace phpbb\db\tools;
+
+/**
+ * Database Tools for handling cross-db actions such as altering columns, etc.
+ * Currently not supported is returning SQL for creating tables.
+ */
+class postgres extends tools
+{
+ /**
+ * Get the column types for postgres only
+ *
+ * @return array
+ */
+ public static function get_dbms_type_map()
+ {
+ return array(
+ 'postgres' => array(
+ 'INT:' => 'INT4',
+ 'BINT' => 'INT8',
+ 'UINT' => 'INT4', // unsigned
+ 'UINT:' => 'INT4', // unsigned
+ 'USINT' => 'INT2', // unsigned
+ 'BOOL' => 'INT2', // unsigned
+ 'TINT:' => 'INT2',
+ 'VCHAR' => 'varchar(255)',
+ 'VCHAR:' => 'varchar(%d)',
+ 'CHAR:' => 'char(%d)',
+ 'XSTEXT' => 'varchar(1000)',
+ 'STEXT' => 'varchar(3000)',
+ 'TEXT' => 'varchar(8000)',
+ 'MTEXT' => 'TEXT',
+ 'XSTEXT_UNI'=> 'varchar(100)',
+ 'STEXT_UNI' => 'varchar(255)',
+ 'TEXT_UNI' => 'varchar(4000)',
+ 'MTEXT_UNI' => 'TEXT',
+ 'TIMESTAMP' => 'INT4', // unsigned
+ 'DECIMAL' => 'decimal(5,2)',
+ 'DECIMAL:' => 'decimal(%d,2)',
+ 'PDECIMAL' => 'decimal(6,3)',
+ 'PDECIMAL:' => 'decimal(%d,3)',
+ 'VCHAR_UNI' => 'varchar(255)',
+ 'VCHAR_UNI:'=> 'varchar(%d)',
+ 'VCHAR_CI' => 'varchar_ci',
+ 'VARBINARY' => 'bytea',
+ ),
+ );
+ }
+
+ /**
+ * Constructor. Set DB Object and set {@link $return_statements return_statements}.
+ *
+ * @param \phpbb\db\driver\driver_interface $db Database connection
+ * @param bool $return_statements True if only statements should be returned and no SQL being executed
+ */
+ public function __construct(\phpbb\db\driver\driver_interface $db, $return_statements = false)
+ {
+ parent::__construct($db, $return_statements);
+
+ // Determine mapping database type
+ $this->sql_layer = 'postgres';
+
+ $this->dbms_type_map = self::get_dbms_type_map();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ function sql_list_tables()
+ {
+ $sql = 'SELECT relname
+ FROM pg_stat_user_tables';
+ $result = $this->db->sql_query($sql);
+
+ $tables = array();
+ while ($row = $this->db->sql_fetchrow($result))
+ {
+ $name = current($row);
+ $tables[$name] = $name;
+ }
+ $this->db->sql_freeresult($result);
+
+ return $tables;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ function sql_create_table($table_name, $table_data)
+ {
+ // holds the DDL for a column
+ $columns = $statements = array();
+
+ if ($this->sql_table_exists($table_name))
+ {
+ return $this->_sql_run_sql($statements);
+ }
+
+ // Begin transaction
+ $statements[] = 'begin';
+
+ // Determine if we have created a PRIMARY KEY in the earliest
+ $primary_key_gen = false;
+
+ // Determine if the table requires a sequence
+ $create_sequence = false;
+
+ // Begin table sql statement
+ $table_sql = 'CREATE TABLE ' . $table_name . ' (' . "\n";
+
+ // Iterate through the columns to create a table
+ foreach ($table_data['COLUMNS'] as $column_name => $column_data)
+ {
+ // here lies an array, filled with information compiled on the column's data
+ $prepared_column = $this->sql_prepare_column_data($table_name, $column_name, $column_data);
+
+ if (isset($prepared_column['auto_increment']) && $prepared_column['auto_increment'] && strlen($column_name) > 26) // "${column_name}_gen"
+ {
+ trigger_error("Index name '${column_name}_gen' on table '$table_name' is too long. The maximum auto increment column length is 26 characters.", E_USER_ERROR);
+ }
+
+ // here we add the definition of the new column to the list of columns
+ $columns[] = "\t {$column_name} " . $prepared_column['column_type_sql'];
+
+ // see if we have found a primary key set due to a column definition if we have found it, we can stop looking
+ if (!$primary_key_gen)
+ {
+ $primary_key_gen = isset($prepared_column['primary_key_set']) && $prepared_column['primary_key_set'];
+ }
+
+ // create sequence DDL based off of the existance of auto incrementing columns
+ if (!$create_sequence && isset($prepared_column['auto_increment']) && $prepared_column['auto_increment'])
+ {
+ $create_sequence = $column_name;
+ }
+ }
+
+ // this makes up all the columns in the create table statement
+ $table_sql .= implode(",\n", $columns);
+
+ // we have yet to create a primary key for this table,
+ // this means that we can add the one we really wanted instead
+ if (!$primary_key_gen)
+ {
+ // Write primary key
+ if (isset($table_data['PRIMARY_KEY']))
+ {
+ if (!is_array($table_data['PRIMARY_KEY']))
+ {
+ $table_data['PRIMARY_KEY'] = array($table_data['PRIMARY_KEY']);
+ }
+
+ $table_sql .= ",\n\t PRIMARY KEY (" . implode(', ', $table_data['PRIMARY_KEY']) . ')';
+ }
+ }
+
+ // do we need to add a sequence for auto incrementing columns?
+ if ($create_sequence)
+ {
+ $statements[] = "CREATE SEQUENCE {$table_name}_seq;";
+ }
+
+ // close the table
+ $table_sql .= "\n);";
+ $statements[] = $table_sql;
+
+ // Write Keys
+ if (isset($table_data['KEYS']))
+ {
+ foreach ($table_data['KEYS'] as $key_name => $key_data)
+ {
+ if (!is_array($key_data[1]))
+ {
+ $key_data[1] = array($key_data[1]);
+ }
+
+ $old_return_statements = $this->return_statements;
+ $this->return_statements = true;
+
+ $key_stmts = ($key_data[0] == 'UNIQUE') ? $this->sql_create_unique_index($table_name, $key_name, $key_data[1]) : $this->sql_create_index($table_name, $key_name, $key_data[1]);
+
+ foreach ($key_stmts as $key_stmt)
+ {
+ $statements[] = $key_stmt;
+ }
+
+ $this->return_statements = $old_return_statements;
+ }
+ }
+
+ // Commit Transaction
+ $statements[] = 'commit';
+
+ return $this->_sql_run_sql($statements);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ function sql_list_columns($table_name)
+ {
+ $columns = array();
+
+ $sql = "SELECT a.attname
+ FROM pg_class c, pg_attribute a
+ WHERE c.relname = '{$table_name}'
+ AND a.attnum > 0
+ AND a.attrelid = c.oid";
+ $result = $this->db->sql_query($sql);
+
+ while ($row = $this->db->sql_fetchrow($result))
+ {
+ $column = strtolower(current($row));
+ $columns[$column] = $column;
+ }
+ $this->db->sql_freeresult($result);
+
+ return $columns;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ function sql_index_exists($table_name, $index_name)
+ {
+ $sql = "SELECT ic.relname as index_name
+ FROM pg_class bc, pg_class ic, pg_index i
+ WHERE (bc.oid = i.indrelid)
+ AND (ic.oid = i.indexrelid)
+ AND (bc.relname = '" . $table_name . "')
+ AND (i.indisunique != 't')
+ AND (i.indisprimary != 't')";
+ $result = $this->db->sql_query($sql);
+
+ while ($row = $this->db->sql_fetchrow($result))
+ {
+ // This DBMS prefixes index names with the table name
+ $row['index_name'] = $this->strip_table_name_from_index_name($table_name, $row['index_name']);
+
+ if (strtolower($row['index_name']) == strtolower($index_name))
+ {
+ $this->db->sql_freeresult($result);
+ return true;
+ }
+ }
+ $this->db->sql_freeresult($result);
+
+ return false;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ function sql_unique_index_exists($table_name, $index_name)
+ {
+ $sql = "SELECT ic.relname as index_name, i.indisunique
+ FROM pg_class bc, pg_class ic, pg_index i
+ WHERE (bc.oid = i.indrelid)
+ AND (ic.oid = i.indexrelid)
+ AND (bc.relname = '" . $table_name . "')
+ AND (i.indisprimary != 't')";
+ $result = $this->db->sql_query($sql);
+
+ while ($row = $this->db->sql_fetchrow($result))
+ {
+ if ($row['indisunique'] != 't')
+ {
+ continue;
+ }
+
+ // This DBMS prefixes index names with the table name
+ $row['index_name'] = $this->strip_table_name_from_index_name($table_name, $row['index_name']);
+
+ if (strtolower($row['index_name']) == strtolower($index_name))
+ {
+ $this->db->sql_freeresult($result);
+ return true;
+ }
+ }
+ $this->db->sql_freeresult($result);
+
+ return false;
+ }
+
+ /**
+ * Function to prepare some column information for better usage
+ * @access private
+ */
+ function sql_prepare_column_data($table_name, $column_name, $column_data)
+ {
+ if (strlen($column_name) > 30)
+ {
+ trigger_error("Column name '$column_name' on table '$table_name' is too long. The maximum is 30 characters.", E_USER_ERROR);
+ }
+
+ // Get type
+ list($column_type, $orig_column_type) = $this->get_column_type($column_data[0]);
+
+ // Adjust default value if db-dependent specified
+ if (is_array($column_data[1]))
+ {
+ $column_data[1] = (isset($column_data[1][$this->sql_layer])) ? $column_data[1][$this->sql_layer] : $column_data[1]['default'];
+ }
+
+ $sql = " {$column_type} ";
+
+ $return_array = array(
+ 'column_type' => $column_type,
+ 'auto_increment' => false,
+ );
+
+ if (isset($column_data[2]) && $column_data[2] == 'auto_increment')
+ {
+ $default_val = "nextval('{$table_name}_seq')";
+ $return_array['auto_increment'] = true;
+ }
+ else if (!is_null($column_data[1]))
+ {
+ $default_val = "'" . $column_data[1] . "'";
+ $return_array['null'] = 'NOT NULL';
+ $sql .= 'NOT NULL ';
+ }
+ else
+ {
+ // Integers need to have 0 instead of empty string as default
+ if (strpos($column_type, 'INT') === 0)
+ {
+ $default_val = '0';
+ }
+ else
+ {
+ $default_val = "'" . $column_data[1] . "'";
+ }
+ $return_array['null'] = 'NULL';
+ $sql .= 'NULL ';
+ }
+
+ $return_array['default'] = $default_val;
+
+ $sql .= "DEFAULT {$default_val}";
+
+ // Unsigned? Then add a CHECK contraint
+ if (in_array($orig_column_type, $this->unsigned_types))
+ {
+ $return_array['constraint'] = "CHECK ({$column_name} >= 0)";
+ $sql .= " CHECK ({$column_name} >= 0)";
+ }
+
+ $return_array['column_type_sql'] = $sql;
+
+ return $return_array;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ function sql_column_add($table_name, $column_name, $column_data, $inline = false)
+ {
+ $column_data = $this->sql_prepare_column_data($table_name, $column_name, $column_data);
+ $statements = array();
+
+ // Does not support AFTER, only through temporary table
+ if (version_compare($this->db->sql_server_info(true), '8.0', '>='))
+ {
+ $statements[] = 'ALTER TABLE ' . $table_name . ' ADD COLUMN "' . $column_name . '" ' . $column_data['column_type_sql'];
+ }
+ else
+ {
+ // old versions cannot add columns with default and null information
+ $statements[] = 'ALTER TABLE ' . $table_name . ' ADD COLUMN "' . $column_name . '" ' . $column_data['column_type'] . ' ' . $column_data['constraint'];
+
+ if (isset($column_data['null']))
+ {
+ if ($column_data['null'] == 'NOT NULL')
+ {
+ $statements[] = 'ALTER TABLE ' . $table_name . ' ALTER COLUMN ' . $column_name . ' SET NOT NULL';
+ }
+ }
+
+ if (isset($column_data['default']))
+ {
+ $statements[] = 'ALTER TABLE ' . $table_name . ' ALTER COLUMN ' . $column_name . ' SET DEFAULT ' . $column_data['default'];
+ }
+ }
+
+ return $this->_sql_run_sql($statements);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ function sql_column_remove($table_name, $column_name, $inline = false)
+ {
+ $statements = array();
+
+ $statements[] = 'ALTER TABLE ' . $table_name . ' DROP COLUMN "' . $column_name . '"';
+
+ return $this->_sql_run_sql($statements);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ function sql_index_drop($table_name, $index_name)
+ {
+ $statements = array();
+
+ $statements[] = 'DROP INDEX ' . $table_name . '_' . $index_name;
+
+ return $this->_sql_run_sql($statements);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ function sql_table_drop($table_name)
+ {
+ $statements = array();
+
+ if (!$this->sql_table_exists($table_name))
+ {
+ return $this->_sql_run_sql($statements);
+ }
+
+ // the most basic operation, get rid of the table
+ $statements[] = 'DROP TABLE ' . $table_name;
+
+ // PGSQL does not "tightly" bind sequences and tables, we must guess...
+ $sql = "SELECT relname
+ FROM pg_class
+ WHERE relkind = 'S'
+ AND relname = '{$table_name}_seq'";
+ $result = $this->db->sql_query($sql);
+
+ // We don't even care about storing the results. We already know the answer if we get rows back.
+ if ($this->db->sql_fetchrow($result))
+ {
+ $statements[] = "DROP SEQUENCE {$table_name}_seq;\n";
+ }
+ $this->db->sql_freeresult($result);
+
+ return $this->_sql_run_sql($statements);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ function sql_create_primary_key($table_name, $column, $inline = false)
+ {
+ $statements = array();
+
+ $statements[] = 'ALTER TABLE ' . $table_name . ' ADD PRIMARY KEY (' . implode(', ', $column) . ')';
+
+ return $this->_sql_run_sql($statements);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ function sql_create_unique_index($table_name, $index_name, $column)
+ {
+ $statements = array();
+
+ $this->check_index_name_length($table_name, $index_name);
+
+ $statements[] = 'CREATE UNIQUE INDEX ' . $table_name . '_' . $index_name . ' ON ' . $table_name . '(' . implode(', ', $column) . ')';
+
+ return $this->_sql_run_sql($statements);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ function sql_create_index($table_name, $index_name, $column)
+ {
+ $statements = array();
+
+ $this->check_index_name_length($table_name, $index_name);
+
+ // remove index length
+ $column = preg_replace('#:.*$#', '', $column);
+
+ $statements[] = 'CREATE INDEX ' . $table_name . '_' . $index_name . ' ON ' . $table_name . '(' . implode(', ', $column) . ')';
+
+ return $this->_sql_run_sql($statements);
+ }
+
+
+ /**
+ * {@inheritDoc}
+ */
+ function sql_list_index($table_name)
+ {
+ $index_array = array();
+
+ $sql = "SELECT ic.relname as index_name
+ FROM pg_class bc, pg_class ic, pg_index i
+ WHERE (bc.oid = i.indrelid)
+ AND (ic.oid = i.indexrelid)
+ AND (bc.relname = '" . $table_name . "')
+ AND (i.indisunique != 't')
+ AND (i.indisprimary != 't')";
+ $result = $this->db->sql_query($sql);
+
+ while ($row = $this->db->sql_fetchrow($result))
+ {
+ $row['index_name'] = $this->strip_table_name_from_index_name($table_name, $row['index_name']);
+
+ $index_array[] = $row['index_name'];
+ }
+ $this->db->sql_freeresult($result);
+
+ return array_map('strtolower', $index_array);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ function sql_column_change($table_name, $column_name, $column_data, $inline = false)
+ {
+ $column_data = $this->sql_prepare_column_data($table_name, $column_name, $column_data);
+ $statements = array();
+
+ $sql = 'ALTER TABLE ' . $table_name . ' ';
+
+ $sql_array = array();
+ $sql_array[] = 'ALTER COLUMN ' . $column_name . ' TYPE ' . $column_data['column_type'];
+
+ if (isset($column_data['null']))
+ {
+ if ($column_data['null'] == 'NOT NULL')
+ {
+ $sql_array[] = 'ALTER COLUMN ' . $column_name . ' SET NOT NULL';
+ }
+ else if ($column_data['null'] == 'NULL')
+ {
+ $sql_array[] = 'ALTER COLUMN ' . $column_name . ' DROP NOT NULL';
+ }
+ }
+
+ if (isset($column_data['default']))
+ {
+ $sql_array[] = 'ALTER COLUMN ' . $column_name . ' SET DEFAULT ' . $column_data['default'];
+ }
+
+ // we don't want to double up on constraints if we change different number data types
+ if (isset($column_data['constraint']))
+ {
+ $constraint_sql = "SELECT consrc as constraint_data
+ FROM pg_constraint, pg_class bc
+ WHERE conrelid = bc.oid
+ AND bc.relname = '{$table_name}'
+ AND NOT EXISTS (
+ SELECT *
+ FROM pg_constraint as c, pg_inherits as i
+ WHERE i.inhrelid = pg_constraint.conrelid
+ AND c.conname = pg_constraint.conname
+ AND c.consrc = pg_constraint.consrc
+ AND c.conrelid = i.inhparent
+ )";
+
+ $constraint_exists = false;
+
+ $result = $this->db->sql_query($constraint_sql);
+ while ($row = $this->db->sql_fetchrow($result))
+ {
+ if (trim($row['constraint_data']) == trim($column_data['constraint']))
+ {
+ $constraint_exists = true;
+ break;
+ }
+ }
+ $this->db->sql_freeresult($result);
+
+ if (!$constraint_exists)
+ {
+ $sql_array[] = 'ADD ' . $column_data['constraint'];
+ }
+ }
+
+ $sql .= implode(', ', $sql_array);
+
+ $statements[] = $sql;
+
+ return $this->_sql_run_sql($statements);
+ }
+
+ /**
+ * Get a list with existing indexes for the column
+ *
+ * @param string $table_name
+ * @param string $column_name
+ * @param bool $unique Should we get unique indexes or normal ones
+ * @return array Array with Index name => columns
+ */
+ public function get_existing_indexes($table_name, $column_name, $unique = false)
+ {
+ // Not supported
+ throw new \Exception('DBMS is not supported');
+ }
+}
diff --git a/phpBB/phpbb/db/tools.php b/phpBB/phpbb/db/tools/tools.php
index 775deccc30..1d7b2ddfff 100644
--- a/phpBB/phpbb/db/tools.php
+++ b/phpBB/phpbb/db/tools/tools.php
@@ -11,13 +11,13 @@
*
*/
-namespace phpbb\db;
+namespace phpbb\db\tools;
/**
* Database Tools for handling cross-db actions such as altering columns, etc.
* Currently not supported is returning SQL for creating tables.
*/
-class tools
+class tools implements tools_interface
{
/**
* Current sql layer
@@ -36,17 +36,11 @@ class tools
var $dbms_type_map = array();
/**
- * Is the used MS SQL Server a SQL Server 2000?
- * @var bool
- */
- protected $is_sql_server_2000;
-
- /**
* Get the column types for every database we support
*
* @return array
*/
- public static function get_dbms_type_map()
+ static public function get_dbms_type_map()
{
return array(
'mysql_41' => array(
@@ -109,66 +103,6 @@ class tools
'VARBINARY' => 'varbinary(255)',
),
- 'mssql' => array(
- 'INT:' => '[int]',
- 'BINT' => '[float]',
- 'UINT' => '[int]',
- 'UINT:' => '[int]',
- 'TINT:' => '[int]',
- 'USINT' => '[int]',
- 'BOOL' => '[int]',
- 'VCHAR' => '[varchar] (255)',
- 'VCHAR:' => '[varchar] (%d)',
- 'CHAR:' => '[char] (%d)',
- 'XSTEXT' => '[varchar] (1000)',
- 'STEXT' => '[varchar] (3000)',
- 'TEXT' => '[varchar] (8000)',
- 'MTEXT' => '[text]',
- 'XSTEXT_UNI'=> '[varchar] (100)',
- 'STEXT_UNI' => '[varchar] (255)',
- 'TEXT_UNI' => '[varchar] (4000)',
- 'MTEXT_UNI' => '[text]',
- 'TIMESTAMP' => '[int]',
- 'DECIMAL' => '[float]',
- 'DECIMAL:' => '[float]',
- 'PDECIMAL' => '[float]',
- 'PDECIMAL:' => '[float]',
- 'VCHAR_UNI' => '[varchar] (255)',
- 'VCHAR_UNI:'=> '[varchar] (%d)',
- 'VCHAR_CI' => '[varchar] (255)',
- 'VARBINARY' => '[varchar] (255)',
- ),
-
- 'mssqlnative' => array(
- 'INT:' => '[int]',
- 'BINT' => '[float]',
- 'UINT' => '[int]',
- 'UINT:' => '[int]',
- 'TINT:' => '[int]',
- 'USINT' => '[int]',
- 'BOOL' => '[int]',
- 'VCHAR' => '[varchar] (255)',
- 'VCHAR:' => '[varchar] (%d)',
- 'CHAR:' => '[char] (%d)',
- 'XSTEXT' => '[varchar] (1000)',
- 'STEXT' => '[varchar] (3000)',
- 'TEXT' => '[varchar] (8000)',
- 'MTEXT' => '[text]',
- 'XSTEXT_UNI'=> '[varchar] (100)',
- 'STEXT_UNI' => '[varchar] (255)',
- 'TEXT_UNI' => '[varchar] (4000)',
- 'MTEXT_UNI' => '[text]',
- 'TIMESTAMP' => '[int]',
- 'DECIMAL' => '[float]',
- 'DECIMAL:' => '[float]',
- 'PDECIMAL' => '[float]',
- 'PDECIMAL:' => '[float]',
- 'VCHAR_UNI' => '[varchar] (255)',
- 'VCHAR_UNI:'=> '[varchar] (%d)',
- 'VCHAR_CI' => '[varchar] (255)',
- 'VARBINARY' => '[varchar] (255)',
- ),
-
'oracle' => array(
'INT:' => 'number(%d)',
'BINT' => 'number(20)',
@@ -258,36 +192,6 @@ class tools
'VCHAR_CI' => 'VARCHAR(255)',
'VARBINARY' => 'BLOB',
),
-
- 'postgres' => array(
- 'INT:' => 'INT4',
- 'BINT' => 'INT8',
- 'UINT' => 'INT4', // unsigned
- 'UINT:' => 'INT4', // unsigned
- 'USINT' => 'INT2', // unsigned
- 'BOOL' => 'INT2', // unsigned
- 'TINT:' => 'INT2',
- 'VCHAR' => 'varchar(255)',
- 'VCHAR:' => 'varchar(%d)',
- 'CHAR:' => 'char(%d)',
- 'XSTEXT' => 'varchar(1000)',
- 'STEXT' => 'varchar(3000)',
- 'TEXT' => 'varchar(8000)',
- 'MTEXT' => 'TEXT',
- 'XSTEXT_UNI'=> 'varchar(100)',
- 'STEXT_UNI' => 'varchar(255)',
- 'TEXT_UNI' => 'varchar(4000)',
- 'MTEXT_UNI' => 'TEXT',
- 'TIMESTAMP' => 'INT4', // unsigned
- 'DECIMAL' => 'decimal(5,2)',
- 'DECIMAL:' => 'decimal(%d,2)',
- 'PDECIMAL' => 'decimal(6,3)',
- 'PDECIMAL:' => 'decimal(%d,3)',
- 'VCHAR_UNI' => 'varchar(255)',
- 'VCHAR_UNI:'=> 'varchar(%d)',
- 'VCHAR_CI' => 'varchar_ci',
- 'VARBINARY' => 'bytea',
- ),
);
}
@@ -298,12 +202,6 @@ class tools
var $unsigned_types = array('UINT', 'UINT:', 'USINT', 'BOOL', 'TIMESTAMP');
/**
- * A list of supported DBMS. We change this class to support more DBMS, the DBMS itself only need to follow some rules.
- * @var array
- */
- var $supported_dbms = array('mssql', 'mssqlnative', 'mysql_40', 'mysql_41', 'oracle', 'postgres', 'sqlite', 'sqlite3');
-
- /**
* This is set to true if user only wants to return the 'to-be-executed' SQL statement(s) (as an array).
* This mode has no effect on some methods (inserting of data for example). This is expressed within the methods command.
*/
@@ -344,15 +242,6 @@ class tools
$this->sql_layer = 'mysql_41';
break;
- case 'mssql':
- case 'mssql_odbc':
- $this->sql_layer = 'mssql';
- break;
-
- case 'mssqlnative':
- $this->sql_layer = 'mssqlnative';
- break;
-
default:
$this->sql_layer = $this->db->get_sql_layer();
break;
@@ -371,10 +260,8 @@ class tools
}
/**
- * Gets a list of tables in the database.
- *
- * @return array Array of table names (all lower case)
- */
+ * {@inheritDoc}
+ */
function sql_list_tables()
{
switch ($this->db->get_sql_layer())
@@ -398,19 +285,6 @@ class tools
AND name <> "sqlite_sequence"';
break;
- case 'mssql':
- case 'mssql_odbc':
- case 'mssqlnative':
- $sql = "SELECT name
- FROM sysobjects
- WHERE type='U'";
- break;
-
- case 'postgres':
- $sql = 'SELECT relname
- FROM pg_stat_user_tables';
- break;
-
case 'oracle':
$sql = 'SELECT table_name
FROM USER_TABLES';
@@ -431,12 +305,8 @@ class tools
}
/**
- * Check if table exists
- *
- *
- * @param string $table_name The table name to check for
- * @return bool true if table exists, else false
- */
+ * {@inheritDoc}
+ */
function sql_table_exists($table_name)
{
$this->db->sql_return_on_error(true);
@@ -453,12 +323,8 @@ class tools
}
/**
- * Create SQL Table
- *
- * @param string $table_name The table name to create
- * @param array $table_data Array containing table data.
- * @return array Statements if $return_statements is true.
- */
+ * {@inheritDoc}
+ */
function sql_create_table($table_name, $table_data)
{
// holds the DDL for a column
@@ -479,26 +345,7 @@ class tools
$create_sequence = false;
// Begin table sql statement
- switch ($this->sql_layer)
- {
- case 'mssql':
- case 'mssqlnative':
- $table_sql = 'CREATE TABLE [' . $table_name . '] (' . "\n";
- break;
-
- default:
- $table_sql = 'CREATE TABLE ' . $table_name . ' (' . "\n";
- break;
- }
-
- if ($this->sql_layer == 'mssql' || $this->sql_layer == 'mssqlnative')
- {
- if (!isset($table_data['PRIMARY_KEY']))
- {
- $table_data['COLUMNS']['mssqlindex'] = array('UINT', null, 'auto_increment');
- $table_data['PRIMARY_KEY'] = 'mssqlindex';
- }
- }
+ $table_sql = 'CREATE TABLE ' . $table_name . ' (' . "\n";
// Iterate through the columns to create a table
foreach ($table_data['COLUMNS'] as $column_name => $column_data)
@@ -512,17 +359,7 @@ class tools
}
// here we add the definition of the new column to the list of columns
- switch ($this->sql_layer)
- {
- case 'mssql':
- case 'mssqlnative':
- $columns[] = "\t [{$column_name}] " . $prepared_column['column_type_sql_default'];
- break;
-
- default:
- $columns[] = "\t {$column_name} " . $prepared_column['column_type_sql'];
- break;
- }
+ $columns[] = "\t {$column_name} " . $prepared_column['column_type_sql'];
// see if we have found a primary key set due to a column definition if we have found it, we can stop looking
if (!$primary_key_gen)
@@ -540,16 +377,6 @@ class tools
// this makes up all the columns in the create table statement
$table_sql .= implode(",\n", $columns);
- // Close the table for two DBMS and add to the statements
- switch ($this->sql_layer)
- {
- case 'mssql':
- case 'mssqlnative':
- $table_sql .= "\n);";
- $statements[] = $table_sql;
- break;
- }
-
// we have yet to create a primary key for this table,
// this means that we can add the one we really wanted instead
if (!$primary_key_gen)
@@ -566,27 +393,11 @@ class tools
{
case 'mysql_40':
case 'mysql_41':
- case 'postgres':
case 'sqlite':
case 'sqlite3':
$table_sql .= ",\n\t PRIMARY KEY (" . implode(', ', $table_data['PRIMARY_KEY']) . ')';
break;
- case 'mssql':
- case 'mssqlnative':
- // We need the data here
- $old_return_statements = $this->return_statements;
- $this->return_statements = true;
-
- $primary_key_stmts = $this->sql_create_primary_key($table_name, $table_data['PRIMARY_KEY']);
- foreach ($primary_key_stmts as $pk_stmt)
- {
- $statements[] = $pk_stmt;
- }
-
- $this->return_statements = $old_return_statements;
- break;
-
case 'oracle':
$table_sql .= ",\n\t CONSTRAINT pk_{$table_name} PRIMARY KEY (" . implode(', ', $table_data['PRIMARY_KEY']) . ')';
break;
@@ -610,17 +421,6 @@ class tools
$statements[] = $table_sql;
break;
- case 'postgres':
- // do we need to add a sequence for auto incrementing columns?
- if ($create_sequence)
- {
- $statements[] = "CREATE SEQUENCE {$table_name}_seq;";
- }
-
- $table_sql .= "\n);";
- $statements[] = $table_sql;
- break;
-
case 'oracle':
$table_sql .= "\n)";
$statements[] = $table_sql;
@@ -679,27 +479,8 @@ class tools
}
/**
- * Handle passed database update array.
- * Expected structure...
- * Key being one of the following
- * drop_tables: Drop tables
- * add_tables: Add tables
- * change_columns: Column changes (only type, not name)
- * add_columns: Add columns to a table
- * drop_keys: Dropping keys
- * drop_columns: Removing/Dropping columns
- * add_primary_keys: adding primary keys
- * add_unique_index: adding an unique index
- * add_index: adding an index (can be column:index_size if you need to provide size)
- *
- * The values are in this format:
- * {TABLE NAME} => array(
- * {COLUMN NAME} => array({COLUMN TYPE}, {DEFAULT VALUE}, {OPTIONAL VARIABLES}),
- * {KEY/INDEX NAME} => array({COLUMN NAMES}),
- * )
- *
- * For more information have a look at /develop/create_schema_files.php (only available through SVN)
- */
+ * {@inheritDoc}
+ */
function perform_schema_changes($schema_changes)
{
if (empty($schema_changes))
@@ -1079,13 +860,9 @@ class tools
}
/**
- * Gets a list of columns of a table.
- *
- * @param string $table Table name
- *
- * @return array Array of column names (all lower case)
- */
- function sql_list_columns($table)
+ * {@inheritDoc}
+ */
+ function sql_list_columns($table_name)
{
$columns = array();
@@ -1093,33 +870,13 @@ class tools
{
case 'mysql_40':
case 'mysql_41':
- $sql = "SHOW COLUMNS FROM $table";
- break;
-
- // PostgreSQL has a way of doing this in a much simpler way but would
- // not allow us to support all versions of PostgreSQL
- case 'postgres':
- $sql = "SELECT a.attname
- FROM pg_class c, pg_attribute a
- WHERE c.relname = '{$table}'
- AND a.attnum > 0
- AND a.attrelid = c.oid";
- break;
-
- // same deal with PostgreSQL, we must perform more complex operations than
- // we technically could
- case 'mssql':
- case 'mssqlnative':
- $sql = "SELECT c.name
- FROM syscolumns c
- LEFT JOIN sysobjects o ON c.id = o.id
- WHERE o.name = '{$table}'";
+ $sql = "SHOW COLUMNS FROM $table_name";
break;
case 'oracle':
$sql = "SELECT column_name
FROM user_tab_columns
- WHERE LOWER(table_name) = '" . strtolower($table) . "'";
+ WHERE LOWER(table_name) = '" . strtolower($table_name) . "'";
break;
case 'sqlite':
@@ -1127,7 +884,7 @@ class tools
$sql = "SELECT sql
FROM sqlite_master
WHERE type = 'table'
- AND name = '{$table}'";
+ AND name = '{$table_name}'";
$result = $this->db->sql_query($sql);
@@ -1173,64 +930,22 @@ class tools
}
/**
- * Check whether a specified column exist in a table
- *
- * @param string $table Table to check
- * @param string $column_name Column to check
- *
- * @return bool True if column exists, false otherwise
- */
- function sql_column_exists($table, $column_name)
+ * {@inheritDoc}
+ */
+ function sql_column_exists($table_name, $column_name)
{
- $columns = $this->sql_list_columns($table);
+ $columns = $this->sql_list_columns($table_name);
return isset($columns[$column_name]);
}
/**
- * Check if a specified index exists in table. Does not return PRIMARY KEY and UNIQUE indexes.
- *
- * @param string $table_name Table to check the index at
- * @param string $index_name The index name to check
- *
- * @return bool True if index exists, else false
- */
+ * {@inheritDoc}
+ */
function sql_index_exists($table_name, $index_name)
{
- if ($this->sql_layer == 'mssql' || $this->sql_layer == 'mssqlnative')
- {
- $sql = "EXEC sp_statistics '$table_name'";
- $result = $this->db->sql_query($sql);
-
- while ($row = $this->db->sql_fetchrow($result))
- {
- if ($row['TYPE'] == 3)
- {
- if (strtolower($row['INDEX_NAME']) == strtolower($index_name))
- {
- $this->db->sql_freeresult($result);
- return true;
- }
- }
- }
- $this->db->sql_freeresult($result);
-
- return false;
- }
-
switch ($this->sql_layer)
{
- case 'postgres':
- $sql = "SELECT ic.relname as index_name
- FROM pg_class bc, pg_class ic, pg_index i
- WHERE (bc.oid = i.indrelid)
- AND (ic.oid = i.indexrelid)
- AND (bc.relname = '" . $table_name . "')
- AND (i.indisunique != 't')
- AND (i.indisprimary != 't')";
- $col = 'index_name';
- break;
-
case 'mysql_40':
case 'mysql_41':
$sql = 'SHOW KEYS
@@ -1266,7 +981,6 @@ class tools
switch ($this->sql_layer)
{
case 'oracle':
- case 'postgres':
case 'sqlite':
case 'sqlite3':
$row[$col] = substr($row[$col], strlen($table_name) + 1);
@@ -1285,48 +999,12 @@ class tools
}
/**
- * Check if a specified index exists in table. Does not return PRIMARY KEY indexes.
- *
- * @param string $table_name Table to check the index at
- * @param string $index_name The index name to check
- *
- * @return bool True if index exists, else false
- */
+ * {@inheritDoc}
+ */
function sql_unique_index_exists($table_name, $index_name)
{
- if ($this->sql_layer == 'mssql' || $this->sql_layer == 'mssqlnative')
- {
- $sql = "EXEC sp_statistics '$table_name'";
- $result = $this->db->sql_query($sql);
-
- while ($row = $this->db->sql_fetchrow($result))
- {
- // Usually NON_UNIQUE is the column we want to check, but we allow for both
- if ($row['TYPE'] == 3)
- {
- if (strtolower($row['INDEX_NAME']) == strtolower($index_name))
- {
- $this->db->sql_freeresult($result);
- return true;
- }
- }
- }
- $this->db->sql_freeresult($result);
- return false;
- }
-
switch ($this->sql_layer)
{
- case 'postgres':
- $sql = "SELECT ic.relname as index_name, i.indisunique
- FROM pg_class bc, pg_class ic, pg_index i
- WHERE (bc.oid = i.indrelid)
- AND (ic.oid = i.indexrelid)
- AND (bc.relname = '" . $table_name . "')
- AND (i.indisprimary != 't')";
- $col = 'index_name';
- break;
-
case 'mysql_40':
case 'mysql_41':
$sql = 'SHOW KEYS
@@ -1363,11 +1041,6 @@ class tools
continue;
}
- if ($this->sql_layer == 'postgres' && $row['indisunique'] != 't')
- {
- continue;
- }
-
// These DBMS prefix index name with the table name
switch ($this->sql_layer)
{
@@ -1383,7 +1056,6 @@ class tools
}
break;
- case 'postgres':
case 'sqlite':
case 'sqlite3':
$row[$col] = substr($row[$col], strlen($table_name) + 1);
@@ -1458,50 +1130,6 @@ class tools
switch ($this->sql_layer)
{
- case 'mssql':
- case 'mssqlnative':
- $sql .= " {$column_type} ";
- $sql_default = " {$column_type} ";
-
- // For adding columns we need the default definition
- if (!is_null($column_data[1]))
- {
- // For hexadecimal values do not use single quotes
- if (strpos($column_data[1], '0x') === 0)
- {
- $return_array['default'] = 'DEFAULT (' . $column_data[1] . ') ';
- $sql_default .= $return_array['default'];
- }
- else
- {
- $return_array['default'] = 'DEFAULT (' . ((is_numeric($column_data[1])) ? $column_data[1] : "'{$column_data[1]}'") . ') ';
- $sql_default .= $return_array['default'];
- }
- }
-
- if (isset($column_data[2]) && $column_data[2] == 'auto_increment')
- {
-// $sql .= 'IDENTITY (1, 1) ';
- $sql_default .= 'IDENTITY (1, 1) ';
- }
-
- $return_array['textimage'] = $column_type === '[text]';
-
- if (!is_null($column_data[1]) || (isset($column_data[2]) && $column_data[2] == 'auto_increment'))
- {
- $sql .= 'NOT NULL';
- $sql_default .= 'NOT NULL';
- }
- else
- {
- $sql .= 'NULL';
- $sql_default .= 'NULL';
- }
-
- $return_array['column_type_sql_default'] = $sql_default;
-
- break;
-
case 'mysql_40':
case 'mysql_41':
$sql .= " {$column_type} ";
@@ -1555,51 +1183,6 @@ class tools
break;
- case 'postgres':
- $return_array['column_type'] = $column_type;
-
- $sql .= " {$column_type} ";
-
- $return_array['auto_increment'] = false;
- if (isset($column_data[2]) && $column_data[2] == 'auto_increment')
- {
- $default_val = "nextval('{$table_name}_seq')";
- $return_array['auto_increment'] = true;
- }
- else if (!is_null($column_data[1]))
- {
- $default_val = "'" . $column_data[1] . "'";
- $return_array['null'] = 'NOT NULL';
- $sql .= 'NOT NULL ';
- }
- else
- {
- // Integers need to have 0 instead of empty string as default
- if (strpos($column_type, 'INT') === 0)
- {
- $default_val = '0';
- }
- else
- {
- $default_val = "'" . $column_data[1] . "'";
- }
- $return_array['null'] = 'NULL';
- $sql .= 'NULL ';
- }
-
- $return_array['default'] = $default_val;
-
- $sql .= "DEFAULT {$default_val}";
-
- // Unsigned? Then add a CHECK contraint
- if (in_array($orig_column_type, $this->unsigned_types))
- {
- $return_array['constraint'] = "CHECK ({$column_name} >= 0)";
- $sql .= " CHECK ({$column_name} >= 0)";
- }
-
- break;
-
case 'sqlite':
case 'sqlite3':
$return_array['primary_key_set'] = false;
@@ -1641,6 +1224,7 @@ class tools
*/
function get_column_type($column_map_type)
{
+ $column_type = '';
if (strpos($column_map_type, ':') !== false)
{
list($orig_column_type, $column_length) = explode(':', $column_map_type);
@@ -1692,8 +1276,8 @@ class tools
}
/**
- * Add new column
- */
+ * {@inheritDoc}
+ */
function sql_column_add($table_name, $column_name, $column_data, $inline = false)
{
$column_data = $this->sql_prepare_column_data($table_name, $column_name, $column_data);
@@ -1701,12 +1285,6 @@ class tools
switch ($this->sql_layer)
{
- case 'mssql':
- case 'mssqlnative':
- // Does not support AFTER, only through temporary table
- $statements[] = 'ALTER TABLE [' . $table_name . '] ADD [' . $column_name . '] ' . $column_data['column_type_sql_default'];
- break;
-
case 'mysql_40':
case 'mysql_41':
$after = (!empty($column_data['after'])) ? ' AFTER ' . $column_data['after'] : '';
@@ -1718,33 +1296,6 @@ class tools
$statements[] = 'ALTER TABLE ' . $table_name . ' ADD ' . $column_name . ' ' . $column_data['column_type_sql'];
break;
- case 'postgres':
- // Does not support AFTER, only through temporary table
- if (version_compare($this->db->sql_server_info(true), '8.0', '>='))
- {
- $statements[] = 'ALTER TABLE ' . $table_name . ' ADD COLUMN "' . $column_name . '" ' . $column_data['column_type_sql'];
- }
- else
- {
- // old versions cannot add columns with default and null information
- $statements[] = 'ALTER TABLE ' . $table_name . ' ADD COLUMN "' . $column_name . '" ' . $column_data['column_type'] . ' ' . $column_data['constraint'];
-
- if (isset($column_data['null']))
- {
- if ($column_data['null'] == 'NOT NULL')
- {
- $statements[] = 'ALTER TABLE ' . $table_name . ' ALTER COLUMN ' . $column_name . ' SET NOT NULL';
- }
- }
-
- if (isset($column_data['default']))
- {
- $statements[] = 'ALTER TABLE ' . $table_name . ' ALTER COLUMN ' . $column_name . ' SET DEFAULT ' . $column_data['default'];
- }
- }
-
- break;
-
case 'sqlite':
if ($inline && $this->return_statements)
{
@@ -1810,59 +1361,14 @@ class tools
}
/**
- * Drop column
- */
+ * {@inheritDoc}
+ */
function sql_column_remove($table_name, $column_name, $inline = false)
{
$statements = array();
switch ($this->sql_layer)
{
- case 'mssql':
- case 'mssqlnative':
- // We need the data here
- $old_return_statements = $this->return_statements;
- $this->return_statements = true;
-
- $indexes = $this->get_existing_indexes($table_name, $column_name);
- $indexes = array_merge($indexes, $this->get_existing_indexes($table_name, $column_name, true));
-
- // Drop any indexes
- $recreate_indexes = array();
- if (!empty($indexes))
- {
- foreach ($indexes as $index_name => $index_data)
- {
- $result = $this->sql_index_drop($table_name, $index_name);
- $statements = array_merge($statements, $result);
- if (sizeof($index_data) > 1)
- {
- // Remove this column from the index and recreate it
- $recreate_indexes[$index_name] = array_diff($index_data, array($column_name));
- }
- }
- }
-
- // Drop default value constraint
- $result = $this->mssql_get_drop_default_constraints_queries($table_name, $column_name);
- $statements = array_merge($statements, $result);
-
- // Remove the column
- $statements[] = 'ALTER TABLE [' . $table_name . '] DROP COLUMN [' . $column_name . ']';
-
- if (!empty($recreate_indexes))
- {
- // Recreate indexes after we removed the column
- foreach ($recreate_indexes as $index_name => $index_data)
- {
- $result = $this->sql_create_index($table_name, $index_name, $index_data);
- $statements = array_merge($statements, $result);
- }
- }
-
- $this->return_statements = $old_return_statements;
- break;
-
case 'mysql_40':
case 'mysql_41':
$statements[] = 'ALTER TABLE `' . $table_name . '` DROP COLUMN `' . $column_name . '`';
@@ -1872,10 +1378,6 @@ class tools
$statements[] = 'ALTER TABLE ' . $table_name . ' DROP COLUMN ' . $column_name;
break;
- case 'postgres':
- $statements[] = 'ALTER TABLE ' . $table_name . ' DROP COLUMN "' . $column_name . '"';
- break;
-
case 'sqlite':
case 'sqlite3':
@@ -1939,26 +1441,20 @@ class tools
}
/**
- * Drop Index
- */
+ * {@inheritDoc}
+ */
function sql_index_drop($table_name, $index_name)
{
$statements = array();
switch ($this->sql_layer)
{
- case 'mssql':
- case 'mssqlnative':
- $statements[] = 'DROP INDEX ' . $table_name . '.' . $index_name;
- break;
-
case 'mysql_40':
case 'mysql_41':
$statements[] = 'DROP INDEX ' . $index_name . ' ON ' . $table_name;
break;
case 'oracle':
- case 'postgres':
case 'sqlite':
case 'sqlite3':
$statements[] = 'DROP INDEX ' . $table_name . '_' . $index_name;
@@ -1969,8 +1465,8 @@ class tools
}
/**
- * Drop Table
- */
+ * {@inheritDoc}
+ */
function sql_table_drop($table_name)
{
$statements = array();
@@ -2000,52 +1496,25 @@ class tools
}
$this->db->sql_freeresult($result);
break;
-
- case 'postgres':
- // PGSQL does not "tightly" bind sequences and tables, we must guess...
- $sql = "SELECT relname
- FROM pg_class
- WHERE relkind = 'S'
- AND relname = '{$table_name}_seq'";
- $result = $this->db->sql_query($sql);
-
- // We don't even care about storing the results. We already know the answer if we get rows back.
- if ($this->db->sql_fetchrow($result))
- {
- $statements[] = "DROP SEQUENCE {$table_name}_seq;\n";
- }
- $this->db->sql_freeresult($result);
- break;
}
return $this->_sql_run_sql($statements);
}
/**
- * Add primary key
- */
+ * {@inheritDoc}
+ */
function sql_create_primary_key($table_name, $column, $inline = false)
{
$statements = array();
switch ($this->sql_layer)
{
- case 'postgres':
case 'mysql_40':
case 'mysql_41':
$statements[] = 'ALTER TABLE ' . $table_name . ' ADD PRIMARY KEY (' . implode(', ', $column) . ')';
break;
- case 'mssql':
- case 'mssqlnative':
- $sql = "ALTER TABLE [{$table_name}] WITH NOCHECK ADD ";
- $sql .= "CONSTRAINT [PK_{$table_name}] PRIMARY KEY CLUSTERED (";
- $sql .= '[' . implode("],\n\t\t[", $column) . ']';
- $sql .= ')';
-
- $statements[] = $sql;
- break;
-
case 'oracle':
$statements[] = 'ALTER TABLE ' . $table_name . ' add CONSTRAINT pk_' . $table_name . ' PRIMARY KEY (' . implode(', ', $column) . ')';
break;
@@ -2106,22 +1575,16 @@ class tools
}
/**
- * Add unique index
- */
+ * {@inheritDoc}
+ */
function sql_create_unique_index($table_name, $index_name, $column)
{
$statements = array();
- $table_prefix = substr(CONFIG_TABLE, 0, -6); // strlen(config)
- if (strlen($table_name . '_' . $index_name) - strlen($table_prefix) > 24)
- {
- $max_length = strlen($table_prefix) + 24;
- trigger_error("Index name '{$table_name}_$index_name' on table '$table_name' is too long. The maximum is $max_length characters.", E_USER_ERROR);
- }
+ $this->check_index_name_length($table_name, $index_name);
switch ($this->sql_layer)
{
- case 'postgres':
case 'oracle':
case 'sqlite':
case 'sqlite3':
@@ -2132,29 +1595,19 @@ class tools
case 'mysql_41':
$statements[] = 'ALTER TABLE ' . $table_name . ' ADD UNIQUE INDEX ' . $index_name . '(' . implode(', ', $column) . ')';
break;
-
- case 'mssql':
- case 'mssqlnative':
- $statements[] = 'CREATE UNIQUE INDEX [' . $index_name . '] ON [' . $table_name . ']([' . implode('], [', $column) . '])';
- break;
}
return $this->_sql_run_sql($statements);
}
/**
- * Add index
- */
+ * {@inheritDoc}
+ */
function sql_create_index($table_name, $index_name, $column)
{
$statements = array();
- $table_prefix = substr(CONFIG_TABLE, 0, -6); // strlen(config)
- if (strlen($table_name . $index_name) - strlen($table_prefix) > 24)
- {
- $max_length = strlen($table_prefix) + 24;
- trigger_error("Index name '{$table_name}_$index_name' on table '$table_name' is too long. The maximum is $max_length characters.", E_USER_ERROR);
- }
+ $this->check_index_name_length($table_name, $index_name);
// remove index length unless MySQL4
if ('mysql_40' != $this->sql_layer)
@@ -2164,7 +1617,6 @@ class tools
switch ($this->sql_layer)
{
- case 'postgres':
case 'oracle':
case 'sqlite':
case 'sqlite3':
@@ -2185,99 +1637,79 @@ class tools
case 'mysql_41':
$statements[] = 'ALTER TABLE ' . $table_name . ' ADD INDEX ' . $index_name . ' (' . implode(', ', $column) . ')';
break;
-
- case 'mssql':
- case 'mssqlnative':
- $statements[] = 'CREATE INDEX [' . $index_name . '] ON [' . $table_name . ']([' . implode('], [', $column) . '])';
- break;
}
return $this->_sql_run_sql($statements);
}
/**
- * List all of the indices that belong to a table,
- * does not count:
- * * UNIQUE indices
- * * PRIMARY keys
- */
+ * Check whether the index name is too long
+ *
+ * @param string $table_name
+ * @param string $index_name
+ */
+ protected function check_index_name_length($table_name, $index_name)
+ {
+ $table_prefix = substr(CONFIG_TABLE, 0, -6); // strlen(config)
+ if (strlen($table_name . $index_name) - strlen($table_prefix) > 24)
+ {
+ $max_length = strlen($table_prefix) + 24;
+ trigger_error("Index name '{$table_name}_$index_name' on table '$table_name' is too long. The maximum is $max_length characters.", E_USER_ERROR);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
function sql_list_index($table_name)
{
$index_array = array();
- if ($this->sql_layer == 'mssql' || $this->sql_layer == 'mssqlnative')
- {
- $sql = "EXEC sp_statistics '$table_name'";
- $result = $this->db->sql_query($sql);
- while ($row = $this->db->sql_fetchrow($result))
- {
- if ($row['TYPE'] == 3)
- {
- $index_array[] = $row['INDEX_NAME'];
- }
- }
- $this->db->sql_freeresult($result);
- }
- else
+ switch ($this->sql_layer)
{
- switch ($this->sql_layer)
- {
- case 'postgres':
- $sql = "SELECT ic.relname as index_name
- FROM pg_class bc, pg_class ic, pg_index i
- WHERE (bc.oid = i.indrelid)
- AND (ic.oid = i.indexrelid)
- AND (bc.relname = '" . $table_name . "')
- AND (i.indisunique != 't')
- AND (i.indisprimary != 't')";
- $col = 'index_name';
+ case 'mysql_40':
+ case 'mysql_41':
+ $sql = 'SHOW KEYS
+ FROM ' . $table_name;
+ $col = 'Key_name';
break;
- case 'mysql_40':
- case 'mysql_41':
- $sql = 'SHOW KEYS
- FROM ' . $table_name;
- $col = 'Key_name';
+ case 'oracle':
+ $sql = "SELECT index_name
+ FROM user_indexes
+ WHERE table_name = '" . strtoupper($table_name) . "'
+ AND generated = 'N'
+ AND uniqueness = 'NONUNIQUE'";
+ $col = 'index_name';
break;
- case 'oracle':
- $sql = "SELECT index_name
- FROM user_indexes
- WHERE table_name = '" . strtoupper($table_name) . "'
- AND generated = 'N'
- AND uniqueness = 'NONUNIQUE'";
- $col = 'index_name';
+ case 'sqlite':
+ case 'sqlite3':
+ $sql = "PRAGMA index_info('" . $table_name . "');";
+ $col = 'name';
break;
+ }
- case 'sqlite':
- case 'sqlite3':
- $sql = "PRAGMA index_info('" . $table_name . "');";
- $col = 'name';
- break;
+ $result = $this->db->sql_query($sql);
+ while ($row = $this->db->sql_fetchrow($result))
+ {
+ if (($this->sql_layer == 'mysql_40' || $this->sql_layer == 'mysql_41') && !$row['Non_unique'])
+ {
+ continue;
}
- $result = $this->db->sql_query($sql);
- while ($row = $this->db->sql_fetchrow($result))
+ switch ($this->sql_layer)
{
- if (($this->sql_layer == 'mysql_40' || $this->sql_layer == 'mysql_41') && !$row['Non_unique'])
- {
- continue;
- }
-
- switch ($this->sql_layer)
- {
- case 'oracle':
- case 'postgres':
- case 'sqlite':
- case 'sqlite3':
- $row[$col] = substr($row[$col], strlen($table_name) + 1);
+ case 'oracle':
+ case 'sqlite':
+ case 'sqlite3':
+ $row[$col] = substr($row[$col], strlen($table_name) + 1);
break;
- }
-
- $index_array[] = $row[$col];
}
- $this->db->sql_freeresult($result);
+
+ $index_array[] = $row[$col];
}
+ $this->db->sql_freeresult($result);
return array_map('strtolower', $index_array);
}
@@ -2295,8 +1727,8 @@ class tools
}
/**
- * Change column type (not name!)
- */
+ * {@inheritDoc}
+ */
function sql_column_change($table_name, $column_name, $column_data, $inline = false)
{
$original_column_data = $column_data;
@@ -2305,62 +1737,6 @@ class tools
switch ($this->sql_layer)
{
- case 'mssql':
- case 'mssqlnative':
- // We need the data here
- $old_return_statements = $this->return_statements;
- $this->return_statements = true;
-
- $indexes = $this->get_existing_indexes($table_name, $column_name);
- $unique_indexes = $this->get_existing_indexes($table_name, $column_name, true);
-
- // Drop any indexes
- if (!empty($indexes) || !empty($unique_indexes))
- {
- $drop_indexes = array_merge(array_keys($indexes), array_keys($unique_indexes));
- foreach ($drop_indexes as $index_name)
- {
- $result = $this->sql_index_drop($table_name, $index_name);
- $statements = array_merge($statements, $result);
- }
- }
-
- // Drop default value constraint
- $result = $this->mssql_get_drop_default_constraints_queries($table_name, $column_name);
- $statements = array_merge($statements, $result);
-
- // Change the column
- $statements[] = 'ALTER TABLE [' . $table_name . '] ALTER COLUMN [' . $column_name . '] ' . $column_data['column_type_sql'];
-
- if (!empty($column_data['default']))
- {
- // Add new default value constraint
- $statements[] = 'ALTER TABLE [' . $table_name . '] ADD CONSTRAINT [DF_' . $table_name . '_' . $column_name . '_1] ' . $this->db->sql_escape($column_data['default']) . ' FOR [' . $column_name . ']';
- }
-
- if (!empty($indexes))
- {
- // Recreate indexes after we changed the column
- foreach ($indexes as $index_name => $index_data)
- {
- $result = $this->sql_create_index($table_name, $index_name, $index_data);
- $statements = array_merge($statements, $result);
- }
- }
-
- if (!empty($unique_indexes))
- {
- // Recreate unique indexes after we changed the column
- foreach ($unique_indexes as $index_name => $index_data)
- {
- $result = $this->sql_create_unique_index($table_name, $index_name, $index_data);
- $statements = array_merge($statements, $result);
- }
- }
-
- $this->return_statements = $old_return_statements;
- break;
-
case 'mysql_40':
case 'mysql_41':
$statements[] = 'ALTER TABLE `' . $table_name . '` CHANGE `' . $column_name . '` `' . $column_name . '` ' . $column_data['column_type_sql'];
@@ -2432,69 +1808,6 @@ class tools
$this->return_statements = $old_return_statements;
break;
- case 'postgres':
- $sql = 'ALTER TABLE ' . $table_name . ' ';
-
- $sql_array = array();
- $sql_array[] = 'ALTER COLUMN ' . $column_name . ' TYPE ' . $column_data['column_type'];
-
- if (isset($column_data['null']))
- {
- if ($column_data['null'] == 'NOT NULL')
- {
- $sql_array[] = 'ALTER COLUMN ' . $column_name . ' SET NOT NULL';
- }
- else if ($column_data['null'] == 'NULL')
- {
- $sql_array[] = 'ALTER COLUMN ' . $column_name . ' DROP NOT NULL';
- }
- }
-
- if (isset($column_data['default']))
- {
- $sql_array[] = 'ALTER COLUMN ' . $column_name . ' SET DEFAULT ' . $column_data['default'];
- }
-
- // we don't want to double up on constraints if we change different number data types
- if (isset($column_data['constraint']))
- {
- $constraint_sql = "SELECT consrc as constraint_data
- FROM pg_constraint, pg_class bc
- WHERE conrelid = bc.oid
- AND bc.relname = '{$table_name}'
- AND NOT EXISTS (
- SELECT *
- FROM pg_constraint as c, pg_inherits as i
- WHERE i.inhrelid = pg_constraint.conrelid
- AND c.conname = pg_constraint.conname
- AND c.consrc = pg_constraint.consrc
- AND c.conrelid = i.inhparent
- )";
-
- $constraint_exists = false;
-
- $result = $this->db->sql_query($constraint_sql);
- while ($row = $this->db->sql_fetchrow($result))
- {
- if (trim($row['constraint_data']) == trim($column_data['constraint']))
- {
- $constraint_exists = true;
- break;
- }
- }
- $this->db->sql_freeresult($result);
-
- if (!$constraint_exists)
- {
- $sql_array[] = 'ADD ' . $column_data['constraint'];
- }
- }
-
- $sql .= implode(', ', $sql_array);
-
- $statements[] = $sql;
- break;
-
case 'sqlite':
case 'sqlite3':
@@ -2563,52 +1876,6 @@ class tools
}
/**
- * Get queries to drop the default constraints of a column
- *
- * We need to drop the default constraints of a column,
- * before being able to change their type or deleting them.
- *
- * @param string $table_name
- * @param string $column_name
- * @return array Array with SQL statements
- */
- protected function mssql_get_drop_default_constraints_queries($table_name, $column_name)
- {
- $statements = array();
- if ($this->mssql_is_sql_server_2000())
- {
- // http://msdn.microsoft.com/en-us/library/aa175912%28v=sql.80%29.aspx
- // Deprecated in SQL Server 2005
- $sql = "SELECT so.name AS def_name
- FROM sysobjects so
- JOIN sysconstraints sc ON so.id = sc.constid
- WHERE object_name(so.parent_obj) = '{$table_name}'
- AND so.xtype = 'D'
- AND sc.colid = (SELECT colid FROM syscolumns
- WHERE id = object_id('{$table_name}')
- AND name = '{$column_name}')";
- }
- else
- {
- $sql = "SELECT dobj.name AS def_name
- FROM sys.columns col
- LEFT OUTER JOIN sys.objects dobj ON (dobj.object_id = col.default_object_id AND dobj.type = 'D')
- WHERE col.object_id = object_id('{$table_name}')
- AND col.name = '{$column_name}'
- AND dobj.name IS NOT NULL";
- }
-
- $result = $this->db->sql_query($sql);
- while ($row = $this->db->sql_fetchrow($result))
- {
- $statements[] = 'ALTER TABLE [' . $table_name . '] DROP CONSTRAINT [' . $row['def_name'] . ']';
- }
- $this->db->sql_freeresult($result);
-
- return $statements;
- }
-
- /**
* Get a list with existing indexes for the column
*
* @param string $table_name
@@ -2622,7 +1889,6 @@ class tools
{
case 'mysql_40':
case 'mysql_41':
- case 'postgres':
case 'sqlite':
case 'sqlite3':
// Not supported
@@ -2635,40 +1901,6 @@ class tools
switch ($this->sql_layer)
{
- case 'mssql':
- case 'mssqlnative':
- if ($this->mssql_is_sql_server_2000())
- {
- // http://msdn.microsoft.com/en-us/library/aa175912%28v=sql.80%29.aspx
- // Deprecated in SQL Server 2005
- $sql = "SELECT DISTINCT ix.name AS phpbb_index_name
- FROM sysindexes ix
- INNER JOIN sysindexkeys ixc
- ON ixc.id = ix.id
- AND ixc.indid = ix.indid
- INNER JOIN syscolumns cols
- ON cols.colid = ixc.colid
- AND cols.id = ix.id
- WHERE ix.id = object_id('{$table_name}')
- AND cols.name = '{$column_name}'
- AND INDEXPROPERTY(ix.id, ix.name, 'IsUnique') = " . ($unique ? '1' : '0');
- }
- else
- {
- $sql = "SELECT DISTINCT ix.name AS phpbb_index_name
- FROM sys.indexes ix
- INNER JOIN sys.index_columns ixc
- ON ixc.object_id = ix.object_id
- AND ixc.index_id = ix.index_id
- INNER JOIN sys.columns cols
- ON cols.column_id = ixc.column_id
- AND cols.object_id = ix.object_id
- WHERE ix.object_id = object_id('{$table_name}')
- AND cols.name = '{$column_name}'
- AND ix.is_unique = " . ($unique ? '1' : '0');
- }
- break;
-
case 'oracle':
$sql = "SELECT ix.index_name AS phpbb_index_name, ix.uniqueness AS is_unique
FROM all_ind_columns ixc, all_indexes ix
@@ -2695,36 +1927,6 @@ class tools
switch ($this->sql_layer)
{
- case 'mssql':
- case 'mssqlnative':
- if ($this->mssql_is_sql_server_2000())
- {
- $sql = "SELECT DISTINCT ix.name AS phpbb_index_name, cols.name AS phpbb_column_name
- FROM sysindexes ix
- INNER JOIN sysindexkeys ixc
- ON ixc.id = ix.id
- AND ixc.indid = ix.indid
- INNER JOIN syscolumns cols
- ON cols.colid = ixc.colid
- AND cols.id = ix.id
- WHERE ix.id = object_id('{$table_name}')
- AND " . $this->db->sql_in_set('ix.name', array_keys($existing_indexes));
- }
- else
- {
- $sql = "SELECT DISTINCT ix.name AS phpbb_index_name, cols.name AS phpbb_column_name
- FROM sys.indexes ix
- INNER JOIN sys.index_columns ixc
- ON ixc.object_id = ix.object_id
- AND ixc.index_id = ix.index_id
- INNER JOIN sys.columns cols
- ON cols.column_id = ixc.column_id
- AND cols.object_id = ix.object_id
- WHERE ix.object_id = object_id('{$table_name}')
- AND " . $this->db->sql_in_set('ix.name', array_keys($existing_indexes));
- }
- break;
-
case 'oracle':
$sql = "SELECT index_name AS phpbb_index_name, column_name AS phpbb_column_name
FROM all_ind_columns
@@ -2744,25 +1946,6 @@ class tools
}
/**
- * Is the used MS SQL Server a SQL Server 2000?
- *
- * @return bool
- */
- protected function mssql_is_sql_server_2000()
- {
- if ($this->is_sql_server_2000 === null)
- {
- $sql = "SELECT CAST(SERVERPROPERTY('productversion') AS VARCHAR(25)) AS mssql_version";
- $result = $this->db->sql_query($sql);
- $properties = $this->db->sql_fetchrow($result);
- $this->db->sql_freeresult($result);
- $this->is_sql_server_2000 = $properties['mssql_version'][0] == '8';
- }
-
- return $this->is_sql_server_2000;
- }
-
- /**
* Returns the Queries which are required to recreate a table including indexes
*
* @param string $table_name
diff --git a/phpBB/phpbb/db/tools/tools_interface.php b/phpBB/phpbb/db/tools/tools_interface.php
new file mode 100644
index 0000000000..f153f73a54
--- /dev/null
+++ b/phpBB/phpbb/db/tools/tools_interface.php
@@ -0,0 +1,202 @@
+<?php
+/**
+*
+* This file is part of the phpBB Forum Software package.
+*
+* @copyright (c) phpBB Limited <https://www.phpbb.com>
+* @license GNU General Public License, version 2 (GPL-2.0)
+*
+* For full copyright and license information, please see
+* the docs/CREDITS.txt file.
+*
+*/
+
+namespace phpbb\db\tools;
+
+/**
+ * Interface for a Database Tools for handling cross-db actions such as altering columns, etc.
+ */
+interface tools_interface
+{
+ /**
+ * Handle passed database update array.
+ * Expected structure...
+ * Key being one of the following
+ * drop_tables: Drop tables
+ * add_tables: Add tables
+ * change_columns: Column changes (only type, not name)
+ * add_columns: Add columns to a table
+ * drop_keys: Dropping keys
+ * drop_columns: Removing/Dropping columns
+ * add_primary_keys: adding primary keys
+ * add_unique_index: adding an unique index
+ * add_index: adding an index (can be column:index_size if you need to provide size)
+ *
+ * The values are in this format:
+ * {TABLE NAME} => array(
+ * {COLUMN NAME} => array({COLUMN TYPE}, {DEFAULT VALUE}, {OPTIONAL VARIABLES}),
+ * {KEY/INDEX NAME} => array({COLUMN NAMES}),
+ * )
+ *
+ *
+ * @param array $schema_changes
+ * @return null
+ */
+ public function perform_schema_changes($schema_changes);
+
+ /**
+ * Gets a list of tables in the database.
+ *
+ * @return array Array of table names (all lower case)
+ */
+ public function sql_list_tables();
+
+ /**
+ * Check if table exists
+ *
+ * @param string $table_name The table name to check for
+ * @return bool true if table exists, else false
+ */
+ public function sql_table_exists($table_name);
+
+ /**
+ * Create SQL Table
+ *
+ * @param string $table_name The table name to create
+ * @param array $table_data Array containing table data.
+ * @return array|true Statements to run, or true if the statements have been executed
+ */
+ public function sql_create_table($table_name, $table_data);
+
+ /**
+ * Drop Table
+ *
+ * @param string $table_name The table name to drop
+ * @return array|true Statements to run, or true if the statements have been executed
+ */
+ public function sql_table_drop($table_name);
+
+ /**
+ * Gets a list of columns of a table.
+ *
+ * @param string $table_name Table name
+ * @return array Array of column names (all lower case)
+ */
+ public function sql_list_columns($table_name);
+
+ /**
+ * Check whether a specified column exist in a table
+ *
+ * @param string $table_name Table to check
+ * @param string $column_name Column to check
+ * @return bool True if column exists, false otherwise
+ */
+ public function sql_column_exists($table_name, $column_name);
+
+ /**
+ * Add new column
+ *
+ * @param string $table_name Table to modify
+ * @param string $column_name Name of the column to add
+ * @param array $column_data Column data
+ * @param bool $inline Whether the query should actually be run,
+ * or return a string for adding the column
+ * @return array|true Statements to run, or true if the statements have been executed
+ */
+ public function sql_column_add($table_name, $column_name, $column_data, $inline = false);
+
+ /**
+ * Change column type (not name!)
+ *
+ * @param string $table_name Table to modify
+ * @param string $column_name Name of the column to modify
+ * @param array $column_data Column data
+ * @param bool $inline Whether the query should actually be run,
+ * or return a string for modifying the column
+ * @return array|true Statements to run, or true if the statements have been executed
+ */
+ public function sql_column_change($table_name, $column_name, $column_data, $inline = false);
+
+ /**
+ * Drop column
+ *
+ * @param string $table_name Table to modify
+ * @param string $column_name Name of the column to drop
+ * @param bool $inline Whether the query should actually be run,
+ * or return a string for deleting the column
+ * @return array|true Statements to run, or true if the statements have been executed
+ */
+ public function sql_column_remove($table_name, $column_name, $inline = false);
+
+ /**
+ * List all of the indices that belong to a table
+ *
+ * NOTE: does not list
+ * - UNIQUE indices
+ * - PRIMARY keys
+ *
+ * @param string $table_name Table to check
+ * @return array Array with index names
+ */
+ public function sql_list_index($table_name);
+
+ /**
+ * Check if a specified index exists in table. Does not return PRIMARY KEY and UNIQUE indexes.
+ *
+ * @param string $table_name Table to check the index at
+ * @param string $index_name The index name to check
+ * @return bool True if index exists, else false
+ */
+ public function sql_index_exists($table_name, $index_name);
+
+ /**
+ * Add index
+ *
+ * @param string $table_name Table to modify
+ * @param string $index_name Name of the index to create
+ * @param string|array $column Either a string with a column name, or an array with columns
+ * @return array|true Statements to run, or true if the statements have been executed
+ */
+ public function sql_create_index($table_name, $index_name, $column);
+
+ /**
+ * Drop Index
+ *
+ * @param string $table_name Table to modify
+ * @param string $index_name Name of the index to delete
+ * @return array|true Statements to run, or true if the statements have been executed
+ */
+ public function sql_index_drop($table_name, $index_name);
+
+ /**
+ * Check if a specified index exists in table.
+ *
+ * NOTE: Does not return normal and PRIMARY KEY indexes
+ *
+ * @param string $table_name Table to check the index at
+ * @param string $index_name The index name to check
+ * @return bool True if index exists, else false
+ */
+ public function sql_unique_index_exists($table_name, $index_name);
+
+ /**
+ * Add unique index
+ *
+ * @param string $table_name Table to modify
+ * @param string $index_name Name of the unique index to create
+ * @param string|array $column Either a string with a column name, or an array with columns
+ * @return array|true Statements to run, or true if the statements have been executed
+ */
+ public function sql_create_unique_index($table_name, $index_name, $column);
+
+ /**
+ * Add primary key
+ *
+ * @param string $table_name Table to modify
+ * @param string|array $column Either a string with a column name, or an array with columns
+ * @param bool $inline Whether the query should actually be run,
+ * or return a string for creating the key
+ * @return array|true Statements to run, or true if the statements have been executed
+ */
+ public function sql_create_primary_key($table_name, $column, $inline = false);
+}
diff --git a/phpBB/phpbb/di/container_builder.php b/phpBB/phpbb/di/container_builder.php
index 638c13e86d..99576f9020 100644
--- a/phpBB/phpbb/di/container_builder.php
+++ b/phpBB/phpbb/di/container_builder.php
@@ -13,16 +13,25 @@
namespace phpbb\di;
+use Symfony\Component\Config\ConfigCache;
+use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Dumper\PhpDumper;
-use Symfony\Component\HttpKernel\DependencyInjection\RegisterListenersPass;
+use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
+use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
+use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass;
+use Symfony\Component\HttpKernel\DependencyInjection\MergeExtensionConfigurationPass;
class container_builder
{
- /** @var string phpBB Root Path */
+ /**
+ * @var string phpBB Root Path
+ */
protected $phpbb_root_path;
- /** @var string php file extension */
+ /**
+ * @var string php file extension
+ */
protected $php_ext;
/**
@@ -112,6 +121,11 @@ class container_builder
protected $config_php_file;
/**
+ * @var string
+ */
+ protected $cache_dir;
+
+ /**
* Constructor
*
* @param \phpbb\config_php_file $config_php_file
@@ -133,23 +147,30 @@ class container_builder
public function get_container()
{
$container_filename = $this->get_container_filename();
- if (!defined('DEBUG_CONTAINER') && $this->dump_container && file_exists($container_filename))
+ $config_cache = new ConfigCache($container_filename, defined('DEBUG'));
+ if ($this->dump_container && $config_cache->isFresh())
{
- require($container_filename);
+ require($config_cache->getPath());
$this->container = new \phpbb_cache_container();
}
else
{
- if ($this->config_path === null)
- {
- $this->config_path = $this->phpbb_root_path . 'config';
- }
- $container_extensions = array(new \phpbb\di\extension\core($this->config_path));
+ $container_extensions = array(new \phpbb\di\extension\core($this->get_config_path()));
if ($this->use_extensions)
{
$installed_exts = $this->get_installed_extensions();
- $container_extensions[] = new \phpbb\di\extension\ext($installed_exts);
+ foreach ($installed_exts as $ext_name => $path)
+ {
+ $extension_class = '\\' . str_replace('/', '\\', $ext_name) . '\\di\\extension';
+
+ if (!class_exists($extension_class))
+ {
+ $extension_class = '\phpbb\extension\di\extension_base';
+ }
+
+ $container_extensions[] = new $extension_class($ext_name, $path);
+ }
}
if ($this->inject_config)
@@ -171,6 +192,10 @@ class container_builder
}
}
+ $filesystem = new \phpbb\filesystem\filesystem();
+ $loader = new YamlFileLoader($this->container, new FileLocator($filesystem->realpath($this->get_config_path())));
+ $loader->load($this->container->getParameter('core.environment') . '/config.yml');
+
$this->inject_custom_parameters();
if ($this->compile_container)
@@ -178,9 +203,9 @@ class container_builder
$this->container->compile();
}
- if ($this->dump_container && !defined('DEBUG_CONTAINER'))
+ if ($this->dump_container)
{
- $this->dump_container($container_filename);
+ $this->dump_container($config_cache);
}
}
@@ -267,6 +292,16 @@ class container_builder
}
/**
+ * Returns the path to the container configuration (default: root_path/config)
+ *
+ * @return string
+ */
+ protected function get_config_path()
+ {
+ return $this->config_path ?: $this->phpbb_root_path . 'config';
+ }
+
+ /**
* Set custom parameters to inject into the container.
*
* @param array $custom_parameters
@@ -277,11 +312,31 @@ class container_builder
}
/**
+ * Set the path to the cache directory.
+ *
+ * @param string $cache_dir Path to the cache directory
+ */
+ public function set_cache_dir($cache_dir)
+ {
+ $this->cache_dir = $cache_dir;
+ }
+
+ /**
+ * Returns the path to the cache directory (default: root_path/cache/environment).
+ *
+ * @return string Path to the cache directory.
+ */
+ protected function get_cache_dir()
+ {
+ return $this->cache_dir ?: $this->phpbb_root_path . 'cache/' . $this->get_environment() . '/';
+ }
+
+ /**
* Dump the container to the disk.
*
- * @param string $container_filename The name of the file.
+ * @param ConfigCache $cache The config cache
*/
- protected function dump_container($container_filename)
+ protected function dump_container($cache)
{
$dumper = new PhpDumper($this->container);
$cached_container_dump = $dumper->dump(array(
@@ -289,7 +344,7 @@ class container_builder
'base_class' => 'Symfony\\Component\\DependencyInjection\\ContainerBuilder',
));
- file_put_contents($container_filename, $cached_container_dump);
+ $cache->write($cached_container_dump, $this->container->getResources());
}
/**
@@ -362,34 +417,73 @@ class container_builder
*/
protected function create_container(array $extensions)
{
- $container = new ContainerBuilder();
+ $container = new ContainerBuilder(new ParameterBag($this->get_core_parameters()));
+
+ $extensions_alias = array();
foreach ($extensions as $extension)
{
$container->registerExtension($extension);
- $container->loadFromExtension($extension->getAlias());
+ $extensions_alias[] = $extension->getAlias();
+ //$container->loadFromExtension($extension->getAlias());
}
+ $container->getCompilerPassConfig()->setMergePass(new MergeExtensionConfigurationPass($extensions_alias));
+
return $container;
}
/**
- * Inject the customs parameters into the container
- */
+ * Inject the customs parameters into the container
+ */
protected function inject_custom_parameters()
{
- if ($this->custom_parameters === null)
+ if ($this->custom_parameters !== null)
{
- $this->custom_parameters = array(
- 'core.root_path' => $this->phpbb_root_path,
- 'core.php_ext' => $this->php_ext,
- );
+ foreach ($this->custom_parameters as $key => $value)
+ {
+ $this->container->setParameter($key, $value);
+ }
}
+ }
- foreach ($this->custom_parameters as $key => $value)
+ /**
+ * Returns the core parameters.
+ *
+ * @return array An array of core parameters
+ */
+ protected function get_core_parameters()
+ {
+ return array_merge(
+ array(
+ 'core.root_path' => $this->phpbb_root_path,
+ 'core.php_ext' => $this->php_ext,
+ 'core.environment' => $this->get_environment(),
+ 'core.debug' => DEBUG,
+ ),
+ $this->get_env_parameters()
+ );
+ }
+
+ /**
+ * Gets the environment parameters.
+ *
+ * Only the parameters starting with "PHPBB__" are considered.
+ *
+ * @return array An array of parameters
+ */
+ protected function get_env_parameters()
+ {
+ $parameters = array();
+ foreach ($_SERVER as $key => $value)
{
- $this->container->setParameter($key, $value);
+ if (0 === strpos($key, 'PHPBB__'))
+ {
+ $parameters[strtolower(str_replace('__', '.', substr($key, 9)))] = $value;
+ }
}
+
+ return $parameters;
}
/**
@@ -400,6 +494,16 @@ class container_builder
protected function get_container_filename()
{
$filename = str_replace(array('/', '.'), array('slash', 'dot'), $this->phpbb_root_path);
- return $this->phpbb_root_path . 'cache/container_' . $filename . '.' . $this->php_ext;
+ return $this->get_cache_dir() . 'container_' . $filename . '.' . $this->php_ext;
+ }
+
+ /**
+ * Return the name of the current environment.
+ *
+ * @return string
+ */
+ protected function get_environment()
+ {
+ return PHPBB_ENVIRONMENT;
}
}
diff --git a/phpBB/phpbb/di/extension/container_configuration.php b/phpBB/phpbb/di/extension/container_configuration.php
new file mode 100644
index 0000000000..4cc7c7c0d1
--- /dev/null
+++ b/phpBB/phpbb/di/extension/container_configuration.php
@@ -0,0 +1,46 @@
+<?php
+/**
+*
+* This file is part of the phpBB Forum Software package.
+*
+* @copyright (c) phpBB Limited <https://www.phpbb.com>
+* @license GNU General Public License, version 2 (GPL-2.0)
+*
+* For full copyright and license information, please see
+* the docs/CREDITS.txt file.
+*
+*/
+
+namespace phpbb\di\extension;
+
+use Symfony\Component\Config\Definition\Builder\TreeBuilder;
+use Symfony\Component\Config\Definition\ConfigurationInterface;
+
+class container_configuration implements ConfigurationInterface
+{
+
+ /**
+ * Generates the configuration tree builder.
+ *
+ * @return \Symfony\Component\Config\Definition\Builder\TreeBuilder The tree builder
+ */
+ public function getConfigTreeBuilder()
+ {
+ $treeBuilder = new TreeBuilder();
+ $rootNode = $treeBuilder->root('core');
+ $rootNode
+ ->children()
+ ->booleanNode('require_dev_dependencies')->defaultValue(false)->end()
+ ->arrayNode('twig')
+ ->addDefaultsIfNotSet()
+ ->children()
+ ->booleanNode('debug')->defaultValue(null)->end()
+ ->booleanNode('auto_reload')->defaultValue(null)->end()
+ ->booleanNode('enable_debug_extension')->defaultValue(false)->end()
+ ->end()
+ ->end()
+ ->end()
+ ;
+ return $treeBuilder;
+ }
+}
diff --git a/phpBB/phpbb/di/extension/core.php b/phpBB/phpbb/di/extension/core.php
index ca4fa5c082..91b321a684 100644
--- a/phpBB/phpbb/di/extension/core.php
+++ b/phpBB/phpbb/di/extension/core.php
@@ -13,10 +13,11 @@
namespace phpbb\di\extension;
+use Symfony\Component\Config\FileLocator;
+use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\DependencyInjection\ContainerBuilder;
-use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
-use Symfony\Component\Config\FileLocator;
+use Symfony\Component\HttpKernel\DependencyInjection\Extension;
/**
* Container core extension
@@ -24,42 +25,89 @@ use Symfony\Component\Config\FileLocator;
class core extends Extension
{
/**
- * Config path
- * @var string
- */
+ * Config path
+ * @var string
+ */
protected $config_path;
/**
- * Constructor
- *
- * @param string $config_path Config path
- */
+ * Constructor
+ *
+ * @param string $config_path Config path
+ */
public function __construct($config_path)
{
$this->config_path = $config_path;
}
/**
- * Loads a specific configuration.
- *
- * @param array $config An array of configuration values
- * @param ContainerBuilder $container A ContainerBuilder instance
- *
- * @throws \InvalidArgumentException When provided tag is not defined in this extension
- */
- public function load(array $config, ContainerBuilder $container)
+ * Loads a specific configuration.
+ *
+ * @param array $configs An array of configuration values
+ * @param ContainerBuilder $container A ContainerBuilder instance
+ *
+ * @throws \InvalidArgumentException When provided tag is not defined in this extension
+ */
+ public function load(array $configs, ContainerBuilder $container)
{
- $loader = new YamlFileLoader($container, new FileLocator(phpbb_realpath($this->config_path)));
- $loader->load('services.yml');
+ $filesystem = new \phpbb\filesystem\filesystem();
+ $loader = new YamlFileLoader($container, new FileLocator($filesystem->realpath($this->config_path)));
+ $loader->load($container->getParameter('core.environment') . '/container/environment.yml');
+
+ $config = $this->getConfiguration($configs, $container);
+ $config = $this->processConfiguration($config, $configs);
+
+ if ($config['require_dev_dependencies'])
+ {
+ if (!class_exists('Goutte\Client', true))
+ {
+ trigger_error(
+ 'Composer development dependencies have not been set up for the ' . $container->getParameter('core.environment') . ' environment yet, run ' .
+ "'php ../composer.phar install --dev' from the phpBB directory to do so.",
+ E_USER_ERROR
+ );
+ }
+ }
+
+ // Set the Twig options if defined in the environment
+ $definition = $container->getDefinition('template.twig.environment');
+ $twig_environment_options = $definition->getArgument(7);
+ if ($config['twig']['debug'])
+ {
+ $twig_environment_options['debug'] = true;
+ }
+ if ($config['twig']['auto_reload'])
+ {
+ $twig_environment_options['auto_reload'] = true;
+ }
+ // Replace the 8th argument, the options passed to the environment
+ $definition->replaceArgument(7, $twig_environment_options);
+
+ if ($config['twig']['enable_debug_extension'])
+ {
+ $definition = $container->getDefinition('template.twig.extensions.debug');
+ $definition->addTag('twig.extension');
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getConfiguration(array $config, ContainerBuilder $container)
+ {
+ $r = new \ReflectionClass('\phpbb\di\extension\container_configuration');
+ $container->addResource(new FileResource($r->getFileName()));
+
+ return new container_configuration();
}
/**
- * Returns the recommended alias to use in XML.
- *
- * This alias is also the mandatory prefix to use when using YAML.
- *
- * @return string The alias
- */
+ * Returns the recommended alias to use in XML.
+ *
+ * This alias is also the mandatory prefix to use when using YAML.
+ *
+ * @return string The alias
+ */
public function getAlias()
{
return 'core';
diff --git a/phpBB/phpbb/di/extension/ext.php b/phpBB/phpbb/di/extension/ext.php
deleted file mode 100644
index 718c992d2e..0000000000
--- a/phpBB/phpbb/di/extension/ext.php
+++ /dev/null
@@ -1,67 +0,0 @@
-<?php
-/**
-*
-* This file is part of the phpBB Forum Software package.
-*
-* @copyright (c) phpBB Limited <https://www.phpbb.com>
-* @license GNU General Public License, version 2 (GPL-2.0)
-*
-* For full copyright and license information, please see
-* the docs/CREDITS.txt file.
-*
-*/
-
-namespace phpbb\di\extension;
-
-use Symfony\Component\DependencyInjection\ContainerBuilder;
-use Symfony\Component\HttpKernel\DependencyInjection\Extension;
-use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
-use Symfony\Component\Config\FileLocator;
-
-/**
-* Container ext extension
-*/
-class ext extends Extension
-{
- protected $paths = array();
-
- public function __construct($enabled_extensions)
- {
- foreach ($enabled_extensions as $ext => $path)
- {
- $this->paths[] = $path;
- }
- }
-
- /**
- * Loads a specific configuration.
- *
- * @param array $config An array of configuration values
- * @param ContainerBuilder $container A ContainerBuilder instance
- *
- * @throws \InvalidArgumentException When provided tag is not defined in this extension
- */
- public function load(array $config, ContainerBuilder $container)
- {
- foreach ($this->paths as $path)
- {
- if (file_exists($path . '/config/services.yml'))
- {
- $loader = new YamlFileLoader($container, new FileLocator(phpbb_realpath($path . '/config')));
- $loader->load('services.yml');
- }
- }
- }
-
- /**
- * Returns the recommended alias to use in XML.
- *
- * This alias is also the mandatory prefix to use when using YAML.
- *
- * @return string The alias
- */
- public function getAlias()
- {
- return 'ext';
- }
-}
diff --git a/phpBB/phpbb/event/kernel_exception_subscriber.php b/phpBB/phpbb/event/kernel_exception_subscriber.php
index eb7831ad34..0a8a0183dc 100644
--- a/phpBB/phpbb/event/kernel_exception_subscriber.php
+++ b/phpBB/phpbb/event/kernel_exception_subscriber.php
@@ -106,7 +106,7 @@ class kernel_exception_subscriber implements EventSubscriberInterface
$event->setResponse($response);
}
- public static function getSubscribedEvents()
+ static public function getSubscribedEvents()
{
return array(
KernelEvents::EXCEPTION => 'on_kernel_exception',
diff --git a/phpBB/phpbb/event/kernel_request_subscriber.php b/phpBB/phpbb/event/kernel_request_subscriber.php
deleted file mode 100644
index ee9f29a59d..0000000000
--- a/phpBB/phpbb/event/kernel_request_subscriber.php
+++ /dev/null
@@ -1,82 +0,0 @@
-<?php
-/**
-*
-* This file is part of the phpBB Forum Software package.
-*
-* @copyright (c) phpBB Limited <https://www.phpbb.com>
-* @license GNU General Public License, version 2 (GPL-2.0)
-*
-* For full copyright and license information, please see
-* the docs/CREDITS.txt file.
-*
-*/
-
-namespace phpbb\event;
-
-use Symfony\Component\EventDispatcher\EventSubscriberInterface;
-use Symfony\Component\HttpKernel\KernelEvents;
-use Symfony\Component\HttpKernel\Event\GetResponseEvent;
-use Symfony\Component\HttpKernel\EventListener\RouterListener;
-use Symfony\Component\Routing\RequestContext;
-
-class kernel_request_subscriber implements EventSubscriberInterface
-{
- /**
- * Extension manager object
- * @var \phpbb\extension\manager
- */
- protected $manager;
-
- /**
- * PHP file extension
- * @var string
- */
- protected $php_ext;
-
- /**
- * Root path
- * @var string
- */
- protected $root_path;
-
- /**
- * Construct method
- *
- * @param \phpbb\extension\manager $manager Extension manager object
- * @param string $root_path Root path
- * @param string $php_ext PHP file extension
- */
- public function __construct(\phpbb\extension\manager $manager, $root_path, $php_ext)
- {
- $this->root_path = $root_path;
- $this->php_ext = $php_ext;
- $this->manager = $manager;
- }
-
- /**
- * This listener is run when the KernelEvents::REQUEST event is triggered
- *
- * This is responsible for setting up the routing information
- *
- * @param GetResponseEvent $event
- * @throws \BadMethodCallException
- * @return null
- */
- public function on_kernel_request(GetResponseEvent $event)
- {
- $request = $event->getRequest();
- $context = new RequestContext();
- $context->fromRequest($request);
-
- $matcher = phpbb_get_url_matcher($this->manager, $context, $this->root_path, $this->php_ext);
- $router_listener = new RouterListener($matcher, $context);
- $router_listener->onKernelRequest($event);
- }
-
- public static function getSubscribedEvents()
- {
- return array(
- KernelEvents::REQUEST => 'on_kernel_request',
- );
- }
-}
diff --git a/phpBB/phpbb/event/kernel_terminate_subscriber.php b/phpBB/phpbb/event/kernel_terminate_subscriber.php
index 3a709f73fd..f0d0a2f595 100644
--- a/phpBB/phpbb/event/kernel_terminate_subscriber.php
+++ b/phpBB/phpbb/event/kernel_terminate_subscriber.php
@@ -32,7 +32,7 @@ class kernel_terminate_subscriber implements EventSubscriberInterface
exit_handler();
}
- public static function getSubscribedEvents()
+ static public function getSubscribedEvents()
{
return array(
KernelEvents::TERMINATE => array('on_kernel_terminate', ~PHP_INT_MAX),
diff --git a/phpBB/phpbb/event/md_exporter.php b/phpBB/phpbb/event/md_exporter.php
index 7f94ca9299..05e898a157 100644
--- a/phpBB/phpbb/event/md_exporter.php
+++ b/phpBB/phpbb/event/md_exporter.php
@@ -99,7 +99,7 @@ class md_exporter
{
$this->crawl_eventsmd($md_file, 'styles');
- $styles = array('prosilver', 'subsilver2');
+ $styles = array('prosilver');
foreach ($styles as $style)
{
$file_list = $this->get_recursive_file_list(
@@ -221,7 +221,7 @@ class md_exporter
$wiki_page = '= Template Events =' . "\n";
}
$wiki_page .= '{| class="zebra sortable" cellspacing="0" cellpadding="5"' . "\n";
- $wiki_page .= '! Identifier !! Prosilver Placement (If applicable) !! Subsilver Placement (If applicable) !! Added in Release !! Explanation' . "\n";
+ $wiki_page .= '! Identifier !! Prosilver Placement (If applicable) !! Added in Release !! Explanation' . "\n";
}
foreach ($this->events as $event_name => $event)
@@ -235,7 +235,7 @@ class md_exporter
}
else
{
- $wiki_page .= implode(', ', $event['files']['prosilver']) . ' || ' . implode(', ', $event['files']['subsilver2']);
+ $wiki_page .= implode(', ', $event['files']['prosilver']);
}
$wiki_page .= " || {$event['since']} || " . str_replace("\n", ' ', $event['description']) . "\n";
@@ -288,7 +288,6 @@ class md_exporter
{
$files_list = array(
'prosilver' => array(),
- 'subsilver2' => array(),
'adm' => array(),
);
@@ -308,10 +307,6 @@ class md_exporter
{
$files_list['prosilver'][] = substr($file, strlen('styles/prosilver/template/'));
}
- else if (($this->filter !== 'adm') && strpos($file, 'styles/subsilver2/template/') === 0)
- {
- $files_list['subsilver2'][] = substr($file, strlen('styles/subsilver2/template/'));
- }
else if (($this->filter === 'adm') && strpos($file, 'adm/style/') === 0)
{
$files_list['adm'][] = substr($file, strlen('adm/style/'));
diff --git a/phpBB/phpbb/extension/di/extension_base.php b/phpBB/phpbb/extension/di/extension_base.php
new file mode 100644
index 0000000000..ba74615e70
--- /dev/null
+++ b/phpBB/phpbb/extension/di/extension_base.php
@@ -0,0 +1,138 @@
+<?php
+/**
+*
+* This file is part of the phpBB Forum Software package.
+*
+* @copyright (c) phpBB Limited <https://www.phpbb.com>
+* @license GNU General Public License, version 2 (GPL-2.0)
+*
+* For full copyright and license information, please see
+* the docs/CREDITS.txt file.
+*
+*/
+
+namespace phpbb\extension\di;
+
+use Symfony\Component\Config\FileLocator;
+use Symfony\Component\Config\Resource\FileResource;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
+use Symfony\Component\HttpKernel\DependencyInjection\Extension;
+
+/**
+ * Container core extension
+ */
+class extension_base extends Extension
+{
+ /**
+ * Name of the extension (vendor/name)
+ *
+ * @var string
+ */
+ protected $extension_name;
+
+ /**
+ * Path to the extension.
+ *
+ * @var string
+ */
+ protected $ext_path;
+
+ /**
+ * Constructor
+ *
+ * @param string $extension_name Name of the extension (vendor/name)
+ * @param string $ext_path Path to the extension
+ */
+ public function __construct($extension_name, $ext_path)
+ {
+ $this->extension_name = $extension_name;
+ $this->ext_path = $ext_path;
+ }
+
+ /**
+ * Loads a specific configuration.
+ *
+ * @param array $configs An array of configuration values
+ * @param ContainerBuilder $container A ContainerBuilder instance
+ *
+ * @throws \InvalidArgumentException When provided tag is not defined in this extension
+ */
+ public function load(array $configs, ContainerBuilder $container)
+ {
+ $this->load_services($container);
+ }
+
+ /**
+ * Loads the services.yml file.
+ *
+ * @param ContainerBuilder $container A ContainerBuilder instance
+ */
+ protected function load_services(ContainerBuilder $container)
+ {
+ $services_directory = false;
+ $services_file = false;
+
+ if (file_exists($this->ext_path . 'config/' . $container->getParameter('core.environment') . '/container/environment.yml'))
+ {
+ $services_directory = $this->ext_path . 'config/' . $container->getParameter('core.environment') . '/container/';
+ $services_file = 'environment.yml';
+ }
+ else if (!is_dir($this->ext_path . 'config/' . $container->getParameter('core.environment')))
+ {
+ if (file_exists($this->ext_path . 'config/default/container/environment.yml'))
+ {
+ $services_directory = $this->ext_path . 'config/default/container/';
+ $services_file = 'environment.yml';
+ }
+ else if (!is_dir($this->ext_path . 'config/default') && file_exists($this->ext_path . '/config/services.yml'))
+ {
+ $services_directory = $this->ext_path . 'config';
+ $services_file = 'services.yml';
+ }
+ }
+
+ if ($services_directory && $services_file)
+ {
+ $filesystem = new \phpbb\filesystem\filesystem();
+ $loader = new YamlFileLoader($container, new FileLocator($filesystem->realpath($services_directory)));
+ $loader->load($services_file);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getConfiguration(array $config, ContainerBuilder $container)
+ {
+ $reflected = new \ReflectionClass($this);
+ $namespace = $reflected->getNamespaceName();
+
+ $class = $namespace . '\\di\configuration';
+ if (class_exists($class))
+ {
+ $r = new \ReflectionClass($class);
+ $container->addResource(new FileResource($r->getFileName()));
+
+ if (!method_exists($class, '__construct'))
+ {
+ $configuration = new $class();
+
+ return $configuration;
+ }
+ }
+
+ }
+
+ /**
+ * Returns the recommended alias to use in XML.
+ *
+ * This alias is also the mandatory prefix to use when using YAML.
+ *
+ * @return string The alias
+ */
+ public function getAlias()
+ {
+ return str_replace('/', '_', $this->extension_name);
+ }
+}
diff --git a/phpBB/phpbb/extension/exception.php b/phpBB/phpbb/extension/exception.php
index 3f7d251a4e..9050449bf1 100644
--- a/phpBB/phpbb/extension/exception.php
+++ b/phpBB/phpbb/extension/exception.php
@@ -16,10 +16,6 @@ namespace phpbb\extension;
/**
* Exception class for metadata
*/
-class exception extends \UnexpectedValueException
+class exception extends \phpbb\exception\runtime_exception
{
- public function __toString()
- {
- return $this->getMessage();
- }
}
diff --git a/phpBB/phpbb/extension/manager.php b/phpBB/phpbb/extension/manager.php
index 76f0e3558e..98d2d27278 100644
--- a/phpBB/phpbb/extension/manager.php
+++ b/phpBB/phpbb/extension/manager.php
@@ -26,7 +26,6 @@ class manager
protected $db;
protected $config;
protected $cache;
- protected $user;
protected $php_ext;
protected $extensions;
protected $extension_table;
@@ -39,15 +38,14 @@ class manager
* @param ContainerInterface $container A container
* @param \phpbb\db\driver\driver_interface $db A database connection
* @param \phpbb\config\config $config Config object
- * @param \phpbb\filesystem $filesystem
- * @param \phpbb\user $user User object
+ * @param \phpbb\filesystem\filesystem_interface $filesystem
* @param string $extension_table The name of the table holding extensions
* @param string $phpbb_root_path Path to the phpbb includes directory.
* @param string $php_ext php file extension, defaults to php
* @param \phpbb\cache\driver\driver_interface $cache A cache instance or null
* @param string $cache_name The name of the cache variable, defaults to _ext
*/
- public function __construct(ContainerInterface $container, \phpbb\db\driver\driver_interface $db, \phpbb\config\config $config, \phpbb\filesystem $filesystem, \phpbb\user $user, $extension_table, $phpbb_root_path, $php_ext = 'php', \phpbb\cache\driver\driver_interface $cache = null, $cache_name = '_ext')
+ public function __construct(ContainerInterface $container, \phpbb\db\driver\driver_interface $db, \phpbb\config\config $config, \phpbb\filesystem\filesystem_interface $filesystem, $extension_table, $phpbb_root_path, $php_ext = 'php', \phpbb\cache\driver\driver_interface $cache = null, $cache_name = '_ext')
{
$this->cache = $cache;
$this->cache_name = $cache_name;
@@ -58,7 +56,6 @@ class manager
$this->filesystem = $filesystem;
$this->phpbb_root_path = $phpbb_root_path;
$this->php_ext = $php_ext;
- $this->user = $user;
$this->extensions = ($this->cache) ? $this->cache->get($this->cache_name) : false;
@@ -154,7 +151,7 @@ class manager
*/
public function create_extension_metadata_manager($name, \phpbb\template\template $template)
{
- return new \phpbb\extension\metadata_manager($name, $this->config, $this, $template, $this->user, $this->phpbb_root_path);
+ return new \phpbb\extension\metadata_manager($name, $this->config, $this, $template, $this->phpbb_root_path);
}
/**
@@ -464,15 +461,17 @@ class manager
* All enabled and disabled extensions are considered configured. A purged
* extension that is no longer in the database is not configured.
*
+ * @param bool $phpbb_relative Whether the path should be relative to phpbb root
+ *
* @return array An array with extension names as keys and and the
* database stored extension information as values
*/
- public function all_configured()
+ public function all_configured($phpbb_relative = true)
{
$configured = array();
foreach ($this->extensions as $name => $data)
{
- $data['ext_path'] = $this->phpbb_root_path . $data['ext_path'];
+ $data['ext_path'] = ($phpbb_relative ? $this->phpbb_root_path : '') . $data['ext_path'];
$configured[$name] = $data;
}
return $configured;
@@ -480,18 +479,19 @@ class manager
/**
* Retrieves all enabled extensions.
+ * @param bool $phpbb_relative Whether the path should be relative to phpbb root
*
* @return array An array with extension names as keys and and the
* database stored extension information as values
*/
- public function all_enabled()
+ public function all_enabled($phpbb_relative = true)
{
$enabled = array();
foreach ($this->extensions as $name => $data)
{
if ($data['ext_active'])
{
- $enabled[$name] = $this->phpbb_root_path . $data['ext_path'];
+ $enabled[$name] = ($phpbb_relative ? $this->phpbb_root_path : '') . $data['ext_path'];
}
}
return $enabled;
@@ -500,17 +500,19 @@ class manager
/**
* Retrieves all disabled extensions.
*
+ * @param bool $phpbb_relative Whether the path should be relative to phpbb root
+ *
* @return array An array with extension names as keys and and the
* database stored extension information as values
*/
- public function all_disabled()
+ public function all_disabled($phpbb_relative = true)
{
$disabled = array();
foreach ($this->extensions as $name => $data)
{
if (!$data['ext_active'])
{
- $disabled[$name] = $this->phpbb_root_path . $data['ext_path'];
+ $disabled[$name] = ($phpbb_relative ? $this->phpbb_root_path : '') . $data['ext_path'];
}
}
return $disabled;
diff --git a/phpBB/phpbb/extension/metadata_manager.php b/phpBB/phpbb/extension/metadata_manager.php
index a64d88fe39..4f080647c8 100644
--- a/phpBB/phpbb/extension/metadata_manager.php
+++ b/phpBB/phpbb/extension/metadata_manager.php
@@ -37,12 +37,6 @@ class metadata_manager
protected $template;
/**
- * phpBB User instance
- * @var \phpbb\user
- */
- protected $user;
-
- /**
* phpBB root path
* @var string
*/
@@ -73,15 +67,13 @@ class metadata_manager
* @param \phpbb\config\config $config phpBB Config instance
* @param \phpbb\extension\manager $extension_manager An instance of the phpBB extension manager
* @param \phpbb\template\template $template phpBB Template instance
- * @param \phpbb\user $user User instance
* @param string $phpbb_root_path Path to the phpbb includes directory.
*/
- public function __construct($ext_name, \phpbb\config\config $config, \phpbb\extension\manager $extension_manager, \phpbb\template\template $template, \phpbb\user $user, $phpbb_root_path)
+ public function __construct($ext_name, \phpbb\config\config $config, \phpbb\extension\manager $extension_manager, \phpbb\template\template $template, $phpbb_root_path)
{
$this->config = $config;
$this->extension_manager = $extension_manager;
$this->template = $template;
- $this->user = $user;
$this->phpbb_root_path = $phpbb_root_path;
$this->ext_name = $ext_name;
@@ -149,7 +141,7 @@ class metadata_manager
if (!file_exists($this->metadata_file))
{
- throw new \phpbb\extension\exception($this->user->lang('FILE_NOT_FOUND', $this->metadata_file));
+ throw new \phpbb\extension\exception('FILE_NOT_FOUND', array($this->metadata_file));
}
}
@@ -163,18 +155,18 @@ class metadata_manager
{
if (!file_exists($this->metadata_file))
{
- throw new \phpbb\extension\exception($this->user->lang('FILE_NOT_FOUND', $this->metadata_file));
+ throw new \phpbb\extension\exception('FILE_NOT_FOUND', array($this->metadata_file));
}
else
{
if (!($file_contents = file_get_contents($this->metadata_file)))
{
- throw new \phpbb\extension\exception($this->user->lang('FILE_CONTENT_ERR', $this->metadata_file));
+ throw new \phpbb\extension\exception('FILE_CONTENT_ERR', array($this->metadata_file));
}
if (($metadata = json_decode($file_contents, true)) === null)
{
- throw new \phpbb\extension\exception($this->user->lang('FILE_JSON_DECODE_ERR', $this->metadata_file));
+ throw new \phpbb\extension\exception('FILE_JSON_DECODE_ERR', array($this->metadata_file));
}
array_walk_recursive($metadata, array($this, 'sanitize_json'));
@@ -246,12 +238,12 @@ class metadata_manager
{
if (!isset($this->metadata[$name]))
{
- throw new \phpbb\extension\exception($this->user->lang('META_FIELD_NOT_SET', $name));
+ throw new \phpbb\extension\exception('META_FIELD_NOT_SET', array($name));
}
if (!preg_match($fields[$name], $this->metadata[$name]))
{
- throw new \phpbb\extension\exception($this->user->lang('META_FIELD_INVALID', $name));
+ throw new \phpbb\extension\exception('META_FIELD_INVALID', array($name));
}
}
break;
@@ -270,14 +262,14 @@ class metadata_manager
{
if (empty($this->metadata['authors']))
{
- throw new \phpbb\extension\exception($this->user->lang('META_FIELD_NOT_SET', 'authors'));
+ throw new \phpbb\extension\exception('META_FIELD_NOT_SET', array('authors'));
}
foreach ($this->metadata['authors'] as $author)
{
if (!isset($author['name']))
{
- throw new \phpbb\extension\exception($this->user->lang('META_FIELD_NOT_SET', 'author name'));
+ throw new \phpbb\extension\exception('META_FIELD_NOT_SET', array('author name'));
}
}
diff --git a/phpBB/phpbb/filesystem.php b/phpBB/phpbb/filesystem.php
index 77517082e5..af56d78845 100644
--- a/phpBB/phpbb/filesystem.php
+++ b/phpBB/phpbb/filesystem.php
@@ -14,37 +14,8 @@
namespace phpbb;
/**
-* A class with various functions that are related to paths, files and the filesystem
-*/
-class filesystem
+ * @deprecated 3.2.0-dev (To be removed 3.3.0) use \phpbb\filesystem\filesystem instead
+ */
+class filesystem extends \phpbb\filesystem\filesystem
{
- /**
- * Eliminates useless . and .. components from specified path.
- *
- * @param string $path Path to clean
- * @return string Cleaned path
- */
- public function clean_path($path)
- {
- $exploded = explode('/', $path);
- $filtered = array();
- foreach ($exploded as $part)
- {
- if ($part === '.' && !empty($filtered))
- {
- continue;
- }
-
- if ($part === '..' && !empty($filtered) && $filtered[sizeof($filtered) - 1] !== '.' && $filtered[sizeof($filtered) - 1] !== '..')
- {
- array_pop($filtered);
- }
- else
- {
- $filtered[] = $part;
- }
- }
- $path = implode('/', $filtered);
- return $path;
- }
}
diff --git a/phpBB/phpbb/filesystem/exception/filesystem_exception.php b/phpBB/phpbb/filesystem/exception/filesystem_exception.php
new file mode 100644
index 0000000000..d68fa9adf3
--- /dev/null
+++ b/phpBB/phpbb/filesystem/exception/filesystem_exception.php
@@ -0,0 +1,42 @@
+<?php
+/**
+ *
+ * This file is part of the phpBB Forum Software package.
+ *
+ * @copyright (c) phpBB Limited <https://www.phpbb.com>
+ * @license GNU General Public License, version 2 (GPL-2.0)
+ *
+ * For full copyright and license information, please see
+ * the docs/CREDITS.txt file.
+ *
+ */
+
+namespace phpbb\filesystem\exception;
+
+class filesystem_exception extends \phpbb\exception\runtime_exception
+{
+ /**
+ * Constructor
+ *
+ * @param string $message The Exception message to throw (must be a language variable).
+ * @param string $filename The file that caused the error.
+ * @param array $parameters The parameters to use with the language var.
+ * @param \Exception $previous The previous runtime_exception used for the runtime_exception chaining.
+ * @param integer $code The Exception code.
+ */
+ public function __construct($message = "", $filename = '', $parameters = array(), \Exception $previous = null, $code = 0)
+ {
+ parent::__construct($message, array_merge(array('filename' => $filename), $parameters), $previous, $code);
+ }
+
+ /**
+ * Returns the filename that triggered the error
+ *
+ * @return string
+ */
+ public function get_filename()
+ {
+ $parameters = parent::get_parameters();
+ return $parameters['filename'];
+ }
+}
diff --git a/phpBB/phpbb/filesystem/filesystem.php b/phpBB/phpbb/filesystem/filesystem.php
new file mode 100644
index 0000000000..2112882d1d
--- /dev/null
+++ b/phpBB/phpbb/filesystem/filesystem.php
@@ -0,0 +1,916 @@
+<?php
+/**
+ *
+ * This file is part of the phpBB Forum Software package.
+ *
+ * @copyright (c) phpBB Limited <https://www.phpbb.com>
+ * @license GNU General Public License, version 2 (GPL-2.0)
+ *
+ * For full copyright and license information, please see
+ * the docs/CREDITS.txt file.
+ *
+ */
+
+namespace phpbb\filesystem;
+
+use phpbb\filesystem\exception\filesystem_exception;
+
+/**
+ * A class with various functions that are related to paths, files and the filesystem
+ */
+class filesystem implements filesystem_interface
+{
+ /**
+ * Store some information about file ownership for phpBB's chmod function
+ *
+ * @var array
+ */
+ protected $chmod_info;
+
+ /**
+ * Stores current working directory
+ *
+ * @var string|bool current working directory or false if it cannot be recovered
+ */
+ protected $working_directory;
+
+ /**
+ * Symfony's Filesystem component
+ *
+ * @var \Symfony\Component\Filesystem\Filesystem
+ */
+ protected $symfony_filesystem;
+
+ /**
+ * Constructor
+ */
+ public function __construct()
+ {
+ $this->chmod_info = array();
+ $this->symfony_filesystem = new \Symfony\Component\Filesystem\Filesystem();
+ $this->working_directory = null;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function chgrp($files, $group, $recursive = false)
+ {
+ try
+ {
+ $this->symfony_filesystem->chgrp($files, $group, $recursive);
+ }
+ catch (\Symfony\Component\Filesystem\Exception\IOException $e)
+ {
+ // Try to recover filename
+ // By the time this is written that is at the end of the message
+ $error = trim($e->getMessage());
+ $file = substr($error, strrpos($error, ' '));
+
+ throw new filesystem_exception('CANNOT_CHANGE_FILE_GROUP', $file, array(), $e);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function chmod($files, $perms = null, $recursive = false, $force_chmod_link = false)
+ {
+ if (is_null($perms))
+ {
+ // Default to read permission for compatibility reasons
+ $perms = self::CHMOD_READ;
+ }
+
+ // Check if we got a permission flag
+ if ($perms > self::CHMOD_ALL)
+ {
+ $file_perm = $perms;
+
+ // Extract permissions
+ //$owner = ($file_perm >> 6) & 7; // This will be ignored
+ $group = ($file_perm >> 3) & 7;
+ $other = ($file_perm >> 0) & 7;
+
+ // Does any permissions provided? if so we add execute bit for directories
+ $group = ($group !== 0) ? ($group | self::CHMOD_EXECUTE) : $group;
+ $other = ($other !== 0) ? ($other | self::CHMOD_EXECUTE) : $other;
+
+ // Compute directory permissions
+ $dir_perm = (self::CHMOD_ALL << 6) + ($group << 3) + ($other << 3);
+ }
+ else
+ {
+ // Add execute bit to owner if execute bit is among perms
+ $owner_perm = (self::CHMOD_READ | self::CHMOD_WRITE) | ($perms & self::CHMOD_EXECUTE);
+ $file_perm = ($owner_perm << 6) + ($perms << 3) + ($perms << 0);
+
+ // Compute directory permissions
+ $perm = ($perms !== 0) ? ($perms | self::CHMOD_EXECUTE) : $perms;
+ $dir_perm = (($owner_perm | self::CHMOD_EXECUTE) << 6) + ($perm << 3) + ($perm << 0);
+ }
+
+ // Symfony's filesystem component does not support extra execution flags on directories
+ // so we need to implement it again
+ foreach ($this->to_iterator($files) as $file)
+ {
+ if ($recursive && is_dir($file) && !is_link($file))
+ {
+ $this->chmod(new \FilesystemIterator($file), $perms, true);
+ }
+
+ // Don't chmod links as mostly those require 0777 and that cannot be changed
+ if (is_dir($file) || (is_link($file) && $force_chmod_link))
+ {
+ if (true !== @chmod($file, $dir_perm))
+ {
+ throw new filesystem_exception('CANNOT_CHANGE_FILE_PERMISSIONS', $file, array());
+ }
+ }
+ else if (is_file($file))
+ {
+ if (true !== @chmod($file, $file_perm))
+ {
+ throw new filesystem_exception('CANNOT_CHANGE_FILE_PERMISSIONS', $file, array());
+ }
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function chown($files, $user, $recursive = false)
+ {
+ try
+ {
+ $this->symfony_filesystem->chown($files, $user, $recursive);
+ }
+ catch (\Symfony\Component\Filesystem\Exception\IOException $e)
+ {
+ // Try to recover filename
+ // By the time this is written that is at the end of the message
+ $error = trim($e->getMessage());
+ $file = substr($error, strrpos($error, ' '));
+
+ throw new filesystem_exception('CANNOT_CHANGE_FILE_GROUP', $file, array(), $e);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function clean_path($path)
+ {
+ $exploded = explode('/', $path);
+ $filtered = array();
+ foreach ($exploded as $part)
+ {
+ if ($part === '.' && !empty($filtered))
+ {
+ continue;
+ }
+
+ if ($part === '..' && !empty($filtered) && $filtered[sizeof($filtered) - 1] !== '.' && $filtered[sizeof($filtered) - 1] !== '..')
+ {
+ array_pop($filtered);
+ }
+ else
+ {
+ $filtered[] = $part;
+ }
+ }
+ $path = implode('/', $filtered);
+ return $path;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function copy($origin_file, $target_file, $override = false)
+ {
+ try
+ {
+ $this->symfony_filesystem->copy($origin_file, $target_file, $override);
+ }
+ catch (\Symfony\Component\Filesystem\Exception\IOException $e)
+ {
+ throw new filesystem_exception('CANNOT_COPY_FILES', '', array(), $e);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function dump_file($filename, $content)
+ {
+ try
+ {
+ $this->symfony_filesystem->dumpFile($filename, $content);
+ }
+ catch (\Symfony\Component\Filesystem\Exception\IOException $e)
+ {
+ throw new filesystem_exception('CANNOT_DUMP_FILE', $filename, array(), $e);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function exists($files)
+ {
+ return $this->symfony_filesystem->exists($files);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function is_absolute_path($path)
+ {
+ return (isset($path[0]) && $path[0] === '/' || preg_match('#^[a-z]:[/\\\]#i', $path)) ? true : false;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function is_readable($files, $recursive = false)
+ {
+ foreach ($this->to_iterator($files) as $file)
+ {
+ if ($recursive && is_dir($file) && !is_link($file))
+ {
+ if (!$this->is_readable(new \FilesystemIterator($file), true))
+ {
+ return false;
+ }
+ }
+
+ if (!is_readable($file))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function is_writable($files, $recursive = false)
+ {
+ if (defined('PHP_WINDOWS_VERSION_MAJOR') || !function_exists('is_writable'))
+ {
+ foreach ($this->to_iterator($files) as $file)
+ {
+ if ($recursive && is_dir($file) && !is_link($file))
+ {
+ if (!$this->is_writable(new \FilesystemIterator($file), true))
+ {
+ return false;
+ }
+ }
+
+ if (!$this->phpbb_is_writable($file))
+ {
+ return false;
+ }
+ }
+ }
+ else
+ {
+ // use built in is_writable
+ foreach ($this->to_iterator($files) as $file)
+ {
+ if ($recursive && is_dir($file) && !is_link($file))
+ {
+ if (!$this->is_writable(new \FilesystemIterator($file), true))
+ {
+ return false;
+ }
+ }
+
+ if (!is_writable($file))
+ {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function make_path_relative($end_path, $start_path)
+ {
+ return $this->symfony_filesystem->makePathRelative($end_path, $start_path);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function mirror($origin_dir, $target_dir, \Traversable $iterator = null, $options = array())
+ {
+ try
+ {
+ $this->symfony_filesystem->mirror($origin_dir, $target_dir, $iterator, $options);
+ }
+ catch (\Symfony\Component\Filesystem\Exception\IOException $e)
+ {
+ $msg = $e->getMessage();
+ $filename = substr($msg, strpos($msg, '"'), strrpos($msg, '"'));
+
+ throw new filesystem_exception('CANNOT_MIRROR_DIRECTORY', $filename, array(), $e);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function mkdir($dirs, $mode = 0777)
+ {
+ try
+ {
+ $this->symfony_filesystem->mkdir($dirs, $mode);
+ }
+ catch (\Symfony\Component\Filesystem\Exception\IOException $e)
+ {
+ $msg = $e->getMessage();
+ $filename = substr($msg, strpos($msg, '"'), strrpos($msg, '"'));
+
+ throw new filesystem_exception('CANNOT_CREATE_DIRECTORY', $filename, array(), $e);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function phpbb_chmod($files, $perms = null, $recursive = false, $force_chmod_link = false)
+ {
+ if (is_null($perms))
+ {
+ // Default to read permission for compatibility reasons
+ $perms = self::CHMOD_READ;
+ }
+
+ if (empty($this->chmod_info))
+ {
+ if (!function_exists('fileowner') || !function_exists('filegroup'))
+ {
+ $this->chmod_info['process'] = false;
+ }
+ else
+ {
+ $common_php_owner = @fileowner(__FILE__);
+ $common_php_group = @filegroup(__FILE__);
+
+ // And the owner and the groups PHP is running under.
+ $php_uid = (function_exists('posic_getuid')) ? @posix_getuid() : false;
+ $php_gids = (function_exists('posix_getgroups')) ? @posix_getgroups() : false;
+
+ // If we are unable to get owner/group, then do not try to set them by guessing
+ if (!$php_uid || empty($php_gids) || !$common_php_owner || !$common_php_group)
+ {
+ $this->chmod_info['process'] = false;
+ }
+ else
+ {
+ $this->chmod_info = array(
+ 'process' => true,
+ 'common_owner' => $common_php_owner,
+ 'common_group' => $common_php_group,
+ 'php_uid' => $php_uid,
+ 'php_gids' => $php_gids,
+ );
+ }
+ }
+ }
+
+ if ($this->chmod_info['process'])
+ {
+ try
+ {
+ foreach ($this->to_iterator($files) as $file)
+ {
+ $file_uid = @fileowner($file);
+ $file_gid = @filegroup($file);
+
+ // Change owner
+ if ($file_uid !== $this->chmod_info['common_owner'])
+ {
+ $this->chown($file, $this->chmod_info['common_owner'], $recursive);
+ }
+
+ // Change group
+ if ($file_gid !== $this->chmod_info['common_group'])
+ {
+ $this->chgrp($file, $this->chmod_info['common_group'], $recursive);
+ }
+
+ clearstatcache();
+ $file_uid = @fileowner($file);
+ $file_gid = @filegroup($file);
+ }
+ }
+ catch (filesystem_exception $e)
+ {
+ $this->chmod_info['process'] = false;
+ }
+ }
+
+ // Still able to process?
+ if ($this->chmod_info['process'])
+ {
+ if ($file_uid === $this->chmod_info['php_uid'])
+ {
+ $php = 'owner';
+ }
+ else if (in_array($file_gid, $this->chmod_info['php_gids']))
+ {
+ $php = 'group';
+ }
+ else
+ {
+ // Since we are setting the everyone bit anyway, no need to do expensive operations
+ $this->chmod_info['process'] = false;
+ }
+ }
+
+ // We are not able to determine or change something
+ if (!$this->chmod_info['process'])
+ {
+ $php = 'other';
+ }
+
+ switch ($php)
+ {
+ case 'owner':
+ try
+ {
+ $this->chmod($files, $perms, $recursive, $force_chmod_link);
+ clearstatcache();
+ if ($this->is_readable($files) && $this->is_writable($files))
+ {
+ break;
+ }
+ }
+ catch (filesystem_exception $e)
+ {
+ // Do nothing
+ }
+ case 'group':
+ try
+ {
+ $this->chmod($files, $perms, $recursive, $force_chmod_link);
+ clearstatcache();
+ if ((!($perms & self::CHMOD_READ) || $this->is_readable($files, $recursive)) && (!($perms & self::CHMOD_WRITE) || $this->is_writable($files, $recursive)))
+ {
+ break;
+ }
+ }
+ catch (filesystem_exception $e)
+ {
+ // Do nothing
+ }
+ case 'other':
+ default:
+ $this->chmod($files, $perms, $recursive, $force_chmod_link);
+ break;
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function realpath($path)
+ {
+ if (!function_exists('realpath'))
+ {
+ return $this->phpbb_own_realpath($path);
+ }
+
+ $realpath = realpath($path);
+
+ // Strangely there are provider not disabling realpath but returning strange values. :o
+ // We at least try to cope with them.
+ if ((!$this->is_absolute_path($path) && $realpath === $path) || $realpath === false)
+ {
+ return $this->phpbb_own_realpath($path);
+ }
+
+ // Check for DIRECTORY_SEPARATOR at the end (and remove it!)
+ if (substr($realpath, -1) === DIRECTORY_SEPARATOR)
+ {
+ $realpath = substr($realpath, 0, -1);
+ }
+
+ return $realpath;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function remove($files)
+ {
+ try
+ {
+ $this->symfony_filesystem->remove($files);
+ }
+ catch (\Symfony\Component\Filesystem\Exception\IOException $e)
+ {
+ // Try to recover filename
+ // By the time this is written that is at the end of the message
+ $error = trim($e->getMessage());
+ $file = substr($error, strrpos($error, ' '));
+
+ throw new filesystem_exception('CANNOT_DELETE_FILES', $file, array(), $e);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function rename($origin, $target, $overwrite = false)
+ {
+ try
+ {
+ $this->symfony_filesystem->rename($origin, $target, $overwrite);
+ }
+ catch (\Symfony\Component\Filesystem\Exception\IOException $e)
+ {
+ $msg = $e->getMessage();
+ $filename = substr($msg, strpos($msg, '"'), strrpos($msg, '"'));
+
+ throw new filesystem_exception('CANNOT_RENAME_FILE', $filename, array(), $e);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function symlink($origin_dir, $target_dir, $copy_on_windows = false)
+ {
+ try
+ {
+ $this->symfony_filesystem->symlink($origin_dir, $target_dir, $copy_on_windows);
+ }
+ catch (\Symfony\Component\Filesystem\Exception\IOException $e)
+ {
+ throw new filesystem_exception('CANNOT_CREATE_SYMLINK', $origin_dir, array(), $e);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function touch($files, $time = null, $access_time = null)
+ {
+ try
+ {
+ $this->symfony_filesystem->touch($files, $time, $access_time);
+ }
+ catch (\Symfony\Component\Filesystem\Exception\IOException $e)
+ {
+ // Try to recover filename
+ // By the time this is written that is at the end of the message
+ $error = trim($e->getMessage());
+ $file = substr($error, strrpos($error, ' '));
+
+ throw new filesystem_exception('CANNOT_TOUCH_FILES', $file, array(), $e);
+ }
+ }
+
+ /**
+ * phpBB's implementation of is_writable
+ *
+ * @todo Investigate if is_writable is still buggy
+ *
+ * @param string $file file/directory to check if writable
+ *
+ * @return bool true if the given path is writable
+ */
+ protected function phpbb_is_writable($file)
+ {
+ if (file_exists($file))
+ {
+ // Canonicalise path to absolute path
+ $file = $this->realpath($file);
+
+ if (is_dir($file))
+ {
+ // Test directory by creating a file inside the directory
+ $result = @tempnam($file, 'i_w');
+
+ if (is_string($result) && file_exists($result))
+ {
+ unlink($result);
+
+ // Ensure the file is actually in the directory (returned realpathed)
+ return (strpos($result, $file) === 0) ? true : false;
+ }
+ }
+ else
+ {
+ $handle = @fopen($file, 'c');
+
+ if (is_resource($handle))
+ {
+ fclose($handle);
+ return true;
+ }
+ }
+ }
+ else
+ {
+ // file does not exist test if we can write to the directory
+ $dir = dirname($file);
+
+ if (file_exists($dir) && is_dir($dir) && $this->phpbb_is_writable($dir))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Try to resolve real path when PHP's realpath failes to do so
+ *
+ * @param string $path
+ * @return bool|string
+ */
+ protected function phpbb_own_realpath($path)
+ {
+ // Replace all directory separators with '/'
+ $path = str_replace(DIRECTORY_SEPARATOR, '/', $path);
+
+ $is_absolute_path = false;
+ $path_prefix = '';
+
+ if ($this->is_absolute_path($path))
+ {
+ $is_absolute_path = true;
+ }
+ else
+ {
+ // Resolve working directory and store it
+ if (is_null($this->working_directory))
+ {
+ if (function_exists('getcwd'))
+ {
+ $this->working_directory = str_replace(DIRECTORY_SEPARATOR, '/', getcwd());
+ }
+
+ //
+ // From this point on we really just guessing
+ // If chdir were called we screwed
+ //
+ else if (function_exists('debug_backtrace'))
+ {
+ $call_stack = debug_backtrace(0);
+ $this->working_directory = str_replace(DIRECTORY_SEPARATOR, '/', dirname($call_stack[sizeof($call_stack) - 1]['file']));
+ }
+ else
+ {
+ //
+ // Assuming that the working directory is phpBB root
+ // we could use this as a fallback, when phpBB will use controllers
+ // everywhere this will be a safe assumption
+ //
+ //$dir_parts = explode(DIRECTORY_SEPARATOR, __DIR__);
+ //$namespace_parts = explode('\\', trim(__NAMESPACE__, '\\'));
+
+ //$namespace_part_count = sizeof($namespace_parts);
+
+ // Check if we still loading from root
+ //if (array_slice($dir_parts, -$namespace_part_count) === $namespace_parts)
+ //{
+ // $this->working_directory = implode('/', array_slice($dir_parts, 0, -$namespace_part_count));
+ //}
+ //else
+ //{
+ // $this->working_directory = false;
+ //}
+
+ $this->working_directory = false;
+ }
+ }
+
+ if ($this->working_directory !== false)
+ {
+ $is_absolute_path = true;
+ $path = $this->working_directory . '/' . $path;
+ }
+ }
+
+ if ($is_absolute_path)
+ {
+ if (defined('PHP_WINDOWS_VERSION_MAJOR'))
+ {
+ $path_prefix = $path[0] . ':';
+ $path = substr($path, 2);
+ }
+ else
+ {
+ $path_prefix = '';
+ }
+ }
+
+ $resolved_path = $this->resolve_path($path, $path_prefix, $is_absolute_path);
+ if ($resolved_path === false)
+ {
+ return false;
+ }
+
+ if (!@file_exists($resolved_path) || (!@is_dir($resolved_path . '/') && !is_file($resolved_path)))
+ {
+ return false;
+ }
+
+ // Return OS specific directory separators
+ $resolved = str_replace('/', DIRECTORY_SEPARATOR, $resolved_path);
+
+ // Check for DIRECTORY_SEPARATOR at the end (and remove it!)
+ if (substr($resolved, -1) === DIRECTORY_SEPARATOR)
+ {
+ return substr($resolved, 0, -1);
+ }
+
+ return $resolved;
+ }
+
+ /**
+ * Convert file(s) to \Traversable object
+ *
+ * This is the same function as Symfony's toIterator, but that is private
+ * so we cannot use it.
+ *
+ * @param string|array|\Traversable $files filename/list of filenames
+ * @return \Traversable
+ */
+ protected function to_iterator($files)
+ {
+ if (!$files instanceof \Traversable)
+ {
+ $files = new \ArrayObject(is_array($files) ? $files : array($files));
+ }
+
+ return $files;
+ }
+
+ /**
+ * Try to resolve symlinks in path
+ *
+ * @param string $path The path to resolve
+ * @param string $prefix The path prefix (on windows the drive letter)
+ * @param bool $absolute Whether or not the path is absolute
+ * @param bool $return_array Whether or not to return path parts
+ *
+ * @return string|array|bool returns the resolved path or an array of parts of the path if $return_array is true
+ * or false if path cannot be resolved
+ */
+ protected function resolve_path($path, $prefix = '', $absolute = false, $return_array = false)
+ {
+ if ($return_array)
+ {
+ $path = str_replace(DIRECTORY_SEPARATOR, '/', $path);
+ }
+
+ trim ($path, '/');
+ $path_parts = explode('/', $path);
+ $resolved = array();
+ $resolved_path = $prefix;
+ $file_found = false;
+
+ foreach ($path_parts as $path_part)
+ {
+ if ($file_found)
+ {
+ return false;
+ }
+
+ if (empty($path_part) || ($path_part === '.' && ($absolute || !empty($resolved))))
+ {
+ continue;
+ }
+ else if ($absolute && $path_part === '..')
+ {
+ if (empty($resolved))
+ {
+ // No directories above root
+ return false;
+ }
+
+ array_pop($resolved);
+ $resolved_path = false;
+ }
+ else if ($path_part === '..' && !empty($resolved) && !in_array($resolved[sizeof($resolved) - 1], array('.', '..')))
+ {
+ array_pop($resolved);
+ $resolved_path = false;
+ }
+ else
+ {
+ if ($resolved_path === false)
+ {
+ if (empty($resolved))
+ {
+ $resolved_path = ($absolute) ? $prefix . '/' . $path_part : $path_part;
+ }
+ else
+ {
+ $tmp_array = $resolved;
+ if ($absolute)
+ {
+ array_unshift($tmp_array, $prefix);
+ }
+
+ $resolved_path = implode('/', $tmp_array);
+ }
+ }
+
+ $current_path = $resolved_path . '/' . $path_part;
+
+ // Resolve symlinks
+ if (is_link($current_path))
+ {
+ if (!function_exists('readlink'))
+ {
+ return false;
+ }
+
+ $link = readlink($current_path);
+
+ // Is link has an absolute path in it?
+ if ($this->is_absolute_path($link))
+ {
+ if (defined('PHP_WINDOWS_VERSION_MAJOR'))
+ {
+ $prefix = $link[0] . ':';
+ $link = substr($link, 2);
+ }
+ else
+ {
+ $prefix = '';
+ }
+
+ $resolved = $this->resolve_path($link, $prefix, true, true);
+ $absolute = true;
+ }
+ else
+ {
+ $resolved = $this->resolve_path($resolved_path . '/' . $link, $prefix, $absolute, true);
+ }
+
+ if (!$resolved)
+ {
+ return false;
+ }
+
+ $resolved_path = false;
+ }
+ else if (is_dir($current_path . '/'))
+ {
+ $resolved[] = $path_part;
+ $resolved_path = $current_path;
+ }
+ else if (is_file($current_path))
+ {
+ $resolved[] = $path_part;
+ $resolved_path = $current_path;
+ $file_found = true;
+ }
+ else
+ {
+ return false;
+ }
+ }
+ }
+
+ // If at the end of the path there were a .. or .
+ // we need to build the path again.
+ // Only doing this when a string is expected in return
+ if ($resolved_path === false && $return_array === false)
+ {
+ if (empty($resolved))
+ {
+ $resolved_path = ($absolute) ? $prefix . '/' : './';
+ }
+ else
+ {
+ $tmp_array = $resolved;
+ if ($absolute)
+ {
+ array_unshift($tmp_array, $prefix);
+ }
+
+ $resolved_path = implode('/', $tmp_array);
+ }
+ }
+
+ return ($return_array) ? $resolved : $resolved_path;
+ }
+}
diff --git a/phpBB/phpbb/filesystem/filesystem_interface.php b/phpBB/phpbb/filesystem/filesystem_interface.php
new file mode 100644
index 0000000000..21ad8252f8
--- /dev/null
+++ b/phpBB/phpbb/filesystem/filesystem_interface.php
@@ -0,0 +1,284 @@
+<?php
+/**
+ *
+ * This file is part of the phpBB Forum Software package.
+ *
+ * @copyright (c) phpBB Limited <https://www.phpbb.com>
+ * @license GNU General Public License, version 2 (GPL-2.0)
+ *
+ * For full copyright and license information, please see
+ * the docs/CREDITS.txt file.
+ *
+ */
+
+namespace phpbb\filesystem;
+
+/**
+ * Interface for phpBB's filesystem service
+ */
+interface filesystem_interface
+{
+ /**
+ * chmod all permissions flag
+ *
+ * @var int
+ */
+ const CHMOD_ALL = 7;
+
+ /**
+ * chmod read permissions flag
+ *
+ * @var int
+ */
+ const CHMOD_READ = 4;
+
+ /**
+ * chmod write permissions flag
+ *
+ * @var int
+ */
+ const CHMOD_WRITE = 2;
+
+ /**
+ * chmod execute permissions flag
+ *
+ * @var int
+ */
+ const CHMOD_EXECUTE = 1;
+
+ /**
+ * Change owner group of files/directories
+ *
+ * @param string|array|\Traversable $files The file(s)/directorie(s) to change group
+ * @param string $group The group that should own the files/directories
+ * @param bool $recursive If the group should be changed recursively
+ * @throws \phpbb\filesystem\exception\filesystem_exception the filename which triggered the error can be
+ * retrieved by filesystem_exception::get_filename()
+ */
+ public function chgrp($files, $group, $recursive = false);
+
+ /**
+ * Global function for chmodding directories and files for internal use
+ *
+ * The function accepts filesystem_interface::CHMOD_ flags in the permission argument
+ * or the user can specify octal values (or any integer if it makes sense). All directories will have
+ * an execution bit appended, if the user group (owner, group or other) has any bit specified.
+ *
+ * @param string|array|\Traversable $file The file/directory to be chmodded
+ * @param int $perms Permissions to set
+ * @param bool $recursive If the permissions should be changed recursively
+ * @param bool $force_chmod_link Try to apply permissions to symlinks as well
+ *
+ * @throws \phpbb\filesystem\exception\filesystem_exception the filename which triggered the error can be
+ * retrieved by filesystem_exception::get_filename()
+ */
+ public function chmod($files, $perms = null, $recursive = false, $force_chmod_link = false);
+
+ /**
+ * Change owner group of files/directories
+ *
+ * @param string|array|\Traversable $files The file(s)/directorie(s) to change group
+ * @param string $user The owner user name
+ * @param bool $recursive Whether change the owner recursively or not
+ *
+ * @throws \phpbb\filesystem\exception\filesystem_exception the filename which triggered the error can be
+ * retrieved by filesystem_exception::get_filename()
+ */
+ public function chown($files, $user, $recursive = false);
+
+ /**
+ * Eliminates useless . and .. components from specified path.
+ *
+ * @param string $path Path to clean
+ *
+ * @return string Cleaned path
+ */
+ public function clean_path($path);
+
+ /**
+ * Copies a file.
+ *
+ * This method only copies the file if the origin file is newer than the target file.
+ *
+ * By default, if the target already exists, it is not overridden.
+ *
+ * @param string $origin_file The original filename
+ * @param string $target_file The target filename
+ * @param bool $override Whether to override an existing file or not
+ *
+ * @throws \phpbb\filesystem\exception\filesystem_exception When the file cannot be copied
+ */
+ public function copy($origin_file, $target_file, $override = false);
+
+ /**
+ * Atomically dumps content into a file.
+ *
+ * @param string $filename The file to be written to.
+ * @param string $content The data to write into the file.
+ *
+ * @throws \phpbb\filesystem\exception\filesystem_exception When the file cannot be written
+ */
+ public function dump_file($filename, $content);
+
+ /**
+ * Checks the existence of files or directories.
+ *
+ * @param string|array|\Traversable $files files/directories to check
+ *
+ * @return bool Returns true if all files/directories exist, false otherwise
+ */
+ public function exists($files);
+
+ /**
+ * Checks if a path is absolute or not
+ *
+ * @param string $path Path to check
+ *
+ * @return bool true if the path is absolute, false otherwise
+ */
+ public function is_absolute_path($path);
+
+ /**
+ * Checks if files/directories are readable
+ *
+ * @param string|array|\Traversable $files files/directories to check
+ * @param bool $recursive Whether or not directories should be checked recursively
+ *
+ * @return bool True when the files/directories are readable, otherwise false.
+ */
+ public function is_readable($files, $recursive = false);
+
+ /**
+ * Test if a file/directory is writable
+ *
+ * @param string|array|\Traversable $files files/directories to perform write test on
+ * @param bool $recursive Whether or not directories should be checked recursively
+ *
+ * @return bool True when the files/directories are writable, otherwise false.
+ */
+ public function is_writable($files, $recursive = false);
+
+ /**
+ * Given an existing path, convert it to a path relative to a given starting path
+ *
+ * @param string $end_path Absolute path of target
+ * @param string $start_path Absolute path where traversal begins
+ *
+ * @return string Path of target relative to starting path
+ */
+ public function make_path_relative($end_path, $start_path);
+
+ /**
+ * Mirrors a directory to another.
+ *
+ * @param string $origin_dir The origin directory
+ * @param string $target_dir The target directory
+ * @param \Traversable $iterator A Traversable instance
+ * @param array $options An array of boolean options
+ * Valid options are:
+ * - $options['override'] Whether to override an existing file on copy or not (see copy())
+ * - $options['copy_on_windows'] Whether to copy files instead of links on Windows (see symlink())
+ * - $options['delete'] Whether to delete files that are not in the source directory (defaults to false)
+ *
+ * @throws \phpbb\filesystem\exception\filesystem_exception When the file cannot be copied.
+ * The filename which triggered the error can be
+ * retrieved by filesystem_exception::get_filename()
+ */
+ public function mirror($origin_dir, $target_dir, \Traversable $iterator = null, $options = array());
+
+ /**
+ * Creates a directory recursively.
+ *
+ * @param string|array|\Traversable $dirs The directory path
+ * @param int $mode The directory mode
+ *
+ * @throws \phpbb\filesystem\exception\filesystem_exception On any directory creation failure
+ * The filename which triggered the error can be
+ * retrieved by filesystem_exception::get_filename()
+ */
+ public function mkdir($dirs, $mode = 0777);
+
+ /**
+ * Global function for chmodding directories and files for internal use
+ *
+ * This function determines owner and group whom the file belongs to and user and group of PHP and then set safest possible file permissions.
+ * The function determines owner and group from common.php file and sets the same to the provided file.
+ * The function uses bit fields to build the permissions.
+ * The function sets the appropiate execute bit on directories.
+ *
+ * Supported constants representing bit fields are:
+ *
+ * filesystem_interface::CHMOD_ALL - all permissions (7)
+ * filesystem_interface::CHMOD_READ - read permission (4)
+ * filesystem_interface::CHMOD_WRITE - write permission (2)
+ * filesystem_interface::CHMOD_EXECUTE - execute permission (1)
+ *
+ * NOTE: The function uses POSIX extension and fileowner()/filegroup() functions. If any of them is disabled, this function tries to build proper permissions, by calling is_readable() and is_writable() functions.
+ *
+ * @param string|array|\Traversable $file The file/directory to be chmodded
+ * @param int $perms Permissions to set
+ * @param bool $recursive If the permissions should be changed recursively
+ * @param bool $force_chmod_link Try to apply permissions to symlinks as well
+ *
+ * @throws \phpbb\filesystem\exception\filesystem_exception the filename which triggered the error can be
+ * retrieved by filesystem_exception::get_filename()
+ */
+ public function phpbb_chmod($file, $perms = null, $recursive = false, $force_chmod_link = false);
+
+ /**
+ * A wrapper for PHP's realpath
+ *
+ * Try to resolve realpath when PHP's realpath is not available, or
+ * known to be buggy.
+ *
+ * @param string $path Path to resolve
+ *
+ * @return string Resolved path
+ */
+ public function realpath($path);
+
+ /**
+ * Removes files or directories.
+ *
+ * @param string|array|\Traversable $files A filename, an array of files, or a \Traversable instance to remove
+ *
+ * @throws \phpbb\filesystem\exception\filesystem_exception When removal fails.
+ * The filename which triggered the error can be
+ * retrieved by filesystem_exception::get_filename()
+ */
+ public function remove($files);
+
+ /**
+ * Renames a file or a directory.
+ *
+ * @param string $origin The origin filename or directory
+ * @param string $target The new filename or directory
+ * @param bool $overwrite Whether to overwrite the target if it already exists
+ *
+ * @throws \phpbb\filesystem\exception\filesystem_exception When target file or directory already exists,
+ * or origin cannot be renamed.
+ */
+ public function rename($origin, $target, $overwrite = false);
+
+ /**
+ * Creates a symbolic link or copy a directory.
+ *
+ * @param string $origin_dir The origin directory path
+ * @param string $target_dir The symbolic link name
+ * @param bool $copy_on_windows Whether to copy files if on Windows
+ *
+ * @throws \phpbb\filesystem\exception\filesystem_exception When symlink fails
+ */
+ public function symlink($origin_dir, $target_dir, $copy_on_windows = false);
+
+ /**
+ * Sets access and modification time of file.
+ *
+ * @param string|array|\Traversable $files A filename, an array of files, or a \Traversable instance to create
+ * @param int $time The touch time as a Unix timestamp
+ * @param int $access_time The access time as a Unix timestamp
+ *
+ * @throws \phpbb\filesystem\exception\filesystem_exception When touch fails
+ */
+ public function touch($files, $time = null, $access_time = null);
+}
diff --git a/phpBB/phpbb/finder.php b/phpBB/phpbb/finder.php
index 28f28825ba..58bc27084e 100644
--- a/phpBB/phpbb/finder.php
+++ b/phpBB/phpbb/finder.php
@@ -48,14 +48,14 @@ class finder
/**
* Creates a new finder instance with its dependencies
*
- * @param \phpbb\filesystem $filesystem Filesystem instance
+ * @param \phpbb\filesystem\filesystem_interface $filesystem Filesystem instance
* @param string $phpbb_root_path Path to the phpbb root directory
* @param \phpbb\cache\driver\driver_interface $cache A cache instance or null
* @param string $php_ext php file extension
* @param string $cache_name The name of the cache variable, defaults to
* _ext_finder
*/
- public function __construct(\phpbb\filesystem $filesystem, $phpbb_root_path = '', \phpbb\cache\driver\driver_interface $cache = null, $php_ext = 'php', $cache_name = '_ext_finder')
+ public function __construct(\phpbb\filesystem\filesystem_interface $filesystem, $phpbb_root_path = '', \phpbb\cache\driver\driver_interface $cache = null, $php_ext = 'php', $cache_name = '_ext_finder')
{
$this->filesystem = $filesystem;
$this->phpbb_root_path = $phpbb_root_path;
diff --git a/phpBB/phpbb/help/controller/help.php b/phpBB/phpbb/help/controller/help.php
new file mode 100644
index 0000000000..9cc3b0c8b4
--- /dev/null
+++ b/phpBB/phpbb/help/controller/help.php
@@ -0,0 +1,160 @@
+<?php
+/**
+ *
+ * This file is part of the phpBB Forum Software package.
+ *
+ * @copyright (c) phpBB Limited <https://www.phpbb.com>
+ * @license GNU General Public License, version 2 (GPL-2.0)
+ *
+ * For full copyright and license information, please see
+ * the docs/CREDITS.txt file.
+ *
+ */
+
+namespace phpbb\help\controller;
+
+use phpbb\exception\http_exception;
+
+class help
+{
+ /** @var \phpbb\controller\helper */
+ protected $helper;
+
+ /** @var \phpbb\event\dispatcher_interface */
+ protected $dispatcher;
+
+ /** @var \phpbb\template\template */
+ protected $template;
+
+ /** @var \phpbb\user */
+ protected $user;
+
+ /** @var string */
+ protected $root_path;
+
+ /** @var string */
+ protected $php_ext;
+
+ /**
+ * Constructor
+ *
+ * @param \phpbb\controller\helper $helper
+ * @param \phpbb\event\dispatcher_interface $dispatcher
+ * @param \phpbb\template\template $template
+ * @param \phpbb\user $user
+ * @param string $root_path
+ * @param string $php_ext
+ */
+ public function __construct(\phpbb\controller\helper $helper, \phpbb\event\dispatcher_interface $dispatcher, \phpbb\template\template $template, \phpbb\user $user, $root_path, $php_ext)
+ {
+ $this->helper = $helper;
+ $this->dispatcher = $dispatcher;
+ $this->template = $template;
+ $this->user = $user;
+ $this->root_path = $root_path;
+ $this->php_ext = $php_ext;
+ }
+
+ /**
+ * Controller for /help/{mode} routes
+ *
+ * @param string $mode
+ * @return \Symfony\Component\HttpFoundation\Response A Symfony Response object
+ * @throws http_exception when the $mode is not known by any extension
+ */
+ public function handle($mode)
+ {
+ switch ($mode)
+ {
+ case 'faq':
+ case 'bbcode':
+ $page_title = ($mode === 'faq') ? $this->user->lang['FAQ_EXPLAIN'] : $this->user->lang['BBCODE_GUIDE'];
+ $this->user->add_lang($mode, false, true);
+ break;
+
+ default:
+ $page_title = $this->user->lang['FAQ_EXPLAIN'];
+ $ext_name = $lang_file = '';
+
+ /**
+ * You can use this event display a custom help page
+ *
+ * @event core.faq_mode_validation
+ * @var string page_title Title of the page
+ * @var string mode FAQ that is going to be displayed
+ * @var string lang_file Language file containing the help data
+ * @var string ext_name Vendor and extension name where the help
+ * language file can be loaded from
+ * @since 3.1.4-RC1
+ */
+ $vars = array(
+ 'page_title',
+ 'mode',
+ 'lang_file',
+ 'ext_name',
+ );
+ extract($this->dispatcher->trigger_event('core.faq_mode_validation', compact($vars)));
+
+ if ($ext_name === '' || $lang_file === '')
+ {
+ throw new http_exception(404, 'Not Found');
+ }
+
+ $this->user->add_lang($lang_file, false, true, $ext_name);
+ break;
+
+ }
+
+ $this->template->assign_vars(array(
+ 'L_FAQ_TITLE' => $page_title,
+ 'S_IN_FAQ' => true,
+ ));
+
+ $this->assign_to_template($this->user->help);
+
+ make_jumpbox(append_sid("{$this->root_path}viewforum.{$this->php_ext}"));
+ return $this->helper->render('faq_body.html', $page_title);
+ }
+
+ /**
+ * Assigns the help data to the template blocks
+ *
+ * @param array $help_data
+ * @return null
+ */
+ protected function assign_to_template(array $help_data)
+ {
+ // Pull the array data from the lang pack
+ $switch_column = $found_switch = false;
+ foreach ($help_data as $help_ary)
+ {
+ if ($help_ary[0] == '--')
+ {
+ if ($help_ary[1] == '--')
+ {
+ $switch_column = true;
+ $found_switch = true;
+ continue;
+ }
+
+ $this->template->assign_block_vars('faq_block', array(
+ 'BLOCK_TITLE' => $help_ary[1],
+ 'SWITCH_COLUMN' => $switch_column,
+ ));
+
+ if ($switch_column)
+ {
+ $switch_column = false;
+ }
+ continue;
+ }
+
+ $this->template->assign_block_vars('faq_block.faq_row', array(
+ 'FAQ_QUESTION' => $help_ary[0],
+ 'FAQ_ANSWER' => $help_ary[1],
+ ));
+ }
+
+ $this->template->assign_var('SWITCH_COLUMN_MANUALLY', !$found_switch);
+ }
+}
diff --git a/phpBB/phpbb/language/exception/invalid_plural_rule_exception.php b/phpBB/phpbb/language/exception/invalid_plural_rule_exception.php
new file mode 100644
index 0000000000..94e3466208
--- /dev/null
+++ b/phpBB/phpbb/language/exception/invalid_plural_rule_exception.php
@@ -0,0 +1,22 @@
+<?php
+/**
+ *
+ * This file is part of the phpBB Forum Software package.
+ *
+ * @copyright (c) phpBB Limited <https://www.phpbb.com>
+ * @license GNU General Public License, version 2 (GPL-2.0)
+ *
+ * For full copyright and license information, please see
+ * the docs/CREDITS.txt file.
+ *
+ */
+
+namespace phpbb\language\exception;
+
+/**
+ * Thrown when nonexistent plural rule is specified
+ */
+class invalid_plural_rule_exception extends language_exception
+{
+
+}
diff --git a/phpBB/phpbb/language/exception/language_exception.php b/phpBB/phpbb/language/exception/language_exception.php
new file mode 100644
index 0000000000..b1258414aa
--- /dev/null
+++ b/phpBB/phpbb/language/exception/language_exception.php
@@ -0,0 +1,22 @@
+<?php
+/**
+ *
+ * This file is part of the phpBB Forum Software package.
+ *
+ * @copyright (c) phpBB Limited <https://www.phpbb.com>
+ * @license GNU General Public License, version 2 (GPL-2.0)
+ *
+ * For full copyright and license information, please see
+ * the docs/CREDITS.txt file.
+ *
+ */
+
+namespace phpbb\language\exception;
+
+/**
+ * Base exception class for language exceptions
+ */
+class language_exception extends \phpbb\exception\runtime_exception
+{
+
+}
diff --git a/phpBB/phpbb/language/exception/language_file_not_found.php b/phpBB/phpbb/language/exception/language_file_not_found.php
new file mode 100644
index 0000000000..89364267eb
--- /dev/null
+++ b/phpBB/phpbb/language/exception/language_file_not_found.php
@@ -0,0 +1,22 @@
+<?php
+/**
+ *
+ * This file is part of the phpBB Forum Software package.
+ *
+ * @copyright (c) phpBB Limited <https://www.phpbb.com>
+ * @license GNU General Public License, version 2 (GPL-2.0)
+ *
+ * For full copyright and license information, please see
+ * the docs/CREDITS.txt file.
+ *
+ */
+
+namespace phpbb\language\exception;
+
+/**
+ * This exception is thrown when the language file is not found
+ */
+class language_file_not_found extends language_exception
+{
+
+}
diff --git a/phpBB/phpbb/language/language.php b/phpBB/phpbb/language/language.php
new file mode 100644
index 0000000000..3298908365
--- /dev/null
+++ b/phpBB/phpbb/language/language.php
@@ -0,0 +1,571 @@
+<?php
+/**
+ *
+ * This file is part of the phpBB Forum Software package.
+ *
+ * @copyright (c) phpBB Limited <https://www.phpbb.com>
+ * @license GNU General Public License, version 2 (GPL-2.0)
+ *
+ * For full copyright and license information, please see
+ * the docs/CREDITS.txt file.
+ *
+ */
+
+namespace phpbb\language;
+
+use phpbb\language\exception\invalid_plural_rule_exception;
+
+/**
+ * Wrapper class for loading translations
+ */
+class language
+{
+ /**
+ * Global fallback language
+ *
+ * ISO code of the language to fallback to when the specified language entries
+ * cannot be found.
+ *
+ * @var string
+ */
+ const FALLBACK_LANGUAGE = 'en';
+
+ /**
+ * @var array List of common language files
+ */
+ protected $common_language_files;
+
+ /**
+ * @var bool
+ */
+ protected $common_language_files_loaded;
+
+ /**
+ * @var string ISO code of the default board language
+ */
+ protected $default_language;
+
+ /**
+ * @var string ISO code of the User's language
+ */
+ protected $user_language;
+
+ /**
+ * @var array Language fallback array (the order is important)
+ */
+ protected $language_fallback;
+
+ /**
+ * @var array Array of language variables
+ */
+ protected $lang;
+
+ /**
+ * @var array Loaded language sets
+ */
+ protected $loaded_language_sets;
+
+ /**
+ * @var \phpbb\language\language_file_loader Language file loader
+ */
+ protected $loader;
+
+ /**
+ * Constructor
+ *
+ * @param \phpbb\language\language_file_loader $loader Language file loader
+ * @param array|null $common_modules Array of common language modules to load (optional)
+ */
+ public function __construct(language_file_loader $loader, $common_modules = null)
+ {
+ $this->loader = $loader;
+
+ // Set up default information
+ $this->user_language = false;
+ $this->default_language = false;
+ $this->lang = array(
+ // For BC with user::help array
+ '__help' => array(),
+ );
+ $this->loaded_language_sets = array(
+ 'core' => array(),
+ 'ext' => array(),
+ );
+
+ // Common language files
+ if (is_array($common_modules))
+ {
+ $this->common_language_files = $common_modules;
+ }
+ else
+ {
+ $this->common_language_files = array(
+ 'common',
+ );
+ }
+
+ $this->common_language_files_loaded = false;
+
+ $this->language_fallback = array(self::FALLBACK_LANGUAGE);
+ }
+
+ /**
+ * Function to set user's language to display.
+ *
+ * @param string $user_lang_iso ISO code of the User's language
+ */
+ public function set_user_language($user_lang_iso)
+ {
+ $this->user_language = $user_lang_iso;
+
+ $this->set_fallback_array();
+ }
+
+ /**
+ * Function to set the board's default language to display.
+ *
+ * @param string $default_lang_iso ISO code of the board's default language
+ */
+ public function set_default_language($default_lang_iso)
+ {
+ $this->default_language = $default_lang_iso;
+
+ $this->set_fallback_array();
+ }
+
+ /**
+ * Returns language array
+ *
+ * Note: This function is needed for the BC purposes, until \phpbb\user::lang[] is
+ * not removed.
+ *
+ * @return array Array of loaded language strings
+ */
+ public function get_lang_array()
+ {
+ // Load common language files if they not loaded yet
+ if (!$this->common_language_files_loaded)
+ {
+ $this->load_common_language_files();
+ }
+
+ return $this->lang;
+ }
+
+ /**
+ * Add Language Items
+ *
+ * Note: $use_help is assigned where needed (only use them to force inclusion).
+ *
+ * Examples:
+ * <code>
+ * $component = array('posting');
+ * $component = array('posting', 'viewtopic')
+ * $component = 'posting'
+ * </code>
+ *
+ * @param string|array $component The name of the language component to load
+ * @param string|null $extension_name Name of the extension to load component from, or null for core file
+ */
+ public function add_lang($component, $extension_name = null)
+ {
+ // Load common language files if they not loaded yet
+ // This needs to be here to correctly merge language arrays
+ if (!$this->common_language_files_loaded)
+ {
+ $this->load_common_language_files();
+ }
+
+ if (!is_array($component))
+ {
+ if (!is_null($extension_name))
+ {
+ $this->load_extension($extension_name, $component);
+ }
+ else
+ {
+ $this->load_core_file($component);
+ }
+ }
+ else
+ {
+ foreach ($component as $lang_file)
+ {
+ $this->add_lang($lang_file, $extension_name);
+ }
+ }
+ }
+
+ /**
+ * Advanced language substitution
+ *
+ * Function to mimic sprintf() with the possibility of using phpBB's language system to substitute nullar/singular/plural forms.
+ * Params are the language key and the parameters to be substituted.
+ * This function/functionality is inspired by SHS` and Ashe.
+ *
+ * Example call: <samp>$user->lang('NUM_POSTS_IN_QUEUE', 1);</samp>
+ *
+ * If the first parameter is an array, the elements are used as keys and subkeys to get the language entry:
+ * Example: <samp>$user->lang(array('datetime', 'AGO'), 1)</samp> uses $user->lang['datetime']['AGO'] as language entry.
+ *
+ * @return string Return localized string or the language key if the translation is not available
+ */
+ public function lang()
+ {
+ // Load common language files if they not loaded yet
+ if (!$this->common_language_files_loaded)
+ {
+ $this->load_common_language_files();
+ }
+
+ $args = func_get_args();
+ $key = $args[0];
+
+ if (is_array($key))
+ {
+ $lang = &$this->lang[array_shift($key)];
+
+ foreach ($key as $_key)
+ {
+ $lang = &$lang[$_key];
+ }
+ }
+ else
+ {
+ $lang = &$this->lang[$key];
+ }
+
+ // Return if language string does not exist
+ if (!isset($lang) || (!is_string($lang) && !is_array($lang)))
+ {
+ return $key;
+ }
+
+ // If the language entry is a string, we simply mimic sprintf() behaviour
+ if (is_string($lang))
+ {
+ if (sizeof($args) == 1)
+ {
+ return $lang;
+ }
+
+ // Replace key with language entry and simply pass along...
+ $args[0] = $lang;
+ return call_user_func_array('sprintf', $args);
+ }
+ else if (sizeof($lang) == 0)
+ {
+ // If the language entry is an empty array, we just return the language key
+ return $args[0];
+ }
+
+ // It is an array... now handle different nullar/singular/plural forms
+ $key_found = false;
+
+ // We now get the first number passed and will select the key based upon this number
+ for ($i = 1, $num_args = sizeof($args); $i < $num_args; $i++)
+ {
+ if (is_int($args[$i]) || is_float($args[$i]))
+ {
+ if ($args[$i] == 0 && isset($lang[0]))
+ {
+ // We allow each translation using plural forms to specify a version for the case of 0 things,
+ // so that "0 users" may be displayed as "No users".
+ $key_found = 0;
+ break;
+ }
+ else
+ {
+ $use_plural_form = $this->get_plural_form($args[$i]);
+ if (isset($lang[$use_plural_form]))
+ {
+ // The key we should use exists, so we use it.
+ $key_found = $use_plural_form;
+ }
+ else
+ {
+ // If the key we need to use does not exist, we fall back to the previous one.
+ $numbers = array_keys($lang);
+
+ foreach ($numbers as $num)
+ {
+ if ($num > $use_plural_form)
+ {
+ break;
+ }
+
+ $key_found = $num;
+ }
+ }
+ break;
+ }
+ }
+ }
+
+ // Ok, let's check if the key was found, else use the last entry (because it is mostly the plural form)
+ if ($key_found === false)
+ {
+ $numbers = array_keys($lang);
+ $key_found = end($numbers);
+ }
+
+ // Use the language string we determined and pass it to sprintf()
+ $args[0] = $lang[$key_found];
+ return call_user_func_array('sprintf', $args);
+ }
+
+ /**
+ * Loads common language files
+ */
+ protected function load_common_language_files()
+ {
+ if (!$this->common_language_files_loaded)
+ {
+ foreach ($this->common_language_files as $lang_file)
+ {
+ $this->load_core_file($lang_file);
+ }
+
+ $this->common_language_files_loaded = true;
+ }
+ }
+
+ /**
+ * Determine which plural form we should use.
+ *
+ * For some languages this is not as simple as for English.
+ *
+ * @param int|float $number The number we want to get the plural case for. Float numbers are floored.
+ * @param int|bool $force_rule False to use the plural rule of the language package
+ * or an integer to force a certain plural rule
+ *
+ * @return int The plural-case we need to use for the number plural-rule combination
+ *
+ * @throws \phpbb\language\exception\invalid_plural_rule_exception When $force_rule has an invalid value
+ */
+ public function get_plural_form($number, $force_rule = false)
+ {
+ $number = (int) $number;
+ $plural_rule = ($force_rule !== false) ? $force_rule : ((isset($this->lang['PLURAL_RULE'])) ? $this->lang['PLURAL_RULE'] : 1);
+
+ if ($plural_rule > 15 || $plural_rule < 0)
+ {
+ throw new invalid_plural_rule_exception('INVALID_PLURAL_RULE', array(
+ 'plural_rule' => $plural_rule,
+ ));
+ }
+
+ /**
+ * The following plural rules are based on a list published by the Mozilla Developer Network
+ * https://developer.mozilla.org/en/Localization_and_Plurals
+ */
+ switch ($plural_rule)
+ {
+ case 0:
+ /**
+ * Families: Asian (Chinese, Japanese, Korean, Vietnamese), Persian, Turkic/Altaic (Turkish), Thai, Lao
+ * 1 - everything: 0, 1, 2, ...
+ */
+ return 1;
+
+ case 1:
+ /**
+ * Families: Germanic (Danish, Dutch, English, Faroese, Frisian, German, Norwegian, Swedish), Finno-Ugric (Estonian, Finnish, Hungarian), Language isolate (Basque), Latin/Greek (Greek), Semitic (Hebrew), Romanic (Italian, Portuguese, Spanish, Catalan)
+ * 1 - 1
+ * 2 - everything else: 0, 2, 3, ...
+ */
+ return ($number === 1) ? 1 : 2;
+
+ case 2:
+ /**
+ * Families: Romanic (French, Brazilian Portuguese)
+ * 1 - 0, 1
+ * 2 - everything else: 2, 3, ...
+ */
+ return (($number === 0) || ($number === 1)) ? 1 : 2;
+
+ case 3:
+ /**
+ * Families: Baltic (Latvian)
+ * 1 - 0
+ * 2 - ends in 1, not 11: 1, 21, ... 101, 121, ...
+ * 3 - everything else: 2, 3, ... 10, 11, 12, ... 20, 22, ...
+ */
+ return ($number === 0) ? 1 : ((($number % 10 === 1) && ($number % 100 != 11)) ? 2 : 3);
+
+ case 4:
+ /**
+ * Families: Celtic (Scottish Gaelic)
+ * 1 - is 1 or 11: 1, 11
+ * 2 - is 2 or 12: 2, 12
+ * 3 - others between 3 and 19: 3, 4, ... 10, 13, ... 18, 19
+ * 4 - everything else: 0, 20, 21, ...
+ */
+ return ($number === 1 || $number === 11) ? 1 : (($number === 2 || $number === 12) ? 2 : (($number >= 3 && $number <= 19) ? 3 : 4));
+
+ case 5:
+ /**
+ * Families: Romanic (Romanian)
+ * 1 - 1
+ * 2 - is 0 or ends in 01-19: 0, 2, 3, ... 19, 101, 102, ... 119, 201, ...
+ * 3 - everything else: 20, 21, ...
+ */
+ return ($number === 1) ? 1 : ((($number === 0) || (($number % 100 > 0) && ($number % 100 < 20))) ? 2 : 3);
+
+ case 6:
+ /**
+ * Families: Baltic (Lithuanian)
+ * 1 - ends in 1, not 11: 1, 21, 31, ... 101, 121, ...
+ * 2 - ends in 0 or ends in 10-20: 0, 10, 11, 12, ... 19, 20, 30, 40, ...
+ * 3 - everything else: 2, 3, ... 8, 9, 22, 23, ... 29, 32, 33, ...
+ */
+ return (($number % 10 === 1) && ($number % 100 != 11)) ? 1 : ((($number % 10 < 2) || (($number % 100 >= 10) && ($number % 100 < 20))) ? 2 : 3);
+
+ case 7:
+ /**
+ * Families: Slavic (Croatian, Serbian, Russian, Ukrainian)
+ * 1 - ends in 1, not 11: 1, 21, 31, ... 101, 121, ...
+ * 2 - ends in 2-4, not 12-14: 2, 3, 4, 22, 23, 24, 32, ...
+ * 3 - everything else: 0, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 25, 26, ...
+ */
+ return (($number % 10 === 1) && ($number % 100 != 11)) ? 1 : ((($number % 10 >= 2) && ($number % 10 <= 4) && (($number % 100 < 10) || ($number % 100 >= 20))) ? 2 : 3);
+
+ case 8:
+ /**
+ * Families: Slavic (Slovak, Czech)
+ * 1 - 1
+ * 2 - 2, 3, 4
+ * 3 - everything else: 0, 5, 6, 7, ...
+ */
+ return ($number === 1) ? 1 : ((($number >= 2) && ($number <= 4)) ? 2 : 3);
+
+ case 9:
+ /**
+ * Families: Slavic (Polish)
+ * 1 - 1
+ * 2 - ends in 2-4, not 12-14: 2, 3, 4, 22, 23, 24, 32, ... 104, 122, ...
+ * 3 - everything else: 0, 5, 6, ... 11, 12, 13, 14, 15, ... 20, 21, 25, ...
+ */
+ return ($number === 1) ? 1 : ((($number % 10 >= 2) && ($number % 10 <= 4) && (($number % 100 < 12) || ($number % 100 > 14))) ? 2 : 3);
+
+ case 10:
+ /**
+ * Families: Slavic (Slovenian, Sorbian)
+ * 1 - ends in 01: 1, 101, 201, ...
+ * 2 - ends in 02: 2, 102, 202, ...
+ * 3 - ends in 03-04: 3, 4, 103, 104, 203, 204, ...
+ * 4 - everything else: 0, 5, 6, 7, 8, 9, 10, 11, ...
+ */
+ return ($number % 100 === 1) ? 1 : (($number % 100 === 2) ? 2 : ((($number % 100 === 3) || ($number % 100 === 4)) ? 3 : 4));
+
+ case 11:
+ /**
+ * Families: Celtic (Irish Gaeilge)
+ * 1 - 1
+ * 2 - 2
+ * 3 - is 3-6: 3, 4, 5, 6
+ * 4 - is 7-10: 7, 8, 9, 10
+ * 5 - everything else: 0, 11, 12, ...
+ */
+ return ($number === 1) ? 1 : (($number === 2) ? 2 : (($number >= 3 && $number <= 6) ? 3 : (($number >= 7 && $number <= 10) ? 4 : 5)));
+
+ case 12:
+ /**
+ * Families: Semitic (Arabic)
+ * 1 - 1
+ * 2 - 2
+ * 3 - ends in 03-10: 3, 4, ... 10, 103, 104, ... 110, 203, 204, ...
+ * 4 - ends in 11-99: 11, ... 99, 111, 112, ...
+ * 5 - everything else: 100, 101, 102, 200, 201, 202, ...
+ * 6 - 0
+ */
+ return ($number === 1) ? 1 : (($number === 2) ? 2 : ((($number % 100 >= 3) && ($number % 100 <= 10)) ? 3 : ((($number % 100 >= 11) && ($number % 100 <= 99)) ? 4 : (($number != 0) ? 5 : 6))));
+
+ case 13:
+ /**
+ * Families: Semitic (Maltese)
+ * 1 - 1
+ * 2 - is 0 or ends in 01-10: 0, 2, 3, ... 9, 10, 101, 102, ...
+ * 3 - ends in 11-19: 11, 12, ... 18, 19, 111, 112, ...
+ * 4 - everything else: 20, 21, ...
+ */
+ return ($number === 1) ? 1 : ((($number === 0) || (($number % 100 > 1) && ($number % 100 < 11))) ? 2 : ((($number % 100 > 10) && ($number % 100 < 20)) ? 3 : 4));
+
+ case 14:
+ /**
+ * Families: Slavic (Macedonian)
+ * 1 - ends in 1: 1, 11, 21, ...
+ * 2 - ends in 2: 2, 12, 22, ...
+ * 3 - everything else: 0, 3, 4, ... 10, 13, 14, ... 20, 23, ...
+ */
+ return ($number % 10 === 1) ? 1 : (($number % 10 === 2) ? 2 : 3);
+
+ case 15:
+ /**
+ * Families: Icelandic
+ * 1 - ends in 1, not 11: 1, 21, 31, ... 101, 121, 131, ...
+ * 2 - everything else: 0, 2, 3, ... 10, 11, 12, ... 20, 22, ...
+ */
+ return (($number % 10 === 1) && ($number % 100 != 11)) ? 1 : 2;
+ }
+ }
+
+ /**
+ * Returns language fallback data
+ *
+ * @return array
+ */
+ protected function set_fallback_array()
+ {
+ $fallback_array = array();
+
+ if ($this->user_language !== false)
+ {
+ $fallback_array[] = $this->user_language;
+ }
+
+ if ($this->default_language !== false)
+ {
+ $fallback_array[] = $this->default_language;
+ }
+
+ $fallback_array[] = self::FALLBACK_LANGUAGE;
+
+ $this->language_fallback = $fallback_array;
+ }
+
+ /**
+ * Load core language file
+ *
+ * @param string $component Name of the component to load
+ */
+ protected function load_core_file($component)
+ {
+ // Check if the component is already loaded
+ if (isset($this->loaded_language_sets['core'][$component]))
+ {
+ return;
+ }
+
+ $this->loader->load($component, $this->language_fallback, $this->lang);
+ $this->loaded_language_sets['core'][$component] = true;
+ }
+
+ /**
+ * Load extension language file
+ *
+ * @param string $extension_name Name of the extension to load language from
+ * @param string $component Name of the component to load
+ */
+ protected function load_extension($extension_name, $component)
+ {
+ // Check if the component is already loaded
+ if (isset($this->loaded_language_sets['ext'][$extension_name][$component]))
+ {
+ return;
+ }
+
+ $this->loader->load_extension($extension_name, $component, $this->language_fallback, $this->lang);
+ $this->loaded_language_sets['ext'][$extension_name][$component] = true;
+ }
+}
diff --git a/phpBB/phpbb/language/language_file_helper.php b/phpBB/phpbb/language/language_file_helper.php
new file mode 100644
index 0000000000..18d7b62e21
--- /dev/null
+++ b/phpBB/phpbb/language/language_file_helper.php
@@ -0,0 +1,71 @@
+<?php
+/**
+ *
+ * This file is part of the phpBB Forum Software package.
+ *
+ * @copyright (c) phpBB Limited <https://www.phpbb.com>
+ * @license GNU General Public License, version 2 (GPL-2.0)
+ *
+ * For full copyright and license information, please see
+ * the docs/CREDITS.txt file.
+ *
+ */
+
+namespace phpbb\language;
+
+use Symfony\Component\Finder\Finder;
+
+/**
+ * Helper class for language file related functions
+ */
+class language_file_helper
+{
+ /**
+ * @var string Path to phpBB's root
+ */
+ protected $phpbb_root_path;
+
+ /**
+ * Constructor
+ *
+ * @param string $phpbb_root_path Path to phpBB's root
+ */
+ public function __construct($phpbb_root_path)
+ {
+ $this->phpbb_root_path = $phpbb_root_path;
+ }
+
+ /**
+ * Returns available languages
+ *
+ * @return array
+ */
+ public function get_available_languages()
+ {
+ // Find available language packages
+ $finder = new Finder();
+ $finder->files()
+ ->name('iso.txt')
+ ->depth('== 1')
+ ->in($this->phpbb_root_path . 'language');
+
+ $available_languages = array();
+ foreach ($finder as $file)
+ {
+ $path = $file->getRelativePath();
+ $info = explode("\n", $file->getContents());
+
+ $available_languages[] = array(
+ // Get the name of the directory containing iso.txt
+ 'iso' => $path,
+
+ // Recover data from file
+ 'name' => trim($info[0]),
+ 'local_name' => trim($info[1]),
+ 'author' => trim($info[2])
+ );
+ }
+
+ return $available_languages;
+ }
+}
diff --git a/phpBB/phpbb/language/language_file_loader.php b/phpBB/phpbb/language/language_file_loader.php
new file mode 100644
index 0000000000..510a29279a
--- /dev/null
+++ b/phpBB/phpbb/language/language_file_loader.php
@@ -0,0 +1,212 @@
+<?php
+/**
+ *
+ * This file is part of the phpBB Forum Software package.
+ *
+ * @copyright (c) phpBB Limited <https://www.phpbb.com>
+ * @license GNU General Public License, version 2 (GPL-2.0)
+ *
+ * For full copyright and license information, please see
+ * the docs/CREDITS.txt file.
+ *
+ */
+
+namespace phpbb\language;
+
+use \phpbb\language\exception\language_file_not_found;
+
+/**
+ * Language file loader
+ */
+class language_file_loader
+{
+ /**
+ * @var string Path to phpBB's root
+ */
+ protected $phpbb_root_path;
+
+ /**
+ * @var string Extension of PHP files
+ */
+ protected $php_ext;
+
+ /**
+ * @var \phpbb\extension\manager Extension manager
+ */
+ protected $extension_manager;
+
+ /**
+ * Constructor
+ *
+ * @param string $phpbb_root_path Path to phpBB's root
+ * @param string $php_ext Extension of PHP files
+ */
+ public function __construct($phpbb_root_path, $php_ext)
+ {
+ $this->phpbb_root_path = $phpbb_root_path;
+ $this->php_ext = $php_ext;
+
+ $this->extension_manager = null;
+ }
+
+ /**
+ * Extension manager setter
+ *
+ * @param \phpbb\extension\manager $extension_manager Extension manager
+ */
+ public function set_extension_manager(\phpbb\extension\manager $extension_manager)
+ {
+ $this->extension_manager = $extension_manager;
+ }
+
+ /**
+ * Loads language array for the given component
+ *
+ * @param string $component Name of the language component
+ * @param string|array $locale ISO code of the language to load, or array of ISO codes if you want to
+ * specify additional language fallback steps
+ * @param array $lang Array reference containing language strings
+ */
+ public function load($component, $locale, &$lang)
+ {
+ $locale = (array) $locale;
+
+ // Determine path to language directory
+ $path = $this->phpbb_root_path . 'language/';
+
+ $this->load_file($path, $component, $locale, $lang);
+ }
+
+ /**
+ * Loads language array for the given extension component
+ *
+ * @param string $extension Name of the extension
+ * @param string $component Name of the language component
+ * @param string|array $locale ISO code of the language to load, or array of ISO codes if you want to
+ * specify additional language fallback steps
+ * @param array $lang Array reference containing language strings
+ */
+ public function load_extension($extension, $component, $locale, &$lang)
+ {
+ // Check if extension manager was loaded
+ if ($this->extension_manager === null)
+ {
+ // If not, let's return
+ return;
+ }
+
+ $locale = (array) $locale;
+
+ // Determine path to language directory
+ $path = $this->extension_manager->get_extension_path($extension, true) . 'language/';
+
+ $this->load_file($path, $component, $locale, $lang);
+ }
+
+ /**
+ * Prepares language file loading
+ *
+ * @param string $path Path to search for file in
+ * @param string $component Name of the language component
+ * @param array $locale Array containing language fallback options
+ * @param array $lang Array reference of language strings
+ */
+ protected function load_file($path, $component, $locale, &$lang)
+ {
+ // This is BC stuff and not the best idea as it makes language fallback
+ // implementation quite hard like below.
+ if (strpos($this->phpbb_root_path . $component, $path) === 0)
+ {
+ // Filter out the path
+ $path_diff = str_replace($path, '', dirname($this->phpbb_root_path . $component));
+ $language_file = basename($component, '.' . $this->php_ext);
+ $component = '';
+
+ // This step is needed to resolve language/en/subdir style $component
+ // $path already points to the language base directory so we need to eliminate
+ // the first directory from the path (that should be the language directory)
+ $path_diff_parts = explode('/', $path_diff);
+
+ if (sizeof($path_diff_parts) > 1)
+ {
+ array_shift($path_diff_parts);
+ $component = implode('/', $path_diff_parts) . '/';
+ }
+
+ $component .= $language_file;
+ }
+
+ // Determine filename
+ $filename = $component . '.' . $this->php_ext;
+
+ // Determine path to file
+ $file_path = $this->get_language_file_path($path, $filename, $locale);
+
+ // Load language array
+ $this->load_language_file($file_path, $lang);
+ }
+
+ /**
+ * This function implements language fallback logic
+ *
+ * @param string $path Path to language directory
+ * @param string $filename Filename to load language strings from
+ *
+ * @return string Relative path to language file
+ *
+ * @throws \phpbb\language\exception\language_file_not_exists When the path to the file cannot be resolved
+ */
+ protected function get_language_file_path($path, $filename, $locales)
+ {
+ // Language fallback logic
+ foreach ($locales as $locale)
+ {
+ $language_file_path = $path . $locale . '/' . $filename;
+
+ // If we are in install, try to use the updated version, when available
+ if (defined('IN_INSTALL'))
+ {
+ $install_language_path = str_replace('language/', 'install/update/new/language/', $language_file_path);
+ if (file_exists($install_language_path))
+ {
+ return $install_language_path;
+ }
+ }
+
+ if (file_exists($language_file_path))
+ {
+ return $language_file_path;
+ }
+ }
+
+ // The language file is not exist
+ throw new language_file_not_found('Language file ' . $language_file_path . ' couldn\'t be opened.');
+ }
+
+ /**
+ * Loads language file
+ *
+ * @param string $path Path to language file to load
+ * @param array $lang Reference of the array of language strings
+ */
+ protected function load_language_file($path, &$lang)
+ {
+ // BC code for language files with help
+ $help = array();
+
+ // Do not suppress error if in DEBUG mode
+ if (defined('DEBUG'))
+ {
+ include $path;
+ }
+ else
+ {
+ @include $path;
+ }
+
+ if (!empty($help))
+ {
+ $lang['__help'] = array_merge($lang['__help'], $help);
+ }
+ }
+}
diff --git a/phpBB/phpbb/log/null.php b/phpBB/phpbb/log/dummy.php
index baa78895ea..5c2d145e15 100644
--- a/phpBB/phpbb/log/null.php
+++ b/phpBB/phpbb/log/dummy.php
@@ -14,9 +14,9 @@
namespace phpbb\log;
/**
-* Null logger
+* Dummy logger
*/
-class null implements log_interface
+class dummy implements log_interface
{
/**
* {@inheritdoc}
diff --git a/phpBB/phpbb/log/log.php b/phpBB/phpbb/log/log.php
index 0c5205530b..4bb2e7a75a 100644
--- a/phpBB/phpbb/log/log.php
+++ b/phpBB/phpbb/log/log.php
@@ -27,7 +27,7 @@ class log implements \phpbb\log\log_interface
/**
* An array with the disabled log types. Logs of such types will not be
- * added when add_log() is called.
+ * added when add() is called.
* @var array
*/
protected $disabled_types;
@@ -223,14 +223,14 @@ class log implements \phpbb\log\log_interface
return false;
}
- if ($log_time == false)
+ if ($log_time === false)
{
$log_time = time();
}
$sql_ary = array(
- 'user_id' => $user_id,
- 'log_ip' => $log_ip,
+ 'user_id' => $user_id ? (int) $user_id : ANONYMOUS,
+ 'log_ip' => empty($log_ip) ? '' : $log_ip,
'log_time' => $log_time,
'log_operation' => $log_operation,
);
diff --git a/phpBB/phpbb/log/log_interface.php b/phpBB/phpbb/log/log_interface.php
index 5932f722aa..86286e6f88 100644
--- a/phpBB/phpbb/log/log_interface.php
+++ b/phpBB/phpbb/log/log_interface.php
@@ -32,8 +32,8 @@ interface log_interface
* Disable log
*
* This function allows disabling the log system or parts of it, for this
- * page call. When add_log is called and the type is disabled,
- * the log will not be added to the database.
+ * page call. When add() is called and the type is disabled, the log will
+ * not be added to the database.
*
* @param mixed $type The log type we want to disable. Empty to
* disable all logs. Can also be an array of types.
@@ -57,12 +57,12 @@ interface log_interface
/**
* Adds a log entry to the database
*
- * @param string $mode The mode defines which log_type is used and from which log the entry is retrieved
- * @param int $user_id User ID of the user
- * @param string $log_ip IP address of the user
- * @param string $log_operation Name of the operation
- * @param int $log_time Timestamp when the log entry was added, if empty time() will be used
- * @param array $additional_data More arguments can be added, depending on the log_type
+ * @param string $mode The mode defines which log_type is used and from which log the entry is retrieved
+ * @param int $user_id User ID of the user
+ * @param string $log_ip IP address of the user
+ * @param string $log_operation Name of the operation
+ * @param int|bool $log_time Timestamp when the log entry was added. If false, time() will be used
+ * @param array $additional_data More arguments can be added, depending on the log_type
*
* @return int|bool Returns the log_id, if the entry was added to the database, false otherwise.
*/
diff --git a/phpBB/phpbb/notification/exception.php b/phpBB/phpbb/notification/exception.php
index 83c4526df7..e416438061 100644
--- a/phpBB/phpbb/notification/exception.php
+++ b/phpBB/phpbb/notification/exception.php
@@ -17,10 +17,6 @@ namespace phpbb\notification;
* Notifications exception
*/
-class exception extends \Exception
+class exception extends \phpbb\exception\runtime_exception
{
- public function __toString()
- {
- return $this->getMessage();
- }
}
diff --git a/phpBB/phpbb/notification/manager.php b/phpBB/phpbb/notification/manager.php
index db92170dd8..38d7a13165 100644
--- a/phpBB/phpbb/notification/manager.php
+++ b/phpBB/phpbb/notification/manager.php
@@ -943,7 +943,7 @@ class manager
{
if (!isset($this->notification_types[$notification_type_name]) && !isset($this->notification_types['notification.type.' . $notification_type_name]))
{
- throw new \phpbb\notification\exception($this->user->lang('NOTIFICATION_TYPE_NOT_EXIST', $notification_type_name));
+ throw new \phpbb\notification\exception('NOTIFICATION_TYPE_NOT_EXIST', array($notification_type_name));
}
$sql = 'INSERT INTO ' . $this->notification_types_table . ' ' . $this->db->sql_build_array('INSERT', array(
diff --git a/phpBB/phpbb/notification/type/admin_activate_user.php b/phpBB/phpbb/notification/type/admin_activate_user.php
index dfc0157558..73ed612480 100644
--- a/phpBB/phpbb/notification/type/admin_activate_user.php
+++ b/phpBB/phpbb/notification/type/admin_activate_user.php
@@ -36,7 +36,7 @@ class admin_activate_user extends \phpbb\notification\type\base
/**
* {@inheritdoc}
*/
- public static $notification_option = array(
+ static public $notification_option = array(
'lang' => 'NOTIFICATION_TYPE_ADMIN_ACTIVATE_USER',
'group' => 'NOTIFICATION_GROUP_ADMINISTRATION',
);
@@ -52,7 +52,7 @@ class admin_activate_user extends \phpbb\notification\type\base
/**
* {@inheritdoc}
*/
- public static function get_item_id($user)
+ static public function get_item_id($user)
{
return (int) $user['user_id'];
}
@@ -60,7 +60,7 @@ class admin_activate_user extends \phpbb\notification\type\base
/**
* {@inheritdoc}
*/
- public static function get_item_parent_id($post)
+ static public function get_item_parent_id($post)
{
return 0;
}
diff --git a/phpBB/phpbb/notification/type/approve_post.php b/phpBB/phpbb/notification/type/approve_post.php
index a9e635b41a..e6c38b2ede 100644
--- a/phpBB/phpbb/notification/type/approve_post.php
+++ b/phpBB/phpbb/notification/type/approve_post.php
@@ -50,7 +50,7 @@ class approve_post extends \phpbb\notification\type\post
* @var bool|array False if the service should use it's default data
* Array of data (including keys 'id', 'lang', and 'group')
*/
- public static $notification_option = array(
+ static public $notification_option = array(
'id' => 'moderation_queue',
'lang' => 'NOTIFICATION_TYPE_MODERATION_QUEUE',
'group' => 'NOTIFICATION_GROUP_POSTING',
diff --git a/phpBB/phpbb/notification/type/approve_topic.php b/phpBB/phpbb/notification/type/approve_topic.php
index 2f4678359c..5f5b96f335 100644
--- a/phpBB/phpbb/notification/type/approve_topic.php
+++ b/phpBB/phpbb/notification/type/approve_topic.php
@@ -50,7 +50,7 @@ class approve_topic extends \phpbb\notification\type\topic
* @var bool|array False if the service should use it's default data
* Array of data (including keys 'id', 'lang', and 'group')
*/
- public static $notification_option = array(
+ static public $notification_option = array(
'id' => 'moderation_queue',
'lang' => 'NOTIFICATION_TYPE_MODERATION_QUEUE',
'group' => 'NOTIFICATION_GROUP_POSTING',
diff --git a/phpBB/phpbb/notification/type/base.php b/phpBB/phpbb/notification/type/base.php
index 4ead06071e..1cf0498138 100644
--- a/phpBB/phpbb/notification/type/base.php
+++ b/phpBB/phpbb/notification/type/base.php
@@ -63,7 +63,7 @@ abstract class base implements \phpbb\notification\type\type_interface
* @var bool|array False if the service should use its default data
* Array of data (including keys 'id', 'lang', and 'group')
*/
- public static $notification_option = false;
+ static public $notification_option = false;
/**
* The notification_type_id, set upon creation of the class
diff --git a/phpBB/phpbb/notification/type/bookmark.php b/phpBB/phpbb/notification/type/bookmark.php
index 4f2d34cb60..e6a695e875 100644
--- a/phpBB/phpbb/notification/type/bookmark.php
+++ b/phpBB/phpbb/notification/type/bookmark.php
@@ -43,7 +43,7 @@ class bookmark extends \phpbb\notification\type\post
* @var bool|array False if the service should use it's default data
* Array of data (including keys 'id', 'lang', and 'group')
*/
- public static $notification_option = array(
+ static public $notification_option = array(
'lang' => 'NOTIFICATION_TYPE_BOOKMARK',
'group' => 'NOTIFICATION_GROUP_POSTING',
);
diff --git a/phpBB/phpbb/notification/type/disapprove_post.php b/phpBB/phpbb/notification/type/disapprove_post.php
index 6c7bcbcaee..7021cdc837 100644
--- a/phpBB/phpbb/notification/type/disapprove_post.php
+++ b/phpBB/phpbb/notification/type/disapprove_post.php
@@ -60,7 +60,7 @@ class disapprove_post extends \phpbb\notification\type\approve_post
* @var bool|array False if the service should use it's default data
* Array of data (including keys 'id', 'lang', and 'group')
*/
- public static $notification_option = array(
+ static public $notification_option = array(
'id' => 'moderation_queue',
'lang' => 'NOTIFICATION_TYPE_MODERATION_QUEUE',
'group' => 'NOTIFICATION_GROUP_POSTING',
diff --git a/phpBB/phpbb/notification/type/disapprove_topic.php b/phpBB/phpbb/notification/type/disapprove_topic.php
index efa5eb7ecd..419cc5b2a6 100644
--- a/phpBB/phpbb/notification/type/disapprove_topic.php
+++ b/phpBB/phpbb/notification/type/disapprove_topic.php
@@ -60,7 +60,7 @@ class disapprove_topic extends \phpbb\notification\type\approve_topic
* @var bool|array False if the service should use it's default data
* Array of data (including keys 'id', 'lang', and 'group')
*/
- public static $notification_option = array(
+ static public $notification_option = array(
'id' => 'moderation_queue',
'lang' => 'NOTIFICATION_TYPE_MODERATION_QUEUE',
'group' => 'NOTIFICATION_GROUP_POSTING',
diff --git a/phpBB/phpbb/notification/type/group_request.php b/phpBB/phpbb/notification/type/group_request.php
index 4baf516fed..19665624df 100644
--- a/phpBB/phpbb/notification/type/group_request.php
+++ b/phpBB/phpbb/notification/type/group_request.php
@@ -26,7 +26,7 @@ class group_request extends \phpbb\notification\type\base
/**
* {@inheritdoc}
*/
- public static $notification_option = array(
+ static public $notification_option = array(
'lang' => 'NOTIFICATION_TYPE_GROUP_REQUEST',
);
@@ -50,7 +50,7 @@ class group_request extends \phpbb\notification\type\base
/**
* {@inheritdoc}
*/
- public static function get_item_id($group)
+ static public function get_item_id($group)
{
return (int) $group['user_id'];
}
@@ -58,7 +58,7 @@ class group_request extends \phpbb\notification\type\base
/**
* {@inheritdoc}
*/
- public static function get_item_parent_id($group)
+ static public function get_item_parent_id($group)
{
// Group id is the parent
return (int) $group['group_id'];
diff --git a/phpBB/phpbb/notification/type/group_request_approved.php b/phpBB/phpbb/notification/type/group_request_approved.php
index d284046ffa..f282cdd158 100644
--- a/phpBB/phpbb/notification/type/group_request_approved.php
+++ b/phpBB/phpbb/notification/type/group_request_approved.php
@@ -34,7 +34,7 @@ class group_request_approved extends \phpbb\notification\type\base
/**
* {@inheritdoc}
*/
- public static function get_item_id($group)
+ static public function get_item_id($group)
{
return (int) $group['group_id'];
}
@@ -42,7 +42,7 @@ class group_request_approved extends \phpbb\notification\type\base
/**
* {@inheritdoc}
*/
- public static function get_item_parent_id($group)
+ static public function get_item_parent_id($group)
{
return 0;
}
diff --git a/phpBB/phpbb/notification/type/pm.php b/phpBB/phpbb/notification/type/pm.php
index 330a70c85a..29b4b79216 100644
--- a/phpBB/phpbb/notification/type/pm.php
+++ b/phpBB/phpbb/notification/type/pm.php
@@ -36,7 +36,7 @@ class pm extends \phpbb\notification\type\base
* @var bool|array False if the service should use it's default data
* Array of data (including keys 'id', 'lang', and 'group')
*/
- public static $notification_option = array(
+ static public $notification_option = array(
'lang' => 'NOTIFICATION_TYPE_PM',
);
@@ -53,7 +53,7 @@ class pm extends \phpbb\notification\type\base
*
* @param array $pm The data from the private message
*/
- public static function get_item_id($pm)
+ static public function get_item_id($pm)
{
return (int) $pm['msg_id'];
}
@@ -63,7 +63,7 @@ class pm extends \phpbb\notification\type\base
*
* @param array $pm The data from the pm
*/
- public static function get_item_parent_id($pm)
+ static public function get_item_parent_id($pm)
{
// No parent
return 0;
diff --git a/phpBB/phpbb/notification/type/post.php b/phpBB/phpbb/notification/type/post.php
index 421eff6372..d6aa8a8af9 100644
--- a/phpBB/phpbb/notification/type/post.php
+++ b/phpBB/phpbb/notification/type/post.php
@@ -50,7 +50,7 @@ class post extends \phpbb\notification\type\base
* @var bool|array False if the service should use it's default data
* Array of data (including keys 'id', 'lang', and 'group')
*/
- public static $notification_option = array(
+ static public $notification_option = array(
'lang' => 'NOTIFICATION_TYPE_POST',
'group' => 'NOTIFICATION_GROUP_POSTING',
);
@@ -68,7 +68,7 @@ class post extends \phpbb\notification\type\base
*
* @param array $post The data from the post
*/
- public static function get_item_id($post)
+ static public function get_item_id($post)
{
return (int) $post['post_id'];
}
@@ -78,7 +78,7 @@ class post extends \phpbb\notification\type\base
*
* @param array $post The data from the post
*/
- public static function get_item_parent_id($post)
+ static public function get_item_parent_id($post)
{
return (int) $post['topic_id'];
}
diff --git a/phpBB/phpbb/notification/type/post_in_queue.php b/phpBB/phpbb/notification/type/post_in_queue.php
index 315b8b0243..e500ad33bc 100644
--- a/phpBB/phpbb/notification/type/post_in_queue.php
+++ b/phpBB/phpbb/notification/type/post_in_queue.php
@@ -43,7 +43,7 @@ class post_in_queue extends \phpbb\notification\type\post
* @var bool|array False if the service should use it's default data
* Array of data (including keys 'id', 'lang', and 'group')
*/
- public static $notification_option = array(
+ static public $notification_option = array(
'id' => 'notification.type.needs_approval',
'lang' => 'NOTIFICATION_TYPE_IN_MODERATION_QUEUE',
'group' => 'NOTIFICATION_GROUP_MODERATION',
diff --git a/phpBB/phpbb/notification/type/quote.php b/phpBB/phpbb/notification/type/quote.php
index 508ca92fa0..51edfec6f7 100644
--- a/phpBB/phpbb/notification/type/quote.php
+++ b/phpBB/phpbb/notification/type/quote.php
@@ -21,6 +21,11 @@ namespace phpbb\notification\type;
class quote extends \phpbb\notification\type\post
{
/**
+ * @var \phpbb\textformatter\utils_interface
+ */
+ protected $utils;
+
+ /**
* Get notification type name
*
* @return string
@@ -31,13 +36,6 @@ class quote extends \phpbb\notification\type\post
}
/**
- * regular expression to match to find usernames
- *
- * @var string
- */
- protected static $regular_expression_match = '#\[quote=&quot;(.+?)&quot;#';
-
- /**
* Language key used to output the text
*
* @var string
@@ -50,7 +48,7 @@ class quote extends \phpbb\notification\type\post
* @var bool|array False if the service should use it's default data
* Array of data (including keys 'id', 'lang', and 'group')
*/
- public static $notification_option = array(
+ static public $notification_option = array(
'lang' => 'NOTIFICATION_TYPE_QUOTE',
'group' => 'NOTIFICATION_GROUP_POSTING',
);
@@ -77,17 +75,16 @@ class quote extends \phpbb\notification\type\post
'ignore_users' => array(),
), $options);
- $usernames = false;
- preg_match_all(self::$regular_expression_match, $post['post_text'], $usernames);
+ $usernames = $this->utils->get_outermost_quote_authors($post['post_text']);
- if (empty($usernames[1]))
+ if (empty($usernames))
{
return array();
}
- $usernames[1] = array_unique($usernames[1]);
+ $usernames = array_unique($usernames);
- $usernames = array_map('utf8_clean_string', $usernames[1]);
+ $usernames = array_map('utf8_clean_string', $usernames);
$users = array();
@@ -187,4 +184,14 @@ class quote extends \phpbb\notification\type\post
'AUTHOR_NAME' => htmlspecialchars_decode($user_data['username']),
));
}
+
+ /**
+ * Set the utils service used to retrieve quote authors
+ *
+ * @param \phpbb\textformatter\utils_interface $utils
+ */
+ public function set_utils(\phpbb\textformatter\utils_interface $utils)
+ {
+ $this->utils = $utils;
+ }
}
diff --git a/phpBB/phpbb/notification/type/report_pm.php b/phpBB/phpbb/notification/type/report_pm.php
index d39143f4b7..1904680d5a 100644
--- a/phpBB/phpbb/notification/type/report_pm.php
+++ b/phpBB/phpbb/notification/type/report_pm.php
@@ -60,7 +60,7 @@ class report_pm extends \phpbb\notification\type\pm
* @var bool|array False if the service should use it's default data
* Array of data (including keys 'id', 'lang', and 'group')
*/
- public static $notification_option = array(
+ static public $notification_option = array(
'id' => 'notification.type.report',
'lang' => 'NOTIFICATION_TYPE_REPORT',
'group' => 'NOTIFICATION_GROUP_MODERATION',
@@ -71,7 +71,7 @@ class report_pm extends \phpbb\notification\type\pm
*
* @param array $pm The data from the pm
*/
- public static function get_item_parent_id($pm)
+ static public function get_item_parent_id($pm)
{
return (int) $pm['report_id'];
}
diff --git a/phpBB/phpbb/notification/type/report_post.php b/phpBB/phpbb/notification/type/report_post.php
index 027cca716b..b64862078a 100644
--- a/phpBB/phpbb/notification/type/report_post.php
+++ b/phpBB/phpbb/notification/type/report_post.php
@@ -66,7 +66,7 @@ class report_post extends \phpbb\notification\type\post_in_queue
* @var bool|array False if the service should use it's default data
* Array of data (including keys 'id' and 'lang')
*/
- public static $notification_option = array(
+ static public $notification_option = array(
'id' => 'notification.type.report',
'lang' => 'NOTIFICATION_TYPE_REPORT',
'group' => 'NOTIFICATION_GROUP_MODERATION',
diff --git a/phpBB/phpbb/notification/type/topic.php b/phpBB/phpbb/notification/type/topic.php
index 5f57087b73..a1a17535b5 100644
--- a/phpBB/phpbb/notification/type/topic.php
+++ b/phpBB/phpbb/notification/type/topic.php
@@ -50,7 +50,7 @@ class topic extends \phpbb\notification\type\base
* @var bool|array False if the service should use it's default data
* Array of data (including keys 'id', 'lang', and 'group')
*/
- public static $notification_option = array(
+ static public $notification_option = array(
'lang' => 'NOTIFICATION_TYPE_TOPIC',
'group' => 'NOTIFICATION_GROUP_POSTING',
);
@@ -68,7 +68,7 @@ class topic extends \phpbb\notification\type\base
*
* @param array $post The data from the post
*/
- public static function get_item_id($post)
+ static public function get_item_id($post)
{
return (int) $post['topic_id'];
}
@@ -78,7 +78,7 @@ class topic extends \phpbb\notification\type\base
*
* @param array $post The data from the post
*/
- public static function get_item_parent_id($post)
+ static public function get_item_parent_id($post)
{
return (int) $post['forum_id'];
}
diff --git a/phpBB/phpbb/notification/type/topic_in_queue.php b/phpBB/phpbb/notification/type/topic_in_queue.php
index 4c60c6b858..cfdf748d38 100644
--- a/phpBB/phpbb/notification/type/topic_in_queue.php
+++ b/phpBB/phpbb/notification/type/topic_in_queue.php
@@ -43,7 +43,7 @@ class topic_in_queue extends \phpbb\notification\type\topic
* @var bool|array False if the service should use it's default data
* Array of data (including keys 'id', 'lang', and 'group')
*/
- public static $notification_option = array(
+ static public $notification_option = array(
'id' => 'notification.type.needs_approval',
'lang' => 'NOTIFICATION_TYPE_IN_MODERATION_QUEUE',
'group' => 'NOTIFICATION_GROUP_MODERATION',
diff --git a/phpBB/phpbb/notification/type/type_interface.php b/phpBB/phpbb/notification/type/type_interface.php
index 5c5a110836..8844ce1a38 100644
--- a/phpBB/phpbb/notification/type/type_interface.php
+++ b/phpBB/phpbb/notification/type/type_interface.php
@@ -37,14 +37,14 @@ interface type_interface
*
* @param array $type_data The type specific data
*/
- public static function get_item_id($type_data);
+ static public function get_item_id($type_data);
/**
* Get the id of the parent
*
* @param array $type_data The type specific data
*/
- public static function get_item_parent_id($type_data);
+ static public function get_item_parent_id($type_data);
/**
* Is this type available to the current user (defines whether or not it will be shown in the UCP Edit notification options)
diff --git a/phpBB/phpbb/path_helper.php b/phpBB/phpbb/path_helper.php
index 5400c1c5a6..7b0d6f0fba 100644
--- a/phpBB/phpbb/path_helper.php
+++ b/phpBB/phpbb/path_helper.php
@@ -21,7 +21,7 @@ class path_helper
/** @var \phpbb\symfony_request */
protected $symfony_request;
- /** @var \phpbb\filesystem */
+ /** @var \phpbb\filesystem\filesystem_interface */
protected $filesystem;
/** @var \phpbb\request\request_interface */
@@ -43,13 +43,13 @@ class path_helper
* Constructor
*
* @param \phpbb\symfony_request $symfony_request
- * @param \phpbb\filesystem $filesystem
+ * @param \phpbb\filesystem\filesystem_interface $filesystem
* @param \phpbb\request\request_interface $request
* @param string $phpbb_root_path Relative path to phpBB root
* @param string $php_ext PHP file extension
* @param mixed $adm_relative_path Relative path admin path to adm/ root
*/
- public function __construct(\phpbb\symfony_request $symfony_request, \phpbb\filesystem $filesystem, \phpbb\request\request_interface $request, $phpbb_root_path, $php_ext, $adm_relative_path = null)
+ public function __construct(\phpbb\symfony_request $symfony_request, \phpbb\filesystem\filesystem_interface $filesystem, \phpbb\request\request_interface $request, $phpbb_root_path, $php_ext, $adm_relative_path = null)
{
$this->symfony_request = $symfony_request;
$this->filesystem = $filesystem;
diff --git a/phpBB/phpbb/profilefields/type/type_date.php b/phpBB/phpbb/profilefields/type/type_date.php
index 90ac9a6703..414484920b 100644
--- a/phpBB/phpbb/profilefields/type/type_date.php
+++ b/phpBB/phpbb/profilefields/type/type_date.php
@@ -72,7 +72,7 @@ class type_date extends type_base
'lang_options' => $field_data['lang_options'],
);
- $always_now = request_var('always_now', -1);
+ $always_now = $request->variable('always_now', -1);
if ($always_now == -1)
{
$s_checked = ($field_data['field_default_value'] == 'now') ? true : false;
diff --git a/phpBB/phpbb/report/controller/report.php b/phpBB/phpbb/report/controller/report.php
new file mode 100644
index 0000000000..f703d1cc60
--- /dev/null
+++ b/phpBB/phpbb/report/controller/report.php
@@ -0,0 +1,319 @@
+<?php
+/**
+ *
+ * This file is part of the phpBB Forum Software package.
+ *
+ * @copyright (c) phpBB Limited <https://www.phpbb.com>
+ * @license GNU General Public License, version 2 (GPL-2.0)
+ *
+ * For full copyright and license information, please see
+ * the docs/CREDITS.txt file.
+ *
+ */
+
+namespace phpbb\report\controller;
+
+use phpbb\exception\http_exception;
+use Symfony\Component\HttpFoundation\RedirectResponse;
+
+class report
+{
+ /**
+ * @var \phpbb\config\db
+ */
+ protected $config;
+
+ /**
+ * @var \phpbb\user
+ */
+ protected $user;
+
+ /**
+ * @var \phpbb\template\template
+ */
+ protected $template;
+
+ /**
+ * @var \phpbb\controller\helper
+ */
+ protected $helper;
+
+ /**
+ * @var \phpbb\request\request_interface
+ */
+ protected $request;
+
+ /**
+ * @var \phpbb\captcha\factory
+ */
+ protected $captcha_factory;
+
+ /**
+ * @var string
+ */
+ protected $phpbb_root_path;
+
+ /**
+ * @var string
+ */
+ protected $php_ext;
+
+ /**
+ * @var \phpbb\report\report_handler_interface
+ */
+ protected $report_handler;
+
+ /**
+ * @var \phpbb\report\report_reason_list_provider
+ */
+ protected $report_reason_provider;
+
+ public function __construct(\phpbb\config\db $config, \phpbb\user $user, \phpbb\template\template $template, \phpbb\controller\helper $helper, \phpbb\request\request_interface $request, \phpbb\captcha\factory $captcha_factory, \phpbb\report\handler_factory $report_factory, \phpbb\report\report_reason_list_provider $ui_provider, $phpbb_root_path, $php_ext)
+ {
+ $this->config = $config;
+ $this->user = $user;
+ $this->template = $template;
+ $this->helper = $helper;
+ $this->request = $request;
+ $this->phpbb_root_path = $phpbb_root_path;
+ $this->php_ext = $php_ext;
+ $this->captcha_factory = $captcha_factory;
+ $this->report_handler = $report_factory;
+
+ // User interface factory
+ $this->report_reason_provider = $ui_provider;
+ }
+
+ /**
+ * Controller for /path_to_entities/{id}/report routes
+ *
+ * Because of how phpBB organizes routes $mode must be set in the route config.
+ *
+ * @param int $id ID of the entity to report
+ * @param string $mode
+ * @return \Symfony\Component\HttpFoundation\Response a Symfony response object
+ * @throws \phpbb\exception\http_exception when $mode or $id is invalid for some reason
+ */
+ public function handle($id, $mode)
+ {
+ // Get report handler
+ $this->report_handler = $this->report_handler->get_instance($mode);
+
+ $this->user->add_lang('mcp');
+
+ $user_notify = ($this->user->data['is_registered']) ? $this->request->variable('notify', 0) : false;
+ $reason_id = $this->request->variable('reason_id', 0);
+ $report_text = $this->request->variable('report_text', '', true);
+
+ $submit = $this->request->variable('submit', '');
+ $cancel = $this->request->variable('cancel', '');
+
+ $error = array();
+ $s_hidden_fields = '';
+
+ $redirect_url = append_sid(
+ $this->phpbb_root_path . ( ($mode === 'pm') ? 'ucp' : 'viewtopic' ) . ".{$this->php_ext}",
+ ($mode == 'pm') ? "i=pm&mode=view&p=$id" : "p=$id"
+ );
+ $redirect_url .= ($mode === 'post') ? "#p$id" : '';
+
+ // Set up CAPTCHA if necessary
+ if ($this->config['enable_post_confirm'] && !$this->user->data['is_registered'])
+ {
+ $captcha = $this->captcha_factory->get_instance($this->config['captcha_plugin']);
+ $captcha->init(CONFIRM_REPORT);
+ }
+
+ //Has the report been cancelled?
+ if (!empty($cancel))
+ {
+ return new RedirectResponse($redirect_url, 302);
+ }
+
+ // Check CAPTCHA, if the form was submited
+ if (!empty($submit) && isset($captcha))
+ {
+ $captcha_template_array = $this->check_captcha($captcha);
+ $error = $captcha_template_array['error'];
+ $s_hidden_fields = $captcha_template_array['hidden_fields'];
+ }
+
+ // Handle request
+ try
+ {
+ if (!empty($submit) && sizeof($error) === 0)
+ {
+ $this->report_handler->add_report(
+ (int) $id,
+ (int) $reason_id,
+ (string) $report_text,
+ (int) $user_notify
+ );
+
+ // Send success message
+ switch ($mode)
+ {
+ case 'pm':
+ $lang_return = $this->user->lang['RETURN_PM'];
+ $lang_success = $this->user->lang['PM_REPORTED_SUCCESS'];
+ break;
+ case 'post':
+ $lang_return = $this->user->lang['RETURN_TOPIC'];
+ $lang_success = $this->user->lang['POST_REPORTED_SUCCESS'];
+ break;
+ }
+
+ $this->helper->assign_meta_refresh_var(3, $redirect_url);
+ $message = $lang_success . '<br /><br />' . sprintf($lang_return, '<a href="' . $redirect_url . '">', '</a>');
+ return $this->helper->message($message);
+ }
+ else
+ {
+ $this->report_handler->validate_report_request($id);
+ }
+ }
+ catch (\phpbb\report\exception\pm_reporting_disabled_exception $exception)
+ {
+ throw new http_exception(404, 'PAGE_NOT_FOUND');
+ }
+ catch (\phpbb\report\exception\already_reported_exception $exception)
+ {
+ switch ($mode)
+ {
+ case 'pm':
+ $message = $this->user->lang['ALREADY_REPORTED_PM'];
+ $message .= '<br /><br />' . sprintf($this->user->lang['RETURN_PM'], '<a href="' . $redirect_url . '">', '</a>');
+ break;
+ case 'post':
+ $message = $this->user->lang['ALREADY_REPORTED'];
+ $message .= '<br /><br />' . sprintf($this->user->lang['RETURN_TOPIC'], '<a href="' . $redirect_url . '">', '</a>');
+ break;
+ }
+
+ return $this->helper->message($message);
+ }
+ catch (\phpbb\report\exception\report_permission_denied_exception $exception)
+ {
+ $message = $exception->getMessage();
+ if (isset($this->user->lang[$message]))
+ {
+ $message = $this->user->lang[$message];
+ }
+
+ throw new http_exception(403, $message);
+ }
+ catch (\phpbb\report\exception\entity_not_found_exception $exception)
+ {
+ $message = $exception->getMessage();
+ if (isset($this->user->lang[$message]))
+ {
+ $message = $this->user->lang[$message];
+ }
+
+ throw new http_exception(404, $message);
+ }
+ catch (\phpbb\report\exception\empty_report_exception $exception)
+ {
+ $error[] = $this->user->lang['EMPTY_REPORT'];
+ }
+ catch (\phpbb\report\exception\invalid_report_exception $exception)
+ {
+ return $this->helper->message($exception->getMessage());
+ }
+
+ // Setting up an rendering template
+ $page_title = ($mode === 'pm') ? $this->user->lang['REPORT_MESSAGE'] : $this->user->lang['REPORT_POST'];
+ $this->assign_template_data(
+ $mode,
+ $id,
+ $reason_id,
+ $report_text,
+ $user_notify,
+ $error,
+ $s_hidden_fields,
+ ( isset($captcha) ? $captcha : false )
+ );
+
+ return $this->helper->render('report_body.html', $page_title);
+ }
+
+ /**
+ * Assigns template variables
+ *
+ * @param int $mode
+ * @param int $id
+ * @param int $reason_id
+ * @param string $report_text
+ * @param mixed $user_notify
+ * @param array $error
+ * @param string $s_hidden_fields
+ * @param mixed $captcha
+ * @return null
+ */
+ protected function assign_template_data($mode, $id, $reason_id, $report_text, $user_notify, $error = array(), $s_hidden_fields = '', $captcha = false)
+ {
+ if ($captcha !== false && $captcha->is_solved() === false)
+ {
+ $this->template->assign_vars(array(
+ 'S_CONFIRM_CODE' => true,
+ 'CAPTCHA_TEMPLATE' => $captcha->get_template(),
+ ));
+ }
+
+ $this->report_reason_provider->display_reasons($reason_id);
+
+ switch ($mode)
+ {
+ case 'pm':
+ $report_route = $this->helper->route('phpbb_report_pm_controller', array('id' => $id));
+ break;
+ case 'post':
+ $report_route = $this->helper->route('phpbb_report_post_controller', array('id' => $id));
+ break;
+ }
+
+ $this->template->assign_vars(array(
+ 'ERROR' => (sizeof($error) > 0) ? implode('<br />', $error) : '',
+ 'S_REPORT_POST' => ($mode === 'pm') ? false : true,
+ 'REPORT_TEXT' => $report_text,
+ 'S_HIDDEN_FIELDS' => (!empty($s_hidden_fields)) ? $s_hidden_fields : null,
+ 'S_REPORT_ACTION' => $report_route,
+
+ 'S_NOTIFY' => $user_notify,
+ 'S_CAN_NOTIFY' => ($this->user->data['is_registered']) ? true : false,
+ 'S_IN_REPORT' => true,
+ ));
+ }
+
+ /**
+ * Check CAPTCHA
+ *
+ * @param object $captcha A phpBB CAPTCHA object
+ * @return array template variables which ensures that CAPTCHA's work correctly
+ */
+ protected function check_captcha($captcha)
+ {
+ $error = array();
+ $captcha_hidden_fields = '';
+
+ $visual_confirmation_response = $captcha->validate();
+ if ($visual_confirmation_response)
+ {
+ $error[] = $visual_confirmation_response;
+ }
+
+ if (sizeof($error) === 0)
+ {
+ $captcha->reset();
+ }
+ else if ($captcha->is_solved() !== false)
+ {
+ $captcha_hidden_fields = build_hidden_fields($captcha->get_hidden_fields());
+ }
+
+ return array(
+ 'error' => $error,
+ 'hidden_fields' => $captcha_hidden_fields,
+ );
+ }
+}
diff --git a/phpBB/phpbb/report/exception/already_reported_exception.php b/phpBB/phpbb/report/exception/already_reported_exception.php
new file mode 100644
index 0000000000..54174044fe
--- /dev/null
+++ b/phpBB/phpbb/report/exception/already_reported_exception.php
@@ -0,0 +1,19 @@
+<?php
+/**
+ *
+ * This file is part of the phpBB Forum Software package.
+ *
+ * @copyright (c) phpBB Limited <https://www.phpbb.com>
+ * @license GNU General Public License, version 2 (GPL-2.0)
+ *
+ * For full copyright and license information, please see
+ * the docs/CREDITS.txt file.
+ *
+ */
+
+namespace phpbb\report\exception;
+
+class already_reported_exception extends invalid_report_exception
+{
+
+}
diff --git a/phpBB/phpbb/report/exception/empty_report_exception.php b/phpBB/phpbb/report/exception/empty_report_exception.php
new file mode 100644
index 0000000000..8c968dca80
--- /dev/null
+++ b/phpBB/phpbb/report/exception/empty_report_exception.php
@@ -0,0 +1,22 @@
+<?php
+/**
+ *
+ * This file is part of the phpBB Forum Software package.
+ *
+ * @copyright (c) phpBB Limited <https://www.phpbb.com>
+ * @license GNU General Public License, version 2 (GPL-2.0)
+ *
+ * For full copyright and license information, please see
+ * the docs/CREDITS.txt file.
+ *
+ */
+
+namespace phpbb\report\exception;
+
+class empty_report_exception extends invalid_report_exception
+{
+ public function __construct()
+ {
+ parent::__construct('EMPTY_REPORT');
+ }
+}
diff --git a/phpBB/phpbb/report/exception/entity_not_found_exception.php b/phpBB/phpbb/report/exception/entity_not_found_exception.php
new file mode 100644
index 0000000000..732aa58a13
--- /dev/null
+++ b/phpBB/phpbb/report/exception/entity_not_found_exception.php
@@ -0,0 +1,19 @@
+<?php
+/**
+ *
+ * This file is part of the phpBB Forum Software package.
+ *
+ * @copyright (c) phpBB Limited <https://www.phpbb.com>
+ * @license GNU General Public License, version 2 (GPL-2.0)
+ *
+ * For full copyright and license information, please see
+ * the docs/CREDITS.txt file.
+ *
+ */
+
+namespace phpbb\report\exception;
+
+class entity_not_found_exception extends invalid_report_exception
+{
+
+}
diff --git a/phpBB/phpbb/report/exception/factory_invalid_argument_exception.php b/phpBB/phpbb/report/exception/factory_invalid_argument_exception.php
new file mode 100644
index 0000000000..19de91eea3
--- /dev/null
+++ b/phpBB/phpbb/report/exception/factory_invalid_argument_exception.php
@@ -0,0 +1,21 @@
+<?php
+/**
+ *
+ * This file is part of the phpBB Forum Software package.
+ *
+ * @copyright (c) phpBB Limited <https://www.phpbb.com>
+ * @license GNU General Public License, version 2 (GPL-2.0)
+ *
+ * For full copyright and license information, please see
+ * the docs/CREDITS.txt file.
+ *
+ */
+
+namespace phpbb\report\exception;
+
+use \phpbb\exception\runtime_exception;
+
+class factory_invalid_argument_exception extends runtime_exception
+{
+
+}
diff --git a/phpBB/phpbb/report/exception/invalid_report_exception.php b/phpBB/phpbb/report/exception/invalid_report_exception.php
new file mode 100644
index 0000000000..03ff0a872d
--- /dev/null
+++ b/phpBB/phpbb/report/exception/invalid_report_exception.php
@@ -0,0 +1,21 @@
+<?php
+/**
+ *
+ * This file is part of the phpBB Forum Software package.
+ *
+ * @copyright (c) phpBB Limited <https://www.phpbb.com>
+ * @license GNU General Public License, version 2 (GPL-2.0)
+ *
+ * For full copyright and license information, please see
+ * the docs/CREDITS.txt file.
+ *
+ */
+
+namespace phpbb\report\exception;
+
+use \phpbb\exception\runtime_exception;
+
+class invalid_report_exception extends runtime_exception
+{
+
+}
diff --git a/phpBB/phpbb/report/exception/pm_reporting_disabled_exception.php b/phpBB/phpbb/report/exception/pm_reporting_disabled_exception.php
new file mode 100644
index 0000000000..2c8ab8cf84
--- /dev/null
+++ b/phpBB/phpbb/report/exception/pm_reporting_disabled_exception.php
@@ -0,0 +1,22 @@
+<?php
+/**
+ *
+ * This file is part of the phpBB Forum Software package.
+ *
+ * @copyright (c) phpBB Limited <https://www.phpbb.com>
+ * @license GNU General Public License, version 2 (GPL-2.0)
+ *
+ * For full copyright and license information, please see
+ * the docs/CREDITS.txt file.
+ *
+ */
+
+namespace phpbb\report\exception;
+
+class pm_reporting_disabled_exception extends invalid_report_exception
+{
+ public function __construct()
+ {
+
+ }
+}
diff --git a/phpBB/phpbb/report/exception/report_permission_denied_exception.php b/phpBB/phpbb/report/exception/report_permission_denied_exception.php
new file mode 100644
index 0000000000..c7069288b8
--- /dev/null
+++ b/phpBB/phpbb/report/exception/report_permission_denied_exception.php
@@ -0,0 +1,19 @@
+<?php
+/**
+ *
+ * This file is part of the phpBB Forum Software package.
+ *
+ * @copyright (c) phpBB Limited <https://www.phpbb.com>
+ * @license GNU General Public License, version 2 (GPL-2.0)
+ *
+ * For full copyright and license information, please see
+ * the docs/CREDITS.txt file.
+ *
+ */
+
+namespace phpbb\report\exception;
+
+class report_permission_denied_exception extends invalid_report_exception
+{
+
+}
diff --git a/phpBB/phpbb/report/handler_factory.php b/phpBB/phpbb/report/handler_factory.php
new file mode 100644
index 0000000000..ec229aac54
--- /dev/null
+++ b/phpBB/phpbb/report/handler_factory.php
@@ -0,0 +1,56 @@
+<?php
+/**
+ *
+ * This file is part of the phpBB Forum Software package.
+ *
+ * @copyright (c) phpBB Limited <https://www.phpbb.com>
+ * @license GNU General Public License, version 2 (GPL-2.0)
+ *
+ * For full copyright and license information, please see
+ * the docs/CREDITS.txt file.
+ *
+ */
+
+namespace phpbb\report;
+
+use phpbb\report\exception\factory_invalid_argument_exception;
+
+class handler_factory
+{
+ /**
+ * @var \Symfony\Component\DependencyInjection\ContainerInterface
+ */
+ protected $container;
+
+ /**
+ * Constructor
+ *
+ * @param \Symfony\Component\DependencyInjection\ContainerInterface $container
+ */
+ public function __construct(\Symfony\Component\DependencyInjection\ContainerInterface $container)
+ {
+ $this->container = $container;
+ }
+
+ /**
+ * Return a new instance of an appropriate report handler
+ *
+ * @param string $type
+ * @return \phpbb\report\report_handler_interface
+ * @throws \phpbb\report\exception\factory_invalid_argument_exception if $type is not valid
+ */
+ public function get_instance($type)
+ {
+ switch ($type)
+ {
+ case 'pm':
+ return $this->container->get('phpbb.report.handlers.report_handler_pm');
+ break;
+ case 'post':
+ return $this->container->get('phpbb.report.handlers.report_handler_post');
+ break;
+ }
+
+ throw new factory_invalid_argument_exception();
+ }
+}
diff --git a/phpBB/phpbb/report/report_handler.php b/phpBB/phpbb/report/report_handler.php
new file mode 100644
index 0000000000..126a206dbf
--- /dev/null
+++ b/phpBB/phpbb/report/report_handler.php
@@ -0,0 +1,104 @@
+<?php
+/**
+ *
+ * This file is part of the phpBB Forum Software package.
+ *
+ * @copyright (c) phpBB Limited <https://www.phpbb.com>
+ * @license GNU General Public License, version 2 (GPL-2.0)
+ *
+ * For full copyright and license information, please see
+ * the docs/CREDITS.txt file.
+ *
+ */
+
+namespace phpbb\report;
+
+abstract class report_handler implements report_handler_interface
+{
+ /**
+ * @var \phpbb\db\driver\driver_interface
+ */
+ protected $db;
+
+ /**
+ * @var \phpbb\event\dispatcher_interface
+ */
+ protected $dispatcher;
+
+ /**
+ * @var \phpbb\config\db
+ */
+ protected $config;
+
+ /**
+ * @var \phpbb\auth\auth
+ */
+ protected $auth;
+
+ /**
+ * @var \phpbb\user
+ */
+ protected $user;
+
+ /**
+ * @var \phpbb\notification\manager
+ */
+ protected $notifications;
+
+ /**
+ * @var array
+ */
+ protected $report_data;
+
+ /**
+ * Construtor
+ *
+ * @param \phpbb\db\driver\driver_interface $db
+ * @param \phpbb\event\dispatcher_interface $dispatcher
+ * @param \phpbb\config\db $config
+ * @param \phpbb\auth\auth $auth
+ * @param \phpbb\user $user
+ * @param \phpbb\notification\manager $notification
+ */
+ public function __construct(\phpbb\db\driver\driver_interface $db, \phpbb\event\dispatcher_interface $dispatcher, \phpbb\config\db $config, \phpbb\auth\auth $auth, \phpbb\user $user, \phpbb\notification\manager $notification)
+ {
+ $this->db = $db;
+ $this->dispatcher = $dispatcher;
+ $this->config = $config;
+ $this->auth = $auth;
+ $this->user = $user;
+ $this->notifications = $notification;
+ $this->report_data = array();
+ }
+
+ /**
+ * Creates a report entity in the database
+ *
+ * @param array $report_data
+ * @return int the ID of the created entity
+ */
+ protected function create_report(array $report_data)
+ {
+ $sql_ary = array(
+ 'reason_id' => (int) $report_data['reason_id'],
+ 'post_id' => $report_data['post_id'],
+ 'pm_id' => $report_data['pm_id'],
+ 'user_id' => (int) $this->user->data['user_id'],
+ 'user_notify' => (int) $report_data['user_notify'],
+ 'report_closed' => 0,
+ 'report_time' => (int) time(),
+ 'report_text' => (string) $report_data['report_text'],
+ 'reported_post_text' => $report_data['reported_post_text'],
+ 'reported_post_uid' => $report_data['reported_post_uid'],
+ 'reported_post_bitfield' => $report_data['reported_post_bitfield'],
+ 'reported_post_enable_bbcode' => $report_data['reported_post_enable_bbcode'],
+ 'reported_post_enable_smilies' => $report_data['reported_post_enable_smilies'],
+ 'reported_post_enable_magic_url' => $report_data['reported_post_enable_magic_url'],
+ );
+
+ $sql = 'INSERT INTO ' . REPORTS_TABLE . ' ' . $this->db->sql_build_array('INSERT', $sql_ary);
+ $this->db->sql_query($sql);
+
+ return $this->db->sql_nextid();
+ }
+}
diff --git a/phpBB/phpbb/report/report_handler_interface.php b/phpBB/phpbb/report/report_handler_interface.php
new file mode 100644
index 0000000000..8dafc392d0
--- /dev/null
+++ b/phpBB/phpbb/report/report_handler_interface.php
@@ -0,0 +1,43 @@
+<?php
+/**
+ *
+ * This file is part of the phpBB Forum Software package.
+ *
+ * @copyright (c) phpBB Limited <https://www.phpbb.com>
+ * @license GNU General Public License, version 2 (GPL-2.0)
+ *
+ * For full copyright and license information, please see
+ * the docs/CREDITS.txt file.
+ *
+ */
+
+namespace phpbb\report;
+
+interface report_handler_interface
+{
+ /**
+ * Reports a message
+ *
+ * @param int $id
+ * @param int $reason_id
+ * @param string $report_text
+ * @param int $user_notify
+ * @return null
+ * @throws \phpbb\report\exception\empty_report_exception when the given report is empty
+ * @throws \phpbb\report\exception\already_reported_exception when the entity is already reported
+ * @throws \phpbb\report\exception\entity_not_found_exception when the entity does not exist or the user does not have viewing permissions for it
+ * @throws \phpbb\report\exception\invalid_report_exception when the entity cannot be reported for some other reason
+ */
+ public function add_report($id, $reason_id, $report_text, $user_notify);
+
+ /**
+ * Checks if the message is reportable
+ *
+ * @param int $id
+ * @return null
+ * @throws \phpbb\report\exception\already_reported_exception when the entity is already reported
+ * @throws \phpbb\report\exception\entity_not_found_exception when the entity does not exist or the user does not have viewing permissions for it
+ * @throws \phpbb\report\exception\invalid_report_exception when the entity cannot be reported for some other reason
+ */
+ public function validate_report_request($id);
+}
diff --git a/phpBB/phpbb/report/report_handler_pm.php b/phpBB/phpbb/report/report_handler_pm.php
new file mode 100644
index 0000000000..2f2a697efc
--- /dev/null
+++ b/phpBB/phpbb/report/report_handler_pm.php
@@ -0,0 +1,137 @@
+<?php
+/**
+ *
+ * This file is part of the phpBB Forum Software package.
+ *
+ * @copyright (c) phpBB Limited <https://www.phpbb.com>
+ * @license GNU General Public License, version 2 (GPL-2.0)
+ *
+ * For full copyright and license information, please see
+ * the docs/CREDITS.txt file.
+ *
+ */
+
+namespace phpbb\report;
+
+use phpbb\report\exception\empty_report_exception;
+use phpbb\report\exception\already_reported_exception;
+use phpbb\report\exception\pm_reporting_disabled_exception;
+use phpbb\report\exception\entity_not_found_exception;
+
+class report_handler_pm extends report_handler
+{
+ /**
+ * {@inheritdoc}
+ * @throws \phpbb\report\exception\pm_reporting_disabled_exception when PM reporting is disabled on the board
+ */
+ public function add_report($id, $reason_id, $report_text, $user_notify)
+ {
+ // Cast the input variables
+ $id = (int) $id;
+ $reason_id = (int) $reason_id;
+ $report_text = (string) $report_text;
+ $user_notify = (int) $user_notify;
+
+ $this->validate_report_request($id);
+
+ $sql = 'SELECT *
+ FROM ' . REPORTS_REASONS_TABLE . "
+ WHERE reason_id = $reason_id";
+ $result = $this->db->sql_query($sql);
+ $row = $this->db->sql_fetchrow($result);
+ $this->db->sql_freeresult($result);
+
+ if (!$row || (empty($report_text) && strtolower($row['reason_title']) === 'other'))
+ {
+ throw new empty_report_exception();
+ }
+
+ $report_data = array(
+ 'reason_id' => $reason_id,
+ 'post_id' => 0,
+ 'pm_id' => $id,
+ 'user_notify' => $user_notify,
+ 'report_text' => $report_text,
+ 'reported_post_text' => $this->report_data['message_text'],
+ 'reported_post_uid' => $this->report_data['bbcode_bitfield'],
+ 'reported_post_bitfield' => $this->report_data['bbcode_uid'],
+ 'reported_post_enable_bbcode' => $this->report_data['enable_bbcode'],
+ 'reported_post_enable_smilies' => $this->report_data['enable_smilies'],
+ 'reported_post_enable_magic_url' => $this->report_data['enable_magic_url'],
+ );
+
+ $report_id = $this->create_report($report_data);
+
+ $sql = 'UPDATE ' . PRIVMSGS_TABLE . '
+ SET message_reported = 1
+ WHERE msg_id = ' . $id;
+ $this->db->sql_query($sql);
+
+ $sql_ary = array(
+ 'msg_id' => $id,
+ 'user_id' => ANONYMOUS,
+ 'author_id' => (int) $this->report_data['author_id'],
+ 'pm_deleted' => 0,
+ 'pm_new' => 0,
+ 'pm_unread' => 0,
+ 'pm_replied' => 0,
+ 'pm_marked' => 0,
+ 'pm_forwarded' => 0,
+ 'folder_id' => PRIVMSGS_INBOX,
+ );
+
+ $sql = 'INSERT INTO ' . PRIVMSGS_TO_TABLE . ' ' . $this->db->sql_build_array('INSERT', $sql_ary);
+ $this->db->sql_query($sql);
+
+ $this->notifications->add_notifications('notification.type.report_pm', array_merge($this->report_data, $row, array(
+ 'report_text' => $report_text,
+ 'from_user_id' => $this->report_data['author_id'],
+ 'report_id' => $report_id,
+ )));
+ }
+
+ /**
+ * {@inheritdoc}
+ * @throws \phpbb\report\exception\pm_reporting_disabled_exception when PM reporting is disabled on the board
+ */
+ public function validate_report_request($id)
+ {
+ $id = (int) $id;
+
+ // Check if reporting PMs is enabled
+ if (!$this->config['allow_pm_report'])
+ {
+ throw new pm_reporting_disabled_exception();
+ }
+ else if ($id <= 0)
+ {
+ throw new entity_not_found_exception('NO_POST_SELECTED');
+ }
+
+ // Grab all relevant data
+ $sql = 'SELECT p.*, pt.*
+ FROM ' . PRIVMSGS_TABLE . ' p, ' . PRIVMSGS_TO_TABLE . " pt
+ WHERE p.msg_id = $id
+ AND p.msg_id = pt.msg_id
+ AND (p.author_id = " . $this->user->data['user_id'] . "
+ OR pt.user_id = " . $this->user->data['user_id'] . ")";
+ $result = $this->db->sql_query($sql);
+ $report_data = $this->db->sql_fetchrow($result);
+ $this->db->sql_freeresult($result);
+
+ // Check if message exists
+ if (!$report_data)
+ {
+ $this->user->add_lang('ucp');
+ throw new entity_not_found_exception('NO_MESSAGE');
+ }
+
+ // Check if message is already reported
+ if ($report_data['message_reported'])
+ {
+ throw new already_reported_exception();
+ }
+
+ $this->report_data = $report_data;
+ }
+}
diff --git a/phpBB/phpbb/report/report_handler_post.php b/phpBB/phpbb/report/report_handler_post.php
new file mode 100644
index 0000000000..ce4ed67d27
--- /dev/null
+++ b/phpBB/phpbb/report/report_handler_post.php
@@ -0,0 +1,175 @@
+<?php
+/**
+ *
+ * This file is part of the phpBB Forum Software package.
+ *
+ * @copyright (c) phpBB Limited <https://www.phpbb.com>
+ * @license GNU General Public License, version 2 (GPL-2.0)
+ *
+ * For full copyright and license information, please see
+ * the docs/CREDITS.txt file.
+ *
+ */
+
+namespace phpbb\report;
+
+use phpbb\report\exception\invalid_report_exception;
+use phpbb\report\exception\empty_report_exception;
+use phpbb\report\exception\already_reported_exception;
+use phpbb\report\exception\entity_not_found_exception;
+use phpbb\report\exception\report_permission_denied_exception;
+
+class report_handler_post extends report_handler
+{
+ /**
+ * @var array
+ */
+ protected $forum_data;
+
+ /**
+ * {@inheritdoc}
+ * @throws \phpbb\report\exception\report_permission_denied_exception when the user does not have permission to report the post
+ */
+ public function add_report($id, $reason_id, $report_text, $user_notify)
+ {
+ // Cast the input variables
+ $id = (int) $id;
+ $reason_id = (int) $reason_id;
+ $report_text = (string) $report_text;
+ $user_notify = (int) $user_notify;
+
+ $this->validate_report_request($id);
+
+ $sql = 'SELECT *
+ FROM ' . REPORTS_REASONS_TABLE . "
+ WHERE reason_id = $reason_id";
+ $result = $this->db->sql_query($sql);
+ $row = $this->db->sql_fetchrow($result);
+ $this->db->sql_freeresult($result);
+
+ if (!$row || (empty($report_text) && strtolower($row['reason_title']) === 'other'))
+ {
+ throw new empty_report_exception();
+ }
+
+ $report_data = array(
+ 'reason_id' => $reason_id,
+ 'post_id' => $id,
+ 'pm_id' => 0,
+ 'user_notify' => $user_notify,
+ 'report_text' => $report_text,
+ 'reported_post_text' => $this->report_data['post_text'],
+ 'reported_post_uid' => $this->report_data['bbcode_bitfield'],
+ 'reported_post_bitfield' => $this->report_data['bbcode_uid'],
+ 'reported_post_enable_bbcode' => $this->report_data['enable_bbcode'],
+ 'reported_post_enable_smilies' => $this->report_data['enable_smilies'],
+ 'reported_post_enable_magic_url' => $this->report_data['enable_magic_url'],
+ );
+
+ $report_id = $this->create_report($report_data);
+
+ $sql = 'UPDATE ' . POSTS_TABLE . '
+ SET post_reported = 1
+ WHERE post_id = ' . $id;
+ $this->db->sql_query($sql);
+
+ if (!$this->report_data['topic_reported'])
+ {
+ $sql = 'UPDATE ' . TOPICS_TABLE . '
+ SET topic_reported = 1
+ WHERE topic_id = ' . $this->report_data['topic_id'] . '
+ OR topic_moved_id = ' . $this->report_data['topic_id'];
+ $this->db->sql_query($sql);
+ }
+
+ $this->notifications->add_notifications('notification.type.report_post', array_merge($this->report_data, $row, $this->forum_data, array(
+ 'report_text' => $report_text,
+ )));
+ }
+
+ /**
+ * {@inheritdoc}
+ * @throws \phpbb\report\exception\report_permission_denied_exception when the user does not have permission to report the post
+ */
+ public function validate_report_request($id)
+ {
+ $id = (int) $id;
+
+ // Check if id is valid
+ if ($id <= 0)
+ {
+ throw new entity_not_found_exception('NO_POST_SELECTED');
+ }
+
+ // Grab all relevant data
+ $sql = 'SELECT t.*, p.*
+ FROM ' . POSTS_TABLE . ' p, ' . TOPICS_TABLE . " t
+ WHERE p.post_id = $id
+ AND p.topic_id = t.topic_id";
+ $result = $this->db->sql_query($sql);
+ $report_data = $this->db->sql_fetchrow($result);
+ $this->db->sql_freeresult($result);
+
+ if (!$report_data)
+ {
+ throw new entity_not_found_exception('POST_NOT_EXIST');
+ }
+
+ $forum_id = (int) $report_data['forum_id'];
+
+ $sql = 'SELECT *
+ FROM ' . FORUMS_TABLE . '
+ WHERE forum_id = ' . $forum_id;
+ $result = $this->db->sql_query($sql);
+ $forum_data = $this->db->sql_fetchrow($result);
+ $this->db->sql_freeresult($result);
+
+ if (!$forum_data)
+ {
+ throw new invalid_report_exception('FORUM_NOT_EXIST');
+ }
+
+ $acl_check_ary = array(
+ 'f_list' => 'POST_NOT_EXIST',
+ 'f_read' => 'USER_CANNOT_READ',
+ 'f_report' => 'USER_CANNOT_REPORT'
+ );
+
+ /**
+ * This event allows you to do extra auth checks and verify if the user
+ * has the required permissions
+ *
+ * @event core.report_post_auth
+ * @var array forum_data All data available from the forums table on this post's forum
+ * @var array report_data All data available from the topics and the posts tables on this post (and its topic)
+ * @var array acl_check_ary An array with the ACL to be tested. The evaluation is made in the same order as the array is sorted
+ * The key is the ACL name and the value is the language key for the error message.
+ * @since 3.1.3-RC1
+ */
+ $vars = array(
+ 'forum_data',
+ 'report_data',
+ 'acl_check_ary',
+ );
+ extract($this->dispatcher->trigger_event('core.report_post_auth', compact($vars)));
+
+ $this->auth->acl($this->user->data);
+
+ foreach ($acl_check_ary as $acl => $error)
+ {
+ if (!$this->auth->acl_get($acl, $forum_id))
+ {
+ throw new report_permission_denied_exception($error);
+ }
+ }
+ unset($acl_check_ary);
+
+ if ($report_data['post_reported'])
+ {
+ throw new already_reported_exception();
+ }
+
+ $this->report_data = $report_data;
+ $this->forum_data = $forum_data;
+ }
+}
diff --git a/phpBB/phpbb/report/report_reason_list_provider.php b/phpBB/phpbb/report/report_reason_list_provider.php
new file mode 100644
index 0000000000..388a61d577
--- /dev/null
+++ b/phpBB/phpbb/report/report_reason_list_provider.php
@@ -0,0 +1,78 @@
+<?php
+/**
+ *
+ * This file is part of the phpBB Forum Software package.
+ *
+ * @copyright (c) phpBB Limited <https://www.phpbb.com>
+ * @license GNU General Public License, version 2 (GPL-2.0)
+ *
+ * For full copyright and license information, please see
+ * the docs/CREDITS.txt file.
+ *
+ */
+
+namespace phpbb\report;
+
+class report_reason_list_provider
+{
+ /**
+ * @var \phpbb\db\driver\driver_interface
+ */
+ protected $db;
+
+ /**
+ * @var \phpbb\template\template
+ */
+ protected $template;
+
+ /**
+ * @var \phpbb\user
+ */
+ protected $user;
+
+ /**
+ * Constructor
+ *
+ * @param \phpbb\db\driver\driver_interface $db
+ * @param \phpbb\template\template $template
+ * @param \phpbb\user $user
+ */
+ public function __construct(\phpbb\db\driver\driver_interface $db, \phpbb\template\template $template, \phpbb\user $user)
+ {
+ $this->db = $db;
+ $this->template = $template;
+ $this->user = $user;
+ }
+
+ /**
+ * Sets template variables to render report reasons select HTML input
+ *
+ * @param int $reason_id
+ * @return null
+ */
+ public function display_reasons($reason_id = 0)
+ {
+ $sql = 'SELECT *
+ FROM ' . REPORTS_REASONS_TABLE . '
+ ORDER BY reason_order ASC';
+ $result = $this->db->sql_query($sql);
+
+ while ($row = $this->db->sql_fetchrow($result))
+ {
+ // If the reason is defined within the language file, we will use the localized version, else just use the database entry...
+ if (isset($this->user->lang['report_reasons']['TITLE'][strtoupper($row['reason_title'])]) && isset($this->user->lang['report_reasons']['DESCRIPTION'][strtoupper($row['reason_title'])]))
+ {
+ $row['reason_description'] = $this->user->lang['report_reasons']['DESCRIPTION'][strtoupper($row['reason_title'])];
+ $row['reason_title'] = $this->user->lang['report_reasons']['TITLE'][strtoupper($row['reason_title'])];
+ }
+
+ $this->template->assign_block_vars('reason', array(
+ 'ID' => $row['reason_id'],
+ 'TITLE' => $row['reason_title'],
+ 'DESCRIPTION' => $row['reason_description'],
+ 'S_SELECTED' => ($row['reason_id'] == $reason_id) ? true : false,
+ ));
+ }
+ $this->db->sql_freeresult($result);
+ }
+}
diff --git a/phpBB/phpbb/request/deactivated_super_global.php b/phpBB/phpbb/request/deactivated_super_global.php
index b6cad59be4..ab56240b14 100644
--- a/phpBB/phpbb/request/deactivated_super_global.php
+++ b/phpBB/phpbb/request/deactivated_super_global.php
@@ -56,7 +56,7 @@ class deactivated_super_global implements \ArrayAccess, \Countable, \IteratorAgg
$file = '';
$line = 0;
- $message = 'Illegal use of $' . $this->name . '. You must use the request class or request_var() to access input data. Found in %s on line %d. This error message was generated by deactivated_super_global.';
+ $message = 'Illegal use of $' . $this->name . '. You must use the request class to access input data. Found in %s on line %d. This error message was generated by deactivated_super_global.';
$backtrace = debug_backtrace();
if (isset($backtrace[1]))
diff --git a/phpBB/phpbb/routing/router.php b/phpBB/phpbb/routing/router.php
new file mode 100644
index 0000000000..2f89d4e884
--- /dev/null
+++ b/phpBB/phpbb/routing/router.php
@@ -0,0 +1,343 @@
+<?php
+/**
+*
+* This file is part of the phpBB Forum Software package.
+*
+* @copyright (c) phpBB Limited <https://www.phpbb.com>
+* @license GNU General Public License, version 2 (GPL-2.0)
+*
+* For full copyright and license information, please see
+* the docs/CREDITS.txt file.
+*
+*/
+
+namespace phpbb\routing;
+
+use Symfony\Component\Config\ConfigCache;
+use Symfony\Component\Filesystem\Exception\IOException;
+use Symfony\Component\Routing\Matcher\Dumper\PhpMatcherDumper;
+use Symfony\Component\Routing\Generator\Dumper\PhpGeneratorDumper;
+use Symfony\Component\Routing\Matcher\UrlMatcher;
+use Symfony\Component\Routing\Generator\UrlGenerator;
+use Symfony\Component\Routing\RequestContext;
+use Symfony\Component\Routing\RouteCollection;
+use Symfony\Component\Routing\RouterInterface;
+use Symfony\Component\Routing\Loader\YamlFileLoader;
+use Symfony\Component\Config\FileLocator;
+use phpbb\extension\manager;
+
+/**
+ * Integration of all pieces of the routing system for easier use.
+ */
+class router implements RouterInterface
+{
+ /**
+ * Extension manager
+ *
+ * @var manager
+ */
+ protected $extension_manager;
+
+ /**
+ * phpBB root path
+ *
+ * @var string
+ */
+ protected $phpbb_root_path;
+
+ /**
+ * PHP file extensions
+ *
+ * @var string
+ */
+ protected $php_ext;
+
+ /**
+ * Name of the current environment
+ *
+ * @var string
+ */
+ protected $environment;
+
+ /**
+ * YAML file(s) containing route information
+ *
+ * @var array
+ */
+ protected $routing_files;
+
+ /**
+ * @var \Symfony\Component\Routing\Matcher\UrlMatcherInterface|null
+ */
+ protected $matcher;
+
+ /**
+ * @var \Symfony\Component\Routing\Generator\UrlGeneratorInterface|null
+ */
+ protected $generator;
+
+ /**
+ * @var RequestContext
+ */
+ protected $context;
+
+ /**
+ * @var RouteCollection|null
+ */
+ protected $route_collection;
+
+ /**
+ * @var \phpbb\filesystem\filesystem_interface
+ */
+ protected $filesystem;
+
+ /**
+ * Construct method
+ *
+ * @param \phpbb\filesystem\filesystem_interface $filesystem Filesystem helper
+ * @param string $phpbb_root_path phpBB root path
+ * @param string $php_ext PHP file extension
+ * @param string $environment Name of the current environment
+ * @param manager|null $extension_manager Extension manager
+ * @param array $routing_files Array of strings containing paths to YAML files
+ * holding route information
+ */
+ public function __construct(\phpbb\filesystem\filesystem_interface $filesystem, $phpbb_root_path, $php_ext, $environment, manager $extension_manager = null, $routing_files = array())
+ {
+ $this->filesystem = $filesystem;
+ $this->extension_manager = $extension_manager;
+ $this->routing_files = $routing_files;
+ $this->phpbb_root_path = $phpbb_root_path;
+ $this->php_ext = $php_ext;
+ $this->environment = $environment;
+ $this->context = new RequestContext();
+ }
+
+ /**
+ * Find the list of routing files
+ *
+ * @param array $paths Array of paths where to look for routing files (they must be relative to the phpBB root path).
+ * @return router
+ */
+ public function find_routing_files(array $paths)
+ {
+ $this->routing_files = array('config/' . $this->environment . '/routing/environment.yml');
+ foreach ($paths as $path)
+ {
+ if (file_exists($this->phpbb_root_path . $path . 'config/' . $this->environment . '/routing/environment.yml'))
+ {
+ $this->routing_files[] = $path . 'config/' . $this->environment . '/routing/environment.yml';
+ }
+ else if (!is_dir($this->phpbb_root_path . $path . 'config/' . $this->environment))
+ {
+ if (file_exists($this->phpbb_root_path . $path . 'config/default/routing/environment.yml'))
+ {
+ $this->routing_files[] = $path . 'config/default/routing/environment.yml';
+ }
+ else if (!is_dir($this->phpbb_root_path . $path . 'config/default/routing') && file_exists($this->phpbb_root_path . $path . 'config/routing.yml'))
+ {
+ $this->routing_files[] = $path . 'config/routing.yml';
+ }
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Find a list of controllers
+ *
+ * @param string $base_path Base path to prepend to file paths
+ * @return router
+ */
+ public function find($base_path = '')
+ {
+ if ($this->route_collection === null || $this->route_collection->count() === 0)
+ {
+ $this->route_collection = new RouteCollection;
+ foreach ($this->routing_files as $file_path)
+ {
+ $loader = new YamlFileLoader(new FileLocator($this->filesystem->realpath($base_path)));
+ $this->route_collection->addCollection($loader->load($file_path));
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get the list of routes
+ *
+ * @return RouteCollection Get the route collection
+ */
+ public function get_routes()
+ {
+ if ($this->route_collection == null || empty($this->routing_files))
+ {
+ $this->find_routing_files(
+ ($this->extension_manager !== null) ? $this->extension_manager->all_enabled(false) : array()
+ )
+ ->find($this->phpbb_root_path);
+ }
+
+ return $this->route_collection;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getRouteCollection()
+ {
+ return $this->get_routes();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setContext(RequestContext $context)
+ {
+ $this->context = $context;
+
+ if ($this->matcher !== null)
+ {
+ $this->get_matcher()->setContext($context);
+ }
+ if ($this->generator !== null)
+ {
+ $this->get_generator()->setContext($context);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getContext()
+ {
+ return $this->context;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function generate($name, $parameters = array(), $referenceType = self::ABSOLUTE_PATH)
+ {
+ return $this->get_generator()->generate($name, $parameters, $referenceType);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function match($pathinfo)
+ {
+ return $this->get_matcher()->match($pathinfo);
+ }
+
+ /**
+ * Gets the UrlMatcher instance associated with this Router.
+ *
+ * @return \Symfony\Component\Routing\Matcher\UrlMatcherInterface A UrlMatcherInterface instance
+ */
+ public function get_matcher()
+ {
+ if ($this->matcher !== null)
+ {
+ return $this->matcher;
+ }
+
+ $this->create_dumped_url_matcher();
+
+ return $this->matcher;
+ }
+ /**
+ * Creates a new dumped URL Matcher (dump it if necessary)
+ */
+ protected function create_dumped_url_matcher()
+ {
+ try
+ {
+ $cache = new ConfigCache("{$this->phpbb_root_path}cache/{$this->environment}/url_matcher.{$this->php_ext}", defined('DEBUG'));
+ if (!$cache->isFresh())
+ {
+ $dumper = new PhpMatcherDumper($this->get_routes());
+
+ $options = array(
+ 'class' => 'phpbb_url_matcher',
+ 'base_class' => 'Symfony\\Component\\Routing\\Matcher\\UrlMatcher',
+ );
+
+ $cache->write($dumper->dump($options), $this->get_routes()->getResources());
+ }
+
+ require_once($cache->getPath());
+
+ $this->matcher = new \phpbb_url_matcher($this->context);
+ }
+ catch (IOException $e)
+ {
+ $this->create_new_url_matcher();
+ }
+ }
+
+ /**
+ * Creates a new URL Matcher
+ */
+ protected function create_new_url_matcher()
+ {
+ $this->matcher = new UrlMatcher($this->get_routes(), $this->context);
+ }
+
+ /**
+ * Gets the UrlGenerator instance associated with this Router.
+ *
+ * @return \Symfony\Component\Routing\Generator\UrlGeneratorInterface A UrlGeneratorInterface instance
+ */
+ public function get_generator()
+ {
+ if ($this->generator !== null)
+ {
+ return $this->generator;
+ }
+
+ $this->create_dumped_url_generator();
+
+ return $this->generator;
+ }
+
+ /**
+ * Creates a new dumped URL Generator (dump it if necessary)
+ */
+ protected function create_dumped_url_generator()
+ {
+ try
+ {
+ $cache = new ConfigCache("{$this->phpbb_root_path}cache/{$this->environment}/url_generator.{$this->php_ext}", defined('DEBUG'));
+ if (!$cache->isFresh())
+ {
+ $dumper = new PhpGeneratorDumper($this->get_routes());
+
+ $options = array(
+ 'class' => 'phpbb_url_generator',
+ 'base_class' => 'Symfony\\Component\\Routing\\Generator\\UrlGenerator',
+ );
+
+ $cache->write($dumper->dump($options), $this->get_routes()->getResources());
+ }
+
+ require_once($cache->getPath());
+
+ $this->generator = new \phpbb_url_generator($this->context);
+ }
+ catch (IOException $e)
+ {
+ $this->create_new_url_generator();
+ }
+ }
+
+ /**
+ * Creates a new URL Generator
+ */
+ protected function create_new_url_generator()
+ {
+ $this->generator = new UrlGenerator($this->get_routes(), $this->context);
+ }
+}
diff --git a/phpBB/phpbb/search/fulltext_mysql.php b/phpBB/phpbb/search/fulltext_mysql.php
index 1a0aba096f..da9de56009 100644
--- a/phpBB/phpbb/search/fulltext_mysql.php
+++ b/phpBB/phpbb/search/fulltext_mysql.php
@@ -188,8 +188,8 @@ class fulltext_mysql extends \phpbb\search\base
}
$this->db->sql_freeresult($result);
- set_config('fulltext_mysql_max_word_len', $mysql_info['ft_max_word_len']);
- set_config('fulltext_mysql_min_word_len', $mysql_info['ft_min_word_len']);
+ $this->config->set('fulltext_mysql_max_word_len', $mysql_info['ft_max_word_len']);
+ $this->config->set('fulltext_mysql_min_word_len', $mysql_info['ft_min_word_len']);
return false;
}
@@ -745,7 +745,7 @@ class fulltext_mysql extends \phpbb\search\base
// destroy too old cached search results
$this->destroy_cache(array());
- set_config('search_last_gc', time(), true);
+ $this->config->set('search_last_gc', time(), false);
}
/**
diff --git a/phpBB/phpbb/search/fulltext_native.php b/phpBB/phpbb/search/fulltext_native.php
index 4d02dd1cbf..42ad97da30 100644
--- a/phpBB/phpbb/search/fulltext_native.php
+++ b/phpBB/phpbb/search/fulltext_native.php
@@ -18,6 +18,13 @@ namespace phpbb\search;
*/
class fulltext_native extends \phpbb\search\base
{
+ const UTF8_HANGUL_FIRST = "\xEA\xB0\x80";
+ const UTF8_HANGUL_LAST = "\xED\x9E\xA3";
+ const UTF8_CJK_FIRST = "\xE4\xB8\x80";
+ const UTF8_CJK_LAST = "\xE9\xBE\xBB";
+ const UTF8_CJK_B_FIRST = "\xF0\xA0\x80\x80";
+ const UTF8_CJK_B_LAST = "\xF0\xAA\x9B\x96";
+
/**
* Associative array holding index stats
* @var array
@@ -93,7 +100,7 @@ class fulltext_native extends \phpbb\search\base
protected $user;
/**
- * Initialises the fulltext_native search backend with min/max word length and makes sure the UTF-8 normalizer is loaded
+ * Initialises the fulltext_native search backend with min/max word length
*
* @param boolean|string &$error is passed by reference and should either be set to false on success or an error message on failure
*/
@@ -110,10 +117,6 @@ class fulltext_native extends \phpbb\search\base
/**
* Load the UTF tools
*/
- if (!class_exists('utf_normalizer'))
- {
- include($this->phpbb_root_path . 'includes/utf/utf_normalizer.' . $this->php_ext);
- }
if (!function_exists('utf8_decode_ncr'))
{
include($this->phpbb_root_path . 'includes/utf/utf_tools.' . $this->php_ext);
@@ -1172,9 +1175,9 @@ class fulltext_native extends \phpbb\search\base
* Note: this could be optimized. If the codepoint is lower than Hangul's range
* we know that it will also be lower than CJK ranges
*/
- if ((strncmp($word, UTF8_HANGUL_FIRST, 3) < 0 || strncmp($word, UTF8_HANGUL_LAST, 3) > 0)
- && (strncmp($word, UTF8_CJK_FIRST, 3) < 0 || strncmp($word, UTF8_CJK_LAST, 3) > 0)
- && (strncmp($word, UTF8_CJK_B_FIRST, 4) < 0 || strncmp($word, UTF8_CJK_B_LAST, 4) > 0))
+ if ((strncmp($word, self::UTF8_HANGUL_FIRST, 3) < 0 || strncmp($word, self::UTF8_HANGUL_LAST, 3) > 0)
+ && (strncmp($word, self::UTF8_CJK_FIRST, 3) < 0 || strncmp($word, self::UTF8_CJK_LAST, 3) > 0)
+ && (strncmp($word, self::UTF8_CJK_B_FIRST, 4) < 0 || strncmp($word, self::UTF8_CJK_B_LAST, 4) > 0))
{
$word = strtok(' ');
continue;
@@ -1419,7 +1422,7 @@ class fulltext_native extends \phpbb\search\base
// carry on ... it's okay ... I know when I'm not wanted boo hoo
if (!$this->config['fulltext_native_load_upd'])
{
- set_config('search_last_gc', time(), true);
+ $this->config->set('search_last_gc', time(), false);
return;
}
@@ -1454,7 +1457,7 @@ class fulltext_native extends \phpbb\search\base
// by setting search_last_gc to the new time here we make sure that if a user reloads because the
// following query takes too long, he won't run into it again
- set_config('search_last_gc', time(), true);
+ $this->config->set('search_last_gc', time(), false);
// Delete the matches
$sql = 'DELETE FROM ' . SEARCH_WORDMATCH_TABLE . '
@@ -1470,7 +1473,7 @@ class fulltext_native extends \phpbb\search\base
$this->destroy_cache(array_unique($destroy_cache_words));
}
- set_config('search_last_gc', time(), true);
+ $this->config->set('search_last_gc', time(), false);
}
/**
@@ -1541,8 +1544,6 @@ class fulltext_native extends \phpbb\search\base
* @param string $allowed_chars String of special chars to allow
* @param string $encoding Text encoding
* @return string Cleaned up text, only alphanumeric chars are left
- *
- * @todo \normalizer::cleanup being able to be used?
*/
protected function cleanup($text, $allowed_chars = null, $encoding = 'utf-8')
{
@@ -1569,12 +1570,9 @@ class fulltext_native extends \phpbb\search\base
$text = htmlspecialchars_decode(utf8_decode_ncr($text), ENT_QUOTES);
/**
- * Load the UTF-8 normalizer
- *
- * If we use it more widely, an instance of that class should be held in a
- * a global variable instead
+ * Normalize to NFC
*/
- \utf_normalizer::nfc($text);
+ $text = \Normalizer::normalize($text);
/**
* The first thing we do is:
@@ -1667,9 +1665,9 @@ class fulltext_native extends \phpbb\search\base
$utf_char = substr($text, $pos, $utf_len);
$pos += $utf_len;
- if (($utf_char >= UTF8_HANGUL_FIRST && $utf_char <= UTF8_HANGUL_LAST)
- || ($utf_char >= UTF8_CJK_FIRST && $utf_char <= UTF8_CJK_LAST)
- || ($utf_char >= UTF8_CJK_B_FIRST && $utf_char <= UTF8_CJK_B_LAST))
+ if (($utf_char >= self::UTF8_HANGUL_FIRST && $utf_char <= self::UTF8_HANGUL_LAST)
+ || ($utf_char >= self::UTF8_CJK_FIRST && $utf_char <= self::UTF8_CJK_LAST)
+ || ($utf_char >= self::UTF8_CJK_B_FIRST && $utf_char <= self::UTF8_CJK_B_LAST))
{
/**
* All characters within these ranges are valid
diff --git a/phpBB/phpbb/search/fulltext_postgres.php b/phpBB/phpbb/search/fulltext_postgres.php
index b6af371d13..5a68f0cbfb 100644
--- a/phpBB/phpbb/search/fulltext_postgres.php
+++ b/phpBB/phpbb/search/fulltext_postgres.php
@@ -746,7 +746,7 @@ class fulltext_postgres extends \phpbb\search\base
// destroy too old cached search results
$this->destroy_cache(array());
- set_config('search_last_gc', time(), true);
+ $this->config->set('search_last_gc', time(), false);
}
/**
diff --git a/phpBB/phpbb/search/fulltext_sphinx.php b/phpBB/phpbb/search/fulltext_sphinx.php
index eb53ca6d40..a5ad96b114 100644
--- a/phpBB/phpbb/search/fulltext_sphinx.php
+++ b/phpBB/phpbb/search/fulltext_sphinx.php
@@ -85,7 +85,7 @@ class fulltext_sphinx
/**
* Database Tools object
- * @var \phpbb\db\tools
+ * @var \phpbb\db\tools\tools_interface
*/
protected $db_tools;
@@ -135,12 +135,13 @@ class fulltext_sphinx
$this->db = $db;
$this->auth = $auth;
- // Initialize \phpbb\db\tools object
- $this->db_tools = new \phpbb\db\tools($this->db);
+ // Initialize \phpbb\db\tools\tools object
+ global $phpbb_container; // TODO inject into object
+ $this->db_tools = $phpbb_container->get('dbal.tools');
if(!$this->config['fulltext_sphinx_id'])
{
- set_config('fulltext_sphinx_id', unique_id());
+ $this->config->set('fulltext_sphinx_id', unique_id());
}
$this->id = $this->config['fulltext_sphinx_id'];
$this->indexes = 'index_phpbb_' . $this->id . '_delta;index_phpbb_' . $this->id . '_main';
@@ -211,7 +212,7 @@ class fulltext_sphinx
}
// Move delta to main index each hour
- set_config('search_gc', 3600);
+ $this->config->set('search_gc', 3600);
return false;
}
@@ -454,6 +455,8 @@ class fulltext_sphinx
*/
public function keyword_search($type, $fields, $terms, $sort_by_sql, $sort_key, $sort_dir, $sort_days, $ex_fid_ary, $post_visibility, $topic_id, $author_ary, $author_name, &$id_ary, &$start, $per_page)
{
+ global $user, $phpbb_log;
+
// No keywords? No posts.
if (!strlen($this->search_query) && !sizeof($author_ary))
{
@@ -601,7 +604,7 @@ class fulltext_sphinx
if ($this->sphinx->GetLastError())
{
- add_log('critical', 'LOG_SPHINX_ERROR', $this->sphinx->GetLastError());
+ $phpbb_log->add('critical', $user->data['user_id'], $user->ip, 'LOG_SPHINX_ERROR', false, array($this->sphinx->GetLastError()));
if ($this->auth->acl_get('a_'))
{
trigger_error($this->user->lang('SPHINX_SEARCH_FAILED', $this->sphinx->GetLastError()));
@@ -755,7 +758,7 @@ class fulltext_sphinx
*/
public function tidy($create = false)
{
- set_config('search_last_gc', time(), true);
+ $this->config->set('search_last_gc', time(), false);
}
/**
diff --git a/phpBB/phpbb/session.php b/phpBB/phpbb/session.php
index bedd581725..6154f384f3 100644
--- a/phpBB/phpbb/session.php
+++ b/phpBB/phpbb/session.php
@@ -92,8 +92,8 @@ class session
}
// current directory within the phpBB root (for example: adm)
- $root_dirs = explode('/', str_replace('\\', '/', phpbb_realpath($root_path)));
- $page_dirs = explode('/', str_replace('\\', '/', phpbb_realpath('./')));
+ $root_dirs = explode('/', str_replace('\\', '/', $phpbb_filesystem->realpath($root_path)));
+ $page_dirs = explode('/', str_replace('\\', '/', $phpbb_filesystem->realpath('./')));
$intersection = array_intersect_assoc($root_dirs, $page_dirs);
$root_dirs = array_diff_assoc($root_dirs, $intersection);
@@ -219,7 +219,7 @@ class session
function session_begin($update_session_page = true)
{
global $phpEx, $SID, $_SID, $_EXTRA_URL, $db, $config, $phpbb_root_path;
- global $request, $phpbb_container;
+ global $request, $phpbb_container, $user, $phpbb_log;
// Give us some basic information
$this->time_now = time();
@@ -257,23 +257,23 @@ class session
if ($request->is_set($config['cookie_name'] . '_sid', \phpbb\request\request_interface::COOKIE) || $request->is_set($config['cookie_name'] . '_u', \phpbb\request\request_interface::COOKIE))
{
- $this->cookie_data['u'] = request_var($config['cookie_name'] . '_u', 0, false, true);
- $this->cookie_data['k'] = request_var($config['cookie_name'] . '_k', '', false, true);
- $this->session_id = request_var($config['cookie_name'] . '_sid', '', false, true);
+ $this->cookie_data['u'] = $request->variable($config['cookie_name'] . '_u', 0, false, \phpbb\request\request_interface::COOKIE);
+ $this->cookie_data['k'] = $request->variable($config['cookie_name'] . '_k', '', false, \phpbb\request\request_interface::COOKIE);
+ $this->session_id = $request->variable($config['cookie_name'] . '_sid', '', false, \phpbb\request\request_interface::COOKIE);
$SID = (defined('NEED_SID')) ? '?sid=' . $this->session_id : '?sid=';
$_SID = (defined('NEED_SID')) ? $this->session_id : '';
if (empty($this->session_id))
{
- $this->session_id = $_SID = request_var('sid', '');
+ $this->session_id = $_SID = $request->variable('sid', '');
$SID = '?sid=' . $this->session_id;
$this->cookie_data = array('u' => 0, 'k' => '');
}
}
else
{
- $this->session_id = $_SID = request_var('sid', '');
+ $this->session_id = $_SID = $request->variable('sid', '');
$SID = '?sid=' . $this->session_id;
}
@@ -349,8 +349,8 @@ class session
}
else
{
- set_config('limit_load', '0');
- set_config('limit_search_load', '0');
+ $config->set('limit_load', '0');
+ $config->set('limit_search_load', '0');
}
}
@@ -413,6 +413,7 @@ class session
$session_expired = false;
// Check whether the session is still valid if we have one
+ /* @var $provider_collection \phpbb\auth\provider_collection */
$provider_collection = $phpbb_container->get('auth.provider_collection');
$provider = $provider_collection->get_provider();
@@ -493,11 +494,18 @@ class session
{
if ($referer_valid)
{
- add_log('critical', 'LOG_IP_BROWSER_FORWARDED_CHECK', $u_ip, $s_ip, $u_browser, $s_browser, htmlspecialchars($u_forwarded_for), htmlspecialchars($s_forwarded_for));
+ $phpbb_log->add('critical', $user->data['user_id'], $user->ip, 'LOG_IP_BROWSER_FORWARDED_CHECK', false, array(
+ $u_ip,
+ $s_ip,
+ $u_browser,
+ $s_browser,
+ htmlspecialchars($u_forwarded_for),
+ htmlspecialchars($s_forwarded_for)
+ ));
}
else
{
- add_log('critical', 'LOG_REFERER_INVALID', $this->referer);
+ $phpbb_log->add('critical', $user->data['user_id'], $user->ip, 'LOG_REFERER_INVALID', false, array($this->referer));
}
}
}
@@ -582,6 +590,7 @@ class session
}
}
+ /* @var $provider_collection \phpbb\auth\provider_collection */
$provider_collection = $phpbb_container->get('auth.provider_collection');
$provider = $provider_collection->get_provider();
$this->data = $provider->autologin();
@@ -914,6 +923,7 @@ class session
$db->sql_query($sql);
// Allow connecting logout with external auth method logout
+ /* @var $provider_collection \phpbb\auth\provider_collection */
$provider_collection = $phpbb_container->get('auth.provider_collection');
$provider = $provider_collection->get_provider();
$provider->logout($this->data, $new_session);
@@ -1030,7 +1040,7 @@ class session
{
// Less than 10 users, update gc timer ... else we want gc
// called again to delete other sessions
- set_config('session_last_gc', $this->time_now, true);
+ $config->set('session_last_gc', $this->time_now, false);
if ($config['max_autologin_time'])
{
@@ -1040,6 +1050,7 @@ class session
}
// only called from CRON; should be a safe workaround until the infrastructure gets going
+ /* @var $captcha_factory \phpbb\captcha\factory */
$captcha_factory = $phpbb_container->get('captcha.factory');
$captcha_factory->garbage_collect($config['captcha_plugin']);
diff --git a/phpBB/phpbb/template/asset.php b/phpBB/phpbb/template/asset.php
index 67dbd7b357..cb00f16549 100644
--- a/phpBB/phpbb/template/asset.php
+++ b/phpBB/phpbb/template/asset.php
@@ -20,15 +20,20 @@ class asset
/** @var \phpbb\path_helper **/
protected $path_helper;
+ /** @var \phpbb\filesystem\filesystem */
+ protected $filesystem;
+
/**
* Constructor
*
* @param string $url URL
* @param \phpbb\path_helper $path_helper Path helper object
+ * @param \phpbb\filesystem\filesystem $filesystem
*/
- public function __construct($url, \phpbb\path_helper $path_helper)
+ public function __construct($url, \phpbb\path_helper $path_helper, \phpbb\filesystem\filesystem $filesystem)
{
$this->path_helper = $path_helper;
+ $this->filesystem = $filesystem;
$this->set_url($url);
}
@@ -152,6 +157,24 @@ class asset
*/
public function set_path($path, $urlencode = false)
{
+ // Since 1.7.0 Twig returns the real path of the file. We need it to be relative.
+ $real_root_path = $this->filesystem->realpath($this->path_helper->get_phpbb_root_path()) . DIRECTORY_SEPARATOR;
+
+ // If the asset is under the phpBB root path we need to remove its path and then prepend $phpbb_root_path
+ if ($real_root_path && substr($path . DIRECTORY_SEPARATOR, 0, strlen($real_root_path)) === $real_root_path)
+ {
+ $path = $this->path_helper->get_phpbb_root_path() . str_replace('\\', '/', substr($path, strlen($real_root_path)));
+ }
+ else
+ {
+ // Else we make the path relative to the current working directory
+ $real_root_path = $this->filesystem->realpath('.') . DIRECTORY_SEPARATOR;
+ if ($real_root_path && substr($path . DIRECTORY_SEPARATOR, 0, strlen($real_root_path)) === $real_root_path)
+ {
+ $path = str_replace('\\', '/', substr($path, strlen($real_root_path)));
+ }
+ }
+
if ($urlencode)
{
$paths = explode('/', $path);
@@ -161,6 +184,7 @@ class asset
}
$path = implode('/', $paths);
}
+
$this->components['path'] = $path;
}
diff --git a/phpBB/phpbb/template/exception/user_object_not_available.php b/phpBB/phpbb/template/exception/user_object_not_available.php
new file mode 100644
index 0000000000..62fd2743c1
--- /dev/null
+++ b/phpBB/phpbb/template/exception/user_object_not_available.php
@@ -0,0 +1,22 @@
+<?php
+/**
+ *
+ * This file is part of the phpBB Forum Software package.
+ *
+ * @copyright (c) phpBB Limited <https://www.phpbb.com>
+ * @license GNU General Public License, version 2 (GPL-2.0)
+ *
+ * For full copyright and license information, please see
+ * the docs/CREDITS.txt file.
+ *
+ */
+
+namespace phpbb\template\exception;
+
+/**
+ * This exception is thrown when the user object was not set but it is required by the called method
+ */
+class user_object_not_available extends \phpbb\exception\runtime_exception
+{
+
+}
diff --git a/phpBB/phpbb/template/twig/environment.php b/phpBB/phpbb/template/twig/environment.php
index 476ffd935e..6e75403159 100644
--- a/phpBB/phpbb/template/twig/environment.php
+++ b/phpBB/phpbb/template/twig/environment.php
@@ -18,9 +18,15 @@ class environment extends \Twig_Environment
/** @var \phpbb\config\config */
protected $phpbb_config;
+ /** @var \phpbb\filesystem\filesystem */
+ protected $filesystem;
+
/** @var \phpbb\path_helper */
protected $phpbb_path_helper;
+ /** @var \Symfony\Component\DependencyInjection\ContainerInterface */
+ protected $container;
+
/** @var \phpbb\extension\manager */
protected $extension_manager;
@@ -37,25 +43,52 @@ class environment extends \Twig_Environment
* Constructor
*
* @param \phpbb\config\config $phpbb_config The phpBB configuration
+ * @param \phpbb\filesystem\filesystem $filesystem
* @param \phpbb\path_helper $path_helper phpBB path helper
+ * @param \Symfony\Component\DependencyInjection\ContainerInterface $container The dependency injection container
+ * @param string $cache_path The path to the cache directory
* @param \phpbb\extension\manager $extension_manager phpBB extension manager
* @param \Twig_LoaderInterface $loader Twig loader interface
* @param array $options Array of options to pass to Twig
*/
- public function __construct($phpbb_config, \phpbb\path_helper $path_helper, \phpbb\extension\manager $extension_manager = null, \Twig_LoaderInterface $loader = null, $options = array())
+ public function __construct(\phpbb\config\config $phpbb_config, \phpbb\filesystem\filesystem $filesystem, \phpbb\path_helper $path_helper, \Symfony\Component\DependencyInjection\ContainerInterface $container, $cache_path, \phpbb\extension\manager $extension_manager = null, \Twig_LoaderInterface $loader = null, $options = array())
{
$this->phpbb_config = $phpbb_config;
+ $this->filesystem = $filesystem;
$this->phpbb_path_helper = $path_helper;
$this->extension_manager = $extension_manager;
+ $this->container = $container;
$this->phpbb_root_path = $this->phpbb_path_helper->get_phpbb_root_path();
$this->web_root_path = $this->phpbb_path_helper->get_web_root_path();
+ $options = array_merge(array(
+ 'cache' => (defined('IN_INSTALL')) ? false : $cache_path,
+ 'debug' => false,
+ 'auto_reload' => (bool) $this->phpbb_config['load_tplcompile'],
+ 'autoescape' => false,
+ ), $options);
+
return parent::__construct($loader, $options);
}
/**
+ * {@inheritdoc}
+ */
+ public function getLexer()
+ {
+ if (null === $this->lexer)
+ {
+ $this->lexer = $this->container->get('template.twig.lexer');
+ $this->lexer->set_environment($this);
+ }
+
+ return $this->lexer;
+ }
+
+
+ /**
* Get the list of enabled phpBB extensions
*
* Used in EVENT node
@@ -78,16 +111,26 @@ class environment extends \Twig_Environment
}
/**
- * Get the phpBB root path
- *
- * @return string
- */
+ * Get the phpBB root path
+ *
+ * @return string
+ */
public function get_phpbb_root_path()
{
return $this->phpbb_root_path;
}
/**
+ * Get the filesystem object
+ *
+ * @return \phpbb\filesystem\filesystem
+ */
+ public function get_filesystem()
+ {
+ return $this->filesystem;
+ }
+
+ /**
* Get the web root path
*
* @return string
diff --git a/phpBB/phpbb/template/twig/extension.php b/phpBB/phpbb/template/twig/extension.php
index 3a983491b9..92f87a0331 100644
--- a/phpBB/phpbb/template/twig/extension.php
+++ b/phpBB/phpbb/template/twig/extension.php
@@ -18,20 +18,20 @@ class extension extends \Twig_Extension
/** @var \phpbb\template\context */
protected $context;
- /** @var \phpbb\user */
- protected $user;
+ /** @var \phpbb\language\language */
+ protected $language;
/**
* Constructor
*
* @param \phpbb\template\context $context
- * @param \phpbb\user $user
+ * @param \phpbb\language\language $language
* @return \phpbb\template\twig\extension
*/
- public function __construct(\phpbb\template\context $context, $user)
+ public function __construct(\phpbb\template\context $context, $language)
{
$this->context = $context;
- $this->user = $user;
+ $this->language = $language;
}
/**
@@ -71,6 +71,7 @@ class extension extends \Twig_Extension
{
return array(
new \Twig_SimpleFilter('subset', array($this, 'loop_subset'), array('needs_environment' => true)),
+ // @deprecated 3.2.0 Uses twig's JS escape method instead of addslashes
new \Twig_SimpleFilter('addslashes', 'addslashes'),
);
}
@@ -177,9 +178,9 @@ class extension extends \Twig_Extension
return $context_vars['L_' . $key];
}
- // LA_ is transformed into lang(\'$1\')|addslashes, so we should not
+ // LA_ is transformed into lang(\'$1\')|escape('js'), so we should not
// need to check for it
- return call_user_func_array(array($this->user, 'lang'), $args);
+ return call_user_func_array(array($this->language, 'lang'), $args);
}
}
diff --git a/phpBB/phpbb/template/twig/lexer.php b/phpBB/phpbb/template/twig/lexer.php
index c5dc7273ba..f1542109a4 100644
--- a/phpBB/phpbb/template/twig/lexer.php
+++ b/phpBB/phpbb/template/twig/lexer.php
@@ -15,6 +15,11 @@ namespace phpbb\template\twig;
class lexer extends \Twig_Lexer
{
+ public function set_environment(\Twig_Environment $env)
+ {
+ $this->env = $env;
+ }
+
public function tokenize($code, $filename = null)
{
// Our phpBB tags
@@ -112,9 +117,9 @@ class lexer extends \Twig_Lexer
// Appends any filters after lang()
$code = preg_replace('#{L_([a-zA-Z0-9_\.]+)(\|[^}]+?)?}#', '{{ lang(\'$1\')$2 }}', $code);
- // Replace all of our escaped language variables, {LA_VARNAME}, with Twig style, {{ lang('NAME')|addslashes }}
- // Appends any filters after lang(), but before addslashes
- $code = preg_replace('#{LA_([a-zA-Z0-9_\.]+)(\|[^}]+?)?}#', '{{ lang(\'$1\')$2|addslashes }}', $code);
+ // Replace all of our escaped language variables, {LA_VARNAME}, with Twig style, {{ lang('NAME')|escape('js') }}
+ // Appends any filters after lang(), but before escape('js')
+ $code = preg_replace('#{LA_([a-zA-Z0-9_\.]+)(\|[^}]+?)?}#', '{{ lang(\'$1\')$2|escape(\'js\') }}', $code);
// Replace all of our variables, {VARNAME}, with Twig style, {{ VARNAME }}
// Appends any filters
diff --git a/phpBB/phpbb/template/twig/loader.php b/phpBB/phpbb/template/twig/loader.php
index 139a413b70..8b12188a77 100644
--- a/phpBB/phpbb/template/twig/loader.php
+++ b/phpBB/phpbb/template/twig/loader.php
@@ -21,6 +21,24 @@ class loader extends \Twig_Loader_Filesystem
protected $safe_directories = array();
/**
+ * @var \phpbb\filesystem\filesystem_interface
+ */
+ protected $filesystem;
+
+ /**
+ * Constructor
+ *
+ * @param \phpbb\filesystem\filesystem_interface $filesystem
+ * @param string|array $paths
+ */
+ public function __construct(\phpbb\filesystem\filesystem_interface $filesystem, $paths = array())
+ {
+ $this->filesystem = $filesystem;
+
+ parent::__construct($paths);
+ }
+
+ /**
* Set safe directories
*
* @param array $directories Array of directories that are safe (empty to clear)
@@ -49,7 +67,7 @@ class loader extends \Twig_Loader_Filesystem
*/
public function addSafeDirectory($directory)
{
- $directory = phpbb_realpath($directory);
+ $directory = $this->filesystem->realpath($directory);
if ($directory !== false)
{
@@ -119,7 +137,7 @@ class loader extends \Twig_Loader_Filesystem
// can now check if we're within a "safe" directory
// Find the real path of the directory the file is in
- $directory = phpbb_realpath(dirname($file));
+ $directory = $this->filesystem->realpath(dirname($file));
if ($directory === false)
{
diff --git a/phpBB/phpbb/template/twig/node/includeasset.php b/phpBB/phpbb/template/twig/node/includeasset.php
index 15195a226b..324823b8d7 100644
--- a/phpBB/phpbb/template/twig/node/includeasset.php
+++ b/phpBB/phpbb/template/twig/node/includeasset.php
@@ -39,7 +39,7 @@ abstract class includeasset extends \Twig_Node
->write("\$asset_file = ")
->subcompile($this->getNode('expr'))
->raw(";\n")
- ->write("\$asset = new \phpbb\\template\\asset(\$asset_file, \$this->getEnvironment()->get_path_helper());\n")
+ ->write("\$asset = new \phpbb\\template\\asset(\$asset_file, \$this->getEnvironment()->get_path_helper(), \$this->getEnvironment()->get_filesystem());\n")
->write("if (substr(\$asset_file, 0, 2) !== './' && \$asset->is_relative()) {\n")
->indent()
->write("\$asset_path = \$asset->get_path();")
diff --git a/phpBB/phpbb/template/twig/twig.php b/phpBB/phpbb/template/twig/twig.php
index bd754d9bbd..6b3cf32bc8 100644
--- a/phpBB/phpbb/template/twig/twig.php
+++ b/phpBB/phpbb/template/twig/twig.php
@@ -13,6 +13,8 @@
namespace phpbb\template\twig;
+use phpbb\template\exception\user_object_not_available;
+
/**
* Twig Template class.
*/
@@ -76,11 +78,14 @@ class twig extends \phpbb\template\base
*
* @param \phpbb\path_helper $path_helper
* @param \phpbb\config\config $config
- * @param \phpbb\user $user
* @param \phpbb\template\context $context template context
+ * @param \phpbb\template\twig\environment $twig_environment
+ * @param string $cache_path
+ * @param \phpbb\user|null $user
+ * @param array|\ArrayAccess $extensions
* @param \phpbb\extension\manager $extension_manager extension manager, if null then template events will not be invoked
*/
- public function __construct(\phpbb\path_helper $path_helper, $config, $user, \phpbb\template\context $context, \phpbb\extension\manager $extension_manager = null)
+ public function __construct(\phpbb\path_helper $path_helper, $config, \phpbb\template\context $context, \phpbb\template\twig\environment $twig_environment, $cache_path, \phpbb\user $user = null, $extensions = array(), \phpbb\extension\manager $extension_manager = null)
{
$this->path_helper = $path_helper;
$this->phpbb_root_path = $path_helper->get_phpbb_root_path();
@@ -89,41 +94,14 @@ class twig extends \phpbb\template\base
$this->user = $user;
$this->context = $context;
$this->extension_manager = $extension_manager;
+ $this->cachepath = $cache_path;
+ $this->twig = $twig_environment;
- $this->cachepath = $this->phpbb_root_path . 'cache/twig/';
-
- // Initiate the loader, __main__ namespace paths will be setup later in set_style_names()
- $loader = new \phpbb\template\twig\loader('');
-
- $this->twig = new \phpbb\template\twig\environment(
- $this->config,
- $this->path_helper,
- $this->extension_manager,
- $loader,
- array(
- 'cache' => (defined('IN_INSTALL')) ? false : $this->cachepath,
- 'debug' => defined('DEBUG'),
- 'auto_reload' => (bool) $this->config['load_tplcompile'],
- 'autoescape' => false,
- )
- );
-
- $this->twig->addExtension(
- new \phpbb\template\twig\extension(
- $this->context,
- $this->user
- )
- );
-
- if (defined('DEBUG'))
+ foreach ($extensions as $extension)
{
- $this->twig->addExtension(new \Twig_Extension_Debug());
+ $this->twig->addExtension($extension);
}
- $lexer = new \phpbb\template\twig\lexer($this->twig);
-
- $this->twig->setLexer($lexer);
-
// Add admin namespace
if ($this->path_helper->get_adm_relative_path() !== null && is_dir($this->phpbb_root_path . $this->path_helper->get_adm_relative_path() . 'style/'))
{
@@ -150,9 +128,16 @@ class twig extends \phpbb\template\base
* Get the style tree of the style preferred by the current user
*
* @return array Style tree, most specific first
+ *
+ * @throws \phpbb\template\exception\user_object_not_available When user service was not set
*/
public function get_user_style()
{
+ if ($this->user === null)
+ {
+ throw new user_object_not_available();
+ }
+
$style_list = array(
$this->user->style['style_path'],
);
@@ -368,14 +353,24 @@ class twig extends \phpbb\template\base
$context_vars['.'][0], // To get normal vars
array(
'definition' => new \phpbb\template\twig\definition(),
- 'user' => $this->user,
'loops' => $context_vars, // To get loops
)
);
+ if ($this->user instanceof \phpbb\user)
+ {
+ $vars['user'] = $this->user;
+ }
+
// cleanup
unset($vars['loops']['.']);
+ // Inject in the main context the value added by assign_block_vars() to be able to use directly the Twig loops.
+ foreach ($vars['loops'] as $key => &$value)
+ {
+ $vars[$key] = $value;
+ }
+
return $vars;
}
diff --git a/phpBB/phpbb/textformatter/cache_interface.php b/phpBB/phpbb/textformatter/cache_interface.php
new file mode 100644
index 0000000000..f6b5f195c7
--- /dev/null
+++ b/phpBB/phpbb/textformatter/cache_interface.php
@@ -0,0 +1,31 @@
+<?php
+/**
+*
+* This file is part of the phpBB Forum Software package.
+*
+* @copyright (c) phpBB Limited <https://www.phpbb.com>
+* @license GNU General Public License, version 2 (GPL-2.0)
+*
+* For full copyright and license information, please see
+* the docs/CREDITS.txt file.
+*
+*/
+
+namespace phpbb\textformatter;
+
+/**
+* Currently only used to signal that something that could effect the rendering has changed.
+* BBCodes, smilies, censored words, templates, etc...
+*/
+interface cache_interface
+{
+ /**
+ * Invalidate and/or regenerate this text formatter's cache(s)
+ */
+ public function invalidate();
+
+ /**
+ * Tidy/prune this text formatter's cache(s)
+ */
+ public function tidy();
+}
diff --git a/phpBB/phpbb/textformatter/data_access.php b/phpBB/phpbb/textformatter/data_access.php
new file mode 100644
index 0000000000..2103bf8e60
--- /dev/null
+++ b/phpBB/phpbb/textformatter/data_access.php
@@ -0,0 +1,228 @@
+<?php
+/**
+*
+* This file is part of the phpBB Forum Software package.
+*
+* @copyright (c) phpBB Limited <https://www.phpbb.com>
+* @license GNU General Public License, version 2 (GPL-2.0)
+*
+* For full copyright and license information, please see
+* the docs/CREDITS.txt file.
+*
+*/
+
+namespace phpbb\textformatter;
+
+/**
+* Data access layer that fetchs BBCodes, smilies and censored words from the database.
+* To be extended to include insert/update/delete operations.
+*
+* Also used to get templates.
+*/
+class data_access
+{
+ /**
+ * @var string Name of the BBCodes table
+ */
+ protected $bbcodes_table;
+
+ /**
+ * @var \phpbb\db\driver\driver_interface
+ */
+ protected $db;
+
+ /**
+ * @var string Name of the smilies table
+ */
+ protected $smilies_table;
+
+ /**
+ * @var string Name of the styles table
+ */
+ protected $styles_table;
+
+ /**
+ * @var string Path to the styles dir
+ */
+ protected $styles_path;
+
+ /**
+ * @var string Name of the words table
+ */
+ protected $words_table;
+
+ /**
+ * Constructor
+ *
+ * @param \phpbb\db\driver\driver_interface $db Database connection
+ * @param string $bbcodes_table Name of the BBCodes table
+ * @param string $smilies_table Name of the smilies table
+ * @param string $styles_table Name of the styles table
+ * @param string $words_table Name of the words table
+ * @param string $styles_path Path to the styles dir
+ */
+ public function __construct(\phpbb\db\driver\driver_interface $db, $bbcodes_table, $smilies_table, $styles_table, $words_table, $styles_path)
+ {
+ $this->db = $db;
+
+ $this->bbcodes_table = $bbcodes_table;
+ $this->smilies_table = $smilies_table;
+ $this->styles_table = $styles_table;
+ $this->words_table = $words_table;
+
+ $this->styles_path = $styles_path;
+ }
+
+ /**
+ * Return the list of custom BBCodes
+ *
+ * @return array
+ */
+ public function get_bbcodes()
+ {
+ $sql = 'SELECT bbcode_match, bbcode_tpl FROM ' . $this->bbcodes_table;
+ $result = $this->db->sql_query($sql);
+ $rows = $this->db->sql_fetchrowset($result);
+ $this->db->sql_freeresult($result);
+
+ return $rows;
+ }
+
+ /**
+ * Return the list of smilies
+ *
+ * @return array
+ */
+ public function get_smilies()
+ {
+ // NOTE: smilies that are displayed on the posting page are processed first because they're
+ // typically the most used smilies and it ends up producing a slightly more efficient
+ // renderer
+ $sql = 'SELECT code, emotion, smiley_url, smiley_width, smiley_height
+ FROM ' . $this->smilies_table . '
+ ORDER BY display_on_posting DESC';
+ $result = $this->db->sql_query($sql);
+ $rows = $this->db->sql_fetchrowset($result);
+ $this->db->sql_freeresult($result);
+
+ return $rows;
+ }
+
+ /**
+ * Return the list of installed styles
+ *
+ * @return array
+ */
+ protected function get_styles()
+ {
+ $sql = 'SELECT style_id, style_path, style_parent_id, bbcode_bitfield FROM ' . $this->styles_table;
+ $result = $this->db->sql_query($sql);
+ $rows = $this->db->sql_fetchrowset($result);
+ $this->db->sql_freeresult($result);
+
+ return $rows;
+ }
+
+ /**
+ * Return the bbcode.html template for every installed style
+ *
+ * @return array 2D array. style_id as keys, each element is an array with a "template" element that contains the style's bbcode.html and a "bbcodes" element that contains the name of each BBCode that is to be stylised
+ */
+ public function get_styles_templates()
+ {
+ $templates = array();
+
+ $bbcode_ids = array(
+ 'quote' => 0,
+ 'b' => 1,
+ 'i' => 2,
+ 'url' => 3,
+ 'img' => 4,
+ 'size' => 5,
+ 'color' => 6,
+ 'u' => 7,
+ 'code' => 8,
+ 'list' => 9,
+ '*' => 9,
+ 'email' => 10,
+ 'flash' => 11,
+ 'attachment' => 12,
+ );
+
+ $styles = array();
+ foreach ($this->get_styles() as $row)
+ {
+ $styles[$row['style_id']] = $row;
+ }
+
+ foreach ($styles as $style_id => $style)
+ {
+ $bbcodes = array();
+
+ // Collect the name of the BBCodes whose bit is set in the style's bbcode_bitfield
+ $template_bitfield = new \bitfield($style['bbcode_bitfield']);
+ foreach ($bbcode_ids as $bbcode_name => $bit)
+ {
+ if ($template_bitfield->get($bit))
+ {
+ $bbcodes[] = $bbcode_name;
+ }
+ }
+
+ $filename = $this->resolve_style_filename($styles, $style);
+ if ($filename === false)
+ {
+ // Ignore this style, it will use the default templates
+ continue;
+ }
+
+ $templates[$style_id] = array(
+ 'bbcodes' => $bbcodes,
+ 'template' => file_get_contents($filename),
+ );
+ }
+
+ return $templates;
+ }
+
+ /**
+ * Resolve inheritance for given style and return the path to their bbcode.html file
+ *
+ * @param array $styles Associative array of [style_id => style] containing all styles
+ * @param array $style Style for which we resolve
+ * @return string|bool Path to this style's bbcode.html, or FALSE
+ */
+ protected function resolve_style_filename(array $styles, array $style)
+ {
+ // Look for a bbcode.html in this style's dir
+ $filename = $this->styles_path . $style['style_path'] . '/template/bbcode.html';
+ if (file_exists($filename))
+ {
+ return $filename;
+ }
+
+ // Resolve using this style's parent
+ $parent_id = $style['style_parent_id'];
+ if ($parent_id && !empty($styles[$parent_id]))
+ {
+ return $this->resolve_style_filename($styles, $styles[$parent_id]);
+ }
+
+ return false;
+ }
+
+ /**
+ * Return the list of censored words
+ *
+ * @return array
+ */
+ public function get_censored_words()
+ {
+ $sql = 'SELECT word, replacement FROM ' . $this->words_table;
+ $result = $this->db->sql_query($sql);
+ $rows = $this->db->sql_fetchrowset($result);
+ $this->db->sql_freeresult($result);
+
+ return $rows;
+ }
+}
diff --git a/phpBB/phpbb/textformatter/parser_interface.php b/phpBB/phpbb/textformatter/parser_interface.php
new file mode 100644
index 0000000000..ad611fb5b4
--- /dev/null
+++ b/phpBB/phpbb/textformatter/parser_interface.php
@@ -0,0 +1,112 @@
+<?php
+/**
+*
+* This file is part of the phpBB Forum Software package.
+*
+* @copyright (c) phpBB Limited <https://www.phpbb.com>
+* @license GNU General Public License, version 2 (GPL-2.0)
+*
+* For full copyright and license information, please see
+* the docs/CREDITS.txt file.
+*
+*/
+
+namespace phpbb\textformatter;
+
+interface parser_interface
+{
+ /**
+ * Parse given text
+ *
+ * @param string $text
+ * @return string
+ */
+ public function parse($text);
+
+ /**
+ * Disable a specific BBCode
+ *
+ * @param string $name BBCode name
+ * @return null
+ */
+ public function disable_bbcode($name);
+
+ /**
+ * Disable BBCodes in general
+ */
+ public function disable_bbcodes();
+
+ /**
+ * Disable the censor
+ */
+ public function disable_censor();
+
+ /**
+ * Disable magic URLs
+ */
+ public function disable_magic_url();
+
+ /**
+ * Disable smilies
+ */
+ public function disable_smilies();
+
+ /**
+ * Enable a specific BBCode
+ *
+ * @param string $name BBCode name
+ * @return null
+ */
+ public function enable_bbcode($name);
+
+ /**
+ * Enable BBCodes in general
+ */
+ public function enable_bbcodes();
+
+ /**
+ * Enable the censor
+ */
+ public function enable_censor();
+
+ /**
+ * Enable magic URLs
+ */
+ public function enable_magic_url();
+
+ /**
+ * Enable smilies
+ */
+ public function enable_smilies();
+
+ /**
+ * Get the list of errors that were generated during last parsing
+ *
+ * @return array[] Array of arrays. Each array contains a lang string at index 0 plus any number
+ * of optional parameters
+ */
+ public function get_errors();
+
+ /**
+ * Set a variable to be used by the parser
+ *
+ * - max_font_size
+ * - max_img_height
+ * - max_img_width
+ * - max_smilies
+ * - max_urls
+ *
+ * @param string $name
+ * @param mixed $value
+ * @return null
+ */
+ public function set_var($name, $value);
+
+ /**
+ * Set multiple variables to be used by the parser
+ *
+ * @param array $vars Associative array of [name => value]
+ * @return null
+ */
+ public function set_vars(array $vars);
+}
diff --git a/phpBB/phpbb/textformatter/renderer_interface.php b/phpBB/phpbb/textformatter/renderer_interface.php
new file mode 100644
index 0000000000..609b0bb642
--- /dev/null
+++ b/phpBB/phpbb/textformatter/renderer_interface.php
@@ -0,0 +1,92 @@
+<?php
+/**
+*
+* This file is part of the phpBB Forum Software package.
+*
+* @copyright (c) phpBB Limited <https://www.phpbb.com>
+* @license GNU General Public License, version 2 (GPL-2.0)
+*
+* For full copyright and license information, please see
+* the docs/CREDITS.txt file.
+*
+*/
+
+namespace phpbb\textformatter;
+
+interface renderer_interface
+{
+ /**
+ * Render given text
+ *
+ * @param string $text Text, as parsed by something that implements \phpbb\textformatter\parser
+ * @return string
+ */
+ public function render($text);
+
+ /**
+ * Set the smilies' path
+ *
+ * @return null
+ */
+ public function set_smilies_path($path);
+
+ /**
+ * Return the value of the "viewcensors" option
+ *
+ * @return bool Option's value
+ */
+ public function get_viewcensors();
+
+ /**
+ * Return the value of the "viewflash" option
+ *
+ * @return bool Option's value
+ */
+ public function get_viewflash();
+
+ /**
+ * Return the value of the "viewimg" option
+ *
+ * @return bool Option's value
+ */
+ public function get_viewimg();
+
+ /**
+ * Return the value of the "viewsmilies" option
+ *
+ * @return bool Option's value
+ */
+ public function get_viewsmilies();
+
+ /**
+ * Set the "viewcensors" option
+ *
+ * @param bool $value Option's value
+ * @return null
+ */
+ public function set_viewcensors($value);
+
+ /**
+ * Set the "viewflash" option
+ *
+ * @param bool $value Option's value
+ * @return null
+ */
+ public function set_viewflash($value);
+
+ /**
+ * Set the "viewimg" option
+ *
+ * @param bool $value Option's value
+ * @return null
+ */
+ public function set_viewimg($value);
+
+ /**
+ * Set the "viewsmilies" option
+ *
+ * @param bool $value Option's value
+ * @return null
+ */
+ public function set_viewsmilies($value);
+}
diff --git a/phpBB/phpbb/textformatter/s9e/factory.php b/phpBB/phpbb/textformatter/s9e/factory.php
new file mode 100644
index 0000000000..9576abe1f0
--- /dev/null
+++ b/phpBB/phpbb/textformatter/s9e/factory.php
@@ -0,0 +1,545 @@
+<?php
+/**
+*
+* This file is part of the phpBB Forum Software package.
+*
+* @copyright (c) phpBB Limited <https://www.phpbb.com>
+* @license GNU General Public License, version 2 (GPL-2.0)
+*
+* For full copyright and license information, please see
+* the docs/CREDITS.txt file.
+*
+*/
+
+namespace phpbb\textformatter\s9e;
+
+use s9e\TextFormatter\Configurator;
+use s9e\TextFormatter\Configurator\Items\AttributeFilters\RegexpFilter;
+use s9e\TextFormatter\Configurator\Items\UnsafeTemplate;
+
+/**
+* Creates s9e\TextFormatter objects
+*/
+class factory implements \phpbb\textformatter\cache_interface
+{
+ /**
+ * @var \phpbb\cache\driver\driver_interface
+ */
+ protected $cache;
+
+ /**
+ * @var string Path to the cache dir
+ */
+ protected $cache_dir;
+
+ /**
+ * @var string Cache key used for the parser
+ */
+ protected $cache_key_parser;
+
+ /**
+ * @var string Cache key used for the renderer
+ */
+ protected $cache_key_renderer;
+
+ /**
+ * @var array Custom tokens used in bbcode.html and their corresponding token from the definition
+ */
+ protected $custom_tokens = array(
+ 'email' => array('{DESCRIPTION}' => '{TEXT}'),
+ 'flash' => array('{WIDTH}' => '{NUMBER1}', '{HEIGHT}' => '{NUMBER2}'),
+ 'img' => array('{URL}' => '{IMAGEURL}'),
+ 'list' => array('{LIST_TYPE}' => '{HASHMAP}'),
+ 'quote' => array('{USERNAME}' => '{TEXT1}'),
+ 'size' => array('{SIZE}' => '{FONTSIZE}'),
+ 'url' => array('{DESCRIPTION}' => '{TEXT}'),
+ );
+
+ /**
+ * @var \phpbb\textformatter\data_access
+ */
+ protected $data_access;
+
+ /**
+ * @var array Default BBCode definitions
+ */
+ protected $default_definitions = array(
+ 'attachment' => '[ATTACHMENT index={NUMBER} filename={TEXT;useContent}]',
+ 'b' => '[B]{TEXT}[/B]',
+ 'code' => '[CODE]{TEXT}[/CODE]',
+ 'color' => '[COLOR={COLOR}]{TEXT}[/COLOR]',
+ 'email' => '[EMAIL={EMAIL;useContent}]{TEXT}[/EMAIL]',
+ 'flash' => '[FLASH={NUMBER1},{NUMBER2} width={NUMBER1;postFilter=#flashwidth} height={NUMBER2;postFilter=#flashheight} url={URL;useContent} /]',
+ 'i' => '[I]{TEXT}[/I]',
+ 'img' => '[IMG src={IMAGEURL;useContent}]',
+ 'list' => '[LIST type={HASHMAP=1:decimal,a:lower-alpha,A:upper-alpha,i:lower-roman,I:upper-roman;optional;postFilter=#simpletext}]{TEXT}[/LIST]',
+ 'li' => '[* $tagName=LI]{TEXT}[/*]',
+ 'quote' =>
+ "[QUOTE
+ author={TEXT1;optional}
+ url={URL;optional}
+ author={PARSE=/^\\[url=(?'url'.*?)](?'author'.*)\\[\\/url]$/i}
+ author={PARSE=/^\\[url](?'author'(?'url'.*?))\\[\\/url]$/i}
+ author={PARSE=/(?'url'https?:\\/\\/[^[\\]]+)/i}
+ ]{TEXT2}[/QUOTE]",
+ 'size' => '[SIZE={FONTSIZE}]{TEXT}[/SIZE]',
+ 'u' => '[U]{TEXT}[/U]',
+ 'url' => '[URL={URL;useContent}]{TEXT}[/URL]',
+ );
+
+ /**
+ * @var array Default templates, taken from bbcode::bbcode_tpl()
+ */
+ protected $default_templates = array(
+ 'b' => '<span style="font-weight: bold"><xsl:apply-templates/></span>',
+ 'i' => '<span style="font-style: italic"><xsl:apply-templates/></span>',
+ 'u' => '<span style="text-decoration: underline"><xsl:apply-templates/></span>',
+ 'img' => '<img src="{IMAGEURL}" alt="{L_IMAGE}"/>',
+ 'size' => '<span style="font-size: {FONTSIZE}%; line-height: normal"><xsl:apply-templates/></span>',
+ 'color' => '<span style="color: {COLOR}"><xsl:apply-templates/></span>',
+ 'email' => '<a href="mailto:{EMAIL}"><xsl:apply-templates/></a>',
+ );
+
+ /**
+ * @var \phpbb\event\dispatcher_interface
+ */
+ protected $dispatcher;
+
+ /**
+ * Constructor
+ *
+ * @param \phpbb\textformatter\data_access $data_access
+ * @param \phpbb\cache\driver\driver_interface $cache
+ * @param \phpbb\event\dispatcher_interface $dispatcher
+ * @param string $cache_dir Path to the cache dir
+ * @param string $cache_key_parser Cache key used for the parser
+ * @param string $cache_key_renderer Cache key used for the renderer
+ */
+ public function __construct(\phpbb\textformatter\data_access $data_access, \phpbb\cache\driver\driver_interface $cache, \phpbb\event\dispatcher_interface $dispatcher, $cache_dir, $cache_key_parser, $cache_key_renderer)
+ {
+ $this->cache = $cache;
+ $this->cache_dir = $cache_dir;
+ $this->cache_key_parser = $cache_key_parser;
+ $this->cache_key_renderer = $cache_key_renderer;
+ $this->data_access = $data_access;
+ $this->dispatcher = $dispatcher;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function invalidate()
+ {
+ $this->regenerate();
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * Will remove old renderers from the cache dir but won't touch the current renderer
+ */
+ public function tidy()
+ {
+ // Get the name of current renderer
+ $renderer_data = $this->cache->get($this->cache_key_renderer);
+ $renderer_file = ($renderer_data) ? $renderer_data['class'] . '.php' : null;
+
+ foreach (glob($this->cache_dir . 's9e_*') as $filename)
+ {
+ // Only remove the file if it's not the current renderer
+ if (!$renderer_file || substr($filename, -strlen($renderer_file)) !== $renderer_file)
+ {
+ unlink($filename);
+ }
+ }
+ }
+
+ /**
+ * Generate and return a new configured instance of s9e\TextFormatter\Configurator
+ *
+ * @return Configurator
+ */
+ public function get_configurator()
+ {
+ // Create a new Configurator
+ $configurator = new Configurator;
+
+ /**
+ * Modify the s9e\TextFormatter configurator before the default settings are set
+ *
+ * @event core.text_formatter_s9e_configure_before
+ * @var \s9e\TextFormatter\Configurator configurator Configurator instance
+ * @since 3.2.0-a1
+ */
+ $vars = array('configurator');
+ extract($this->dispatcher->trigger_event('core.text_formatter_s9e_configure_before', compact($vars)));
+
+ // Convert newlines to br elements by default
+ $configurator->rootRules->enableAutoLineBreaks();
+
+ // Don't automatically ignore text in places where text is not allowed
+ $configurator->rulesGenerator->remove('IgnoreTextIfDisallowed');
+
+ // Don't remove comments and instead convert them to xsl:comment elements
+ $configurator->templateNormalizer->remove('RemoveComments');
+ $configurator->templateNormalizer->add('TransposeComments');
+
+ // Set the rendering engine and configure it to save to the cache dir
+ $configurator->rendering->engine = 'PHP';
+ $configurator->rendering->engine->cacheDir = $this->cache_dir;
+ $configurator->rendering->engine->defaultClassPrefix = 's9e_renderer_';
+ $configurator->rendering->engine->enableQuickRenderer = true;
+
+ // Create custom filters for BBCode tokens that are supported in phpBB but not in
+ // s9e\TextFormatter
+ $filter = new RegexpFilter('#^' . get_preg_expression('relative_url') . '$#Du');
+ $configurator->attributeFilters->add('#local_url', $filter);
+ $configurator->attributeFilters->add('#relative_url', $filter);
+
+ // INTTEXT regexp from acp_bbcodes
+ $filter = new RegexpFilter('!^([\p{L}\p{N}\-+,_. ]+)$!Du');
+ $configurator->attributeFilters->add('#inttext', $filter);
+
+ // Create custom filters for Flash restrictions, which use the same values as the image
+ // restrictions but have their own error message
+ $configurator->attributeFilters
+ ->add('#flashheight', __NAMESPACE__ . '\\parser::filter_flash_height')
+ ->addParameterByName('max_img_height')
+ ->addParameterByName('logger');
+
+ $configurator->attributeFilters
+ ->add('#flashwidth', __NAMESPACE__ . '\\parser::filter_flash_width')
+ ->addParameterByName('max_img_width')
+ ->addParameterByName('logger');
+
+ // Create a custom filter for phpBB's per-mode font size limits
+ $configurator->attributeFilters
+ ->add('#fontsize', __NAMESPACE__ . '\\parser::filter_font_size')
+ ->addParameterByName('max_font_size')
+ ->addParameterByName('logger')
+ ->markAsSafeInCSS();
+
+ // Create a custom filter for image URLs
+ $configurator->attributeFilters
+ ->add('#imageurl', __NAMESPACE__ . '\\parser::filter_img_url')
+ ->addParameterByName('urlConfig')
+ ->addParameterByName('logger')
+ ->addParameterByName('max_img_height')
+ ->addParameterByName('max_img_width')
+ ->markAsSafeAsURL();
+
+ // Add default BBCodes
+ foreach ($this->get_default_bbcodes($configurator) as $bbcode)
+ {
+ $configurator->BBCodes->addCustom($bbcode['usage'], $bbcode['template']);
+ }
+
+ // Modify the template to disable images/flash depending on user's settings
+ foreach (array('FLASH', 'IMG') as $name)
+ {
+ $tag = $configurator->tags[$name];
+ $tag->template = '<xsl:choose><xsl:when test="$S_VIEW' . $name . '">' . $tag->template . '</xsl:when><xsl:otherwise><xsl:apply-templates/></xsl:otherwise></xsl:choose>';
+ }
+
+ // Load custom BBCodes
+ foreach ($this->data_access->get_bbcodes() as $row)
+ {
+ // Insert the board's URL before {LOCAL_URL} tokens
+ $tpl = preg_replace_callback(
+ '#\\{LOCAL_URL\\d*\\}#',
+ function ($m)
+ {
+ return generate_board_url() . '/' . $m[0];
+ },
+ $row['bbcode_tpl']
+ );
+
+ try
+ {
+ $configurator->BBCodes->addCustom($row['bbcode_match'], new UnsafeTemplate($tpl));
+ }
+ catch (\Exception $e)
+ {
+ /**
+ * @todo log an error?
+ */
+ }
+ }
+
+ // Load smilies
+ foreach ($this->data_access->get_smilies() as $row)
+ {
+ $configurator->Emoticons->add(
+ $row['code'],
+ '<img class="smilies" src="{$T_SMILIES_PATH}/' . htmlspecialchars($row['smiley_url']) . '" alt="{.}" title="' . htmlspecialchars($row['emotion']) . '"/>'
+ );
+ }
+
+ if (isset($configurator->Emoticons))
+ {
+ // Force emoticons to be rendered as text if $S_VIEWSMILIES is not set
+ $configurator->Emoticons->notIfCondition = 'not($S_VIEWSMILIES)';
+
+ // Only parse emoticons at the beginning of the text or if they're preceded by any
+ // one of: a new line, a space, a dot, or a right square bracket
+ $configurator->Emoticons->notAfter = '[^\\n .\\]]';
+ }
+
+ // Load the censored words
+ $censor = $this->data_access->get_censored_words();
+ if (!empty($censor))
+ {
+ // Use a namespaced tag to avoid collisions
+ $configurator->plugins->load('Censor', array('tagName' => 'censor:tag'));
+ foreach ($censor as $row)
+ {
+ // NOTE: words are stored as HTML, we need to decode them to plain text
+ $configurator->Censor->add(htmlspecialchars_decode($row['word']), htmlspecialchars_decode($row['replacement']));
+ }
+ }
+
+ // Load the magic links plugins. We do that after BBCodes so that they use the same tags
+ $configurator->plugins->load('Autoemail');
+ $configurator->plugins->load('Autolink', array('matchWww' => true));
+
+ // Register some vars with a default value. Those should be set at runtime by whatever calls
+ // the parser
+ $configurator->registeredVars['max_font_size'] = 0;
+ $configurator->registeredVars['max_img_height'] = 0;
+ $configurator->registeredVars['max_img_width'] = 0;
+
+ /**
+ * Modify the s9e\TextFormatter configurator after the default settings are set
+ *
+ * @event core.text_formatter_s9e_configure_after
+ * @var \s9e\TextFormatter\Configurator configurator Configurator instance
+ * @since 3.2.0-a1
+ */
+ $vars = array('configurator');
+ extract($this->dispatcher->trigger_event('core.text_formatter_s9e_configure_after', compact($vars)));
+
+ return $configurator;
+ }
+
+ /**
+ * Regenerate and cache a new parser and renderer
+ *
+ * @return array Associative array with at least two elements: "parser" and "renderer"
+ */
+ public function regenerate()
+ {
+ $configurator = $this->get_configurator();
+
+ // Get the censor helper and remove the Censor plugin if applicable
+ if (isset($configurator->Censor))
+ {
+ $censor = $configurator->Censor->getHelper();
+ unset($configurator->Censor);
+ unset($configurator->tags['censor:tag']);
+ }
+
+ $objects = $configurator->finalize();
+ $parser = $objects['parser'];
+ $renderer = $objects['renderer'];
+
+ // Cache the parser as-is
+ $this->cache->put($this->cache_key_parser, $parser);
+
+ // We need to cache the name of the renderer's generated class
+ $renderer_data = array('class' => get_class($renderer));
+ if (isset($censor))
+ {
+ $renderer_data['censor'] = $censor;
+ }
+ $this->cache->put($this->cache_key_renderer, $renderer_data);
+
+ return array('parser' => $parser, 'renderer' => $renderer);
+ }
+
+ /**
+ * Return the default BBCodes configuration
+ *
+ * @return array 2D array. Each element has a 'usage' key, a 'template' key, and an optional 'options' key
+ */
+ protected function get_default_bbcodes($configurator)
+ {
+ // For each BBCode, build an associative array matching style_ids to their template
+ $templates = array();
+ foreach ($this->data_access->get_styles_templates() as $style_id => $data)
+ {
+ foreach ($this->extract_templates($data['template']) as $bbcode_name => $template)
+ {
+ $templates[$bbcode_name][$style_id] = $template;
+ }
+
+ // Add default templates wherever missing, or for BBCodes that were not specified in
+ // this template's bitfield. For instance, prosilver has a custom template for b but its
+ // bitfield does not enable it so the default template is used instead
+ foreach ($this->default_templates as $bbcode_name => $template)
+ {
+ if (!isset($templates[$bbcode_name][$style_id]) || !in_array($bbcode_name, $data['bbcodes'], true))
+ {
+ $templates[$bbcode_name][$style_id] = $template;
+ }
+ }
+ }
+
+ // Replace custom tokens and normalize templates
+ foreach ($templates as $bbcode_name => $style_templates)
+ {
+ foreach ($style_templates as $i => $template)
+ {
+ if (isset($this->custom_tokens[$bbcode_name]))
+ {
+ $template = strtr($template, $this->custom_tokens[$bbcode_name]);
+ }
+
+ $templates[$bbcode_name][$i] = $configurator->templateNormalizer->normalizeTemplate($template);
+ }
+ }
+
+ $bbcodes = array();
+ foreach ($this->default_definitions as $bbcode_name => $usage)
+ {
+ $bbcodes[$bbcode_name] = array(
+ 'usage' => $usage,
+ 'template' => $this->merge_templates($templates[$bbcode_name]),
+ );
+ }
+
+ return $bbcodes;
+ }
+
+ /**
+ * Extract and recompose individual BBCode templates from a style's template file
+ *
+ * @param string $template Style template (bbcode.html)
+ * @return array Associative array matching BBCode names to their template
+ */
+ protected function extract_templates($template)
+ {
+ // Capture the template fragments
+ preg_match_all('#<!-- BEGIN (.*?) -->(.*?)<!-- END .*? -->#s', $template, $matches, PREG_SET_ORDER);
+
+ $fragments = array();
+ foreach ($matches as $match)
+ {
+ // Normalize the whitespace
+ $fragment = preg_replace('#>\\n\\t*<#', '><', trim($match[2]));
+
+ $fragments[$match[1]] = $fragment;
+ }
+
+ // Automatically recompose templates split between *_open and *_close
+ foreach ($fragments as $fragment_name => $fragment)
+ {
+ if (preg_match('#^(\\w+)_close$#', $fragment_name, $match))
+ {
+ $bbcode_name = $match[1];
+
+ if (isset($fragments[$bbcode_name . '_open']))
+ {
+ $templates[$bbcode_name] = $fragments[$bbcode_name . '_open'] . '<xsl:apply-templates/>' . $fragment;
+ }
+ }
+ }
+
+ // Manually recompose and overwrite irregular templates
+ $templates['list'] =
+ '<xsl:choose>
+ <xsl:when test="not(@type)">
+ ' . $fragments['ulist_open_default'] . '<xsl:apply-templates/>' . $fragments['ulist_close'] . '
+ </xsl:when>
+ <xsl:when test="contains(\'upperlowerdecim\',substring(@type,1,5))">
+ ' . $fragments['olist_open'] . '<xsl:apply-templates/>' . $fragments['olist_close'] . '
+ </xsl:when>
+ <xsl:otherwise>
+ ' . $fragments['ulist_open'] . '<xsl:apply-templates/>' . $fragments['ulist_close'] . '
+ </xsl:otherwise>
+ </xsl:choose>';
+
+ $templates['li'] = $fragments['listitem'] . '<xsl:apply-templates/>' . $fragments['listitem_close'];
+
+ $fragments['quote_username_open'] = str_replace(
+ '{USERNAME}',
+ '<xsl:choose>
+ <xsl:when test="@url">' . str_replace('{DESCRIPTION}', '{USERNAME}', $fragments['url']) . '</xsl:when>
+ <xsl:otherwise>{USERNAME}</xsl:otherwise>
+ </xsl:choose>',
+ $fragments['quote_username_open']
+ );
+
+ $templates['quote'] =
+ '<xsl:choose>
+ <xsl:when test="@author">
+ ' . $fragments['quote_username_open'] . '<xsl:apply-templates/>' . $fragments['quote_close'] . '
+ </xsl:when>
+ <xsl:otherwise>
+ ' . $fragments['quote_open'] . '<xsl:apply-templates/>' . $fragments['quote_close'] . '
+ </xsl:otherwise>
+ </xsl:choose>';
+
+ // The [attachment] BBCode uses the inline_attachment template to output a comment that
+ // is post-processed by parse_attachments()
+ $templates['attachment'] = $fragments['inline_attachment_open'] . '<xsl:comment> ia<xsl:value-of select="@index"/> </xsl:comment><xsl:value-of select="@filename"/><xsl:comment> ia<xsl:value-of select="@index"/> </xsl:comment>' . $fragments['inline_attachment_close'];
+
+ // Add fragments as templates
+ foreach ($fragments as $fragment_name => $fragment)
+ {
+ if (preg_match('#^\\w+$#', $fragment_name))
+ {
+ $templates[$fragment_name] = $fragment;
+ }
+ }
+
+ // Keep only templates that are named after an existing BBCode
+ $templates = array_intersect_key($templates, $this->default_definitions);
+
+ return $templates;
+ }
+
+ /**
+ * Merge the templates from any number of styles into one BBCode template
+ *
+ * When multiple templates are available for the same BBCode (because of multiple styles) we
+ * merge them into a single template that uses an xsl:choose construct that determines which
+ * style to use at rendering time.
+ *
+ * @param array $style_templates Associative array matching style_ids to their template
+ * @return string
+ */
+ protected function merge_templates(array $style_templates)
+ {
+ // Return the template as-is if there's only one style or all styles share the same template
+ if (count(array_unique($style_templates)) === 1)
+ {
+ return end($style_templates);
+ }
+
+ // Group identical templates together
+ $grouped_templates = array();
+ foreach ($style_templates as $style_id => $style_template)
+ {
+ $grouped_templates[$style_template][] = '$STYLE_ID=' . $style_id;
+ }
+
+ // Sort templates by frequency descending
+ $templates_cnt = array_map('sizeof', $grouped_templates);
+ array_multisort($grouped_templates, $templates_cnt);
+
+ // Remove the most frequent template from the list; It becomes the default
+ reset($grouped_templates);
+ $default_template = key($grouped_templates);
+ unset($grouped_templates[$default_template]);
+
+ // Build an xsl:choose switch
+ $template = '<xsl:choose>';
+ foreach ($grouped_templates as $style_template => $exprs)
+ {
+ $template .= '<xsl:when test="' . implode(' or ', $exprs) . '">' . $style_template . '</xsl:when>';
+ }
+ $template .= '<xsl:otherwise>' . $default_template . '</xsl:otherwise></xsl:choose>';
+
+ return $template;
+ }
+}
diff --git a/phpBB/phpbb/textformatter/s9e/parser.php b/phpBB/phpbb/textformatter/s9e/parser.php
new file mode 100644
index 0000000000..b7d0b2b90b
--- /dev/null
+++ b/phpBB/phpbb/textformatter/s9e/parser.php
@@ -0,0 +1,390 @@
+<?php
+/**
+*
+* This file is part of the phpBB Forum Software package.
+*
+* @copyright (c) phpBB Limited <https://www.phpbb.com>
+* @license GNU General Public License, version 2 (GPL-2.0)
+*
+* For full copyright and license information, please see
+* the docs/CREDITS.txt file.
+*
+*/
+
+namespace phpbb\textformatter\s9e;
+
+use s9e\TextFormatter\Parser\BuiltInFilters;
+use s9e\TextFormatter\Parser\Logger;
+
+/**
+* s9e\TextFormatter\Parser adapter
+*/
+class parser implements \phpbb\textformatter\parser_interface
+{
+ /**
+ * @var \phpbb\event\dispatcher_interface
+ */
+ protected $dispatcher;
+
+ /**
+ * @var \s9e\TextFormatter\Parser
+ */
+ protected $parser;
+
+ /**
+ * Constructor
+ *
+ * @param \phpbb\cache\driver_interface $cache
+ * @param string $key Cache key
+ * @param factory $factory
+ * @param \phpbb\event\dispatcher_interface $dispatcher
+ */
+ public function __construct(\phpbb\cache\driver\driver_interface $cache, $key, factory $factory, \phpbb\event\dispatcher_interface $dispatcher)
+ {
+ $parser = $cache->get($key);
+ if (!$parser)
+ {
+ $objects = $factory->regenerate();
+ $parser = $objects['parser'];
+ }
+
+ $this->dispatcher = $dispatcher;
+ $this->parser = $parser;
+ $parser = $this;
+
+ /**
+ * Configure the parser service
+ *
+ * Can be used to:
+ * - toggle features or BBCodes
+ * - register variables or custom parsers in the s9e\TextFormatter parser
+ * - configure the s9e\TextFormatter parser's runtime settings
+ *
+ * @event core.text_formatter_s9e_parser_setup
+ * @var \phpbb\textformatter\s9e\parser parser This parser service
+ * @since 3.2.0-a1
+ */
+ $vars = array('parser');
+ extract($dispatcher->trigger_event('core.text_formatter_s9e_parser_setup', compact($vars)));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function parse($text)
+ {
+ $parser = $this;
+
+ /**
+ * Modify a text before it is parsed
+ *
+ * @event core.text_formatter_s9e_parse_before
+ * @var \phpbb\textformatter\s9e\parser parser This parser service
+ * @var string text The original text
+ * @since 3.2.0-a1
+ */
+ $vars = array('parser', 'text');
+ extract($this->dispatcher->trigger_event('core.text_formatter_s9e_parse_before', compact($vars)));
+
+ $xml = $this->parser->parse($text);
+
+ /**
+ * Modify a parsed text in its XML form
+ *
+ * @event core.text_formatter_s9e_parse_after
+ * @var \phpbb\textformatter\s9e\parser parser This parser service
+ * @var string xml The parsed text, in XML
+ * @since 3.2.0-a1
+ */
+ $vars = array('parser', 'xml');
+ extract($this->dispatcher->trigger_event('core.text_formatter_s9e_parse_after', compact($vars)));
+
+ return $xml;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function disable_bbcode($name)
+ {
+ $this->parser->disableTag(strtoupper($name));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function disable_bbcodes()
+ {
+ $this->parser->disablePlugin('BBCodes');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function disable_censor()
+ {
+ $this->parser->disablePlugin('Censor');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function disable_magic_url()
+ {
+ $this->parser->disablePlugin('Autoemail');
+ $this->parser->disablePlugin('Autolink');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function disable_smilies()
+ {
+ $this->parser->disablePlugin('Emoticons');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function enable_bbcode($name)
+ {
+ $this->parser->enableTag(strtoupper($name));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function enable_bbcodes()
+ {
+ $this->parser->enablePlugin('BBCodes');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function enable_censor()
+ {
+ $this->parser->enablePlugin('Censor');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function enable_magic_url()
+ {
+ $this->parser->enablePlugin('Autoemail');
+ $this->parser->enablePlugin('Autolink');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function enable_smilies()
+ {
+ $this->parser->enablePlugin('Emoticons');
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * This will convert the log entries found in s9e\TextFormatter's logger into phpBB error
+ * messages
+ */
+ public function get_errors()
+ {
+ $errors = array();
+ foreach ($this->parser->getLogger()->get() as $entry)
+ {
+ list($type, $msg, $context) = $entry;
+
+ if ($msg === 'Tag limit exceeded')
+ {
+ if ($context['tagName'] === 'E')
+ {
+ $errors[] = array('TOO_MANY_SMILIES', $context['tagLimit']);
+ }
+ else if ($context['tagName'] === 'URL')
+ {
+ $errors[] = array('TOO_MANY_URLS', $context['tagLimit']);
+ }
+ }
+ else if ($msg === 'MAX_FONT_SIZE_EXCEEDED')
+ {
+ $errors[] = array($msg, $context['max_size']);
+ }
+ else if (preg_match('/^MAX_(?:FLASH|IMG)_(HEIGHT|WIDTH)_EXCEEDED$/D', $msg, $m))
+ {
+ $errors[] = array($msg, $context['max_' . strtolower($m[1])]);
+ }
+ else if ($msg === 'Tag is disabled')
+ {
+ $name = strtolower($context['tag']->getName());
+ $errors[] = array('UNAUTHORISED_BBCODE', '[' . $name . ']');
+ }
+ else if ($msg === 'UNABLE_GET_IMAGE_SIZE')
+ {
+ $errors[] = array($msg);
+ }
+ }
+
+ return array_unique($errors);
+ }
+
+ /**
+ * Return the instance of s9e\TextFormatter\Parser used by this object
+ *
+ * @return \s9e\TextFormatter\Parser
+ */
+ public function get_parser()
+ {
+ return $this->parser;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function set_var($name, $value)
+ {
+ if ($name === 'max_smilies')
+ {
+ $this->parser->setTagLimit('E', $value ?: PHP_INT_MAX);
+ }
+ else if ($name === 'max_urls')
+ {
+ $this->parser->setTagLimit('URL', $value ?: PHP_INT_MAX);
+ }
+ else
+ {
+ $this->parser->registeredVars[$name] = $value;
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function set_vars(array $vars)
+ {
+ foreach ($vars as $name => $value)
+ {
+ $this->set_var($name, $value);
+ }
+ }
+
+ /**
+ * Filter a flash object's height
+ *
+ * @see bbcode_firstpass::bbcode_flash()
+ *
+ * @param string $height
+ * @param integer $max_height
+ * @param Logger $logger
+ * @return mixed Original value if valid, FALSE otherwise
+ */
+ static public function filter_flash_height($height, $max_height, Logger $logger)
+ {
+ if ($max_height && $height > $max_height)
+ {
+ $logger->err('MAX_FLASH_HEIGHT_EXCEEDED', array('max_height' => $max_height));
+
+ return false;
+ }
+
+ return $height;
+ }
+
+ /**
+ * Filter a flash object's width
+ *
+ * @see bbcode_firstpass::bbcode_flash()
+ *
+ * @param string $width
+ * @param integer $max_width
+ * @param Logger $logger
+ * @return mixed Original value if valid, FALSE otherwise
+ */
+ static public function filter_flash_width($width, $max_width, Logger $logger)
+ {
+ if ($max_width && $width > $max_width)
+ {
+ $logger->err('MAX_FLASH_WIDTH_EXCEEDED', array('max_width' => $max_width));
+
+ return false;
+ }
+
+ return $width;
+ }
+
+ /**
+ * Filter the value used in a [size] BBCode
+ *
+ * @see bbcode_firstpass::bbcode_size()
+ *
+ * @param string $size Original size
+ * @param integer $max_size Maximum allowed size
+ * @param Logger $logger
+ * @return mixed Original value if valid, FALSE otherwise
+ */
+ static public function filter_font_size($size, $max_size, Logger $logger)
+ {
+ if ($max_size && $size > $max_size)
+ {
+ $logger->err('MAX_FONT_SIZE_EXCEEDED', array('max_size' => $max_size));
+
+ return false;
+ }
+
+ if ($size < 1)
+ {
+ return false;
+ }
+
+ return $size;
+ }
+
+ /**
+ * Filter an image's URL to enforce restrictions on its dimensions
+ *
+ * @see bbcode_firstpass::bbcode_img()
+ *
+ * @param string $url Original URL
+ * @param array $url_config Config used by the URL filter
+ * @param Logger $logger
+ * @param integer $max_height Maximum height allowed
+ * @param integer $max_width Maximum width allowed
+ * @return string|bool Original value if valid, FALSE otherwise
+ */
+ static public function filter_img_url($url, array $url_config, Logger $logger, $max_height, $max_width)
+ {
+ // Validate the URL
+ $url = BuiltInFilters::filterUrl($url, $url_config, $logger);
+ if ($url === false)
+ {
+ return false;
+ }
+
+ if ($max_height || $max_width)
+ {
+ $imagesize = new \fastImageSize\fastImageSize();
+ $size_info = $imagesize->getImageSize($url);
+ if ($size_info === false)
+ {
+ $logger->err('UNABLE_GET_IMAGE_SIZE');
+ return false;
+ }
+
+ if ($max_height && $max_height < $size_info['height'])
+ {
+ $logger->err('MAX_IMG_HEIGHT_EXCEEDED', array('max_height' => $max_height));
+ return false;
+ }
+
+ if ($max_width && $max_width < $size_info['width'])
+ {
+ $logger->err('MAX_IMG_WIDTH_EXCEEDED', array('max_width' => $max_width));
+ return false;
+ }
+ }
+
+ return $url;
+ }
+}
diff --git a/phpBB/phpbb/textformatter/s9e/renderer.php b/phpBB/phpbb/textformatter/s9e/renderer.php
new file mode 100644
index 0000000000..8999f1d25f
--- /dev/null
+++ b/phpBB/phpbb/textformatter/s9e/renderer.php
@@ -0,0 +1,338 @@
+<?php
+/**
+*
+* This file is part of the phpBB Forum Software package.
+*
+* @copyright (c) phpBB Limited <https://www.phpbb.com>
+* @license GNU General Public License, version 2 (GPL-2.0)
+*
+* For full copyright and license information, please see
+* the docs/CREDITS.txt file.
+*
+*/
+
+namespace phpbb\textformatter\s9e;
+
+/**
+* s9e\TextFormatter\Renderer adapter
+*/
+class renderer implements \phpbb\textformatter\renderer_interface
+{
+ /**
+ * @var \s9e\TextFormatter\Plugins\Censor\Helper
+ */
+ protected $censor;
+
+ /**
+ * @var \phpbb\event\dispatcher_interface
+ */
+ protected $dispatcher;
+
+ /**
+ * @var \s9e\TextFormatter\Renderer
+ */
+ protected $renderer;
+
+ /**
+ * @var bool Status of the viewcensors option
+ */
+ protected $viewcensors = false;
+
+ /**
+ * @var bool Status of the viewflash option
+ */
+ protected $viewflash = false;
+
+ /**
+ * @var bool Status of the viewimg option
+ */
+ protected $viewimg = false;
+
+ /**
+ * @var bool Status of the viewsmilies option
+ */
+ protected $viewsmilies = false;
+
+ /**
+ * Constructor
+ *
+ * @param \phpbb\cache\driver\driver_interface $cache
+ * @param string $cache_dir Path to the cache dir
+ * @param string $key Cache key
+ * @param factory $factory
+ * @param \phpbb\event\dispatcher_interface $dispatcher
+ */
+ public function __construct(\phpbb\cache\driver\driver_interface $cache, $cache_dir, $key, factory $factory, \phpbb\event\dispatcher_interface $dispatcher)
+ {
+ $renderer_data = $cache->get($key);
+ if ($renderer_data)
+ {
+ $class = $renderer_data['class'];
+ if (!class_exists($class, false))
+ {
+ // Try to load the renderer class from its cache file
+ $cache_file = $cache_dir . $class . '.php';
+
+ if (file_exists($cache_file))
+ {
+ include($cache_file);
+ }
+ }
+ if (class_exists($class, false))
+ {
+ $renderer = new $class;
+ }
+ if (isset($renderer_data['censor']))
+ {
+ $censor = $renderer_data['censor'];
+ }
+ }
+ if (!isset($renderer))
+ {
+ $objects = $factory->regenerate();
+ $renderer = $objects['renderer'];
+ }
+
+ if (isset($censor))
+ {
+ $this->censor = $censor;
+ }
+ $this->dispatcher = $dispatcher;
+ $this->renderer = $renderer;
+ $renderer = $this;
+
+ /**
+ * Configure the renderer service
+ *
+ * @event core.text_formatter_s9e_renderer_setup
+ * @var \phpbb\textformatter\s9e\renderer renderer This renderer service
+ * @since 3.2.0-a1
+ */
+ $vars = array('renderer');
+ extract($dispatcher->trigger_event('core.text_formatter_s9e_renderer_setup', compact($vars)));
+ }
+
+ /**
+ * Automatically set the smilies path based on config
+ *
+ * @param \phpbb\config\config $config
+ * @param \phpbb\path_helper $path_helper
+ * @return null
+ */
+ public function configure_smilies_path(\phpbb\config\config $config, \phpbb\path_helper $path_helper)
+ {
+ /**
+ * @see smiley_text()
+ */
+ $root_path = (defined('PHPBB_USE_BOARD_URL_PATH') && PHPBB_USE_BOARD_URL_PATH) ? generate_board_url() . '/' : $path_helper->get_web_root_path();
+
+ $this->set_smilies_path($root_path . $config['smilies_path']);
+ }
+
+ /**
+ * Configure this renderer as per the user's settings
+ *
+ * Should set the locale as well as the viewcensor/viewflash/viewimg/viewsmilies options.
+ *
+ * @param \phpbb\user $user
+ * @param \phpbb\config\config $config
+ * @param \phpbb\auth\auth $auth
+ * @return null
+ */
+ public function configure_user(\phpbb\user $user, \phpbb\config\config $config, \phpbb\auth\auth $auth)
+ {
+ $censor = $user->optionget('viewcensors') || !$config['allow_nocensors'] || !$auth->acl_get('u_chgcensors');
+
+ $this->set_viewcensors($censor);
+ $this->set_viewflash($user->optionget('viewflash'));
+ $this->set_viewimg($user->optionget('viewimg'));
+ $this->set_viewsmilies($user->optionget('viewsmilies'));
+
+ // Set the stylesheet parameters
+ foreach (array_keys($this->renderer->getParameters()) as $param_name)
+ {
+ if (strpos($param_name, 'L_') === 0)
+ {
+ // L_FOO is set to $user->lang('FOO')
+ $this->renderer->setParameter($param_name, $user->lang(substr($param_name, 2)));
+ }
+ }
+
+ // Set this user's style id and other parameters
+ $this->renderer->setParameters(array(
+ 'S_IS_BOT' => $user->data['is_bot'],
+ 'S_REGISTERED_USER' => $user->data['is_registered'],
+ 'S_USER_LOGGED_IN' => ($user->data['user_id'] != ANONYMOUS),
+ 'STYLE_ID' => $user->style['style_id'],
+ ));
+ }
+
+ /**
+ * Return the instance of s9e\TextFormatter\Renderer used by this object
+ *
+ * @return \s9e\TextFormatter\Renderer
+ */
+ public function get_renderer()
+ {
+ return $this->renderer;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function get_viewcensors()
+ {
+ return $this->viewcensors;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function get_viewflash()
+ {
+ return $this->viewflash;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function get_viewimg()
+ {
+ return $this->viewimg;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function get_viewsmilies()
+ {
+ return $this->viewsmilies;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function render($xml)
+ {
+ $renderer = $this;
+
+ /**
+ * Modify a parsed text before it is rendered
+ *
+ * @event core.text_formatter_s9e_render_before
+ * @var \phpbb\textformatter\s9e\renderer renderer This renderer service
+ * @var string xml The parsed text, in its XML form
+ * @since 3.2.0-a1
+ */
+ $vars = array('renderer', 'xml');
+ extract($this->dispatcher->trigger_event('core.text_formatter_s9e_render_before', compact($vars)));
+
+ if (isset($this->censor) && $this->viewcensors)
+ {
+ // NOTE: censorHtml() is XML-safe
+ $xml = $this->censor->censorHtml($xml, true);
+ }
+
+ $html = $this->renderer->render($xml);
+ if (stripos($html, '<code') !== false)
+ {
+ $html = $this->replace_tabs_in_code($html);
+ }
+
+ /**
+ * Modify a rendered text
+ *
+ * @event core.text_formatter_s9e_render_after
+ * @var string html The rendered text's HTML
+ * @var \phpbb\textformatter\s9e\renderer renderer This renderer service
+ * @since 3.2.0-a1
+ */
+ $vars = array('html', 'renderer');
+ extract($this->dispatcher->trigger_event('core.text_formatter_s9e_render_after', compact($vars)));
+
+ return $html;
+ }
+
+ /**
+ * Replace tabs in code elements
+ *
+ * @see bbcode::bbcode_second_pass_code()
+ *
+ * @param string $html Original HTML
+ * @return string Modified HTML
+ */
+ protected function replace_tabs_in_code($html)
+ {
+ return preg_replace_callback(
+ '((<code[^>]*>)(.*?)(</code>))is',
+ function ($captures)
+ {
+ $code = $captures[2];
+
+ $code = str_replace("\t", '&nbsp; &nbsp;', $code);
+ $code = str_replace(' ', '&nbsp; ', $code);
+ $code = str_replace(' ', ' &nbsp;', $code);
+ $code = str_replace("\n ", "\n&nbsp;", $code);
+
+ // keep space at the beginning
+ if (!empty($code) && $code[0] == ' ')
+ {
+ $code = '&nbsp;' . substr($code, 1);
+ }
+
+ // remove newline at the beginning
+ if (!empty($code) && $code[0] == "\n")
+ {
+ $code = substr($code, 1);
+ }
+
+ return $captures[1] . $code . $captures[3];
+ },
+ $html
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function set_smilies_path($path)
+ {
+ $this->renderer->setParameter('T_SMILIES_PATH', $path);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function set_viewcensors($value)
+ {
+ $this->viewcensors = $value;
+ $this->renderer->setParameter('S_VIEWCENSORS', $value);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function set_viewflash($value)
+ {
+ $this->viewflash = $value;
+ $this->renderer->setParameter('S_VIEWFLASH', $value);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function set_viewimg($value)
+ {
+ $this->viewimg = $value;
+ $this->renderer->setParameter('S_VIEWIMG', $value);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function set_viewsmilies($value)
+ {
+ $this->viewsmilies = $value;
+ $this->renderer->setParameter('S_VIEWSMILIES', $value);
+ }
+}
diff --git a/phpBB/phpbb/textformatter/s9e/utils.php b/phpBB/phpbb/textformatter/s9e/utils.php
new file mode 100644
index 0000000000..e21dedecc4
--- /dev/null
+++ b/phpBB/phpbb/textformatter/s9e/utils.php
@@ -0,0 +1,85 @@
+<?php
+/**
+*
+* This file is part of the phpBB Forum Software package.
+*
+* @copyright (c) phpBB Limited <https://www.phpbb.com>
+* @license GNU General Public License, version 2 (GPL-2.0)
+*
+* For full copyright and license information, please see
+* the docs/CREDITS.txt file.
+*
+*/
+
+namespace phpbb\textformatter\s9e;
+
+/**
+* Text manipulation utilities
+*/
+class utils implements \phpbb\textformatter\utils_interface
+{
+ /**
+ * Replace BBCodes and other formatting elements with whitespace
+ *
+ * NOTE: preserves smilies as text
+ *
+ * @param string $xml Parsed text
+ * @return string Plain text
+ */
+ public function clean_formatting($xml)
+ {
+ // Insert a space before <s> and <e> then remove formatting
+ $xml = preg_replace('#<[es]>#', ' $0', $xml);
+
+ return \s9e\TextFormatter\Utils::removeFormatting($xml);
+ }
+
+ /**
+ * Get a list of quote authors, limited to the outermost quotes
+ *
+ * @param string $xml Parsed text
+ * @return string[] List of authors
+ */
+ public function get_outermost_quote_authors($xml)
+ {
+ $authors = array();
+ if (strpos($xml, '<QUOTE ') === false)
+ {
+ return $authors;
+ }
+
+ $dom = new \DOMDocument;
+ $dom->loadXML($xml);
+ $xpath = new \DOMXPath($dom);
+ foreach ($xpath->query('//QUOTE[not(ancestor::QUOTE)]/@author') as $author)
+ {
+ $authors[] = $author->textContent;
+ }
+
+ return $authors;
+ }
+
+ /**
+ * Remove given BBCode and its content, at given nesting depth
+ *
+ * @param string $xml Parsed text
+ * @param string $bbcode_name BBCode's name
+ * @param integer $depth Minimum nesting depth (number of parents of the same name)
+ * @return string Parsed text
+ */
+ public function remove_bbcode($xml, $bbcode_name, $depth = 0)
+ {
+ return \s9e\TextFormatter\Utils::removeTag($xml, strtoupper($bbcode_name), $depth);
+ }
+
+ /**
+ * Return a parsed text to its original form
+ *
+ * @param string $xml Parsed text
+ * @return string Original plain text
+ */
+ public function unparse($xml)
+ {
+ return \s9e\TextFormatter\Unparser::unparse($xml);
+ }
+}
diff --git a/phpBB/phpbb/textformatter/utils_interface.php b/phpBB/phpbb/textformatter/utils_interface.php
new file mode 100644
index 0000000000..6d3fd13021
--- /dev/null
+++ b/phpBB/phpbb/textformatter/utils_interface.php
@@ -0,0 +1,56 @@
+<?php
+/**
+*
+* This file is part of the phpBB Forum Software package.
+*
+* @copyright (c) phpBB Limited <https://www.phpbb.com>
+* @license GNU General Public License, version 2 (GPL-2.0)
+*
+* For full copyright and license information, please see
+* the docs/CREDITS.txt file.
+*
+*/
+
+namespace phpbb\textformatter;
+
+/**
+* Used to manipulate a parsed text
+*/
+interface utils_interface
+{
+ /**
+ * Replace BBCodes and other formatting elements with whitespace
+ *
+ * NOTE: preserves smilies as text
+ *
+ * @param string $text Parsed text
+ * @return string Plain text
+ */
+ public function clean_formatting($text);
+
+ /**
+ * Get a list of quote authors, limited to the outermost quotes
+ *
+ * @param string $text Parsed text
+ * @return string[] List of authors
+ */
+ public function get_outermost_quote_authors($text);
+
+ /**
+ * Remove given BBCode and its content, at given nesting depth
+ *
+ * @param string $text Parsed text
+ * @param string $bbcode_name BBCode's name
+ * @param integer $depth Minimum nesting depth (number of parents of the same name)
+ * @return string Parsed text
+ */
+ public function remove_bbcode($text, $bbcode_name, $depth = 0);
+
+ /**
+ * Return a parsed text to its original form
+ *
+ * @param string $text Parsed text
+ * @return string Original plain text
+ */
+ public function unparse($text);
+}
diff --git a/phpBB/phpbb/user.php b/phpBB/phpbb/user.php
index 882e9cef26..c33070d6f4 100644
--- a/phpBB/phpbb/user.php
+++ b/phpBB/phpbb/user.php
@@ -21,8 +21,11 @@ namespace phpbb;
*/
class user extends \phpbb\session
{
- var $lang = array();
- var $help = array();
+ /**
+ * @var \phpbb\language\language
+ */
+ protected $language;
+
var $style = array();
var $date_format;
@@ -42,35 +45,63 @@ class user extends \phpbb\session
var $img_lang;
var $img_array = array();
+ /** @var bool */
+ protected $is_setup_flag;
+
// Able to add new options (up to id 31)
var $keyoptions = array('viewimg' => 0, 'viewflash' => 1, 'viewsmilies' => 2, 'viewsigs' => 3, 'viewavatars' => 4, 'viewcensors' => 5, 'attachsig' => 6, 'bbcode' => 8, 'smilies' => 9, 'sig_bbcode' => 15, 'sig_smilies' => 16, 'sig_links' => 17);
/**
* Constructor to set the lang path
+ *
* @param string $datetime_class Class name of datetime class
+ * @param \phpbb\language\language $lang phpBB's Language loader
*/
- function __construct($datetime_class)
+ function __construct(\phpbb\language\language $lang, $datetime_class)
{
global $phpbb_root_path;
$this->lang_path = $phpbb_root_path . 'language/';
+ $this->language = $lang;
$this->datetime = $datetime_class;
+
+ $this->is_setup_flag = false;
}
/**
- * Function to set custom language path (able to use directory outside of phpBB)
- *
- * @param string $lang_path New language path used.
- * @access public
- */
- function set_custom_lang_path($lang_path)
+ * Returns whether user::setup was called
+ *
+ * @return bool
+ */
+ public function is_setup()
{
- $this->lang_path = $lang_path;
+ return $this->is_setup_flag;
+ }
- if (substr($this->lang_path, -1) != '/')
+ /**
+ * Magic getter for BC compatibility
+ *
+ * Implement array access for user::lang.
+ *
+ * @param string $param_name Name of the BC component the user want to access
+ *
+ * @return array The appropriate array
+ *
+ * @deprecated 3.2.0-dev (To be removed: 4.0.0)
+ */
+ public function __get($param_name)
+ {
+ if ($param_name === 'lang')
+ {
+ return $this->language->get_lang_array();
+ }
+ else if ($param_name === 'help')
{
- $this->lang_path .= '/';
+ $help_array = $this->language->get_lang_array();
+ return $help_array['__help'];
}
+
+ return array();
}
/**
@@ -81,6 +112,8 @@ class user extends \phpbb\session
global $db, $request, $template, $config, $auth, $phpEx, $phpbb_root_path, $cache;
global $phpbb_dispatcher;
+ $this->language->set_default_language($config['default_lang']);
+
if ($this->data['user_id'] != ANONYMOUS)
{
$user_lang_name = (file_exists($this->lang_path . $this->data['user_lang'] . "/common.$phpEx")) ? $this->data['user_lang'] : basename($config['default_lang']);
@@ -98,6 +131,7 @@ class user extends \phpbb\session
{
$lang_override = $request->variable($config['cookie_name'] . '_lang', '', true, \phpbb\request\request_interface::COOKIE);
}
+
if ($lang_override)
{
$use_lang = basename($lang_override);
@@ -108,6 +142,7 @@ class user extends \phpbb\session
{
$user_lang_name = basename($config['default_lang']);
}
+
$user_date_format = $config['default_dateformat'];
$user_timezone = $config['board_timezone'];
@@ -187,6 +222,8 @@ class user extends \phpbb\session
$this->lang_name = $user_lang_name;
$this->date_format = $user_date_format;
+ $this->language->set_user_language($user_lang_name);
+
try
{
$this->timezone = new \DateTimeZone($user_timezone);
@@ -197,17 +234,6 @@ class user extends \phpbb\session
$this->timezone = new \DateTimeZone('UTC');
}
- // We include common language file here to not load it every time a custom language file is included
- $lang = &$this->lang;
-
- // Do not suppress error if in DEBUG mode
- $include_result = (defined('DEBUG')) ? (include $this->lang_path . $this->lang_name . "/common.$phpEx") : (@include $this->lang_path . $this->lang_name . "/common.$phpEx");
-
- if ($include_result === false)
- {
- die('Language file ' . $this->lang_path . $this->lang_name . "/common.$phpEx" . " couldn't be opened.");
- }
-
$this->add_lang($lang_set);
unset($lang_set);
@@ -393,6 +419,8 @@ class user extends \phpbb\session
}
}
+ $this->is_setup_flag = true;
+
return;
}
@@ -406,103 +434,13 @@ class user extends \phpbb\session
*
* If the first parameter is an array, the elements are used as keys and subkeys to get the language entry:
* Example: <samp>$user->lang(array('datetime', 'AGO'), 1)</samp> uses $user->lang['datetime']['AGO'] as language entry.
+ *
+ * @deprecated 3.2.0-dev (To be removed 4.0.0)
*/
function lang()
{
$args = func_get_args();
- $key = $args[0];
-
- if (is_array($key))
- {
- $lang = &$this->lang[array_shift($key)];
-
- foreach ($key as $_key)
- {
- $lang = &$lang[$_key];
- }
- }
- else
- {
- $lang = &$this->lang[$key];
- }
-
- // Return if language string does not exist
- if (!isset($lang) || (!is_string($lang) && !is_array($lang)))
- {
- return $key;
- }
-
- // If the language entry is a string, we simply mimic sprintf() behaviour
- if (is_string($lang))
- {
- if (sizeof($args) == 1)
- {
- return $lang;
- }
-
- // Replace key with language entry and simply pass along...
- $args[0] = $lang;
- return call_user_func_array('sprintf', $args);
- }
- else if (sizeof($lang) == 0)
- {
- // If the language entry is an empty array, we just return the language key
- return $args[0];
- }
-
- // It is an array... now handle different nullar/singular/plural forms
- $key_found = false;
-
- // We now get the first number passed and will select the key based upon this number
- for ($i = 1, $num_args = sizeof($args); $i < $num_args; $i++)
- {
- if (is_int($args[$i]) || is_float($args[$i]))
- {
- if ($args[$i] == 0 && isset($lang[0]))
- {
- // We allow each translation using plural forms to specify a version for the case of 0 things,
- // so that "0 users" may be displayed as "No users".
- $key_found = 0;
- break;
- }
- else
- {
- $use_plural_form = $this->get_plural_form($args[$i]);
- if (isset($lang[$use_plural_form]))
- {
- // The key we should use exists, so we use it.
- $key_found = $use_plural_form;
- }
- else
- {
- // If the key we need to use does not exist, we fall back to the previous one.
- $numbers = array_keys($lang);
-
- foreach ($numbers as $num)
- {
- if ($num > $use_plural_form)
- {
- break;
- }
-
- $key_found = $num;
- }
- }
- break;
- }
- }
- }
-
- // Ok, let's check if the key was found, else use the last entry (because it is mostly the plural form)
- if ($key_found === false)
- {
- $numbers = array_keys($lang);
- $key_found = end($numbers);
- }
-
- // Use the language string we determined and pass it to sprintf()
- $args[0] = $lang[$key_found];
- return call_user_func_array('sprintf', $args);
+ return call_user_func_array(array($this->language, 'lang'), $args);
}
/**
@@ -512,24 +450,22 @@ class user extends \phpbb\session
* @param $number int|float The number we want to get the plural case for. Float numbers are floored.
* @param $force_rule mixed False to use the plural rule of the language package
* or an integer to force a certain plural rule
- * @return int The plural-case we need to use for the number plural-rule combination
+ * @return int|bool The plural-case we need to use for the number plural-rule combination, false if $force_rule
+ * was invalid.
+ *
+ * @deprecated: 3.2.0-dev (To be removed: 3.3.0)
*/
function get_plural_form($number, $force_rule = false)
{
- $number = (int) $number;
-
- // Default to English system
- $plural_rule = ($force_rule !== false) ? $force_rule : ((isset($this->lang['PLURAL_RULE'])) ? $this->lang['PLURAL_RULE'] : 1);
-
- return phpbb_get_plural_form($plural_rule, $number);
+ return $this->language->get_plural_form($number, $force_rule);
}
/**
* Add Language Items - use_db and use_help are assigned where needed (only use them to force inclusion)
*
* @param mixed $lang_set specifies the language entries to include
- * @param bool $use_db internal variable for recursion, do not use
- * @param bool $use_help internal variable for recursion, do not use
+ * @param bool $use_db internal variable for recursion, do not use @deprecated 3.2.0-dev (To be removed: 3.3.0)
+ * @param bool $use_help internal variable for recursion, do not use @deprecated 3.2.0-dev (To be removed: 3.3.0)
* @param string $ext_name The extension to load language from, or empty for core files
*
* Examples:
@@ -540,11 +476,14 @@ class user extends \phpbb\session
* $lang_set = 'posting'
* $lang_set = array('help' => 'faq', 'db' => array('help:faq', 'posting'))
* </code>
+ *
+ * Note: $use_db and $use_help should be removed. The old function was kept for BC purposes,
+ * so the BC logic is handled here.
+ *
+ * @deprecated: 3.2.0-dev (To be removed: 3.3.0)
*/
function add_lang($lang_set, $use_db = false, $use_help = false, $ext_name = '')
{
- global $phpEx;
-
if (is_array($lang_set))
{
foreach ($lang_set as $key => $lang_file)
@@ -555,6 +494,7 @@ class user extends \phpbb\session
if ($key == 'db')
{
+ // This is never used
$this->add_lang($lang_file, true, $use_help, $ext_name);
}
else if ($key == 'help')
@@ -563,7 +503,7 @@ class user extends \phpbb\session
}
else if (!is_array($lang_file))
{
- $this->set_lang($this->lang, $this->help, $lang_file, $use_db, $use_help, $ext_name);
+ $this->set_lang($lang_file, $use_help, $ext_name);
}
else
{
@@ -574,8 +514,37 @@ class user extends \phpbb\session
}
else if ($lang_set)
{
- $this->set_lang($this->lang, $this->help, $lang_set, $use_db, $use_help, $ext_name);
+ $this->set_lang($lang_set, $use_help, $ext_name);
+ }
+ }
+
+ /**
+ * BC function for loading language files
+ *
+ * @deprecated 3.2.0-dev (To be removed: 3.3.0)
+ */
+ private function set_lang($lang_set, $use_help, $ext_name)
+ {
+ if (empty($ext_name))
+ {
+ $ext_name = null;
+ }
+
+ if ($use_help && strpos($lang_set, '/') !== false)
+ {
+ $component = dirname($lang_set) . '/help_' . basename($lang_set);
+
+ if ($component[0] === '/')
+ {
+ $component = substr($component, 1);
+ }
+ }
+ else
+ {
+ $component = (($use_help) ? 'help_' : '') . $lang_set;
}
+
+ $this->language->add_lang($component, $ext_name);
}
/**
@@ -585,6 +554,10 @@ class user extends \phpbb\session
* @param mixed $lang_set specifies the language entries to include
* @param bool $use_db internal variable for recursion, do not use
* @param bool $use_help internal variable for recursion, do not use
+ *
+ * Note: $use_db and $use_help should be removed. Kept for BC purposes.
+ *
+ * @deprecated: 3.2.0-dev (To be removed: 3.3.0)
*/
function add_lang_ext($ext_name, $lang_set, $use_db = false, $use_help = false)
{
@@ -597,109 +570,6 @@ class user extends \phpbb\session
}
/**
- * Set language entry (called by add_lang)
- * @access private
- */
- function set_lang(&$lang, &$help, $lang_file, $use_db = false, $use_help = false, $ext_name = '')
- {
- global $phpbb_root_path, $phpEx;
-
- // Make sure the language name is set (if the user setup did not happen it is not set)
- if (!$this->lang_name)
- {
- global $config;
- $this->lang_name = basename($config['default_lang']);
- }
-
- // $lang == $this->lang
- // $help == $this->help
- // - add appropriate variables here, name them as they are used within the language file...
- if (!$use_db)
- {
- if ($use_help && strpos($lang_file, '/') !== false)
- {
- $filename = dirname($lang_file) . '/help_' . basename($lang_file);
- }
- else
- {
- $filename = (($use_help) ? 'help_' : '') . $lang_file;
- }
-
- if ($ext_name)
- {
- global $phpbb_extension_manager;
- $ext_path = $phpbb_extension_manager->get_extension_path($ext_name, true);
-
- $lang_path = $ext_path . 'language/';
- }
- else
- {
- $lang_path = $this->lang_path;
- }
-
- if (strpos($phpbb_root_path . $filename, $lang_path . $this->lang_name . '/') === 0)
- {
- $language_filename = $phpbb_root_path . $filename;
- }
- else
- {
- $language_filename = $lang_path . $this->lang_name . '/' . $filename . '.' . $phpEx;
- }
-
- // If we are in install, try to use the updated version, when available
- $install_language_filename = str_replace('language/', 'install/update/new/language/', $language_filename);
- if (defined('IN_INSTALL') && file_exists($install_language_filename))
- {
- $language_filename = $install_language_filename;
- }
-
- if (!file_exists($language_filename))
- {
- global $config;
-
- if ($this->lang_name == 'en')
- {
- // The user's selected language is missing the file, the board default's language is missing the file, and the file doesn't exist in /en.
- $language_filename = str_replace($lang_path . 'en', $lang_path . $this->data['user_lang'], $language_filename);
- trigger_error('Language file ' . $language_filename . ' couldn\'t be opened.', E_USER_ERROR);
- }
- else if ($this->lang_name == basename($config['default_lang']))
- {
- // Fall back to the English Language
- $reset_lang_name = $this->lang_name;
- $this->lang_name = 'en';
- $this->set_lang($lang, $help, $lang_file, $use_db, $use_help, $ext_name);
- $this->lang_name = $reset_lang_name;
- }
- else if ($this->lang_name == $this->data['user_lang'])
- {
- // Fall back to the board default language
- $reset_lang_name = $this->lang_name;
- $this->lang_name = basename($config['default_lang']);
- $this->set_lang($lang, $help, $lang_file, $use_db, $use_help, $ext_name);
- $this->lang_name = $reset_lang_name;
- }
-
- return;
- }
-
- // Do not suppress error if in DEBUG mode
- $include_result = (defined('DEBUG')) ? (include $language_filename) : (@include $language_filename);
-
- if ($include_result === false)
- {
- trigger_error('Language file ' . $language_filename . ' couldn\'t be opened.', E_USER_ERROR);
- }
- }
- else if ($use_db)
- {
- // Get Database Language Strings
- // Put them into $lang if nothing is prefixed, put them into $help if help: is prefixed
- // For example: help:faq, posting
- }
- }
-
- /**
* Format user date
*
* @param int $gmepoch unix timestamp
@@ -808,7 +678,7 @@ class user extends \phpbb\session
if ($alt)
{
- $alt = $this->lang($alt);
+ $alt = $this->language->lang($alt);
$title = ' title="' . $alt . '"';
}
return '<span class="imageset ' . $img . '"' . $title . '>' . $alt . '</span>';
diff --git a/phpBB/phpbb/user_loader.php b/phpBB/phpbb/user_loader.php
index 24e663b150..0b192e4452 100644
--- a/phpBB/phpbb/user_loader.php
+++ b/phpBB/phpbb/user_loader.php
@@ -175,7 +175,7 @@ class user_loader
/**
* Get avatar
*
- * @param int $user_id User ID of the user you want to retreive the avatar for
+ * @param int $user_id User ID of the user you want to retrieve the avatar for
* @param bool $query Should we query the database if this user has not yet been loaded?
* Typically this should be left as false and you should make sure
* you load users ahead of time with load_users()
@@ -188,12 +188,14 @@ class user_loader
return '';
}
- if (!function_exists('get_user_avatar'))
- {
- include($this->phpbb_root_path . 'includes/functions_display.' . $this->php_ext);
- }
+ $row = array(
+ 'avatar' => $user['user_avatar'],
+ 'avatar_type' => $user['user_avatar_type'],
+ 'avatar_width' => $user['user_avatar_width'],
+ 'avatar_height' => $user['user_avatar_height'],
+ );
- return get_user_avatar($user['user_avatar'], $user['user_avatar_type'], $user['user_avatar_width'], $user['user_avatar_height']);
+ return phpbb_get_avatar($row, 'USER_AVATAR');
}
/**
diff --git a/phpBB/phpbb/viewonline_helper.php b/phpBB/phpbb/viewonline_helper.php
index b722f9d911..89915f2228 100644
--- a/phpBB/phpbb/viewonline_helper.php
+++ b/phpBB/phpbb/viewonline_helper.php
@@ -18,13 +18,13 @@ namespace phpbb;
*/
class viewonline_helper
{
- /** @var \phpbb\filesystem */
+ /** @var \phpbb\filesystem\filesystem_interface */
protected $filesystem;
/**
- * @param \phpbb\filesystem $filesystem
+ * @param \phpbb\filesystem\filesystem_interface $filesystem phpBB's filesystem service
*/
- public function __construct(\phpbb\filesystem $filesystem)
+ public function __construct(\phpbb\filesystem\filesystem_interface $filesystem)
{
$this->filesystem = $filesystem;
}
}4[Vر.}@od-3szS*},ݮwfiEbOo9' et_u36#bԲA!wtIKO-w*MunIHTU;1%@LD j7kQv( Oxb3*X7if*dUqppr󶁒Z[JuD&47s Uyr 81lGP9N XKZO:."m~WVGI"~4mrIY`FMa6Fp; dUhT \H\%d "?3A$ocP~wp&`;DUJ@jdl5٤iٝϨkeT$Z#gq4@0}!!T4Jo̲@K@H(ߛ\?)ߎ~-3_yy*;p#Luʶ祈#pyP1n"9$IF̫(a꾢a/oǀU 2Bn|Wa k~,>B~Z`? ty8u'/j]k! v?-lg5"g%D"oڽI񅶅ۜ _X^jSfա +nF'Ҡz, +Y;F^*V>녯 'Lܩ0\}wj®ǖd ?1Ţ9G#prIP*SaFdyT]4 ֤ȁb8PaރT?OBȥEC`w.m EBgYUۄ,7!di>I!cY^1C32;3Dp튏&d78ݴ߽59Cmdq~{UH`򡷻$eJRn&uH!fKrHHlV;j /\{ u-FOTvmhZLH!ًGdy8̨8>֢` aQ8͇)9[=(ĝEb>z^(`_np*ap{eU"N/qtlhn0^uc}Nc"맳mզaxzx䔕 E3<)m`!3<?Ggp?.*vSc\dWU= 6Q7I yr=cڧc\S>t3'DBB?/_]9HRn6)ɮ-YY54$D$J8.Ag?),ctmn_"t%.j\eԝU;TFCMRѦ '^c3Zr<'ݍ"!@Ф(YS=74XOb萅&ت7s!oq{a d*Xooc1XuN[2vUS?:vTGpڞQsa-]&{ ׌L5^~ng۔0F;Z:ShHAxv$6aJ Uĩ"(YVm%TㅋS[b-CałU ېًyhwކ2.hђܒ Le NpHo189@"(E xZ`c,@ToT)V({>0[%Ec5Ի0I`6<#1M H{6 |:S#ܟywu۠ԴjH1^I!B#\W`GD@;K:Wm+#c~?E/eL|n؞gbs+\yi%c\ߚG/x/r=&v, u LdI$ҭ H8[-^t\[Ґd@B於u  $H tvjkgyxHlF+C$T xyش[ʵ~' 9Zx+Ns)tb[Jr#.90Rvu9,2M\/JL՞y:7N}EIpD ّB:Ga(euvDuv]>F w\)TE΂ԐR܆ /1:2itN=tjO46]37Ү*@Da=%Sݑd<v`ϋm}:k?Y~1/ýQnr\F:=? e Mp9R=S&A#C-*Hg73:q+<̘T_av,tC'2246t&"̂Ҙ/6*=3bd_]łuRMjl)А >=KyFZ;~~| zRhE7IhbjnhZaQN1C\D)Û|Ml"\˓7t cDhŦI! W#!:+U 1lykY~ޅ6$M!LhIM 3k__s/MRm }rM Մ5VUP7x_xJ:5쓵c4xʻ0 Pۯ <0:u\yI]P4*J~Lp^XsXebٖw7kA\q_8IIZ?Yb;}tNޛY' }32c- w.RA|-+-r-ir($k<~&`#Bm4ѣѿ-XKe4U!})}i.)0ݯODr N-%t2n[B {ʢ-`*B Y Qf^Eb[xf9!~|ۥmxlf/b`Z'2b4=/HεkAm;^3g<2Zūc;4L$pڻ%"Ss*r,(dd k"י•v! S^HaZ-lt̾j j!#i6 u%lX%>Jxå\}U"͟b-D7HhjR=hPpX# XG0 ޅxij#~/Y&0-@a&b"vc0>6ƋmE5:n;wҝi6G'Iݢߍ4'x_҇q }V21RėM/ 6 0z.MG˄jT2VO*K5Pa!]=E|Zɇ uϿߘR5Cg+AJ sA!=7fZ"8@q; NL7C+Aqx<ݛxp{Nl\$iqFYlcFиKUP$} YDb`Sܽ(]<͊_wFДG36[az酞gf)K4oFIP~yg_&Y2Pw#&++,H,/ qҔ!$D;::>C;Av`yCY xwє;9ys%~{fjm )KKkz7:yO \iqikoS)2M&p4lnƆF09F_V4$8B4Ⱥxx\1h;ұ \Uύc,N?'Y~`Bv4|ϴl@Q0 .OX&EN.&E vcup*#iaD|\ YsƧF8 sr[n.tk Ƴ?G Hv]UCKG$S&1spdE[MPB^Y? |%oy.:N'E PӫyтaGAsD~$ζC ?Y%X6Y041]ZH!-4 ;&OI47-n6Ƶ_46 CVpfk?}Fh =XF1Shw{(C5@lgoA⻺tѵCv"̹l];Q9D"4N y^,]э1>&5#de$~]1$9ٴ@l"D# DHIf@7HF|޵'aCNFe-s l Hhtd1l` 6chcAgrh[ 0!1A A $ilf0c L{,K%Q,QH v@eNz]lk5$zyPǹ\XTVF6R"![V-Ae}[t͸lWI-rϰb׳rxOuK#j ?YT8QBPݕU3O5?>v6b#=X }M^wfqjaՕ_Q_Ⱥ8+EyŌF[@լH01 '"|.j1TsFdhd\Q`dVn!h-SMQwt&$%ŒJhhI\R"_=|〈o(?x%08 "!!~ZŰ\1nP73N5*% G˗D ʫi³_[V ,!{#)[WگP.5"O u-KT820X3m͵sSk0- ):n]vvV|?xrK**f-O ;M7U5"8o>>%Яwȡ#<;8Fs㢳M>";N9h-G^D _" : ]&{~ȹ-X#Ve(=V9Zz3SBrڊ6 `XZK3vqcN&2(``? ]&#iLmMugA|Z2Ȝ6P9lq,]e%#)#[+'~é6ӑ0 ^vXdf9=pcGd}(L)<6dk9BܭCDXLDyCtA,~>NI3+H^Pʊ/ MQp։XYNlJo11y<~g*P}eWF"P?dvc7'2:f;A=vPAa!(`rh@6 }+Я/dYXKcme kk%)+woznSGb^w3ξLݦo{(!ḯ sٌ;p4bzܔ.߷{~纺SS7sYBHz rU$D0l‰ӭq,C1ك@MK !aX'jBfg)*Bk"|c]57P@, tX(3 ! xKre2TR`Ch $DƤ4 RviX+4n$H#AۘwۉX)>N@Z]{ϣ?,`SiL;PQCytT FC8l0ckj~Xȫ$ *7<)72qDCZ&vjy-3M,dif6 T% ,B"ն3.{xv^Q8`w^D)""0:Tsnt~t]zbȁ.Fʊ UcP^fĹ/ȸWtVrWirt*5Dc"Fp3Ze՘֎ >16a Ӟ=K_|oD _䄻Z )]08W8{yGL#j[ߑgW1v}ϙ6Lqub8l_-_޷S%%HM:IJMC@&m\`7F!Gl߰TD6D^卺 :B٫ 聴UiL$wD* NefCƍdr2i_XY1&N!lK U?0lE<7v673?h i5jAF ZB)&t;V" e(MJ0 ni&.T {"Y?Mfꥸ`ܹuk[8*#u1V m c! (.R8i4*Wr; 4V1 }~jg3D2;R+݂*RK6 [MQEQVBV!~?4\:jMHviU88=ft' ^KWo8OϢ YiUEIKU ېO@]{N>Y뉋8]+9l{Nس5iTϣ`Cx&#׽/QA1m3=/Zh^'%'@!W]tΈ\s|v^ď1a[aO9>Rgii-m#S%,;0tؿ}%D1_ň5Y.arLO^L>f# v L/tۈLqM}^>PX bN8f2T4s<;byѯ1Es?(xP1r!WbC1ܢ obz?Hb0;YIf8מɔl7#t|͔jG L䨽DYSp>ޟgMN\aTҠ-\Z10C|,II mXjHn}4p{ml=C#l#ONڊۺ. z0WS8ml\M>Mo"u+pX߱V38ڻm'@~H"HXq)34k<}Jï7Cb8vntԯlϭĠI#l2xk.@򝇗$/{SzWMZ=l/UB*8iH=QiRI~±T4Iaոv_} zEܫUI mؐ(`@ $J`F>kJH!D(jE0)4 #M p0i&4\СipbG0Jk .[̌@`x!0iCCml`VGLƖoc/w?S}^w7?  VH%&cm m4GIlAofa+^ MzCx0U 쀄 URTЦHH( LA@* gPnC ![m LTƓiĥiDKh KбK$LKcER)\+mQ HC(qV K"Q M,R,AŐRL ! `_J0" PH_ưP;K{cZDHk;wd9XWuܧ$ 7t43#eD|.j"$Saj ͛(A.eŎ @ƱS =[S75c.7}l8nDsT%^6,x mngGZ 0cb=ٵ%!f`^AI(hA@i$/-hf]S߆O\B`"ga7A2ށA\\ytY[N4O=qbXpB]ح4j_E)`88n)<|KЪeg2͝_ԡh݂>H;CE^OU)f,{=[y88E&[޾ik[ q*`& nb oҙr )S^’R[3,򦝞KB:=j4Y|ZY;dw}6.g'b Fҗ~I8yF_"/IdИɕ&`SvˋC例DG/XUDR}?. TB3RtX" 7K' ȃYvs%PEɠr[ v8WeAA!(YelJ+z\5qb%m(짖XC(0#f >iYp3WǟRID$48tF5C>4q8mt:<|sC976r;#ԧ.0AX,fϕt}׆Ӥ]1mo$рacE|UQ<<~`zIΗڹf~)-uzfɋt7CP@a}Uwa5vLA-5iNexN)UZ" 09}ȎK(@iNrK2D'4DчfwD (0/o0 (Sj:p̺cg:/UXIPV>kmwbHc݄sGO&gXnKMٓ0X$yD(isFxSnM:ādcn<]L}R\W]En8,.#MfsLvx̔y‰EIԏ}v3{$ŠeK]h4UaEF0 ! KfL4ֵ&dPoCE$ع84e!*LRe ٝm +9Z nd* HPhL6 BhP1&mT.QRuUlbwbQY ̄PÉ jL1K\ 3RR) X0h 7BmQ'Y`%"ysC,JH1P#=CSQKUbu|  ڵz\*!ᡴ0"!MxM$8"#ՠHlm%qZ0ֈ`,T2,Q 2Z6lu ʎ`B&3!2`F eaĈq/Le3h4l3* wL4ö1zq)+7A00lMQjdxC&fKnFCmdhx9K&&Yx(R\ M6bLm##bPM@2 Mui1,%LPE,u j88sjhhax!Mejw{")[D'87 ,2b_2U Ψ ^>υ+HVigUaG#эΉdF䚑ҋCGK'ge2E}~mDdCl_"@ۆs.c%^_&{iL3o`-2$$!AA]'9]Կ4 uZ@B@-ǃ$bo b!$cHi6 ȟ>mCXW9ew c[ 2"^ִ; +HV% A`dWk'x0I*oǂ~ `VNy'8m4aeJ:*~wE qD @=7 &Jy)o)XCEYF($@)f2i@`͍tbY  X$(` &;KaYVd@Um{4Vhϝ HUڸw0 )Y :Q%0@9֞

) CC_ϓ[OFfٴt*ģ쾭S42Xg*V8k3lF/P5oO_}`A3HABC/Fu{6gA&#pZh*KڏXqIDm7 CsOu3; wH ~/vT׽Dp^( I"%z#0sNjzSMG+ӷU4 qu KsPL@w 0@0b $/E}njC:VEsZ>`iN齈MED!1v`5Z("uN܋?P6>l1Pp?+t8-3{koMV~=b29.s&GC7+5i2"aa00W .C~_uwy)1Zz?lmZ@ 1x KOgKUC߳=ZuW2#[ 48/j҉@|2O%_N>ٚb(܌q%\2aQNlţn7M%B%O*B/r*ҟ#,E1GK 󀂘>D NaUғ!8p IS+הݑ9WЮ=А}(;ڿiJyG1 ȿ%bx_.|EVuXx&al]^,"I7[L5@QD~&yv8FلC2<c[Ǝ"A"so7+;%x9h))bi/z\_>I? (o C,p'.)0&Yi䆠uүTaqMҬ+]~yu lVq1*aBb{FXL^a?̯lq:́ r>' pFF(K?A XR;Eq߯ 3 ke6<%"$o68GZzT;" K;y(^ {=!$_{lt8>+w" S+S|hYq鵫XxW+?'Ĉ\ƨ*`&iY|Bk@^"C$.yU̽Jro{WB!<֙GH!xퟺbL0@lm6 }r:u̖v)4^2'S("``->z߼B f*䦦SSfrmց^pܥՅĴEqBp ="~KF=nn?o͚ݷ֟;'F@1A<؊6 Fp`RBI9v@f!r7P0 # ],=3.?EDhi0cOv"mwؐyK( Kh`p6.blDBM7bGjClcmbEa $Y-;p.9<]V}9̹ެG}14Qd8p$XL9NaG4FJ%FAُĈ?!vu'l>H1 £\M.+bma듏ie i/ToglW4q?xq-6Giu9sԧYܪ./Lfl#\@ԪZk}o8sqGtpDǀ9玂B|{?ek:k4AjjЏa$uN }^?6G#,V\@kvUط0y0D7˂:.(ҐY7ܧ~7xPT5];LvzQfBq1n~ Yݯ5<a X1c/=߼v\|hy2٨a4v`&&"ڍTL *.c-}b5IznwzKa9vLc랿 y Px;h)t+V[i-~C~#ObHGt*xU!J, ۸|Ho=G-lwT!tAJjskp}#C:p%yըŸǺ^ e[꥟Ow=$X@: GRZ)b%iz3*[8*p;}s鈙WGA fFzA_>-׷͹} Ę 4(`Ro{؛t(jk̚ q]gkRmَNh.xZQ|FDԡݜ/{ C)ġr6^|& 5Uǘ &3m F@@!YԂq{ƇTuнn]*\g:]POg(8R7>X%$MB˳az=r#1 M56NܧCchd)L27)*c8C,&U:?:,/03=J?.a}\ 7}BJV1~o侑׾j_7j)לt}|< N$n 8F=FC/Fp36d̩_W Xc0=̨ ӑ ,lOaMJ̑ r̝82;`w>:b&i37Ө2o t[L࿡s3)E}4Vn)\tiMد.ƒ:Y=6ʵÈk͑!<4ƶٜ\<)I/+*7dŎ6g~\<8$6 )RFi#_횰*K" <_d\g %K )dq7 F(["[ 2EIѭl{$-/ S{-ވy Os ?9W٢ggɻkE㤝aoZ, vFof ˦smD%|psLTXbwⱶ6bdt`Lxo5q PGCIsѝd{VյGxo)s +c;`jor:6km3QZg^F#RT'qm܉qL:UV@c3ʍ/3ËuN*o%#,p6l}c%':f?Cť*RLX˼ EgCKaoJk:$ ? <k]6 <ԯDDK(}ILL b =f2/뿲Ҙ.Cx.&S$@Zk-#80@~xMS[9#3 uKrNZhaWd`[E4(S1 PT;{&K;ɴRG5Ĝp g`YL36Q>Jڤr+3 Zq!;:=AL_He8e3PGPr|I$ ߧ.%cnDpF6ί0j4Hy+J4Ayγ` Mk]*g3V44Rδ~T]`\_:sDXqd!ݤĉ0H=sm0H•n47~De05 wsL\ek#&3;D Jvs5JB;k[9,z5N ONݛ.ebNlT`xZNXk|]Z \ytnqOwy졉cǛ3a6^ ~0Cb| VD4d- w0f8ϷT܀=FF|egjjp/X?|V#u#aį10-wmZbS5+ \w" <imc$cH@K{^{e-WIqg7}Ifb Ad(DRmPa)0UkG 5{ 5` 6R3YNKaVw3Xj UjR8{ w`+4O@ X@{< W<lւuKcyֳܛ|b<N\`x@ ABƤF*h$.R m,w;>{Myj\#m3l˼M?;?u͇*w3m 30NYKZp; u{B(%>\oB.~bsF9'yk(N?F?T XxV0;N^Ix,"2טY?_ۅ͟oqI@,eb4;{ dmMcCF3FF`20 Koi?KԠޙ{Dprq(eCwشJ]֣,]TZR6?n/IJwMz-a1̽$&,zU0ZM[X zmdHhzcO!*ղ+ݸ-1zxJCYPK2dq/ӵLD#Aҽe0kNȵ>B/d!.}K~rw4q}rR)/KJz$hMó;o< '@&[F%Vh1sXDLȮHTyuJmx؜ vPEJ\֏*!"td-a_cن{mL̷T0-sU`Y;GAoCLכևi9D;!W ,5-mW'l^ԧgh ؼ88Č'2"C aDQQTwZL?JoR.P;Pd]+g7Z5@ƑBqb%RE$NU'MmRϙ2Qj]ȟߥ : ~^R*(q?`h'n'9|Z _\o9#jb欈GasO{v*≛ztL ɀ)sf^uev-C=T˿'y<9#a/>g4Ufk! cꓷ9 קtp/9Ɩؘ\\D DӡF^<0IJPJRV{sxrNºu\AB4!2P3p*X>6ҁΩ;h]ceIn`]2Եrӷ XEOVf Z.HT*VѠ۷sz b<@gA` 7|R:G}}/۶)yŪk au{&v,l8"3Md*~E*j|l(^fKK|Ɋxf$#. "c q#C/Ю'!U$[Sjl[w˙>Eغ4&)_:6gLg?uۧWg xX^h`H}Ie>d~-3T[v\ gY=DK_)^߬wzUW"93.ɳ$7UY +>:x*CڕG$D=);`Hؐ9 2hOx闐6x=#=M`mC^:>%ݵ#Ƙ1LG?K/vp)g>]ngz-KD|}=lf3GWn8 @%#0CۖZG~rhMSYqr2Y?AK2`KRo1cE$(A5hQ0 jL«󊽡?͜lub@ ozEzKo9 8M9[9`PKMhU1B1㦘t^?dpfe#* 6-34̨zFNMD2 ;jᐤ`Ffc{әoIb(/:AdUѝPΌb %)1 6*8.Z+H/9^X_|ƒ: Cz?$boۚt֦8@5 {$}H @4/p8m$0@B$)%ζYN| 炮DSiy;~oM6Yj䃲]>nD 9=jS n ^8oKִZj`mvN Y??CMMI`XDq`. %Adɓ3@{_ٺ#$S?e0u6ם-`d'ՈWK.KSa #1ل4xh4J~sh2Y]1Y9yZQ~!͐(,&US)*L%5%FC:L! $Z1]wzѠ6 'e=7 U'hN`+,oISꕭtAT0yw⤋Z~_Q%ֽOrWxk`.e (QG;Ht!@X2Hr?^Ff7WzG5|5  ڑ ~fY.?&2q},`Wo yP\1(*$.sN%|3#Ћ5x[yC]*Ǥcy1qw:>A3:e cXn E?f _nKme}+v VIG x֯.>pbTө|"b[m}Eǵsl o%+oL|r ،LTeޟDHHI;;[$?k5D}NQG)qA,l7W~-YRSDğ:<ᙞ@X#yspdon̋ki~)[,dh|OFq}b)XT\^h<=]{? o>-FADi2siK3ɥ *Z~PL>K>s;{FAb&;/Y#5PV+:6yV\scU/ t}2S{Ym lp+KF}lտƥ-K>&bY^Aژv?!$~ʓ?y.EFЪ3:I(0`/#<._! c^ x1VW4 U[|GoPFv ScAGaHj9W_jѵIVD{Fh*?I[!J~]C?6^2F?J墳N@_6KL=a#m AH؀i 1%Z 0d``Lc4И^xކۢ{xRhf+vhxVܜtk s({g=?&$FN402ccLm616&c@ƚ`ƓLi& 1$`ƓLiq-_HiALmTO  b]\dy'wh(D&PmRz**#LZT ҟ /_w0 Cd/wd? ?pOGmYC2x,#s|JU} USąB}hU[UZр5Gmpælݾl)d_Q)ɦ^63S:jE㪕Ϫ0XdL0U aAxOV+fc.6Xjݯ~lZύ}Yx|]`hh$j0S4@ $@*H'>鼳.#7y4|sg=G7=mhWc{;ڴ usmpI1Lb)5Ā )8L_XT(;t W{mmv'W!)D  Wq bB(6! 6 U%k}GGxRv'` #g:߆ùۤ5+)SNlYxP 3 2>@@ #h bc}< es>ϒ E]]BC AQmT|TTꋱJM, 2z! +%y4#|Xmg"63w"7\۲r #xhlk(hSb\x?˘YprВv_3okB맖f%%܇MaUVT/ìiGQG&"B-s`}_V}<6.5U٥ r^j,458eG(NQ-F1߰#0RըjtB0/P=`` 6db%E|c>H+:ѱb:hwf hR6>7xVKݎb;իE."y,}8G=dᦧ8lpLp|{/ec p$nb @2)00n'̢d6?$Orr!# b$Uľ<31Gߟ::4 ^ɰAj/_Ne^{Q]2ZAo)dyӭ7ቁ-8J<~y:ׁl|HU A&@")P$3SDB.']`)hJ $lB7} GlѮ}O]u9qy"u0ǭķR9Uĸ@ŀkv:P>tzܨ* !f.ib+TⰌ%塶l\a\?CǏ@"``<~`q BbaVv8ÊKwNw::'摁F&aּ2X(]4v>Z̸ۛnj'8]}ߖ9zSx:bq:'& ";WT?FFf*F`zԄ4F)#5jwP!"0O)Q^כcl+;8bF4Y@/F#짍fdͩmI˳L"C!ꔇ!9a\M 1K'nJ=&·3)"At!Ypuȯ}gǂS4I,?1;xfa0堽Ȅ)0sׄ/j2?u#eǵ|i.ѭ=uYqF:JW> )+nKN語zv lmMM|~FJu+0`Di}-u <,ʷ3kIGa)ցf`X Ѹ:+GGy:x;\5s%,RU*̭z_.hvX2 ‰.y@؏xj%>fWHVM~|L̍h Alor^Sثɼ !whii H7y4"'/[֩Wum=Z$k: +t{_7 p9ᣮ0[~Z\,¸" åM*wniL^ٵpJ߼ 6.ȶY ӝE/vC=f!0*ZX#]1t>f c}gnP\QC`foɭQ_U[ WW*+m.t/3ץs={n%GSa{6hZS(0&(l[Y+VV}… TóY7JnTr(\iuG=[=m/O$>K!iX(u`vY}vpE $^bყܹmK!e"VꀣC? JY}w-a<>t!ïTR5uWރ?Xl4BVS,G?L&Mj'fB#^_J6#WC󕮊9fm1:.2dx ÛiB@:W:H-Jsc)ԥ[W77PM,l^k:UjA_(Gy=4Mu.";'Dp}r+Ďw/-Ѝaڵ)BixXM(ύ_5zǽDO"@iBIp?è>7knPz*QPii IOb߷n/o=Gzt HQIjP?@m-_2wN.6S ,~<~`Ԥ=w]% JwE"5PV);uua#WWåtB`|7rGpJL+TJ~5 i?ε*7RS6̇=$ÜnWGgt&뤦: :L|$` 4F 灃YAK`k}?ԪķǍvJxz]AZ;'zoU@YR#.5a/L57ħEKPjW_>՞seP#4]ESC)`9 "fW}۰79(/qtTviپC/VS'xT_`}֚8/oe)yg;ov?.H+u&ʲ8Xpjk (O:Tt T,c뫆Al`ioгBeӲ//QDT qdn),c֎ J|Do=\rFOc-d[et w]LI >Gq@)$6/,s?u$1pӹO7z{14 !]t ۫>\$iBuA!.$Yђ#-ߎJ5WxҋT{Y;oT#Ƚ'9%xr״>3Tt1fbx 3>)CTwFt]Qaz9cgҡhOmsԴK^A$ceO\'!7XkVsp^\gLHct` ͿHR2,aK9q#ᘻHT}(w;`Öֈ8Bbꐟ4])jn)A(n)x~b6d.FňiQ׾&I0>Ȃ(maVp~~ Jj(I4dmI3ۗ4}n p]i}^wH-`.GI:QP|6s6i'@#[wdD|57jD>7Ui*@k<43K\?c8iw@rV )}ݰ0+!T-$VP/p:9-r/Ud4lt/.7ʒL񚜬/yZ~$a] i<E1y-HvaP 2Box?ǠpUd~5y9A mE ĮٚU1s)˟tZ5®Wz9"d뫯ƅĽ(c!ېo4oTDoOF{v⟋xGbkDL859'Κ͋$Lj?L`LevYhkBRBpՉocteYqn2$0\^-jdzahpT+o٦5fDeFpb>{s)M0G ;@0DHcpƲ4h@DJ~v0I[}sL Cwd B/ ڲ?v_?]'~|Nm \cb˂Cn n(kɢ myOLn޼g-DNFbZChy\rحDI~1꛹a9FUޛf1ިT=ooxv\wDWWY QoN~+HY͟&qĂj\O,:! fL8F! 0?)@Z6;LlP#}d!8 kE (PA/]DplG {Bn2r%(.8n:={Ehs{|NR7l­/VC Ќphq`aGd$#u+NSAB<^`!hyg/1.MÈ_]cY;jiZ1p?#@JP$~N-aZbsCl,yT9qVx5-6ctwRY*w 6`#RAݕb_F2&/ :Zev8o6n [QS0Oy"C2esUPEFx$iNSBZ(FC۞bd03PJ`T{GvTp>!Um5La)ΔziOmվ9h/ 8*ȍoѼHZ{;h X9xCg1{wi2|b9҄ߙ1'-Ky+*FRY)/PHrz{MJ7Y;XaopwuFj]ՀvA=HGǡ~),-ur)h)_ Wl!4z`@ 0ΪV:sVp w6i[Q"`h ?0j-u{>R'wNVpʷ90k%O`XAB,<4'҄&O'ϢŬ۶M :mu 4' Knrz@?$'p8ƕTt[o.ֶ 4o( @L+OD(_le\c3~+Z`1f)ԁSB*zlJ;/pKz ֮~;g];KKr?`ZÏ׈[EA̦#OR$pXTE`/lm6\8;#zef("D7C ̏.O6}Pq+ml JTi6$ooNy, \ hPD!P !.daP?\k!~w%L;nNAyA8\ɒ]骈7ċ+oRDgІN)ś4L,]m(V2.Z*!P%<|a0L~_[W*ÐXѕ7 m@!ɰ:T;IL$LH+9=|XfB3WqMPz) 74F怄ͶT AFT%i`D91["RjҀJd SCI;l}i7^Do;<[|ƁAcxD)BRa1TRTa:\th̨{$GjT?OD+#i89|Y^zMM` ,3KyO'yn.YǶi>]FXa?8n6uJrR|XҢkh;(k$fzؑXeR5惘=IJxb|b7C$&B(FA;٩~KAѢ&;MW)k`c&XnjP3U݆M%2&@j12~X:bE#ztaGwuDL]Vz5[rO$)Wf.lǕ`t`%IS6E l&ND7Ksb?%H6 *FugLlzV@rלsVsr6 ;ZҦk7F N ħ 9nE7\C,|?xwJ}n%4N3l <H}䄴h~x[d BPErc(Z|op~-VryPzy\ sv#I ͒:xߢV[qD)Bԧ9$aܯ*/uVxLVv;9:FE4@mN6fwE,[?,S?ZEX=FrSU'EPw˒%s /'Xz}z۾/i#^;kRJbN폣PGojɁF8dFT$0V!.N%ߓkDRXY?7Ԏ*i5 D9۔ VV|ڞ)}ݙ&\s˷M+DI&WhwkD5ᖃ(8;y߬Զēb(?~1U!0U9Q>yBiC>lJ7*3ҍsGix"xb5;6ēZ7>ym&.8q17;4l_;s_ANi[iNq9Q aD=x?:R꘠0AIYo?9[~=P UU}#XFjٛT=={9mGCS5X0݇qOjluWw?qic43P@*Tj0.qR7BboUmυG?/ V*x1^8$ao"YSv&d1`lPv֪+0pMVm<8!T7[퐠[Ft[=Vt>-RIZ ͫVLX4E& ƿ";;Nx]Qjq*Nk=]77>}^7rndlK( N t!6悎}Bž )ˮcM8N{'x#msi A+SO0D@%7{y%baŦʽƴ8;Sm[VVCYɈy+-S괺1\3C6PP΁8'>+7ó{]a4dќ 6qA[m|$}eo%:⽜z&"v@'ʍ'7y4lY8Z^ Nߺs|!R !).Rf?$?8y] O9hщ>(*-b8}ab8x !a=h:XyJqې3P;LEܛI3Nl¥s v1 ZL-vо_?dB#PyPu/N`L2#GcDf/=2 S/{py荒`I-1 )s z xÍ5y Y1 Hhԩ?]Q@ڵdh yFuPlW:x; xmMOՂ*HId-u}!e,R  RmT"O?r#`䬔¿c*u፬~S*`1k]ޗuDnqn;B h3a:m \B2*4Ȟ.?ۯc^\*O)MFb~Wj=tPP~3Uq(R^#[]3 bi3|Vq\JUşNs8g~ʵ*2E#KC T: \+hQ 8LSE>7/ѿZb_7Pp_M'unݘ(|=Y N;M;lt-|zR^4σƀp oTZb݌TG&v^N^ d]]Hj0A˨fkJ_t웈.q߬WDDwS9=z>AJ~Rlf(Ch ~#d=P@q$Kgw*jАOL zW 忱 CdF+ oΒh7#TES)"H4tW?w]焎SM`mQ#6Nf,)ک>]'"ONFDf'6%QZ4 H{Vz8Cbg7M,rr妊.O+4?.N@F`0m̑Z@55JXR"<mtRVJl4qc-$Fp Ó^)~^1)dԡdq=bW0, Sַuלx02!my!5XSN J.gM Ć ޴hqcvo~jt"X{N BI7eѹFsMub1<Ͽ+|iǯ~7W[$m-7dġL/c 7!"X$2"bAa Jy=KWk0=j,U֊?)b}Yͤ8e@- \.n~̼t?!a錀TTt]2/A*~wK9Iqn?vVvklQ<]Voqq( sRnᐊ̀^Hy%\tM.luPYҦۯÌ;Ɏ2i2V fJ;sx^R1ٍCX+ LԖdKU:O .֋zxvJB)QQV{~揦8}8GTAz  íf\lnS;JN N-5U2tl)J]++V+ǰ{Ŗ 0 p)llKc?ߒ h ҇FVeq}hEnRI3Rn1t+?m e*NJL xEԻ)%M<%,L 12}C#%~oAm jls 7J~Ok ('dDy,$^WmeHZ0AhDhz QzԮUv7TG!P r:fQ-H& 6=uBxD ϵPC6{kn*x7e sz&zˠ4:pʖLz~{\ݿQĻ Xt%h\'ETikk`~i<{ղ mjG0v1W-t;4`(D97jebCF9.ϠKJa$&Mqm TU>SY]w4UH{Oɷ{[mǛWR-ytHeP2ՉJϮD}1)2O!0m &ICzju>vDCg*JH0mV͙X}T$(a;}KB¿g%9RfG>=O_< wNG#{sCYQg'<5@A0M0Gy#cU?(ߍӜe/aF >\Ϻh_TR9r#J?Q'UB2 4*&A'XkѮqpJ d DhlCCb2LLJ!J*Ԏr D ӘPj6"ZMZ!"m4KQ$ 0cC) ML`b" "" "" "" ""!"" "" "" "" "8" "" "`DQ(A B!D0q p2 KRLKRLKRLKRXm b1-I1-I1-I1-Lm%0K`$.I\md$.I\& rLm$` dIY$,`L%KBsұ`rC@QԀ1JU'hD0PCIC@6"3ّ̒'by ֪IO7!8uBc3?(WR-jquG?:><$ ^L-AE៹Jd} jh~)cdAX$@) \~kc~y W8C=-* }J<_َf# CG`E^X12;{R=ϸ#yGӋ9$R;D庎SR XgnviTlE0} u74eqI٤֊P9\g5v&btKpax6_V6DJb)ԛ3rA$;V]3^9uñ>k7)^a 0dY; 7"he]ghHۏ \dD  W#I {r 6rri$ Carv~O`1Za'b2A. &".QlZ28kHBg-f-iFMpzP3HKm+謠O}  QV7gR@coǠf i{Vb.@x RJ DLx:rɀ%K-vʐ$?f6&Orj`XKigw 1*v,0Gۡ6,+6&[ Ti*+*i|nׄ x9&7 E|w!gWo"m[c0qRX'K5q;E9OU8kbZ>|'P di˪X詗 npaNwoE%qڑX^|DXgѻ;UNʆ\ϯx̧fӤ<41fܲ!R?ͽ XD/M$\.evpr?t Oڅs?T((r P n#V:vșUc/2VcQ 2i*kCKY XgRK]V1q׾  g'ܲKbPzb.cHRM; \><"Up&W'om^fJյ;//_-S<%dۂZyz30yN7ZY^윛d萠QmpiQʪ\>\nj9Ul3p=ijJKqXUW X zgyafHi">MHXP"| wVAcd9rl+'K. x{9m>kg—ٟ\5bh8lxB).Zb5F.x*m=\g&l:}fg:3m@cT@{'󮚚#Lẖ?y?U>x?F ٧W)>DcRlsgkE&H 0u[Gefy^h\`ݮhkI( VDEZ5ʀ-XmQ'\S%g#z;x~A27q3sփ9oLVV WRjR F "T^ n=cH UX/Q<B@#3;H߫F4_X摱)mϣMfKi}4̿c3./tcm1*M N-RNP2\ AV=tmT64&".m: *2% 7ͬ{;~{ox`6 GisB?Af)pT~7G0e)f0}κDֹO:Յޙ*+"֨݉yB!㯈eHv_?Hj`,G,~6Zџs(| FDD֩%cPsDiȸi]<- ju  sdY@&3 %bQ1 <(@S 5$gi9w{W?,k;sfj$M |Hԡ})C 6"Pz[vyPD*SO˹į\H ީ{<4 <0 q}RҪ($Sf} +P 3Xp6%` Uͳ0F?a #Ƌy<CrwUg"xw,s4lh&"|~+cZF>10] Tÿu-J= 4 :J6m_^):j$L#ڀ.vĭ;/NR9+boT$dX$\o@йKṶӹYS0v;Z`Pwv<2(3"0dgp^r*܌l18y)+DrGkqB N:03,Y25VNN El{kORYYE կgeD7 /hRvA9wћTV9 )}2B(!slYܩdZ[*&3| N?.A+NHLV:GCndaU(偢H'YoQL0vPەXD鹻Jw9^`#FW@ŇA.ǧ0Ĝo|]UQj˲ ep;i(U^;}y>ޕoFmcG.5ztFrW:>QS [ c#{f^Oc(_ÛXsĝJXd4z;Jd<:Ew~/TdN|DzHpmmemAs{ Ǽk!y*Y \0ŁnY`AS )VK*)YKOw+Ro5.:; (y_" *^@aΰ<+hP|,éj.fu&NdruK<fx[ 2_MU)"x'ji˃}ִ- U_%]Vǹ,I\q̵AcCht&ssB0 .%5W-EKMei_usl}[S)CM!C|Q ΤǕ~K楫sU(v}j}A@j'f; {VNZl58_7h8g' W-z-jJӞg$vKI'r~@E l1W^Xjv[1ʘ^( =Hal D KPl}<U;U6[-QdSܘL&qS}s_S;1~9hx'}ɤWtڍ"u}`Ul_<1jW®U<̆>U-]kݗ|i7ҋě\KPx;fp?ٟL_^</mDb*W_ӢƓ}DIkz{4l-#/ONfIM@b{LO{]ܤ L~PKK:#9s/ ǟo!0Gt[C?qD q3oʼ x~gdW])$^oG[| O hsgwuR<BK("U8RW%ag'zG|nj}NpР0"# H&=)mNLD@8AFW"LQt~l&v}>"Oq&@1gB#pB;#AGst~(Ё" iwcgxA,|Ih\sVT أ* 0$Zahy 3tDYn-b@Vg'Y,u!Ew[$bfwXYLzOeU_`8ad1!iy" W[EUXԇcL'-$/Q*`.$y/io7CY4iЩ]Pq7UlNv4tQ,f]*3pA8NlB~|zo:兩ދrewR#oXJC|-zSQ߽v\=ߓ$bP"a>.6RIExg m:{;8:J'/eXXI>`&|5=35J+EGxGX-[J-1GcuSzV1׮~ M(% DL͊#Q/ 8J,~S?AD CJ`4Dm#z=QQwe8IOo:#{/spB%|ߵP5lo/q%<@(1W$tGx[Ck2|{l*Cԁ]&ReYS1k< 8)^ '1&[a#H`]}q6]*>0맣_U>>.QMJ0ۇ-R'[Ҋa1 &yR2Sk7x G(pɹ jV&%”YWjb6g>~_Y@q+^PܓuKxf-I@WukS LŽ ANc[wcGrԛ_Dӏ'<>$@waLI Bfdx2_X'%)g7Q~/.8A:hNEG 狥OvԺ{J +@\JO0\ Ӣsj##hy žݸp842%L34L.k\eI1E Ӎ؟"q/B_WeQ/ՓGW Ԏr )Nf'C#ĝl4уx;E"(* %Wr%٭EG$)/ U]2`hO7@lIp@) Ut``hG_(N(ρ~<Wq+DwBZDVw Q= a'95*J۞\֌B7knΐY~^XYrTc26WGHLkݭw-#\O\;sWo'NihLuGȼ K!PQ|c!I8Qܕ{]`*6P@q2=WGx t`1O;*/0 Hg7"'Є6} :=yФ'ae+k-!x@sz;$DO$míxyOhQ p)mjqwK^țA]迵J!" wdIn:a} _Ft=Fn#-Ĭ5BK϶ԡǷ+dH2 ?uD [icER{#>HP/%1y Gv*PETd5YY93.*d`.S!C8>Q) KJ<ǯٜ?[Qz5dRZ9rN9Vv?0[/lKŢs$;ZWZmqt/gv{ v!Ok¡Rd[l$/0.` ;Yfcd#Pϛ`f 3]0BR?36\ոD\:AiUHef%PyE!Ck! tHY%KR1Ja:?EGWݧ O'W8QPqxqތa+X/R̻efq5Op9ڞg4s7E}C>1ʪ{c|L򩲴#^r`tv|GBY.ႱFTK(\bk.B?3 '>Nܪ&dYaY#d#[[*U[P&VDAHGrBHYTV?S]aW]>}^E ڮB")f1(>:ҎZ1u?}WbÙtbo>>\4 T wfoVaO3xԒ,_v:\l)|ePy;z VGW]6O_74n dg B8CaJ@:SjZG3NnfxK!º73F1{WJ T>*F/޹.#8 D[sYy}=@Y:Y,AhJLO"p+/<ɓIxW}gBOeQ-՞ثʕ|~pA_4a#C1WwzW^#4U3dcH;V򚇏Cv/ՠyL.MӫvKU'7aTtj6* `(H间ۀmq6~dO 6n4h,.j9ˁƿp'>pFl:IJ*؂&$H3&K*gFB !vO9eHj"_ t * q7hBtAgc7KttWj)6 0V:ԋfQiX룔1 e}L$5H8Q}@)UmTPtM fǰhGx֪7ov9 E5Dv5^䵂UݝLo/*P ?a)68"C$&0mIQGǀlQ%mG~ 81*])@z͇XMB' !+Fm FKc[4iכ:ƼTyh/a˿]HS @ X!(FTD\kP4W@xʩuqX{=Is֝oT+>]Y_9Yp`' :g%sZ~ixLj0r\ db(#]_f~MNmL~ڥ7Am:с-QC7DAI =uGu(H N15>k{2$%DZ?GT< fy%ZV#=|MX o!)n;whP  :%L} a "Rw'`feG Թ_ c9߁o_0% ;b~d/N]zP7^$!HRY ?kI^ǭcD j,3(ĂQniӿq.ƣ+-^$ R?N!L=NL928SNso*;+G˹~3tM&hPDi^@] :a UwYKr}DÝ}NtF沇Xpy[ W{YMrtdδ^hYAvcFpwz%^mg[p,FcYaUQ6δSAE;cC`ufnLF?a*B#HOش8RHg׳&Z(Y̹ -xZ$74M^oLoOP))Pq/Ј +D=yy^S.?vH S=o?H+=P~rJҹ?cU }`c^Z{߂9{V'>Ȁ"kD4,_g,|x3W>4>(FDY65n Nv0?f%Qwpj]fڗ?ޏG$#A ? W2Y@zA.By^|8UG#̺ʼT{zl>*V&<I`uq _FOeRv~(ڤoߪ R'T4@-eھW0(v ə> ӗL͚N\d`)/*lED/?B|Ki'"sI9LED ~ƅ? 1-Xw:M9g? 2Pt`5%= Q3.f#IOfϓ٧1J/2+ҽژv`{JRc(;hw"dd1t279op^uX%{Ï[{#,_* Lmֈcm1᰼)ob ul>[y/kHqK>;Lg❬Lȼ80!LPKSܷ'.Qj%I DwCzTz:HG|ኈB;#mOhlj8>5Gtº_#I4!/iX։G'S[`H$J`"o Ͳ8eShE@V_p,|]g';G)ۢ]9T&3qن2dRSM K$bfq%DwA7U6ڄ!֟[#.Np~SJiN}1d')|kGЖ!nGcmŸc͆ ;1g@@%U^HBA wwJ?{Zwg4H]k% zg ~_Rwua1(=QKHXa [953P= .#<7KW;<#2lAPg]yu}tl'u/3Fe/|u&J<%t "ADs&9ް PE*bݍ}Ւ⣨@o>ǫ)C-t0BŊ7ъuO1&jJ}ꞽ2m$"_X>k87|ج,u&m=\.ǠX^/u.$Mohw Pr7LQ2|< BVkɌ0Loz'K):t.ĸ\OTCQfKɯ8V6dhf-ʥ0yxRjt0Loi\@jӼ NsgQNШ`}TdG PgI]!7/fΟU=DŽ/0ȴu(N0]Hr,iB32YSkEC7gmUpL ͥ T$`^⭘*3uWoy_92{BnJ߶8G8}w#Ƨ'|)ԾfF=ۤڃ/$N7hq`fL~2qC7ƨwL* gc o1Q3+j[l|1&C qe*ww$ w1~pfIC{jvϩAţ(|U[EʜGd,_~fNZJV/#C-^9NM|wG qƊ8(`-Fp r#`erҨӲxFNlaǍ.U?\H&l(kya]gC=.3Ck= %7 6j2ރn܎hlCzB8ў*i)yߩ% S-+Mљ21*qlFOR!t_jj`b4`Τ i6ƙ!CWCI'ZBBf\ߚɺuQ ^lXh(YSsU*\;㔇 &jEBX%>('PWLrPާfkx#PqzG+|k#,uܿ75ErI/Ө'Qt/C~ t(4K{ k^&vҲ9C3\+eW`Q[w3E$4MM!p$EvuM$ܸ5Ȥ@q+]`D 1ԹjFa1T-8zq<ŷr|?$Kӧ@X߿ bm YP)}n*6HHc|WŧޖĻi?׀KK6.Iv 'T<&{ea,cTb`fڂ?`uwǜT#U ж;%ߐ83aw)JDIYZ{Wj$o(Au1YKK CTʥ)I?vn@}6xHg>) (By)$t6o=(e ŗDCzftRc^ЫStX;@\Χwvߞ)Lqڧ`N5ie`/O3So\rGԬD|0Bv)>L>hٻM{ se7Gtw{m܋DABǠ 3kv(|L q8{w5 DW g-vgz=gl3Q;tkȈGOG)]?9oCߴ秂f`ʃJ}7@P?Z1zmg\%AU~5KY}Y Xjq )Ϛ?{7=}% W88v.BTzҽkC_SdZu<&Fx`,WuΤ~Y@LTJAzrֱms+0F; ܹNru ;|c#NZ23tpnCo9=;=*1:ՀPB>矓$߹V-J{ƸhSIuPjam]n8t8ߒ阪,EUJ7ߝL6X )rvac&  A):ybM̈́`;cK .}X;̆xLa6uZASgZa4Em-Ug1gv|`YYEb1'H%˓WRnaL לŮ}:0ÿE$|p5y$0ѩ(q!~(n 6׍r|K>2ZnaWw9ᯌg9es4\ >R4q°Or>4;Be_fVBKD+pt yύ*fiUЭm(J$&FDW,j9xG-ÄէDhvվShf*3/iJ,U|Ɇs[.qlR RTdbc5`UDo}\-C62{65$㍭Yxs.jw*J M>mXf5YYLz|4-1]9 Bkhzeɇ26`Y(@H{To(ZàOA$3Z=l:G38wOyt5I/|QSydyYt[,I8Y'}ŘsfqL:2>3qyɁ5r:[,%봦I FAYf0@xKflxX'YL4pW슩{4j [ԩg=U ^QA'S% 42k f;仿\sv_pWDԨ*40tYE~bmÁޜCy<[ťWQW @l/D@Zn}{ \o%bIl꾷Z Suf96c@A' lu,|!dv %xDl{ôF .KV@;X}3@M_y.d@ucrkYoLYyVpO&B8(@-`SE#!E¹/}t霱Qtkr܆iƈ¶A%D4/P`{ZEǘeu#$wڟY! ?i*6(o1  }At8}.D~<8/XͶb*VJA;r Es=T}%r-Hl]A$^!H VPpƾ$ڪ h39椭q L1G|Bs,@S,4o=" POygwc^ű\fZg%S_ͣ>>r%u&[HN}{HIYwv|usOz"JrPІmj@wK?(ׅ__]F]}#^C w9HTLط-KX|ʣO8b{ͮvO 'gF`-˵z}[0-.ZgqWSxv=1&fVvKrA%` : ;ȏ71g$<)!vPF);uQF~GFCU$XݝG.ONiڞ~10tcaȂ|ЉG$Ev 5E*u3]Y(7ԇRmj<ҕLoۂS/ [Qg ,!@ӯXu:B(9G(MN2㌌n=v0qf>Z9f~Tخ~E1Ԗ}%w9W"d?.lXG& z!FMvg2ҏA FGܤ5SEu͜TWy8G%et v{Ix~*hp0N^ӲAjok %Ɯao=~y^tt~l3<4NM]X%}R7fMĬgRJŅwΞQM2BZ cmPIKh+D:mKj˓Įg~" ^e.ZqV;Qqܳ3eFy<{Ӷ 8"爖YUk7Т i %`bhl[Bb[׈qރ-wkEFײԝ\ )jvBBF+H</1I~ENx ߞll2cꙙxDCrdUEV fG\f\ )zQO Z`&moy cJ]NJ%"WҾ>uz>]N*O ruݍd ӊ(@ -noe霧):^V!2kZu[Y#ɍT\)pJ'<8n$txxs(B+݋mR>E9Ъ0KSh` ?YC{4:& Ld1UoT~O5E6tPNxc=X:~g>ǖ0"z4,d `UOɃ> W*CxWڹs寪]丿7u"_XCpW֭%~@JxC9|>'{jacMO|GA43B"[Jv u}&c?H%5#h@u**iXH,6>\1'!*+Y|aI}!a1|Ζe1l9׬x6Kf4ݳ>`-5DRFfw~ZC0` qeŒdi w/FG=Ŗ ̦d~{_h|* }Em|@#s0d}iOTT.ęk'K_X:e"Lg}@Lⴷt.(V g{. }y:GkX C"(7Z3GɋGq_7^C6*j" <#Ǽ/y(;/X`"qwlIڋi)4UU{Jbc3 ɭ§VAne;7gd;ww8Fem"yN8Ex HÚߟRAu?s₥e,ފZ"O54-'~T+'<\kTj:nVDYstr`T0Y g9GRk6`7<#D,t3.zJ(&Ѿﳀi~r^-Ѝ訁eۦ,% Z7QOBН%⬵}l]5ģ)~;ifTLew7v4E414ңvg̘f7݅Y`8&LEjuFF}ݐ~ZN8P ˂sR4|WHOjfR|s_zNk^BNǫ\Bߖ7P pœY.$)wȺU**"yfr\ X޿w<*\^2{wc1S BK󮟋AۓjAkbW[$oEwG0N@PN ,j\Łp5w@ч=h6zM9~}?춞4M;?*0+ FG+Jho^~B7*q6! TvqZtE($BR=~iWe 7=tؓ~^hc9Vոl| yɻD,95zĘ{A'2)xl/?%T = L8q[o02zUȻ)mxې{7ildcZZi-ك{[&!t4qWi\@=i`Q" XokQuk\ƱWe3^(6;${.DgRQUW_Y$9e:Ul;ˤ>3D-M{4Ne9*(-a 8^J3ZC Ӗm5ݸH#CLfk ?Rf|@2qr)($5Bqw $>JŎ p, ݘ4TR`]k>@e;X}ˆ_~ՎmQX3>Z1:.JT~(7yȥЙZxy!63йlf6x^~IevيV{కΖIcKʈ Cx6^2~Oi_>!֍_5k$3^MBN?XL .2AM-Ruc%g*]&i@ZFXKj\Dn1ku-/!UnԎx_+&Nc˞o8{`ꁱ՜%OBvD) [5¿#4Lt;dLP[P#T`Un>ή{{À=k%yeaEI4j0K9:xLc;"Az{o2$}2u VQ8fd?~"j(/Ǐ@~t;Sr0\+VE3ꖰS/~46L' @zYq-)%ɢo /]_LDN6[FQv™[+JiYk}rL_.6 "\k,h cq| 3eP۪Ȱ1XƠr}HFJMn@,=~'\y =}*Po_(v,笷IҸfcEVp XK~f2*UD8\=ϯ79酢yJh8Deig@ acΟTUiCtԟ"l3~71Ӗ1eCk辳1*ڌ<-@FEEpsit[)ujGbz(`\dBHRuiyZ;ch?]`T {X|@&U ̲~Q+ސd(dpùs ٞ414zZ)3#ŁV8,PRIݶˬV{*svrR\odH0fC=te02ʤ+?wBwr$e^^*vsH|%[gz@ :A̽ nSKrsj$iG7e//P̵ٰ1Q׊B6^x]ZwIu&S!ER #ziۚ)OʧJ.gAm" ! '2Q qb]dx0 "6HpkDj. B鸾F(Df+du[wѝX(l:W\et Q;g x:]MƓ~$F~pp3a5X=8sfPh)YM{ħ鋌]QDT!k*-6#W}@q(Zശ1C|Sp їc̮8RH낅)]k"-G~cKCͰQ99ѤmaNEaM)|}"CTH8G<.M jJ+arfC|Ww*Urr1Ea.N{r- Fb;inĐ;z,HJP,rv5ob0p:vptl6ˁbyA"`Gwxv@ wG-8z x M,xǡe;"h ~1VбE=хgZoi׍w?S{n2UBuj;T~>F,ٿkWT~P%HIlX `wPS;|(snvtHv𗫱F.dRmA ?ض!~ndNxܛMJp6VJ.~1!5[h *!|.wK6 #Fgupt2vRyW>"L\q~ *M|^ZfN&iY egz%Lhɱd>(ƹR۟sdU~IpexTɜ5xS0fʗ# :;(MW ))R-pz,t#3goU`@W-:]'@:iɻP5rH0dhMP.>ʸhF`z!X6xZ[r0ޭ_~{V6d4&SO&=`<Ԃ_A - iHc0(/1'8K9 1ުI#X/1*G=#6xX&QQ maI%'D=ӂHw(g;揞G2v!?J4%U]dݕbQ^,f _44$Ӷ=#<{>7f wZH͂\o`$)= !#ΐ1Zh J\ k\+*^6#ڊ("17\)VȤXPD|$)amUO22t(i:}=e:6(Q`iL+R{g{@ciŵwiB3} vu1uD+m VqB3nF/5^&.^b˃áT_;ӱ|۰Hb_u0ɐarb=!οA`wcqȶI$&Zp}~ڊ:_\:o.%)i_Y0`zP3f,zXɜIÜ"F*ݭz8M4ݗ{~dI}NbHFoբދ-q[?d V$OhNUU1)IGWe@:/U4 }ё߲'KuEޔԧ/2ackKlġ} @+qCoxhrz897IXu sC[LRD]ݶZq hPIw]%.–(Ze-1U޾tv͉4^gl8` ~$S¨,N@r! IڙMA˵v5 #< x5XRh1^b)Jn1+Yy6\'ÇY O~OW;c5;p̣rX?Ro)vSHE>يpy',۵@8VFZ?J2[=di2x0[]%oH:}{ -'fbfZѷlvZ-1"U||"ujDbr,DQx dBz&/nhEث7o8" uONK"mXAiz B FrNc塀i/mll{y!Cǹ#C V^bVyöz?߻twKN ~E /PD(L͍ /hE``qY)MbfPE`0PzJZ enMaeblp@pd3o֪ln:Jx0y zi1xX3"/*'@; pH:y%L*cMľY$ÖM)=>cI5#Nca9eY %l*v5kٽ\p^^K_[+^ɮq{T, H%0!EJc@[GT*UL:޳Tx5i&O^h~KVtUT%&5r(\WB nC; Gn>`G'8p_O;B23  1p(+K@dc4UX:|^99H q:xC;PGՙ,&r&ýH\I2c >f9-Xbtg#RB-Ѓ*qj|}bE!Ȣp F~u k&5y[)vL~G'3mlf)>OʵzcB@gEr By)c"I:XV$bhMo?k't]S4BA?7J56->ғl!쳟sXQ+#`_X}'ں$udr|}bxjyzJ&K2|āv&$Gr`i tK@9(ja>dl}vG;XSbcl׫ 1war邕o1o׹<{Haz-ƪ&O-VF>k>9?Jѕa['IaAɅ S%X6V6$&HZ-Md;"04"OP|X-앲NW~<8~T[y `nF{ A[x,HWȋ:_L43 +ATym=eVɈ0э2/r mʃ͹~D1~XS=tyM[1NBØeR1@P.notS aQ-B^=/.YlZ[4h~~bꢯ5Py~uYӜp:wpʵ~`wEq$5#037I_ie,T+R#9!TD'#K9G"gƙT] 3b8Fυ@cV^X* *hEM]\!$.]7יVYr9$k*p@ [Q@tQZzxٔƴ5lݪ.~Ojh(1}+O )ʖ'm^|<꼁Ad9yigrPR(?.9AҧԱ<B%b*^"k7p96GN-j@-4qK%hܐ&O7y1'=iz~9T} Tz'SDHƘ?17YTCӥ[!Td5 qaO V ԙEü@GUn 6U>F> r/9%)x fK~@cfŖN8¯H2/ف%[ՋD-=h.;_QHb]NAR7F%WY<;j6|L)~|m/ym (JMem`s+k1I1umەM3LzG!S6bAߧ-{H7">1CVX~H YJFi%Q;ճVZ0(<N= 8oN=-< <ʾXjb &240;f'Lh_tϿKc+&ūe*)G8qWN= b8Q&$y5gJ`ϘrA9Le\O;C0ɏX\U?Qғ b  `V:'M11D* _*;H.>9t7n8v^-3&*7 @*ݧ&+dKν5wͲt\ITu` # rr?a' ] $zX88 1.lCB{a3T:P ž|^(w|'ID:Q!n%Hytq,澁I:.eKv`6m6]?-#yd@͛-GN9&X}qoXkZ& lUk)c\#?k(Aw˫,IQG}>%_Yd LoHr1)H8D!aHK[/?.'ɨɄ, m>y+E[ٽ7 d@πzo(Mp&Z`j 'N w\$Z> ޷[/-Sxڟz{^6y&e|S`v{fC!rLN7ϧIrQJ~jCaw1o@d\)eo43Hܝ.;2Ʒ)^JӦD~mKX,B>ۋgG/ŭ, { Ppp*7wL~ , ۝ocw} c \K6ę6I# \Y0 zkWmql5JS.2;k)dfyuw5SMc0p 3- /H;Jr{$J񙴬"ܛo~eb@^zx= Dz}MIqҶϿL -GA ԖQ+(vnϿ']Ԉ4e LK@cm~ gꝲb\`7L#mMkJ;Un<^}z.;MF&}mȓmwj8@uD㣾}Xxv2L%⟌}m3)-f?lD*0#@|9;Zߛ[)1?"Tro'a_Xp 纙g9XC`Хtyg~~ Z[Zȷ1^xbANps'Uv nsOfXǜA:rO:rEV2"?GΖKӱ ?:)zcj./G~hiz Y{4s{ʝ윝Uy QkQGFpS}ܕa JIZs;Ҁ@"R/+\7tYm4t5=-ro9LXHlݬ ?;ͩ '#p$] VޟNОNA a;/=""&?AH=qE1X:ئ ۮ@Hd q2iClVWs1d񿃐(w{?~X@k}g7sG hJY5|S5ıDcч}ٞ=d x` pzmqfgjWUc ".'@\"@ѦepN>2Jqtg_ky3Hԙ@B"5I-0`W|i+B4 z/J&g;#!t|U^x2j5A|ůyoeߣ5˻홁ۀKCJ&ᷜK)' e~3' Ah䪾8p/tcش,38pdU}GB =eMFoW25#rd?|.2v຾K܂6 Ȼe+Cd2`KB^V\dBs\G{&?jx^+uap֘5t9 E5IȓY;'Ylʙ_\=%̐ާ"/\OZjo~{gy΃a(ZXPBF˹s@;5ʰ6CתR:6 pVD*i*k C|WKbڲ n84<CnO:/JB8Y! KVQ_Z/ t\VH+arK5R6OoYT[Hף8W7#8;?YDmC!CTsUil?0Ac%n6LY w$Vzbq|^h4&ke*:C+^k|-i tZ o[(͗?f>;EU R;Mhe :fl}lŗ}q>^Aۘ WĶ"4]q֜57XPlq<! $ DfIRx.6 Wm-GwI#pw,F{˿% B.+` .'(]y,L%^^N!ڸce3 @:ljfM)ggDA?袓{2ܨ` ~^z^Xb%M70CWFZ%8EPkRQ cQis! <~ْԏv\ ~˹>R~nņ6E.FI:/4&i|Ƣž׷Xg{|dq'yF?)EvΧ:,S/[܋5B]8j]F3iMn''oX) P6|QFU[J; lMjn򘃝krty+,ZڂzKjʥ% EzC:vi)`l92?W[jtZ#7\0%AuNj8*]fr`I`'vS-cOsk%zq׷(Q6V TX4Nᇬ]zƉ]`-bEH?s|xɀ..aTĤ& &{:r-,KԖ 8\rƽϖ0XIMGJ_&?w4S :hIQkbۍѦ>K:Td|SkSa_tM/xY7QWő21Lʠg7 5.dH&?tYq(HoxS`K/5@8mpņ;P=N{XƑr_B/|sZm\{X8dIB0,UNJ+zޥvF~*VaɳAn>ы؞q8D>15]Mi~_6TuTmN`Q]7% 4ƃ9GH5- ]ݮy [^$p};]T~wHH`e_Q@li4;(>G1cf 0my:q x~6΅׹+<Qk[nf0/U~z/.r5oVD繁#2 v)/KNl2>wmm-|@x0  ځGH3%)MY  ?NOe-sU"kN-xqw^,ϟXT>WΫ:l{Xe߶ p{%:rs(^Ƞ) @tej6DCД%( AU˯P8A Ā"XJ].p렋t+MSsS@4rv" BglZM4H|)MA7U o FTU2or0 %a9-{^ʁAFږcPK"ھZ"=$]znDbD c'϶I,ʌa䊌m\1 Xs!Qyw}`c%pIѯ,8 G`/-.|JFb0mj!q Dbt꿛(9hI_,bn* `c%oiJ%@3$\Ltw߱ܜ5{dST#YD^[qw!B 9 &c1AcmcEVŽv`[E߁O?I  ! b 6J!% NE#$d,dD69rB lciH3hZG>JL1OϜkXJ(qŤ Ji%ˎ$K`3@ƄA\Қ Sdp_sCG4m-e"Ce9PD'l۠N[3(>M>^PL:^a>@E;(`G˧!*R֫A:ev!Ob} 9&[g9MB]rFoj>TA⚓[apn&wpԢZl?7 -JfqFK~,aۊ̀vꮽFf*4m﷿Y#ٳ [.`u`p"j,=0.h34;VZa{{ LJ,W* е 'z]**EڃV`Bw7ժd 娎6wD~܍VFV%,y1.t4P߁Q&B]1=cUR-G(LS׼<FVjE|ʕO1 4CZ:[v#,ԓ9#f5' ],?[x}EYTyAr ZU{|tm^OpOk};}@M (Iy)Fꃤ,ٰђ$Ie(DX|n$HZ&b$8*6-r&%8R/x߭ބG _FԱ2L{ۈ !ƶ^M'W>lbTs'`4,vw+9?,By]z (TgT5˷&};n+(rM\Qo.m(H=a68 =;Z *%N{\Iuz n>8R#:>#j"U%iF˻d Qfp7 $;ĚhŐ[6Uh=S +q?[1u"a!Wkex"bZ*jv>|sy{F&x7^Ʒ?<=qU$jj3QsP>HY"϶jޟ*4&KY@JОo&GBV0m{R2 {Ouj&f`;m"$X*{؀TiG@q^{};@{{;]\o /EH)&xzC"ɀ/1ʷNjuN @(dZDEɱz4P WC`1>4QZkxgG_ `~l ({sz:~A7Rͽ18| &pu9"Bp%'';o/hu8}䉥Y|(x0 Mhf.U` ,FRU[1A7JRj!.;NEOnФ$C"aD> HE5|zFC Vu>T|&4,3* "ә55k0N]6WX1!00f?*z-^tʰC\w z%\>/>tGYwv2 ԪC.[B4hLU/w=[ oŋ1=~^}SWus$oMsU so";j3g;j?l]3v@ 0G20R6?2R07*ÅM&$Be-$yL=MWc/b@O)DIO;R!; l/ ^ QXZf-1:z7_]cajdg=eL"JU&<ʹq.ªepFMire: `@^fE`am:'Ne+:{n >gvJ4@0/ ´煂p%M %̡"\!ه˥J@x%Fn_FfE0N0exP%Ģ^y" SNPH1IUjaLLʱbuSPQfKR&0pH Eb!V[pMgyăqsyǐR1U{fksr/yk* 3x͋`qEX !g8Ѩ'edxqVۮfWB*+@  &Y|N֋/ESHgN\8Fx(i p - לL 3BYFh=I ,9rXԠCX!C JGr#^ 3D Q@'ΖW%;J'1q佬ZW0t3CMHM5 PYC#Z4aUD]$,fbxV=ݤsP@2ou0=~G^B”td8%ÆvWN: if"3mnKYaZF62&`ЫrT6*!8swPusƳ"Ji׽8yOįr*&^˻WTK*%ޢ 4Fiv$PPDZ"ϖ&}N!_<"TYQbR4 qPfv';9s.qSS=g9TpgMph%~>!f tlmX66kDMvPHb XIp-%K!> _RI v:~ywR,OQT 褓BP =_|u٩:}g~^HKi ݤ T6y~4M^7ηwdV '~oVyhY^+]Rj}'pwm.HwuP~fK* p1U0J1NxBN%;oJt&*|O"; O7Mچ7ߴmux\Mb%2:V2:/>r`2ĭi;eQ3$ 5:i&g9#JK΢1:/pAcR A",37Mڴ"5_}fM$A o Fn3l}_!QPf6lj E`8cx"`JW|+Tbᫎeq1~ohV6^ wHc=rGSxR2X*O,Ā50ķiAxB>ucؔUēɭGkcHdxng꽖k6ECf(4 ݺŭϷֺ݇[`]\-bzjQ#6sK}sz4͈I_< c*+xC"` F$.TkΟ]e_U8 eqyS5\U3j2`Q}u`ױmijU3‡4*!zn\߃xo߁ K[fPs0)w?6PWѭL`Ú"-ىѕ2s^=ԅ:7Dk&1El[ИO@>nb]XQ=<C4]OSs+}Z-n_82OMS^,uxwfCc.-v|>)4V H$&(0pӀc"DTDZ%l Pѫz'~ i 'mj(v%sW>GFhujnj<߇Hwlgiqޓeȉ1A#+[ z r@TEj܌o7#J&U@J'(u=uV@Xe&.qZC,0i(Bi3U\4+ӏ9>xΠ~GXٽï/ S.hQ ")2)i" N፶6M؋4@1˛) gp}Plc9o HO;9އ NMn+uoIpD @j!׮ێ#R^ALmRxw8-1\-\1NpI umZJv7Vi3n(0~ 4G)&C ysuu3Å9-pHgZQ< ɜB‰nCh?&myg{ mēM/)pSPAI, Š# >_6><8Oя #i!V<ҽ&dE3cEFEeE7R:xp 42+iunkvl]N=?G4Q')aXJWj\,Rq{xcJДHJFspI  M$\&0ǹ@6hbHm06$mhC`b &6 66 sL$mFS=r  y)a֑% l 3;#`FR_MPt;,GCT@_57ᗡqe@W~.qaЈBcP씳m4t# bX桃5Gqp~L$D-)J҄JE3ax\]$5"+ D0h#Lo=0͌Yo~K s-/D7ALDD}yJEB3 ?kX" Z3RFqכRd6\AiB* x1aݐodn w{L[,9 NP"uć("rPDdMIYg-Q5K30YFGt,l@,` V20R"Rq].M0W o1ڠĆ`B+@]̑6(Tk*6)^ary< .X#V~7Hjg,ȁąRY.2x$FQsc܂P!{0\ y!B:$IA([0֪Šm4 gqlYc[P ZCyՖo>k0/KH',d@D%י r lڜl6 5N}JiWQX܉4\E@(mMm%Ғ4 )j1θ4^ 4@hP.!J̓ cم,m a&)ͦx9Kg ěv1{`8Q0DoʛA3Tah3Nv*J7+`( Rγ0R34R!蠭U]Dc%/ׁQ[5%*SAU$qXYdI'w[0bk$[ BxYvTmY[݀|٠U$rhFy`ěѱ؎a l GC<'8W*IUEErj3_LZ! ,#܃S~‘ #$р@` , p bcɨC4@,W4X+ ( ( 1mJ}Gz5 `fq<@z"Y} ~z k„mxgǬ7C5.D<Gj)1v6ZVLw"rŶ0vJeRUQ|S)tb(FKRƄF`aÃ>˶ڪ5_mƐw n|867;5z?˞\ 3u=-[=fAdX,G NsyVPL;!|GaHV;sMjB#}{w~?{\>%V}8 ^ (#TݨyCҔND'hZ!/0Y,\蘇x", 9Zi2B𴘱!́1DBCCs8k\YF;GѠn52#o5Jpc=.Mۀvqv lU'bԇtt2]@\!7@l}K-B2@Ui.Z3;Vr]W0U2V9Mٞ3.˚٧uFGayȼs:;Ȅ2*@# 0N,6s"CfQxl6-]ڮ4[I$v,Е.w(C\6ޒ"^@ƛŋ@D@/&LmBMa,Fl{7_Š QuoiRB٦FI8CIᤡo=aġ7KM~}~PZ1]NfƎ> < hYBt:%IzAm -_kQ9M0n`Π[4_!vpL( Ȱ/\D|i~[U2ߩ4N_X2G|CCi4 E˶N&⯀7YIَ֛'j#zWy8s P5Kidhi. mf!sKu3N KZ,<)9qB^ :rWHaVZzB6缕^Ukċ$KvC;^ci`f<,~a?s ?]؄ǥ)$o]h|`}&`SŸƺ؛VYWPpGPe#5` ^!⎙ &xiԛI\VxvY0VZwݖF@hTy 2 ݙ#\'FS;yw9gVz:ci̲݃ˢCQ_ba?1.Q>/-j9gA虗7oOpha<ׄ2&b=ZT-1m/7: d僞OLn? IvФBIi#R2{ji[cx9MGb͏s\ l(Qc=lRUa*X ƿSQ˼L)d0RsAQ [NdF.UTJԇu~9B |yvE߽GykuQa%o4TV!aˠ}/=f!-{TRoiPg+ՅV|j@JlժxcYN %ȶ3F7 ru="798.؀/ƗP@%)tq|;\RmX9 FކٯY+7g騩f)$&Z?; +Vi樗l[PL0]|e7?i&ӥ}\&Nה Q6۪g{Z7>@,&00a ogC70w b+d}Lx\U,K5϶8An\O"6wA寓;lvE.=4_Kouiͦ !I ^^iy-HZ%)z uV, gv}QDnܜlXc6gSZ\u_R{Ϝ 7 {A` !AeLVB:egn5xUE^-dbmu5yD~^ӖWWaVn< ^ېmU ?!O/+0u~֚`ŤE/bU iX*~d" f5nDpb<߇@TpH CW>J=,>)zb#&j wFQa!qi8hRG+Aÿ-ɋLn><uh[q{ZX>0bͻX;4\ln?9ߢja0pQaSonJO:Q汏PTYYtX?-)GQqN9x@5O&J9`# Ây]1*{]BJItX*vrFB#_r{`zCj#(OL@}|LQ ^5sD~[A;;"x-cF뀬A Vo_DžS\4'οU?X[.6ÌMqܓN3b8ڐ;[,hAD3w7_ o\8ˈmY )$q!y*? 5/z3)3ك&\Fs ZvP-&1(\fVFxq͊S1+yz' _r,|PTԋ:9Wի"5|Yʠy,IZ1w\y}ʞWG й;@™zIuSыjL9*FNW [oPg3LlP l8rո'`ᕡzmXLXŦU42 O1N<,i}p?6>~vvJT䮮G[zXե%vU.[yJQ,sc1Bk>ǵ 'YV//<[Rhl1pQ1|oԀ~2&ՖyVv|L ܁@2!|?~3JؔB/콨|piO.&zgoVJQ?]RC&#tWkD ]A]}znk[d15^DpK:[gۅ[;UjTr8Z_#>?>*<;|0ӿ>>MG{vP= Leb}&P6 !wwqk!< .>++P=`>'NMTy^KN-;'3|?aۖ߱|;ޏA@[w?H8J@VQ5['95?y8|]1q02<Oq[(x}c Ȋp`OVHż,2AB 3- ).LHh2*{]\^2ZT!$o8ݡk^ۆ*RI =ȵk)xHxJѱkTBʯYP}PIM -m4!'c!jўC ̖'N8{);؟N^uF)3\S‹:XKl!i=@ ~g%p`h&m666mmcM7q M4S!21sL3TH@ }4,@B54CD6ĕ c6cDA)Ko}}ˤB]ڡA?_Eu>f ^ܾ8Xd4[EU0DiB?teB9Kg Ꚕñ$Y"4!%7AQVz-6 Hl9?m_{n7^cuv#; ?i Cea8e: R)qX~%Sxz}t"p~v.Ր@s2]S,:a%R\@6`bȁSQn鞃WAuD9h:" ]@ !WH[P+nA(hB:Ѡj4fR=i9hǡ W]@2- Iيȩ8"M*c)`;,~ec<3"_BM^_hEF|$T3§30pk/,HuktI#6 @` 3k _{`I_?[е,ke.OU}m'̿[47VFLWOI+>_lOeL]ʎQuKۋ 3ną@o q@/qHnEo˷(6yZ[r th?/Pd {<,='ziXzؙfh@tC7nVȻ"j/xnһ/ǵz:ZUFyI;€F>Lâ9C27e,~OxZwPޏ! ղ2)(5TPuG̙>>!K +|1HTuokje%y7|pn6[$vQ{M,qH~4 Zl?9E酊nc%-҂g?A07KAnCh5]饸 \c$ p+&nw㴑* sk{ٖle3Yn+R3y?QEC0 \ 0H3@Sb71V>06Rɾu@P`<߳Q7R$ \ !Kd{']F?}ΟB:lLm]^Q ݫ^ ?8,2i5+n~[%$O;zwv"HR8'CZDt\uF9J!<?[OUj-1_|[p6 -aRa!fqW[!4<{Nƫg싃aG @׈@pVց\@[dɲlaPʴbm[Nu7rL^Mh/$tge͇`t^0LR->g[/ƒHkJ") Nl{_jRHBk(k=FT;u Ѥ.rJ~F5#xS+J3󛲾W! St]vIrnW |\)z_GD8JuRV/YAebbJLuyR2BiRXXA}k?jlF̰q\cB^8떢;.,cXBGϊ9!mEDč14Wtx)1SE^o~|Q1XgZNϫWE!I۫W>ϵ@7< 0pȚ&ɣ}>cِv J'ýx.3R BĦX~$פ=JB(W$ wlTb{a:uwuV@ƃOm焫dȅKUъЄօq9'YE 0d7_rḏc{@qBJDHhzJ]d-<矪e`OḞ f|ۛnsZFX.޽nxL^4H֮QrTR,F \IwE fLJ̛c/9u+w?gم<=r 3|diXtV?9&g/DH@ HgT;^sM8zK,@FoW+S6wUכe?׀0bkd 12 l6DCK[hd68Cm$&.H 8Li JXcd1B!5H@VAU토iD@y0wo{Kzg ˱Ļ1.л]GA$au|;̓@b`7_c-wT}9 +`X۔n5C0P[ǀ!N"߂cs^JO< Vڡez?TaЊCqWb3˹\nSNll( =!Mq:شƃ< o9HM}f_9H??F-%Q& :$Aj&n^t![3Z1@s7Ӣ|0;4_(7;nW%!c)ER{'rkbʏRRV{LVftXinn2 YvIr/um^iS(TiXa,'ZZ"i7$k\QC[2 S&MZ%QEƔX!fT.m񲆉q_e5˸8-V“͜jєp c\DCJpUw-^`(tHVN9Ȳ0n|swQ0-2/+bZzNh0(/4NBOSE'wuW1 T/c3iܷ 坷hޚhЂg>H>7ueLpQcȽd-jsuq#_A'zO-Ѽc8*ؙRI/W*^Voe[(AX8Y 6!"Ic}sit |14|R"rU chyTN y}c.r:[.\JJXGS%rR_M,(Ɋ8逧e3m7u""2\_pH+se"&n|}J~_o9pZK,6qqG7dء@63GnuOJ}ŤA.0~yI@,\{[ vstImKyPϵUW_?u!ādx AM)q"[Qfsdk"KXacMfvgv; p^yD~LcR'-?`1{MMh1$nwjlUDWzETu5!d8^' j&ݻNgk1#mjCzBSfuu=AK:|#' a5ej io\AP 3DiaaSu61jD$#&G9Zt(Ex$ W~?>F  =jc&kI DŦ M|3am=LW Agz܍# ץxcy kJ)H)Vabbۉvr{zCCtq廽`FFܒɕ?>|ٖ#~Y`Qx~5hDh0 h eu^~7ih!b=hȩ1y9+kT?L+6D% R/,e%7|rxپ)փSkcCàOׂci@P JxZ:EB$TcwjEMh am<0T1Xwjr 1#.YC&mlDCy4n4iw#VDX=7oR7p/,N^߻ӏKշMv^L1ƕ:V\* Wb;̮jB[ͅfoyCZ{e5M&ݻ1. L˚BC& W8(?p IP*XJ!*U55ҰTFLo(,dnY_iI"/"Ax[7;̖"Dw(N;*|g`FVn Դ+-qV[`_z6!^/۸a H^Q " dԎijj8"ɌWƢ|;YH2| 9H%\3b08\C~}V`6D㰛ejm_jyDXVKwwI!Df6QZ 7qFФO$ `\8f9* AFoD[6Lb];QJ)%ɚJFfT vFS8 < vF\AT#.t6 CMT& \)D'|wT@Ό!CnBccMD項몪)JA3]Z) I6 6R A܋{PƢР<¼ɤI" %RÔ U8i0;pl@S&pYiyM)s"a&bjP"=O:ϡr_3HK57Dt#jY{Ҍ:% bix Mlc[UdV]YM.;&`'V?P""Y7~-&Y|7/xp:,Bm}VdD@tTӨJu{yMF?;.ʁT0`ӗ߽ uXwm*?5'eI1Q+;s׎u$~8⒜4%PX_^2VV$KyK\/)_] mھö́nkrj= :2 fiA;hW=viw;濫H.qO, = +s8ޘjQ>9E.AGr;^H۩ʳs|,E6` eͲa\=sX6>1Udj|ѤBo8\ spG}Z$AEꩂ->t}J)Fzީ;ʶtLntr'η z>9"UpWL0Ά2o/&/ɷPy]xji ,7s>w5O{]KL*'&R`;j{t{5 1a2d @j..9 zQߨX$iL E=x8q}3&NMgػa^~Ԏ/K9&,)i :X$2wIWidlSHeaF,etJ Ӂ9><`C^鏘[? ru=afrC4MWD^(3ʋ^\)Lo_t+O*o6Kva>A0ҞYA~أ'HG= bYe#ёޣ( :h"+ݗ8oRɎg}GEi UY{SŸ-#Y(@4nz녅#㛙ss6^<>;%!/`3m:Apë#}LǍpEs*I-Dp t@p F s؜P&_$r}&h<ܞfi-tQ7Mg8Fs{N6 а!2Oh(4iePa# EBw=>sa@D(ݻycfդwk]"Ebm)e+)#`C:}_ZWzG]gR{{Ѐ|#ƈaikưpzM|聭3[;X E؅p1fh7GIA0m劇ac 0ȚJґ& R5-ً+x [޵ PiFK1̰bX0@f.'A&&E, I!REK sYԺX6E+ƒPD+DŽNdf`IC` ExD33)J)g*A*iZP&L$ D@0Nn`hM&'&L6'21px( ,be"(@n0p =ncIP,A%PGk"s]ÃX$Uh@;-U7g`xd BfS,V& i8"35J:%QA#Z 0kѠ'V iskc=53ʕ0k$R L$BfLqDQqGa Tx)@K!Co (MMnR]7Nsc la$)Pha>!p^I&P N`V֡c] bЂUP~ԾrZΈ yV,^u#Qm~mu%jp|1lRRH2Ŝ%S~^l⸬@5r鈉S;1Ux3HA񉦱j ˊ qu)h Y0${ўB1`c$DNA:lTWUUB-(l|";|8&3qlJcrKEf6bsXC -(H%rP PҌ0s|]7U,G4uε1a BO=#$JT F9r]Sm >{)wh%nْ^{ɊU[!27Db{:JAJQPL*!H]ZELp2h+|l b6R8gƆ8e1D@8JѢGu`qe`m @> ymfG6J1mRQRS`at:U p7 1e:"EFZV.ta"A&# X2QIAgRO3RJd*#(堮H܆"IdIw! D00Nl]F>3a+Lu ,J9Ѣ!Y &!9nuBI 0:BY80(-:>~{ۭxߌ?˧ JFQDݝ dd a1g]KaW'@r\ɖaJTeJF  UBvH@(6~XY1LpXUxf@l>P9 2lơf1! wat/ Ґ &4 |/]_C:-f% jZhhMHʹ@yTT*|w+cl{ 8P_T;]M9X+u>~3Лfh&ТA2Jj0oſ2]~o}I|Mo{| ["$Yw\_t /i2/.xAyLi(DdA|Z4v0XwC`1@#ͼu M1 F$aMYȂѦhb)WJJ x̟i(*A"oQ'ᨰ06iI! hxm>mrmۼh&ȅZؗK S U,4Cl`';J AI(8TJ`)@tjβYhf,Y=&KDApH)ܰd"3LTZ{9ZT~aڇ1~ }s-QN&L87 /C$@FQ"Dh?*Gc䱸D0>?u|ax:F2?Hޔ(u\━?>>>*"&]7k.#K3HW&&3˓ 34yC+tg `|Jvς4aǞC^x{\%v=c'?o5ɼ38o&T6u:A.2eDS'>pRxm؍Fn R1%N,#QpaO6yel`QdhF |Ql,Q wwa%@T嶹:,XiQfTupAIFҹ"0pc;#!Z֐BxezytL>3C1<g8OTcJ|V1Tr6WC9JUSñy$of;YnU_{̑t^ojM6s}%@Iw46GOݠ?_Q- C5G3i7Z޿Xcpi'G>#hɻֵ֬<.hBM4"W1  % !60s2%$ M Xrh7 !̄'_2 42n_>Q?o޼# 1 RZ#&f\޸hkfDDb0.~,D@)$6;1%с;]|҈?Wo(/*`hnyoW%o So ͺ:pA~ilNY9 +zeM#}9$)eu?gxQDH:TA/.d "5ϟݽ{C"^7hd#ӛ~__Uv]S` m) /* Jʖ@@=[ ZH BqACNӝv!/T>} TC<ǰ.xG!%^=淌gIҸgiՍt|D׳m8&\7jP{:h:AvԊpv8 }("@m~M@ d`D"Ꮩ}EBX?6{- @nSżѠsb˭ &D;*J0{?9gF 䭉."ZyhU]r~$)"Tn"/`_0Ga^B#E8-M^= 9^xw\TƆЙ(H`SYHx 6,Unj.k.s?rlD(cDDDmJġ$  nb59 b/巄xlj#03۾b+fa+39S {:nr9ϫv'%nr;/qI[WoW_{[scpkK^y 2̂TE7HE4yFMYMKP@Q' 0w{vz?4ET"O؈<>bcG~{@BX<_ S(B`彁3jh<.8$pKP.E/{ge;}eϟZ95TB u\ڱ}ӎo<%ϓjafѭeQ_mo"r@ugP3 nnlogʜX/B#VQ  ][SZ&覧>2K"\`G[bͬ]bKMme"PXTF"}|9 l*?}هC,ެ&54Ȯ{"*!BMܭP 6U*,`6Y߽(K~ɾ .gZou鱷(.M"p r z4 8B1'ANσy_ 00>/2 !eҥ4kt"zoo7z3FP˥4 M16I @AnB`e;Ył`<|pvΔ,s.G2)(o2QmXX]cC@C Γ1 -k }ܥޚ 65 As!6xFRP&dImjـ{Oa{9x_aK۸{h.3WIHrߦy$t Q1t/4V#iZJTH0[I dC1Ej8>0ṛI$ ܜ= [06W,&CDMkaQ[100m 0L^#u'2 mFaD3.{)pmST6at!aXIfBj*iTjO59@(r Sg=+S>3`"1iU V<5p,nYVY#LNJP^6 f= F0Ȑ* qz[3MNPLaFɽP tou{]L,X];3cU-vͲzKZ2 Pi_-E(ZU0LĘL;ffn mNT˛^hky+\! I:=[K3LEnޠނg7A 'kkB^0A~;} CoKtsi0&M&qW(1O /VCp4P< "..fXfFM1:8GBI!V˂n16 ܑt\#qwF^7wr]uDq~-)@d.9H`wm! -S<\#f<0K {a`8\}U J$*%Et炰fz{d S!pll3ȒxFCzif֤HWa3n9+8nolc~Yo- 6͚aa{[xq1ǎN9&b]GӠheIF4'z8s0֧H@[fڤhx<&VDA7c+wG1HO\<5cK/J/ n=TB*Cҫz q TĜ,)@a7RИq֟B~'l{ u<`m}s9ynS(%錎&}pӦ&:Io~ʹWEo a'-aĹǵ^ad/GcjaEa˅\WGD?ddeR"lfU!hWͅmM BW /r}óz0;jjbU1?EUBAU},XޞyW6h \ `6 [=0pc܃'Ҭ09M`7=K3"C}gk[uU/pu=68.YoaVe<n-Yvֶ=IXBP8?KSc`Z}=Λ8jLB:"0x,MLdz7[(~}Et!OcwnkdQYTSͫ93dz!R%7sUj;2.v(Ч9VcɄ;7,Ld.\,|c?P԰w2)  ZGOw9AɊ|x'DJFLD5l>"2 d>0K7*E#쌛dX@MDZ ?!:_\g}< t޷Қn#81g.vyBb.f| /+ a[;>X./8N R4o|* d\|p,# *JM^é+RI"h$Ur8XgbkbDg% lOEOޒlwibm\4?{Ӟ M 65Dl (RHpBCmc`ȁlMAhkްe^F- `rtSI~a i/i4|;Y@{˧; `v",I-Nt-/`E|R: 0>tKVvwPAfuVgca*`w;ҏ~nbh릥UI82٪6֓U׼'9zryߍ K.dC愷o/{Wpek]ύgYEEou?|ww>Su sۆv&7ZaXVJ*pjyRx W8,(lo/?gIzhm,1Lh:nqJJY`U0Rxj_7X㶏8>sVQ{5fdžD: f!^wt©LoFOmni[U[fpm20͆hzEy:aUT?J'mQLMvU)Z5q)Jb4) N}m," .) @%/q*`ĴF2 3 7A[*)"HDf+F/,=F~j-7 $l,'볹 _>biynM6󑇥xəϲgD?2%<>P%%Yfɨ`W qD bx=mn(^D$9owk}x≢i&.?aVXЭBulAj .v SNmM}D/N!f{؉*O},( ;o-FdlBr\*ؽD^! Vpz/)6TioyGq{BFb ˟>F _ kѷ喫-7m8~3sսبC8 C߽8')د]/#6B◓0t­Vi?^Y<-o׼ȯ GxdhN2zϞz{vIIŚ^?H$ ^Q3jfHI?cc5%͆*4',fϝr~΢l~C^-b83(@gJ$jO9mqxk(@@YΥ81J"`>"OP^4ƃtM J83u̱5*8\Ӑ4;,02>$ fF04w#޺.h;:B&8B[@#S9.ۯbBKz.uP 99(=YCCI Hv6qZcB0|Fl x//{h{a|5$뫀ٻvbcDPZdjH};ݻԼWu0DaCSaz&'wecw#j(K阊BY`+BH3z-RFqG"C̅OwW7c;þ/!vA$M>X8Jo13lTT&Ϻ_5بU l'$W4v<:)$HJLn>֨ F:1G8,N2$NJ'[U@#ܘT#$fQ gfU2dm^Cȟ?#|{)BG*/Bg1k=xfTaQ951S!Ft7(X:w >I1m~% 9 ?>e)ɵ\NauN'Uc>TkrƋCDP  1P} 6 5>}#,7{}ؖoc-:>owkLG y s̀]Ti/sE>X9|ޥYum{iBkLb`!esdM`&Mlt|"َ'=5Z㞻HOl}<Ł3Uj/I;*ؼư/,dL6ֻXrVY|^*\Z֧U!>Eϯ>i 2D%D=HHs0<J[\G|x񛲕k8I*W;Az;,S@ȍuTh{m H'$IM͟KknF ]&4$ oWdE#yTR@[sLuCORG\N5msh} #;Ybڍs-8x#o20`:E=p=(k6G@}̡ Uo*QV+b+3eN_| '$&|`b,}i?O ʪjĄf8bT n "Ƣ!ږ:]wđ`Jh@dՔG^ R!4 V0z-2oÿ}ǿs#\R \䤛A1`ՄG R]J;EZf7AR`D8N㖄P† )BA#uc#?]Icyj;z[VۓrMf%ء@hZkD55a)Ds#u;&T^!{@xƣA c]']ux?uvBIR`=U@LWC8! Ԁx\ Vo0:S 8Xjs}D|J$KIllmihc?'A] MfW1x^?lSp!*(mhҽb ?tQQ]ja1u/u ;M Nf$oՁ؏SDE~뀳-nPY57@f \'DM#K llnvx_gt0d4.e/(U"= ﲋ'D-E,12^_)`Y^40i4mQaw9-y^ dA#'pʹMa"R]m'~I 0PוB y p.ri 5q0sДV!! AL! 1h{)BRxnP3 oJfPV!f`Z0Œ9 i Ӄ"n"F$ؐÂ+^}Iǔ?D_K_`3tĻa1X˱3Ad_~t!M{<+:;{o~8ymƘ qrշKm.  #M?˫ H&aI;<Y9QA׾\r(hvfY6SՕ im@#h!E ҁ1}7@XD%?PD? }^p_S KThHh̄:Q]1ڂM+'Ų{;)K y z| .Ns6@dD/ <ʧqyg!- wݳABגi|$% RQcfBEv* Lbm@-mjY :uTB$Mw[HYFaج6* mm $VPk>@c(e{qtm*0K=̙ޚ=T*tpӇZ˩OG<6:DQƗKky2$3T~ &.r=$١CPBZRڮI7KNik##!蒝}^~9DJ(j'2ǃ/|^剦?;v4+1c7Rf;oK8(L<|~IQBL9,^\A4fA.g23No 8ACIYx` Л8e*MTg#oQ_SLĮ>uݘ4(m&,xp@-n^ߞO߆Xnj=4aDul-Ft(O%1RVwz> r;}1~ gS^,?|ΫՈ$qYrCY -,HìWMK {9 L|Z&GI; mUߥ7zCJ)R [|_A%m10Ω p8( ,X输- ?CÁ/_ ;*﬋)blKde0eebt2,N1*B󼉱|##mY99W3 ].RU@M` !` ]4!cI5* 8"p1I2J!BQ04KRn ?O7"o_G=gh~k^#dgskVkZa+"f_ AFApUynt9Y%O2Dl~|IFLRݠB0`%]Wa#225Lbm0bi4 LC Ȃ' )T)ZUgAv$szݝ}-seq2RE"j߈!2UBfFM"!wȒJh&u}}C.P@(@.꾵&}[St/̼ 5C}^<;䕇ٙ : 0O^>q.0dfe(O8k:/1rJAnIe/`̎k4$ȁS-gD+t[Z6C 崐0Hjq;d(w[c u4H6Rӯ`؃UPc%*irTg=VCa!"KYRi cb Zb#1X3B(p1ITBw>_z?chfaMrtBcu)P0@60"#0ić0 6C1pZHJ"Įpٿ!4r&"~n DbCm$aPUR_^|;A?Zm1n cpDDA-5Ӡ.^͚E#8}Y3]1\^m0]S H5ЁgaC;8v+(fdQ4ձz;߃}ŷx^E۪Qj‰~@r yRA&\Dg\lw$a IU 9Km 66Mi!* H((؉C";Ȑ` #g!qBk( _HmBF u1Ĵ7h㗛!LȮhw!#$$\H P7MCkT:0y Q4rCAdq;IW^ }>˥H4~Qsq\[ Sᰌ|>cmzqW|a8AD"@qX \Gww>7JyRԭŸ l޳Q d*`:H_ .6D bu7nE}8 ~3 ET͈5~:(Ј>.^u HF8֦Q=Wl! zExqfEE}g0@FZPaKS  #j )GٟCu(MGG"wOMK0tzRN@.2/4_u8IxS@6@ D[_\&T݉76^Q5ڊkVπ;Y3Sj rZt=sQ0fR (~v]Hoc*Ä}t>:Q$q燲ors)oEUP`vRUzMɕ޲}F=M3=bdgi=  Vs6[8gQ<eVi6_?/ܩj9C;R}[2"2|ƢD@,3v:nw&x{Fè^}G0WG؏(/Qw<'Rt߻ #d@}rt/lG(ᲧW i /E(ڜGT%H"tK(pc8GAE鄈+~Y3~蟟ņ1mzBL68s0E.\BM*WaꚘcrce hf) S"o+a tC"rȉJ|F?g\#Wh֌`W7l[XX~hBĂtE~8zB<4YeEgNN~T FJX WKYdy-"qUZQC 1߀pĀc6cWK+65z)-aAׁc$fٸ4cÆFP; .i]iFtYtl0k؅3-@!4FcA^ڜjϴ`qxHgR.| p^ +YJ}jDJ_]Y6Ǔq:9&%&Pbvt'Gȷ!83עG_w!؇ß Ew8TwZ^ D$f @8;ݢNU(!V) ?՟ÜdB"7]u=J#0a<1*,MG*+P+"TW{4S^9S)Ҟ;#YX0Uc&0d"/nZJ\0"GHw2=[֎ fqߺ*x ܈MDȁ7ߎy|6ę֩ldaܮB@o^ ]r kFL|^k5e!2H+/7H$IAgM̓g;p(%F[^($7d,l˲3hHi n:/YdǨBf^顂H^!i$ ?O Eot6҆ D!W1] ؓBclcMҕRŽAu[W JRɃW32RxL! 5Q^cP@@HcH444vpF)A H''x.)D`^# Ԑ@g 0`h9 T+D`VYD@ĮMɗR*2GJtߡBA1$1Wjqa{B䄴K}d#KE@߅T'x1HPM H\RIPHcBI]-$@5/`Q0dcRӄϫWRtfTBli@ܙ'> N,S3\$w87#`sP,lP3,T("~0k9Xӥazl(ުe=[|24pzƒ+S*3l8ljsmqRcFͦ5sڸ RP6@lĻ{d)@1Wm8q2hbxuU;1OX̄Kd3y$fu $QOcK5J}Z֘T $"6mEuPҾ2ׅ=~` 9V$AH#8ೳ bw_wֆ,8G@3Ag Q" i@&b봈*0r3QVE]6`+tE༼=@'uDjǫ\2e=4-JPD4yZpA/J Pȫ [G?;d;c&Ow4.s^VzSD73 >=S0ܒL kWW!k IM<}wTccOUNF-!Cly>Y!Nq ɁAyHeC])>'S#n{g.:[B߽I$i hqq-Rư.qzȍҡ!o-6tmX[t YRj0ޏLf-lln@$b@7ZYՑplh9&Cx68p^0nS3L &DZ!1i!6 nkK"GhGa lwQ<;kᬦk 49Em`5^`6NN(A\A`mRZ$M <$S6'UZi*Ԥӷ)TY4rZI5 @Cr-"& !7ֈTG.>l&PhBW0`@j,2@09kGҘBgKfC3fc Ip;܄#j"S„v@3 ݑϸ@w(Lt&A mmm "@h5MDa d .vG pDTZuF`i]35@Ph\f hfvc4oYvQ,4[R O Hi!6dmrUNCI/;wENY2ʹMQB Ff,l7`<Ǡw<PsЅ0 h-i)yT{nF۪@w8EPm`vMH{X$hw9c=2ɜcǭjdѓ,efjR ahJ(K].~30b2$$JDݢy& &6%rŸf0\A t¥bBTk B! 6I:D;7  8PLlC(DTm1m+0-Q{ᤍ<33o{%0\t-KBACi'?s$gk'ē0EbחHá@2I\LqhH*@QXBDPMMATT/((`QT(ŀ7K+^L42R>iG=;L[h0*g9666؆&l1 V]:aʥUݡ8 8^di?>3D¨1_B v}mjΡ,aD:ݢ" )qA+N/KSn+3VMoV\φ XJTʔgnjr]?,}z1s 4i-+KqZ(,#7xA*d%R cbF 8d8U?%ANyh/YYSRuc{R0A+u3Tr=z( *չ>N%ZmeI^sXc>.\'DeaeO ˕yL5~j3vpj3Ch,‰+mx3OmbA,$׉-&n%x.8 vK]2$!jz?=de6#Ue>3!uI`t`;weEpN6'ڣ!bA"/QxG @g_,`XB^9+9DLoV<؈%m]zGr|(q%4猘iR+s|7J}+V`V^82rp_sD^Z;[G$s6p禎Tzٗ@l&a:%D["N5o$KE*'&t~Mp(rx{mbջ3=5iiIVAAk!QGkoN уn&dgRÓSxTT%G5'gti:~Y ݥ0 PSM)SV@s(x |ƠqC:'>cwdA}ZyGmB|w씹oK)K O]JFd`̌dhbmؽS?Rj>έ"\CCc ׵|&ƛRǺ1hF#[taRdQVZahu&;"? )&Q, {gz*Z3>N'?S=+;Vz =L (LGi}veI<:9,0$y*D·kxք͂LdY<M|y^ nv\;WڍS̏ 󯮋ox}-RfwG|U;.QP8CO,\<ŠB0;xO]: 5Ȝ:IoL$! "⪇"k wkH |?[B0iM[Ҹ%A̖ފ3moAAqE°-gKz>\BlW\&Vn_ϼ$^ RQd̮F\@g 1~.s 7S]ܨ@hrhj}q4|Vƹܔt&#pJN^[=n8VB)<_,r_n'U} |DeԁcN(u(" ~So6pe׾%b'}. j\ۀ}_hp'߷uo`'CٿQ۽|:ͯ&pW 3:jC#&VQ} QM>҈-Z_߃n%AJ"2fk3."#A 0mn &PƎoC(p,-;qt٥W~[Cr qWNT (D ](&8Ak)I2dRtSHrټ}$YkNjPD}$^G?$=^gxU_9w`dl;/wQ EIկah6]8m&zmc/y=2CyU3 j^.ȋhEb|1#x9ٿ$\d?9Z'sH J1T"ί3dޫv snj0i/j1nme%Mo<:Y \U=rA&}7{ ;dm&Ꙓ^q϶pAGDb=^[% NY.Ƿ!49J?A/AvO+ î`t%r6}C c*fchIu̇4wvF&!5{E7]R'ErW-Xl|-@^ܱe%1`ד4f7aLڠiF٬gQC%]Lò%j MP?yDW3ֿٞ?qu޾Z)'vx[j{Nf7-fFi#<d=> fx^Oyr7[Usö{0;۫qyuáOi?G#}w6&v6o¬ڪ\ibKJ%tØ}}'C\# f ~c([ĺhz][Z"폧qdf_Itݼt.1|`@L}yW@/;IdYeTXZ}S'/1'{!6Ғ%̮jǴ?8 RA4v NX.7"M-g㓠ձILD3;@+Сp!J?f5ݸ;KPO\e5,zOxB( UAWn<2h;2L+|JGZ5^L"P YcX%!5LMPd4*}%,Y &Kԡ$p. J.T˄ i(Ն7L.< -g3.%?(]rEiE A YkZ[FC2@rP7A]EE)K9t7b"i >qV(e?u~O2޹n \hPPB)J(?|_o'|Oh-!G9:{$}m^IÈ{HA#(! M6pNKYAѠpy`Hҗ? BaRg򧗊w; ClFe 71 MIXw>l2S9XQ a(2u#p0v)[;$>1t;5yI+$ 0 Jt@(xJ&2)#D GpM%_b׊]8 ‚6fSBl!|{'g83=/8j(Hv僆dr= {`I/Aq8P;pN'ǖҨC*a@RYb1Ec=tv!gi Ѵ,}%oQ 4f@@2/nK”7)9Wj(!YrEvܴx!U̥M%xHヲ*=GotDZ¿ڜ 5*iIׇfbLRn g*D'ӂi )A, R3MR_G65qQO`;[.eX{'LnGT`W#.T)/7t?WzY=9WRTKD-`* d ȉ0$?.6ę4 !R >;PD &sZ傢 LS"L-0 l | 6i2dgmFոn0CҶƠG;Q ]\9aas?yQS"Fv$$0cLHm$m 4mSPQ]a}y/KKe"f (3]xp4fm:@4O(-:e2{pxQL fibvΉ-6t@K8e,5hkkRE`UwձH:+9R=mYAR˜1Z+QdMC3 ;٠. -9%$R71߲@U56ٚ,S)zEY0aOf6{`PʙmFK&d(7M&& ĩ[}s|YH2k;EE,A)DW ,NաpS:IUQ16TҰ [ j?gP[IP8KI m*c|{9J>iR Nѷ֞Kz( l IJFóǜQUE)(ydUĪ3yV.}i(bu1 o -yor4ӕx>N=HKU!(b*E¹#̄b A5QU.%@l(kƀ* i9AhBHP (.orŶ,rz`9TE2|$4Vdn6h&k 0Lu '4 kG.7z>wԫi36yTuv- !bToi]QR!7SUEQb딑wy U%/2_ŭ=Snj$ƻvzj}ʧ,x{99tZX٧}N*`JygA @3331I4HwTp D%-ka(z/G~q{(!J0 p#SszC]:xOFwi>\5I"ߙz_ŝO,o\76x;eP2N=Ff퓹 \ͨ2䭥'}Cb7ی\ev9R6|Cޡ-= pCg^>J$; X3ԮHl-wi=emB+nPOs%c)o:M,DHæsL`$ *eY=XA j2MOl |V>(#r`Rc: 9Gr|?IK_yB"ь>_\oy\ѝeg*o v:7rSo>K̖T xiSv o~/YվFo4 E-nx{Ha4^25ɤ$Z%d:Z&[>_yr`X +HF^68P@p7É)" ۭj6nej+(|➾e,)s] 2#=]Õp)"3TPCPe ^w]\K_ts#N5t67 l7>?O}\b}G6IW Bi_L⩬b}Ƃ-mRd( N0RB "jd&藢N}(Tsݒ6{S^쁎/2528겈ޅC ~J׽~ɓ'|,yMͬq?A -2VzI~7 eAV^ɸ"M\3ܹ9p=O2`ls7mmƲ}Άv%"j~om{M?pwqMQO\^,@XQOÇQ$J,ad̒zKkRԤV@/?)~sRH}E!mh0groiR%P 'ƍ~î D3 v|aͣO,OWF;3Xx <@%Yc&|:+\e#SN;:."SkĺYFK=QQG*q,͗w dpj䧬:Е9sh9H8cH?|wMow 5` bA{X ,ffܙ .VֹBOk$eYrŧ$,d6}ˍ(U O!e.?S7ln 6Mi"aI_} IX$ MI(8.azAD07a1 e%_a- UmQ^h7l]N$%bJ\80h2V bzř`[Ipml[E|֋zW5ybұA{ƦIi6?3wdFl^464:Fú&mM]$ڵhy2X0 a໬Ic{Fchp薚Fc*·- }}_bvd#H>h TW|j/b@-+^j}EZDG ƽ+HQ$ c `B,*  39jlU7BBzJ Kʺ*4rfJ{Vd_SæGUi:_Zڵ⽍~R&8ȳB@jtB ( } )rHĹLB+@ndLQ ѹEVHY&(k0`0cQ }uid[PE݀cʇt 8Ă `dF}]!:ɭ9@R4WVNk1ItT\<|icA8[R/&a4WwsQL#"1S Vcpδ FC`i 6 dhD!85kaLۣ!a=E&w*ake`^ r.P 9Rt_̍Cm`q 3H"!0KX)؍+^W7gw|%:ɻ?92f"?Vqb%Ė&e"B@""EjXl?uQSn e*N֬CLiȟwu u$1^ 3HAHePi Mmvl FW 6Zۛ"NfS۪ p슁A*^gZL m(te)|XW_eʎ|J/sXX LH`݋\A:\,"!7dtK81N=;:36;_ sis1U*akdsQ" Kڛm~Df,] '#}nΕ>!2l%.LRstSD#ض1ۆU+}dU#R{`W_ILs~$M:sT u)~,a8c"dѐV=_֛z֛> ǡ".MkDqkl7gV:¸߇%}josN';o\(E*ie BEtb4.!qԗZ+ ZEN ?q!='|CRHWD0Nxr,Pԋ+8mCQ8DI{MTj02W_:% D-{law4m /+u VA0u2Z%ǣ8 LVDĭxoƨ-&њǒûd} &)EVȊPs¹,CCX5 s.Գ3@Gf=mI?'mLZ;sJ8|F.Q3cԳP_"@(sU~͛a83O7--l}>[ 諗1#mO\U'x`\M]8D"do[( /u 'l5# G< ؾu~ e&9dr5ʈUpKP $/B%#e&B^H;%TE%.uALkG0:RuR-&ҹ( ںfuVFH*(fEL]ix]0A@褬HX@ 0rPHEdP 2SEB1"QdM2GT d9NbӋv%G֒ mt;jR.DKZE&kj<ْ. 6$?:tjrJJѸlՁX 32g`Ӌv ʐ(FUIXZ Eu$qKgx MKՒpMHKHZS-5ZjH[0:(HnZfcR&Ai&RCe0rҡbZRi6YnrQPʓJ*8 R]*g f1'v_S{i uQ [-&OQD;&F˘$f`EZNu}RgJ06i=mՆǔ~#07!I(5`Y%UfCMAa} OSH 5P?|+`in~ZQ{+墧f~o )3t@hĮ"\+ w bZKII$dAs0Ȋ2vvj,xJésL7 ؅ iPhx Ù5U3^Xtk~ߒ+GW= 1)|Z/.,ܭ0~0.hIzք#Hi#@4 0`S~lNUlY'6JVEPlJk[3gli:Y*!8V@rd0 ;GkPN`arXr"-?>^gdLD$9wHBXb{eUy0#5-MÀ$.ǔ$_0*6 10f@̀9(!Qo\ ,- ljwHq9eKh{fX{KebɡI.[`0nJ'OL׋˛Q (b`,?B I Ur{ "'gZS9UHa46zc Of=EFWvF|b݆>LSҗ3r5GswwNƩ 衣mJͽL&4{A:&:ڝؠ6@2 ҀP!rr BB I(IwK\=L`w1J^Aۏ 㴮hlěCۙrD(]r [%QB>`]JQ7KIõIJ/e )8@$$Bhs4<_ Xm0mZb@H`0 .IU̝ A7T)Yx l逑3  kjCm#_܏?,&Ǘ!2UJݒ%Kh<3<"@.g?GhD@IljmY&*)ZI "JrIR*"8ֈAF1-$XF ?}R. !6^DD&* ?Am6i?eKi\Гi8d Y.X$mD _{yu-$hVBHi!1N!1X^4^5IC0@::%PZk)& T}ttT] ]6fWL9K`"cd%J7ZPiCP^,Z ^{ EW0m"6v5qra}QL6UeSȥXrte0Q17МY{RӗP I|HPm }䔋/1U'MlpA2I2B SF.AgV+Q%bEpȱ\.hϨ׮yt;*p8! 2+(T = Rv%'Knr=2\砈c"!k A#4$BI@agigloT+DٲISh `d0l ,XU=zYy.y䔬$` &$āCbJP1V K%@ֹ R!0-KCqQD C4N@PBC\3(x 6M/AfC$q  .ͤHlT.jF(-f* (\) cLi ap}J% jWz$\ Y DG` !J+DE'6B@R(M"\s`D$rۨl0p.Qx\ZV r wtqel,2"4r(]YFb檠!j[Lm#(e?hjHlēm4:& bCHY7y5s+3ؽJmgIwW]CxM irk L)TfdI wV0]d̛ԍ=oD]H\i?PؓmCp4"7V[PjQD;:\!ƒfx^Z`Ky=xQ2D- ô! YKTP]}W1@Npy(phzGF1K Хʎ#TyH214طkQ]5h!ehFz^'TȆ9P`RꤠHAF0׭ ? y z @K݉|)fWD&jbDH$Q0ECTa 5F''A%i V!ш LTb!Ryak8ȁ m4$! @2Z iJ(CCp { %6Ƀ&hbc CI61JJ)~nC/qm(X@ܠlli)i4) p1…$ņ mC(rԍBCdL%ÑդKBc@JCpR"` D*JYp@ m A'~5I %e`\&؛clR@ iD"--JZBSF( ) I@le֡h,1,@KHMEՐ:( &Pr)rVSZRT $hZ42 1&C"GZF{*1s_d6W$4ʬKh Nhh0oU eЙdz.hA-ޤܗGl \=@9Zb/ո@ЅZpZ< mRB UIO!NSz)ry5er|Q/ lxdduUo<{O,R.K/rF>$(Xsntj|#e򊎈gg܏i ]sY5~͖% 1c8FrM*3gҦ~7S%4 #Juch}ҡsH{z m@ FW-AvS=V]023eT{h2Vҏ7wS_ŰgiϮ~@Q^hc`GD"c >#I{$2oml/ֈ,V7wz"'c?,<?Q z.b}{ T3iC Q`Cs7E %81QaxejCc*)e!XDmQB>y\󪶿ӭ;*8l p?ۚ/E1k)0;%x];]C=It(&b`)}ﮟ_+h;GTD}aU^K#2Kzy]&vY꿇~^\@{/|p̞Zpy4osuL2'K)@ؒƅͦ[TPAƆUJDlS@1T~:{wrnA\+{0)%!L\$ 荽`e%m,P`ܯ R=o7؏_rQ8>Žoz5{qe@y*2@q]م0 Ru ={WrWgfҫdbw"|%O_O=z +w A_iSs/WΙNIUD=`W[Q$N䗣LKBIGsow3wzs]P K-u$~F^FU0ȔQm Υ%JaRU 1 oW)FiaJIr ,f4PiN8ӸB<Õxz7"gv ф} 8RÎ`(yG$b)چb ,3[6?a.y~㭇Yw瓘LL!dو'@`{7b 91K$58@yh[ή_am`J'n* .6V 9qj+Yc+,D^;Y r?׶o> ?C`PV<, *avÙ+P؇dVL8wzi j̕E<_׾'b8@I9!{xg5)PBPd.Ze" }w &+9ͺ:nJi(0")`5I5Jt\B"ŨQ ѓ8?[׵iۛWu٬ql5w=Dn~>8;Z QVm61mcAx,O$d *Hbhc.FdR =Qo.n@ rI`A,x#MA8 Cٳ} {\ []Ӡضiߥ1لܽH=NB~MM/rS<Φbl9 :-Q\7z Kx%7_4j pזpX}l$J-lSBRV$ i a)% %6~gWՔ3JF8ك e #@pDbY[.gq/F*[Clh炁fMXYmϰB$T*A P#%ޥ%= a,݆Y\PʭhL͵Nn&F1N(63 7=R `Mh>D=U` g_--\ DB aRɊ6Qq iup40 ud 2bH qA1acidUmэ&mJ()h!1!ܭ'̤*Q}Z4[mC؄htjCԀ*1CIJw 6Z t V%-a*J8U5Fqm m M&ĆЛi̍L ~ )ID beg4T@0R Йb;Hx77m|ZVfH9\uq5WNӾ0PzВ $HȂyNB!"S04[z ɂȐ63@vaT0`4 7b&Mcl2@ӛ_PU Φo>'IXY/6ñU{l5|ujs4MH80quֈu7<3Ҳ ,RSiANص1ꄜkx~ vyDi(R&FzQsy8z|YaAɔ nc&YyܜeuZ1K*;J<Dz ~zi`_\'t IFNc8~ԝ煜WYQ>t2<>9e;>;_ 8L=AnzSmtTn L[+Ptwws~[nr]c\ e Q0f RHis9cE>GATK-cqCxSlj&jTѹaJ+Hf.6o:@ظ"S.%}r_ks~#sggBReƾe]Mt]R|*q.@l/o*ž`K/KKc('0C Rr7ql}s+M dd?I!aBM2ƲWwoɸ)}9R!W`dhd@A(;F܄DG% [#ףEr bp(ܣƋNĻ)ӣ'p)!ρ L0L0H4ڵe$})y2yhih`*xНRU[j7xO?hՋqBI4 VY@ fSD91`<$ F)ݶL<"({0DCb"@;nL ɾq^#̕+gT,5bnE $DrvQ[A:)Pdkv{iEy^wwJtΒ09`)bz5MNGn& sFwYN iFjL̯c0S}3A=iV}U1l3 "v@ B Az@wQ=o W #ډF"-u%2Byw@+wb_@E:" Da+wPͲ@U03 @$N#A Xx~Sӯ 2}KЉ0BlH=K, @۟S*#@ D>4b B8 ?go,;(Q鎘LOo1H5b̚=uwyǓa,Fڿ8֭H}^*f˭" ]d/kr6V=JYL۱q#BTFJ6p.R:|q\%16 -Vv>d:,`(B ~}l/E(G p@h#i࡝6`o'v$Yf:h@A EI ¡AX`l HCh(i8@3q$x C#DFƒ5!KXNp_;[v΄?߽Oh#]du: qհɫmDs@[DdJGhThl+@D Q$@$82҂W=YR4r``1BhHĉd"J,+(iC@C /7XxB$TYY #eMQpRzyf jvPQa+HhI4¨\O^݊Km\Q*ml}FAvǑ:A#2u傩21FZ"p '8IiB(D⮐i7kSUĤ)$s)0 f+AΪBE!37/V.FVi$y*wCX glߍ/ ِ5B#з;t"X3  Wή[5Xf*< p L #p D 0JpXY'gA`! ~irY lB $"%?B Q+!]i{]:F嶤Y|ԭDbG`WrMv)z_>}5H-cq܅_)4V8 *n}Z,{ΡJ cCzqa2 @IHž^cASYS{Ö,]\[lO6_eWYu A"> 0}=@nq~fP=9:ffqPCYrPYɁP>VGw0*C5c;O?TaYL:a&z[wNƟҸeMU]!-{EPC?E.A,Aap!O P*_n(Z/"v 䂐\ lmPOcȾ*-#wtC 2jtpS،a"H0BԐfGaCWn:ni2|gT%1%_pĴTT (tmG ] aO.:zOL:vE J PNΙ2Gq .ә5'7=B7OG!=cTXUTyt%tUcdLd#p~j,N};E&Ǹp?#&0U786*u{@|v-m3mZUs%m}ȓȪ${tvwr'bgo'ݖ}f:vjmE߻ϧߥ = 'Ҁ7 ;5af[uً+l$ؐSOSq)fP]Ed}T3Q7XjNe71IgG[ 9|_ KԒ0,ľZs6aW9.3%MFHT`7SaD9I : eqށ5-sS;GMy48&20v}nQGε+ ?t}##@?rztHf@^w:5a}(7i-jY o"F 'uO;3{g< >O-'Bt'/&*<ގߊ|YWsЫJR'=${gqy:)86/XKAJ;\LxC5dw(I~!Q>?HheT @gC/Z&paXXO1O%ܻW.禄[+eeQiW3-#-Hl8.dDz2@Vx-A}ҥ-̞.Vze B p)7lDG7FmkufL跗C1o<0+,%ZyN5?h!|X 'H&3!y  Dj-ze(I"|c“:rDC-f+iE~PU $@(e$39ث(,Yp/g DU%4KJUq [ 9z{]a% 22\@ir@0fFBȈ HbE~|&ZzՐXDN1 bN,%s/bz<(%|GQg!jAJGq1. qK Jz9"85\[+JWpA8jnbWƙ!. c/|g:>;yUH644 \Hhxdy]K$/{Jo?Ma|"J2Y1m0aTsě>4ý3Y/Z}o 5dDbB2Z\PXA@Ѡo F%ؙ[X}~B5cj[&XMUe̸ hʭ3Lmޱ[B41& #\U5 3ӡ"*qVb oO.hL~{sPM\@%t!~{^>$"|hA$gZkͦ~T4R{EP,Mȶ].:F?eXǐ\1H{dgxHi.Rh$LW;+nO͢m ^ ָTG~eƏ  XFbDRFؔNBgp `4\iq~S<EH23ڜUy9ܬ1##)uI0JqT uk2 r%F cֻzɕQd4hdz]C:Vpm|Ү~: MbmZͫ'u” O>d=bQj19\aVޗֶɍC_*b 9iKH >%zpyz[8s * DP}}y[U du$g HD(^C@6 Ao'*9a[:` 8cxH  ,$CIChMP!$(;w4Q~cm\! %4u9-!cK :{A$%&G1tdR0VlnoGױ #:N$to"Ρ ]G{V66>d. ~D$_5CWMHY0@f9Ko3]BCC*<'HΦ ,cbxi1SB7Z J !2# 30'[ำc1t2~+71 C~2?՞GieW f3:?>~k >[NRrKIa* p0!wr#?QcvǏ<RW$邀Pgc㲙_S IUH.lG]!X,=>g/·&J}JX;:yp`/,XB{I -BMn.66dXAe:U:@O#("Ϭ!-vیX;\SԊ9ޭIz};iYP!c6^WܩYֲ̌ɫgxP;?X>fȠļሑB Pw_y=o΂ieY!D\#Y j,4y Uĝ yWI A?q+R~ $L(`V>֏qocev~X0-:GhEj#6%ZA,Kj 1H W"si-qVp,݇c\5 4l-64%fGI.ʐN1`>ەr-Axazs+Z HJ&zs/I ^5%q$\E1 /r(AHenQ h!#AiB* yd"U(p2展 l""RP6\Qh3]f5FHf)ukD)-ixDAk8hA #מgtZBQDϴ` ˖9ur*(W ,%ZM|iCiom$7BBhd"jb{$4l "P,36՞V PmW }A<3PvaD( .Ҁ. a% 2MSrYcffDUL,P/a ܆um`!6b8!M\[B @vTcxbD @``QBji` r]5ކ^djB۟(cC=r\^\EX*V \K? r0+DGm&q$cFi  Ɍ\4 xH|'Db_ jvRTM'M&2|8F^} 0XF r fdf`4M7L/JA,!uT@mD4f@ӒO7YtȻ6^&Q ^1WEPV]I BӤ(P`q5^+hp1NP\h/bgD8-mJ}w7Z٣>Guהa%aDl3w  w Y,^9KT7BJ)ILj>F< W*]j|_$QUFeD)9ИAF#b(TV 3U~R_lz^ALlRLTTjfݍDi @#1~@7ŕN򱛾Ɂw~:IP:G)ݸ*Jd B$PJlCkj楢Fރ(%{h@jf_"i\Y:Z9zC`i 64HMkJB6w8 #a 3;FD`wl5^f `PרaZ!|aIjaְl!JIdG*m߬df Dav/ElexЄJ 0AX)}e{f[ѵV/Czϡ1yx]ySrVv J1 m641$ &@b"&Wv>=zpڦ} ϑBC.2u7;.O ioT|ʞr@B)ÚRFCÖKܤm(Kq!qmrHqH #Ow+{$&߇$k nLRʉ BJF w3A=8$p"? +xOfzC<`/.~#XM/b*WI THsːHn)@]ǿ|: V(RK AmW,QJUW!CI &?8k YDƻpv> !)A! R ӎ՜MD V7R&h^aBWAr|F$K&Lhc  q|}?AS{2~ |vQӕv&wO_=[Kvzqf KRs6χ1R|9lϝLw+a{wͯ7zpy^ [0Me{2UD2).v@qvDݰ\K_ZK 0Ftc=w.Í $&T n{XHz)ЛlRw ja(u BkՍ㲪gؔ>^ǫ63-e=,ԂyTj9{ ŵmﰦҤqF2aw-StI̟:Zǵz^xy48 YP۵KZgw0eLz'KIMP[(Ṅ$` )bJI_ܞwAZ~Su 8ͳXu3<i~Wci !h`\*xl{\W>>kaӮ؆d3I1qzWw=Di>T+T{cu)g~sW,R63L$?%7`{֥q.1V*3ػEN EQ9=J/ݤ`}i@0 PgA'iakLq:\H^ D`\5ުYxxgE  [SA%X3fDΣ97,]zʺU |Mp%>{{7%gA(φ$XcBXSHިcGJio5.Y}-`X% K2~eMOo=Rhu@+?惑:gE*SKJOtBqG%A]@) >9?avqCy-ְ^A*DDQ]|C1"0Lv Zs6 (SNBR %c]LSByPD] OOd)9DǠioMZ(rvDe0#âp(.Aгc) B e3)gS,C)4mV*;ŝ7`Y u@vp.b (AM2d)0.V , u-+f; 񄁜`pei`ʂQP%o S8I4HEI3mRWD$ IJF22L@ub(Y& G!!^< MFԚa0Md (D*q;e%0-')=  3s1 0Qɩ8؟Ҳ,aE1Ϗ)'pj=thЇV`4{lF*]0U.TftMJb4I'#>Vg2[&vj.l{p!2_a0g`1đ!N&&BUBdIc+ŅF bV&!*ān!& m+*@@Z pn?Y[3rd#w(*Ȏp5x[D0 քhYM`)Hڐ0P)5pAa^+N>*Ar< 1!T݌N4r$7E̦ ^<MYaaC5DC33MF="Bq\:t]A7z2I%47&A210$lZ(d!L{612nUfDZ3 BUe9kY,Rl&˙|Q2F&CN;9_cE̳+?gyY3(.@X.I%M3Ha pdK$ɧ%:.+ȋ2 Iw^vy˼ 0I'j5TDT keOFs3_=[fe˵D%h_UhU)Я&ñ]{L/h AX1]hy=V~.u%9MA@Mr1a(Qb6AA[4'?D=uҹr wU$Ʋ .ecVgQB4cmv8ػ1^lb W`4 K w(&8Nh!h)P0Fd90'!‹mDo#H4y`#e68Ԑ&D J˅c72( (  a@Ǽ"] 8Tjae '9-rx2gA;\5NFN*b `,Ix4kRj fha 6Rԥ>}Dbh-mxq:Ia d̔ !!0V%e JV%͢CXZ< *+koڻFQW(}JM&oF\+b 5Gv֛v83o)쑈+Bڡ:A4mZ;8u^e{JD)mʸwJw]RL]ufꚖ{g\70(LHF +V5b$ @,,m# B&L RH/hDq>Vk ȅ{8wN'>K|֙Q\Pۇ/y}oP>P_MH›^$VF׀Ja8zZ6ɾjܨVϤ8Sڽ :,R<<]}wcK%Y-*}#Upe˱Kd2y+c͌ 6)N㇃z|rfX^uҔiI ( 5>Ʈ;*b$)-Õm^'jH"V,[fj=sYSN&)aUt4NJnF `c 6.-X!;CFcRpR_|{W y_ezx|@tPz%hKP7L"-Ol9-xd#Aǩ=Z]dLjCl!3#`3bG % ~>k.%]փAiH46dQ=Ih1i1P ftFL$F5A"Y$g edR3 xy0ak@1rh6a)*xHTcͫtPIjIHH&S)F"ɑi99e#(#j:RL50+7VR]A`mp/w!JB*1U`@nʙkJsAYAu'~ ]L*jcu1Dh썴mau7ٮ(;9ipGFtx*\ 33Ua#AŚ͆Y1sKq4d{H`6k2SaR@C2՛Z8g`jkK8jpY!~/nÅ>oP2mp_4Ɗ8bC&D41#9H\$ @ĐD$ߠs90z { |5 쏞~hdZ #ͳAij9q-8.m:{@Vp(V@&hXVw6Xkadաj RД Fch@S;FB'eRP."-=1 ɈUMUs]Alc,-44P6ᶇ;iUKP@ِENP Ͱ,ybؕ@<ts)F9( qNA#%]QR!'Ъ=Kbbb]\wZk:D.~g3oəQŰN5ns,4\zK1E!+3 !Ҍb;DT`CelGvM ] Z @U*8Ӣdq1 2TV>NS~'umPh5NS{?U#yf'ԱEFۑf˥"b)JԗI%2h*iR9Z! &X T}4 kt`W22y *6V$JDJJ(ZQ5CqD674JDfz.Ѭ .n5@I;JY!JD 6OP0LwOF>#M0TYt x9Qí G =D)%2TcE`&cI% Fه wP^.A1@;vZLHD 2DBjTU` iDnq8 FQ1w7"gV3Hxs.@%a-|fw6Q@fD9i |Y ֬W['Z-3ґ)-5 Z0m12&_U^^i|RNgҗc/@ldYlTO<ûtSI'GyV!-VjWj䐥6 $Cq9  Xd5h4d@4,JRt  vR{fLg6~|o~AH77| K5·p{1Ez ,W?ggF cp $B#ᴓUG#BՈR 9zr !$bQH.+_#Vd]&{izK&2}gH{TFb%@Ž_\aЋ6"8Xue wTAsp3 4k( c5p_.iʕ[) s}KQn_ -$ qCʤ[P`rB ՍRKdsBbA`/uN[L F}ftJ2Y98DJm{QcU)Cf3Wc'- P@zD^"&gZR +k2ζYl.&04N -cX;fa@Pp@*gRkR% JQxdnӜ"猂oS )fF R4bpA8# Ჭ>:>K0G@peF8+E63PqgXAb-MUԖ!;HR6Z%f3hWRL8 D mQ#!EL0)&DD=),$m1ZU62LBP;oVw4B5B=O"̊fY$lSdLzyclUMQ-/wod_6ѵm; Sb9Ā~`QJ*Vk`@@x%I=/~>xjWg's6g e@XEϝՌe#WFRR1C p4iQɯh)&h307X?F10'8R*dEe.2@j>yA *LBT& Iҕ%*:inY$5wȂ[ F5⽾oGLHIwd*P &V a^6<| ΡΘKA Q̂7e8(B6pwZu[{~8 +L/U^x.<&yR$"ӟPdOrFy&Ջ~S_(bJ36 µ}%<:,tTO]7lߝ}p*{uQ֌)Zx΁o?|7߈`i6 SN*d4lq^̀xOu]5ܚhlm61.5 E(A)LQoK 2Q}P.HuɉB  ^PbT/A#Q<(Dt*!Hgfh(E2)z P+}֘/]B1'RU2fmW&Ai b%UAfӫk · C_xɼ ; 'MCX ݌%I v13NJ@)xf!:*Np#v/_G7nZk3oxm#F٭XÞEŭB\.P1 Q0'DBʒ7D$+1bfd8\8]֛ٽ7ڨ,oލ1\rӴL$s40VN-ZIrT8:k\`,@QYffvJ3RmhhTE~Z9o?3^+ktNJ㮟wImjF ;.HFVIz_?_olF.3|? C4 0@oX:p9sU3%ݼjH3J(x.IȱR@B7pgXE ,]ɬ$vF,]ꋋѹ2Z5'2).+5Twsjv+ιyKySxr~1eU=L#mr]|GlgU\iłE8bO,aہKP8$/khK%.=?(i⩖͆ѾB뢇4=}mS ʛ$L0iaqڤ lS4`sCtDqL>pv<cрáp:>Sve #ҽ # B̈[Z߻0A?bNk 5lez4ҢDt +oY%~wu4Dxqj~rK2y[&2!HIk|lj\|Qt ?V |0oH0v~OR$1̐(gd-(%_-pNC.X4\۪({u˃zJ1DPtjSژpp*`n7 S^LYk.b׋\/,i讐2QJTsd9_ؤ!/Su6G"aJ1Qi VO 5BpA7jlj,%?h`.#uʪ`x wŊ@1a!Oz!$$6;]&9gwO5v; O= ;Bڴ&Zw#bǕŹs{G0o24ŻYZ #BL\GDP1FYv!~  xwy쑧]:#)pciRO?S;f/;zSF=O}VU󢏨JJwomG iL33'4Q u2aovkofkuy3Aj߿cӑPT|$- >rsMȂI$AkxUr>9j A2-B` d?jr 6dCO֪m8F \Q)H ;`E\d#%.x,Vwwf.> N"RՈ;k]3$eqUQbMƐI!6Ms<xguGVl)jY 8MM1 v/Umj~W!\QK GvD5sԬՙC(5)JsyAz2\f0@(wnfH>$6zءe|P$ĚHM cIhcj,chm,[{ ۬-WMcg% |(s-EZ@p,Zl0td6~ܭ]Җ\֣9F, b,DGa &lYسp`5;3siv$u(QzzRJZѹ*OH8: Hd@Cz$MQ(w'OE&biXW?B@i^ZV(%Tb&H^glȞfճeU9fIi&L1=E]c`heԄi:0ƓIi&L1=E]c`h`M4-4M1cz0 :Gg2bvy^ 0X}F%yuBi5nH(ļE %ͷ=iMU:[奪@E &{(.d톾B˪ITwa66Mh XKӹ`^^(E@,Ncb2ꍏ0dvYHpuKGz Txfez{V:ۮk q.-*@!9!nM5r@lqWOA 8pvm_S/~?Vt#<1GG?`aS_TFA#Wvޅc0!DR\0Er>~=++^luzbIU _1`'G̙ PH֥vY;掾B8k׼ӒЎ JdndOM , zYc7_qKۊb^=qGG&)۲xi{Y'ϽIr6``v ]tmuٕ+Ƿ\Ӎc/壟:02eP _. [\֎L.n_l=m9PA}.Gf0lmc\Gj7^JvһGnWc+y 7͹ݐ\(ZD ܋*GJ @>q}!g_nʶ[7=h{{~PPZ@x@0uV _&FuLce_3Qkfg19q uO)* 9G%Sht{A@0a{ JMn-3L㔟 ْi?|tq5iU5afx=?r R ES[.(]B82xMق%ɵ\ ۶m`$hɚ5^$.1x\%2be ÀuuDa{0>%B%?m; [͔XNUYK*bݏ./ IZJ%v{j{5~̤уsiV+ ԻƘ:A|\k_iyk?n%"ܿf+ˤ;{j'8T{FɃorg)k6^7:;f*l`brKH.cWG* |=)J̰jxOaMx\G}_w2ۗDHPaУ"TnޣP]I"KQzľN8Sd<tcj)KV=U勠-Y^qWkfWc0F,PJL8(؀M164~ύ,5mgk9QlP,P+^QS[l牵H ̇YcWG_+f@6lAms)R >H$֬ J{ws2Y2pPC^ug0g@ֵg D\VQyEfPJ䠦NeKǽ/CYj O~Ts.Uز?B3Gj\?#qpL2B d(BS3t" ij?U ̺JIDAՌQ?$Ij]\1x2PL)>R?1iit z)CMͼV=`sKkm,rwz F4(fY  (2)0f`\eU"|?vİɧF,4u&5Do%)2\H0RhQRT)!bA#P1 +1%fH9ۯw8}O_ W_։ eV|>{X)NU^t b`00b^j Q¤X!p2"d֦zÚ@WS;X48sPyϞ8:PzVJ~ 1BV٢ !5KA_[e(pH/PpOӠ8˿>Ҁࢴeb,H:B [+T.ϗ'VfELny9_帘jƫ`*DWDs<I. vL s֪02H{O4AZsɢ%>ͳ&XhDbr 'X#(c2?U"{|pw0яJ:w 46BX`骄 d@OV{) }=o"X }H3ۨ"aB(}µP'$(*nxLhvL[o& YSltEu֡4Du3QW.vmLgddwåWdZgc'F/\J'J}m۽tiFօړ˗=Dd6#&SCͧ$dh ]o^GFT!>E`0 08҂ s[G3w3* S/iɊǂ۷ː$v9=hLZ[}/>ڼ+]ƵL {=|(  ) 83_*It0jtMѩAEB~ܘ:t[]et p/X,R}xxeN gtZb7r9xo4oiv& OV(s-r ٯԪ/sjh3ȗ $ kOI#j0J*p[ rEKHǐJg_xug>!DD yt$1 G]:&2h*~s59忿UGsn)*kKLgno>dR^W5ZKAXviTK(:d & juؕ A(Ho?X@!AH`1 ?b$hLfbVoOuݴt0ܯX?OdweJX a7@?~NQSKޏkx}[\h' Fsp[ #͝ rE k'yE $B;Qv|\ ű ok^˄' <[Rݹ]ŧgI`D#'Tps"!lli& v>/Ϫv Gjn?׬ϩ}2 V`2enT;Pr}?!$.i2\4ŏK)贕'?3vi 6wZU(e)w`ky:+?xh,pȞvT"?smj7N IWئ ,hm'kW9B2/|&"LDe_1qZL"ڈB `ks\Wo-ol3_KٯWM)`EdA8.rمLsɛɱ!sk[FJ\JPQC )@Bi|/^ī fЦ#?u`O2T[W`\2o0P#c48LMó"C`=iwz7<m!H2JLDu꾲^0EKX [K;Ek8;Q圧ߖ"ktw?wJR" <F:8AQ|𨵋ne(TX04J6y?x#\~~|;&ή+IT:x{Ӽf-)% oF# X22=;R}""sm>*,WldJZFz鱗DC AbFDžw _|"A-_KBy^1jwP٥%(%)1R|ES (N(6u {,Ft;OsPvH]!l#IŃR3JOH g>_j鷫T'4:0>/C껇nʼn aZ$)y},Řj<8ʜ?Ƽȹyc憣8<15aƢYUQʤe}m&_2h*93x9GqFu;9Kt8|;9Ҿv`u m0LY *́ I5R0z$I[jw0gW<;@3DQ!(4A% y{)ЃA\?W*pF[C'V~ÀHR1[t(S,1z91I`W6Zul0ѨIsc̣E@~y^ABJ8o؍Y؂(c ɕ'Ƥ PD^ugۑya ō~$t+m<-.B۸xlyB` Ԙ|?M9+c?ۘ]53[&nK匈h08p@j?D hP*:.'a8ika-u^Җ`wBhN;B8q&-0B{]^2(ED%XV;. B59@~ 6!.צ@vs}߂76zoi1 0LGJ S=uIek0K؅\v+^/~p?@>; ]Dcr"=p(w_'e<7Ì6I0;x&qԘi0(Bdސ78Vb9G@PYE崡pl,"=>ЧŇ Q h)OUf <#Iic7PhE cf\rKߦ7nN$w?X1LsM GT1Vs~UDP`4XG|NSiu}ܜ PBZ R2F|9)p($˙\\Ębν=/ [/fEA X" f%fOڰ DB_;c-^D.u$#cnZB9%ߛU qxy|$s@#qGP3YO}{J],7ť)8Kx%Ffƍ^8Г3I+wrÁ@$ӼgcZ&;_#*o!4XoI--xi#m܌=Rpwl5,a6NwE8p影DT̓,]YXlr}?d &{deNu[z ۊeۓdWa19n>#c;_sk034BRh2M'fuk%EF.\d=+ dOa'Nh0c듅|@b1PMmˀ@pHBHx XvdOPO"pEd)B[{,O`nyOXnZ_)/`1hy*h8oxFD2uTQXtˏ 6%$'cp2{r|c+6Qc.m 5;|dH%_MGᡯnk--kW bi3>9&:"%քBY]5*B9;;Z?zR: Po]|;s8Z|K1}D],UY_X,\wce"$ǝeWDogoOrATѪXv~&);x1} v9nJ[?3_S |hH$[ZMAe0"cjS7_,(ZA&/dƊT2#M%!߳gd2"iK呞-x&k&&rj50np#7٩lc,˟E 9yOC7T^3cꌲhYeNdX/G` >6m2( L0R]P*paYAq^a+?R}ITl-W ħ}gŬkT@?7`^anƗct)`Ϋgʏ{ݹ=) ƅBCP@c[KK=DK ~טЬ6+yJ(HTB3M^}R>@/BiZ6œ n 2 (:Eb gF0?nx.kgϽPPK,D䬎JhQs'g]7Ki@ȸZDQ#Ei]VRmЁ>pK2тjԓBcdeڧx&wA((iOͦҐga#A .QIy rn4}S'+R[1l1uϼͲ )5+9v(Ukg2\ƄHnȨ1b,\,F!P"^g*]g(-_NF5wlGQ,$| B7%WY mlxr+HʩҨw%(A%31fjo*1]37+0()6t:X4pLˆ )n'W]YEt7[9IqLEne^ޘ"Yxj dзIO o䘗v#n৪^e[@C\/{GBſ-%8Ⱦ܍8Îrqc+*8e7`Td/?h?Tm$ ѧqwuI wȢUgHEp1m!h8R)c7k\u&U.Eh+%x oD%|,#OgvUGq+}7n;xl nh?#Pp jf◉Dڝ<]:x$L=4qWEA kdoD[k㕏{ٞ`s2Lb_"v>DbNfLo c5fL4XT'm ۆ4-DT&Кe1>}AK)d:eFhAkW8iɡpR-"ЖL9K"<3TM/?@#.'qnlA$w5)p0]$އjPdc9փ/Z˙!DJKT|@c~=T8 %kڡhoR:dhE.a i(TGj` ze2t`ue4GU!1mVbx*/;?'+ dm!>B# Y2vʺ})yߕ4//H'[NbW_"/TܔdsH&uXVj;taf4+;v>n+Eg`#܌lZ߷Y_[:I5Hw x|'_OgS ?ILk dB/Y:%76rj֗23z].5QX0mt=>*ǽ<XL(qKB*0*f+E$v ҌO $jaSt o:x'Y s|fxZ=0sΰD[*="V-| Br v.P&>Me_\EEOfn+9B+bUʐSGպmgRW"=r Ij@Hlfl(k?v0{T V.RٓD*PR%&,"y5Ma*'S} I+Đ>/4>1ixwAk}"s%UjF$n )IH|Ox=p: deTb) a0lnOCȳ: 2 GAj W=)AoWePw> {n:,"XT{\1Y^n(a{(AZ8AJEǘ4gaR5!6wam47;ީEa7E$E--0AWyxa( ( ?)1RȄKT$(V&} @(3 ;į~hI$R-Yxx. Ň38ۡCPHڝbៀ,;`?aZ=+I"XPX4 9 !_ E pA*6_hV w`2.,<{Y<zLk'"莆nL8N3'AեDr;vxZvj*S;J$;ĚEPb`X ד.ۅ㳰qLY9lMn/ 6PFuBp@`mV={fNn+aXzT(zeIpD9 q$ ' uw^o")Wz me?vzAΪ۶os Gӂ}NyTtr%,3ܚ#\i }IJA_w/RBkW;ꋛpl^hITrUwN R SԹTW_]CS[ߋe7&Lt@s^R2.U> ӡcb5TN!?~7 Qlx1d&G_nTOŇ?’gHْ;R5nk33M=IjCS=M0/6֟wDTqM*ffij-Jl!D'ե,~NdB ul[uT-jm ^~!XGT?T&.If,lgɌJܙkq~LN<^$/Riqf`b#sdV,yl~x*gHy g` ;<ĔSWZ5<¬BczI$]f=|Û VI rBl҇0l * u*GC:4`p1Kzoߧ50/,!lT[ {ى/΢2~j_ h=B1ZPjb(u] 9];UVL !BJ♑5 Oņ:ң:O ,I;i)ExY'-f+<5iy,ISt$gvӦ`nG;BN@qf"qh1GP;ѸqZS1`"O ʕ+y ,쑠*m mܳ_퀽W>Uy'M9Y(/}r JRD+Kwn{113jW<+;$n:}46)iC8~1j^b3".KT > p+xҰ9(>#ߊ)m L^7Ut&f5-J{1dH2F3`BbZ,r!\\jR zOs<&,[!z;0A ԼDo_ Y,`e= i3PL7HӉ(&}9y:G?G^h-+Gxҿ?x:* N5{?G%b NwvXV )S{}dWItaf?HGЕf*\RRMenw#bQ7NP=awƙ7蚜vݔ1RL,'FRwWGG7H;2QhT(֖2S>wDQ$Q0NIxF* x0SL@o 2)S'ft}oM|akUs(|ՁE!~w]MDr569I.u~ amWshLp%gV?$bj5dюґ>/j/RޜՉ:M$ mW}?E;|v [DyZTi52I\bڞo]rIҙF5o^ /YX]Chuv?[ҰN?yq2?ɳ%Wb Y̜$P1_o@<΅ҥxRY2b dC95u@ +}OBk$tU.'uK@RZX]Nlwmړڛ:,Ԭ 00Pzjg`dEu4ZSH; 8-*hCm I$?*r[>:TLk榜# A5MiWW&.<uÏ LU/bd )q& .j|;qzb(x[¹U3]s ma$((}!E/-.&.ŀfv=,6BNټ7Oξ_`d4d]oewi`L2tmM)y[6")J>o5׸IWLi$#$ c}AWQ,H hAi;p\eu8ی/l=;)o7x*u!;#Rן ;b )#nЫf<(`Zg-G;bJnIJAM̍ Z\{LUu,Q5.[nS`iOg[4o ]$?d+%vy)榜 .Ɠ|dca*28K B@{*c}.y*dxjAdDCPzn2qFo-{=}o[o#|r8oyͲQ^ܑ)ZKe#K+oߵvR! P4 Ʉ;ח9mfW=)~ U"BJm24^pqgyM^I ^{S"0+1Hw)69˴\}lGgA.L E /vjc[K1rd(*]]SaSݵ%zJ ն W/hs(k}lTYqkY)X әxK*fZh+ 7k# K1ł $P!ϰȑi\?k6^O >vx"3TOuLyǭ6MlM6C4_;s6xn7Γ[M۠,|]_ CW%IT}I Qd\uJWsBD7 ۍR-s΄>@ 0{"&'GA c B!"('\<1UvD OcHc]\J33f*DB]2M.@Nl9MSo,1D[B XqRED853ڪ A q%WKzb:P7.,;xVl.՛Jtq۪:ҲjB =7Wpj ,G 켂҉)'0,YѶv +]"tCs-h'XG HYף8{5Gw};0_EW/BY+HA4a1 S|SaT冏- E$@Le>&v 5_Vy ag\tKbF xغ2< T S' xo~|ݟ[?qjV?|CӊYme_ 0#KhϟIVrF+H!/&XYe)ʵ۠!((R3+5mB[?Tг{3%,F]P7sKq1P88a1WqȢp gShCct Μ\\X_m T=ӃF tҾgňBUH:f'kKc ;aİràe VL7Tïx t9 IItklua]1.v%Y]cDcY{n=RLY Zo<7El0ގ\SXN&;ΘwꙀH:^Sh7d3sE/;XnWg X0H~Zs;=$3^غۢ2), I+ǁmɭ2 *b`yHdt ?WbUn 0[P Qnz &j'S$}\'&~$JQL>^oY?WБu(p4JXS͂I$>r"I vQ$?tequEyo)&%P$:S0]kM9w%B|VW+\9kO%sݨ^$KS̆' "ɘ +!n#q,wsH3-zK  m E^u|ICZ3Jg6Nć  #Κ# -Ok'rSD B~g*(.vTS96iswL}\q4}6tj `MQO[޾{tlTxrሉ[=#hpxү@ʍw >Cȯ~xsn-P~ o\f7WA̓啕\Rwü2hH}ЀcH+Dm䲞rw@A 8ǻf z2Z¢F\7MI(ô%sqaegY:0)<%~Bؐz߾ыv*0}5dz{!* V 45d)j5Г['=gDxfw(e`$pYDXUgӡyB `OA (^q /M+A ,I)\c mMFlbqy eHϟ-GW:}3˟\OaQXkosR " 1pr;%Oìd8CRU+xn f }&V~c'h,D;#Y(Pї%W<2'9ͫ1\ O Zp=0 ?Gd0#Opgr6>rr>l$s+r xMY02Fp=/=:I.kk;" !$4YwW-9V7^v@s'2q9jj_&= ~싍!3sU_OA~=DT25K^AQ}7\紐X}",Ea#dV-rjNK=g/氼I]"%}A1dתL(@1.5`]x+=#3Y᳻F 9 X]" l&8>!!CMâF=Ǹ>zAFʐ * \~qHSܛBhڀۧ4p g  ޼swnbط1.y*h43&kٯ' PWyj ahX,>=G5T1 ŝ_XI␤y3h|he]ـχˎYIo>8`PuHwDPR/kVmL=kC1; .+=^PvBcxZ3ԯ_yu G{F=΃Z]Vvf1P}F$t2YdwɦHw},OJ?lk~ŖHFl=?3͚sr 7;֋Np㮀sw˘Wm|0A V_EoG &OuQzKL|wܓ5i2p4;8hY=c(L' {Z+Xl:zŒ6لM[IY=෎zcr-D`!^DBvGb әk C~:XPu!"NמwKz} ߕ/࡯ o|h[` _Ͼz42.!G$ˑQLn&1k@k+%H$םe֮Kx^$#`鈖^Mzlb!ZEyq'ڕwѓ.Y\!3,ȍ i+QͭXP*sI_xf56VF퇩ߖ P6~Ych/ޟz2 cp <ˡFH28_dǰ(j Ϊ?*ixÃ0(F׹X`T-erC,WFQCd~%AGqV=@g"s(tP)YwcqD)kRgϕ!yqAqÀ@]㽦ŖE2.FyɩdzhِKUq10}j?g) dt^ 3|5kTY"}~b=STٌkF5d /V[$}00DP?,z.rugcqq+BbrF{~aC*whVU ?+˸}7ƠrUZY!6)rQhJͯ]9tQ"`6wvK<wL4.(<'TgG=^DLYASJx$3F-ǰCǥi3+od(J~`ftֿ+FTiJܛi"ϱ$SKw>V-\kCsO}r|L[cbSVa:z$ >ӂKhS3=vyzsQK 2n"᳡l r^q}hK %1ErMƍ+ 6`5t;/%4u`H6pt }D`\#>ةdS&mC鿍ɒl4S/d4.jP3+85G3jnz˻BaoxGտt8%ČSRnO n/(Wʹ0-]6\LnNjd G0(]qldce#[!wU"TՅ켸c臺hoLZr>b ٩QZإQf%*(p^3-.! sq`Xl/5PYZKm GwMAmz EHW =+G ړ;j], _=|fS0);ϟKl__'mn4 10cP^cf[a~Ջ5mkM~թm-PؕUck 5oG8nw+)JCj| +z/ ]VGwNJ8'κ+"poW`aUBaȳugO4+/=<+]( Jpxzkb\7,#pS 'q=.!O*Jq{o;q(j,Cߓs%b]ԭ܁95UO z w[{m~0wMg!ؼ@?*7waԦDw}@x$KF fOG{)\`u Ŝ6גzDL+shj;9NJo G߂PK; cBZBocǿHQ'<.9n8.cx'!|[ٴ<m:4؂(DBHCkHjc5y}G:ԃc(oMҾ{\wm=hXC7k>_;#;L]rg1aZJGb@Х TmHˏckD|sHiE'tiWu8f\R?_m/wtM\ZgG ˞r`=:bT\-P|gKbE&ëx&X>jyg {\f#gDj=|Ù(%~ο2# u2UJYR͵3|%fm\Ykz^(cz9TEiu1f•>WHiQ^ψzƵTOihȞj~YU.U%A>|7aa) g.B!mfXl[mCsQ%:ͧ"ENؘV dYԽU.,&u*:s{81 ~v^+kQOLřPx3֦]>8aœoZl>tߍř(T<2#s XZzo/񤠚]NjA/M߱Eds-,Rg F8*'hHN MܪٲάCb`~LikH,Mϯ U&ه,\V+;eOj&ac~6a+creuKC vT"u wF V;PbRި-*[0 =Tנ֐:.F5 Kt/IH _g;a˟xבΉW. ˠ :|}{(4Z &zr`@K2 ">jUGS"A!xISB1=z#`G [{9 C(&b]w$dV율jfz.6|u}D&Bk}lAL[=oZ"]?oDצ)kzU]?eA eCRZ} Rڜ.[߮ zt|$ C\^ۥ#8WHL)r|A̍v I: `b XgN`GFݣq2ZSBސw+1GD[ۘ!]}P lM_SuG(xP7 7K1\0$3$ #]x }(>M/vLjCqr#8h=$!uF:WpFLT5M$PU&{E8aj(2q9>^[h^!~X8AcՀf#-R"V{/` C2!.oK?ws'q) 5XnQhP#9wUIpj|8ϷVHH=ܨZk rt'-Ji#?vAZ/MrwPd-E, ھ]c՝bUW9HpNsh,H-lHqY R~s/!WIsnpš_mw޵MFhb}sr*5K_=RlFCd(n1V2Z((vg4.cg={A]DRz-Z.&Xz87/E=4;yfؼe2ÆTGVpܺ_~"Ů}TR<^(o @ &o7:s1CXJİge,Z/@?ȋP[%'ǎp<C綏iO,Ͳ`40HLO+ULW:_rS^ CD4b^;cT:ly%#.feغ<<'dhhF'[SCh6̽Ș+y2p[ؕhAvq^L_f TXFxbx)dd[9U7^rEs`zY !ϋ,7VM^ULڄCgZz&2uϝLA2R}<RThEGc`4vn4\'9^3:;h~50'{!~4$:;{nG R_x~ AG:فm?Qی ဵ5X 4 "q9ʸgDDMXVYy^/sL6f2rzz3*^ g'˫ѭےylVv*?U*7HP$Bc_ ]I2EBQeK=vK>{>w7@9cbBA~S(r$-xt{$vB^N櫼ȑ-[p{"H^ n]RMq ΠDCyNÇ 2/9`dzF\7{o }>4iR9 )ܫ`HhNM,W YBdehgyaL9_Q|].ޟ/ԃi&ɫuaF~J!`weTg(nf<{c.kQK,Fp)3Q?G. 23tR HH]`53i9C tߦ.HZ4.X~j .`SkxN>rӋPs==(ω]T<qw=8l]tmD5U_?ĺK1F9%4C@pWʊ~mLVݥ5BYNp a6Lf~UoTz ּlsP`yjMܻ{Erq㇓i]TB/2Ŧ\HįS[`ھh?qݣۼYkwΞ2==MTH7m ד<3XV,jw+; $G4č7Sr4 ϩD}K$X܆,Nt.zSߖ?$9%I*:i }Bd>)Y @r7^ڶJءf[]n2 o;ךkQpˇFTuI-ZIZ&RU=DPnWVVtFiXrS:[>?B=AJs!$6mZ X9vAJ;`M_b݋?<^н}gPڕ@F4H2IUg2V܉6N1\7rB, KD%?Iv0o]$8MGW)e Vv!6:lJϪKI بgvi(Qtb'ya;*U ?x2C+uߌѱ( A ,Xt]+bkrUO6mB#硐c[cŤr[TcC9IN5266I[mD]f`X;bp4-\}dV$p$wIDYNw˓ Nu򵈪Tٟ6]=h?rg ~1?Dz*y]mX :)ARt=/6f:SC n/~q]/ ^gBFtIK3۽ .$6Ҫq4!o,U/u(j(5(KPo? lD̨b45,Ǝ(h9x˕$m~姩A{ss$e꺻6gZ.qɾ`TM&}w(f3Hktr]e~K`'dOXK*0bEiqK1W&+L[n=%zP@1&Ċ0=?wCM7ShQyhBm}zj z@Wn *M[{^ֿF'< ʺ*Y:墡?uJtU44쿊u$ 8)^Ù%;a\ʊ'VSZ:4_Kf 0˥:C冡)~>p:4yHDIht]DtI9*eos6.C bfŊ]ǝX1 y}8$VpZ獰4=n >'Q=ڔp$^ x >Y d% ׼XR*h:~z*+ŢƯcg=zJc~E@tm}Kd}+O8@"Uv2ֿ(?@[^\kٳi"GM{C%*hWd pahq|T)a7܊D-Qcɍ:T =r 3>89'K\ЂQ.E sTAa iyO qcU2x?W˛B9\骄'Od[Zcm[q5 o<˵n.9N kAZHzK }ן Q? K(WDGHm:r֎;)_6~6s~iQG|r'cl=9P y>fŖ!HA./ O'X,:5^"/sx1rpY;lw،|3]M3ϱUf| .{ / %TNC0~[J%!M,%FAIKl P5d$q ckpߴ/l^gtMS_s0|၃_nA`A00{ޚ*B^rͼ<<(xhKEJf%AMėyۢ L D r0UΡ kU#gH)_0]z`9&j6,|R6UA?[Sq^[<ktf߮Aޔln ? oVߊtiQ$0q0hOHQ~8N8]P)J= hjH5HppF}yYѢh ~SC-bbT1 ;M%`hI KjɁ5S$ߑAKsb].@+F2!l2\vw[y'yZeM<7=k[n)ox -ju0&P(p˓ZdZGODPȫܾoyKo%pyT\<ux!6^meB%T,N,דS_zvH}oJH4g^6ᩀ#}ه%44tUUM=]^tH/ ႣW9K|ICVRO&H1}2: W:Jx84\k^sJs_m H0 9ߒ"_{^u4 =Gm$a d|Z`mc+|[_0ws͛466Zeo$])P3IX&p1{k)Q-a,6:wi m9f S>6hJIAUl͇?r '/K%A5n ~  t6[;%Ѻ|7OR|63V3|e/6A* ٪l =+@c寕VsziGFRߣ`fnzwZ궍i#T|h^`1cXHD ^\83z,=$.q24}\Z$#_(@Q_?TLzTC 7C zq%Gİhp)ÛRD>'r宠y6nD*j@\L =H L16R¶[Jpk"qɞtJ<Y2}]?Vn WtkNDzU(ô`锣MR<}~.4 Op&TrcB-#kYlVVhYa U1c ƒg[A7/&3*r8{$ݖ?_q:&m/T܈*H }0sf*+xҿIi"y '5\ ?VWv8e[]V:6a7 94T &-) ". 6KaK &ـ9pZ@*(3rS,TqWxR^aklbOx.B tD5 |0+K  r ~3S/V7Wmi$g8 'o首2dso'fU<=RS !ߒ6h zARKk2}? $mſ I)E)(gq/Nl5LVlۥ'~0՗{;6 >A): =zܣab 70b>7ɘs2`pAFG3z+5趃_'`΋\,wV_(h|_ Q}SAgEh*G;"^}dxN_]i~zo{tioʎxdZi_?$G!Nh5J\$}ce0C˕I,RO,3ݥ2mK*q~j/S#JZxCOUoNGr櫝ڭ_YGnkng0!jh~&N1haQU&&b."NlYg!3g{gu \PSOb]KO42:*C:ZPN\Vh<}sga8*+'#? hVDBI --YqW>8$@^iӨ~[ַmnlK&&N["z79*? !ZçL猳FX>;VS} q^1 m%@7Kn|wp}rϔ@,`U!JK0wk[ $=oeʞ~ERoC,$l@$D !kLrcsط[_qX<=B4c3H =&|cXWK[1+r䙑sv0^y7!%>[*U񭡓^͉/xBmXkWg[n26rp>.{U#Y;t(BpkL>`6mOVQihUi34VELl@*x_Fs2&`. ʓ|,eS$#+%}]zMo y{Y}=ϑ rϾi,&4D.V#/j)ʸbWo^IiJ  ws33p*#3]b6b)ps)/OCպE[o(KNjCCi t%d[,Lt7|_;n09L2׿L#8Kqv~WFKMP!9ȓ #—%]np?F?g3Ygc9@bF,𙵓?Ʌ3YW`aGډs* 3ba` %ex[csA~}ٚ5UG](4aK`4A Me"'~a+)b6X18WÑn%h,[B2 T=2 1'Sh>?peUa5rjgp<4Cِภźe=ysw̵Mk6~4`Z5DΉ/Q Ar΋yfTZJ;HDz9tHf4 » heM]%P0T:w-=E{샙 6&>/C'Y@40'Q48.G6_jrĽP(2ZBMTZ)p5c#xT f]{d}ь?/"O;&ՙD~Q<K{>fĒ=@/~C/M@҂BԂY:BjFi7Koz3֗ѫ~<#-FcDl;hbn$y{3?'t_}Wmg>bR،Zŋti .&2J@7՘s"v(MUMvE< P`7q l5mUWk4ܠ|TύʸEv@odnս-z L\1Սp(< 2וM`ޫcxM dżz96 .ҩ>t6 T3g,h*:1`]HR"6atTgqX.nWNۅS"#1x|Pd1D=9I-‹龫߲{w#%%i€Ѭ8l1}'8 >IG^Ъ`єzN)݊[Gqߣ]Ʒesh qZdzo:l_JxFSjٌ'@ -!9z]x&? W\;yY1r:ŀIG$7T +5o:q?hD!.IWln`Y t\ y6YO;o;3R9mISwag+y*ط9srW5 JL'0OF OIJrk7輪Bwr;$WneA~`}]QM}Q4לy%kөF8ַ /N_8uφ _bBT~'C}1͘ LRCP+ }X kUAWu*F,he:Cc ~#͊lVLGd _IOs\ƂKʊm5C.E0j`#[Q'2L,^2߉'8z3[O١2x0g@C@D(Hu4^ `b0y6*#Rt &Ny i :kfiaYKq7sQ}賦U$0o&E u޽NG,&/YJ2`edBKR3ͷxugd99+&d)s6f@4%[.O4ݺ 2˦y V>@C-@c6:k$Ay;G6)QރXW=%)9l7T8DppTY=U#bJB!q# Ts /Luh(eE:#o?xbGДPtD{`ό\Q_PT.^<b`b7e%ýJ#4JK87."\̌4bX#-i& 4 &}XU722DvC|HW~"7~6,j^R(Os+à :9u ]xXi6bd\'?c(UwOVxeʻRXRjAjB oPyQ?A"gzqWU;](7$Y}n_FZ{CBOׇ^o"E;$y&+О}0I%Jt(hI V2% aďћ>Nb"zJ =jB~Qi@-- *';'DݻpxE]5F%Yi lժv]ړ'&+nEd8+!ļDq(.7E b1❱Y@R M_#ַ8&BL_5ugh;>C_EU;ݍ!G҅bhp`\+s"$4`poݓޟҧdz|9!̗_K*M'&_6T&ۉҌOs6y2U+'pMZAgFK%ja\XAOYtN; ˝!w&O|5re )o# Ccf* Q( W7G>GI`}bO tX[Z@ esDGw_9=׭g`~utiYs,M>E`r,EܤSYrxR|-h-´lmBKctPB+t Vz#]D(TXK Ik **<œ"ΓM"M/R;Ɛ 2A:fsD{𷨈: Ѿ/Z,4ݳAin=ch1 ЍvT'Rm"{>3Ux-HNˤ?9 ͮfXDZK&)vV ͛Dan` nki֬/bNd_udko'SIch1pZXi ;vXjmq숢B@j.&g/I0m3#(y,^4I^>2.<;G'Xd vF;Y_uo gHp0QxpF#}c(&^0Ei;~Zh/D?f[1j[ߍs!]R"_].3NYf8 E}hC8 s\g7';?IJPr(g1˧XKm-OȠ(BîTWylvO-vm$- mޞT@9rO|/1kC|7sd{ o ӕkT[d'R1h%RQP]ZrMX7| ONWp NS)B__cG!Eͻ,{JWW~nC=Nu/N2w}-|_ӥ<;>}C\z1/gܭ~!@x ;,u#d;Җ^@gS&Ѿahw?~J4g"FGޓbyB'  g,@J $V&112tA2zJs113xgCx %,=qTz9O1 oyh9 .< I[$"puRV6 =IF xm;Y-\3dҕ3guY l`7tCYxn|-WM!o+69.(6{Kk#ly? .: y0i䌈՛N3MU5`X@b0x_#q2zlu ='DZPIkj])3p$ldq`TTlUc)X'|DggS}/U?dקjQ]M-Acѿyupym?F=A682W1;Q_e~eͰb+Z}ʠۄ?y?C 1U1~#T;!5"sfkgwǏ<|z|S[qFx@NAػ<85ڻY7}p"$dz⅀/i"$1SwC]s@A OwNEKܚC}oލ_`iI~Ngd@ܫˮ}XBnk+5).wv ą4PViۛcC%odDO84-l+P5cFf!py7i m3-Q 5慀 "KcV^yapX{=G) !]_qH]d/H5pp~OxJBG`:PH rkOe{_FgC{˖7p(r5?RT*BmQL| i]}% @TcYܖ;w;Hڵ4KW\1> ,dd9lS5構~kn ;$}4< [bi¼^Jc58əojp7$ٜhs83{8S_o%.Zyg?(ΦDwV! A ,0]-#.Av÷6]-~e1CRov8R)7j_GWo ōnOu\{Qn-1;9UIfukIV7`N khG(wb#ؕ>A9`y[i>;4KMχ$ gP9"}a>S K;Bf|JC+ ISkZ|Ty%07h-<Q2$5jZopraW6ڙI q6%08R=[&H;BUWo~y޹m%_FJv^zQkAqLX{%u"ڠ KwO _N sXNUnaP$r֤ \+Ǣ(Ы0F*nB5xp4,g9E?#Xcj"3qxNV0hdJ_P |ezýC[R)_2?~z$i'yُY+/6B!o!i#~]b?7ӛ23LOHjUW.pyNQR&rQk~mK-ZUu k\yKN"_q!D$ucWG :e1Y^΃hz7' ig*(Cꫠ%1)Ǎ"_̠=4OVx됪ȕUWg/aYftF80⸋g=Bnװ[n}[i ᢓX= Olm8?S}EpD/|h{nO:$a9[ ˰9dbeI=|q}9Z$e䙜ײN[t#8ϩYVW6ٙH&kq~k Aӿ\Ů}MMI*)lI6 Az 7$%w"ÃÙ'=K6MQ;i/ DB9_7ADпَ\0\kr2zB-PG`*j1w%|4`XLG, V\u n[q(0ɉBOwXxzby!ģkd\T[,=h8N sHDr!ǁ4)1JUW7n~b)xx!0 ZJl{38jhYOQI1?\eCD p;T-;w+&c9# L=eWG4-JWro0nǥ] .{Ҙ~>}l]&LzOsQ PH쒦Czz0*&7oy&`TV=a$Kcm$@jbX >8Gl^c V2òŢHMԳޟ&q[ `Pi([e;9JA LjS@ʳvPT: VW>O;O{M~TVoYj,Ck] xpJ9y-Vv<9TkV3S mdߵ_vev΂6Wz[|OYp lhqow҈cp/(e_ĭZ+Ѫwhp ŒKAmvLD{LSe'e8'+nsƛ*HGxcLlXG!is0)=byW_UwRMUF dA?*ۭ,BD=N,֚($9 ȈpU'W@5oӍ!jhC_y/ th108 D-E#oxÏ˖W]Yn/V^@a{Ң{ync2p #i1<[GY5wEI@`짵_}8hq4ηϾf9Y,FƟ]c_]7Of?+9F칶1m+ZL(@HCor?Mh MK,Jj~:ׁ{m;&g4MPgGs>H0mr9OA⤸KXaٲ;p$TwyǮqBW#}Tu^ PkvLX'#ưo+cUt!Ehr |r)tZ_Ah s~Y?,qВV@Ga].K${Z _z6^V)o? !RNFw9*i߇lQ۪ z6zU"CVn(jd0Ix+/7xA§^5@xc'Z6HpMD0 cs *L3E4IOX£Ƥf7h%,CcbƖX.р56ŝ@V(,jK_Կ\z> `5"HzXyqPљ-15x̮~>=(ēZ8j}0H9ETa{@WֶF[]є;¨å rAHZUr9xLVsE鷾M<<CU-J:*.B~z[ j<`ZIHTL?1EkQV-̣;y ƂGEBl'jӭyn77hrى3!H~ԋni2+}[5z ԙ-#s%Al De6wdM`)1Tb u:o!.ȾH=3x[hgO5t}i´G$^wAJ\DeZHv& U6c o-+ b̳x (?Ly;Bjr 7㩀$"!`~${Fq=o{gݫt(-*̖@x< jOנ\nIH"p ,KOI B9({{nEr^lVaFFG , \!>cxϰr $qӬ-֖b C nI el-N*3cծuX̙ YLsm&1 $@\݁'5J&:h[g_-'՘oP@y\aTL3JA_n˩KZ5_j)5؊/Zx;Ewqk/ Bl1 ~^vA;[iFM8eϟ1ɬC2 qóI'ʐPL+sx>Ʒ/o ~东56׶AWp\Z!V3sR:mO 'bȈ(J}"0P_>]Zwt M#&S(&C҉ HgĈ`E`~|4T6_>F{ѽ_u8#56B Ñ֦]k죁d>qzFtw06H/ TG*(zuyk0$v8٢`WUqwzvQ*޵~:jgҏySOF@Q$r5:ƉBY)1BYj.keym MQ&jr2W;RRQr [#R mTT7hGBepg R[jؓ.A]- 0zXfTMHUhd?';9c`#.M+ /VRMMß/g Ŗ30˺OФ #ř޹Z}|~N}+Jӭ"הF5j] "B\Ԯ gְgsM?*^l0R:jܓhdl$ʰ0\IZ3^)iILc;,\+-'.%}4Id+m!UG  Lbl:!Q2Ԥ٥^(䗦i30|ԜbC.Io Wd5@Tߓì|sk}SL|A 4YWFCpDbyS̄|hlAF!&*޲ʚ))sB溥A֢[O?iW㨆Kme%&6⸹wGUjyyP8e. bq# _3k ޼Z!P?/QgOor N\a_ܴwm5K>,&ICv]}Y)>Rw#1dޒSm٫P9RR4^\YVa$jj4!ߘ 0C>DvS܎>z2I%4#[XDwɥ_I1Kpq>:z[j*=_^3G`:Yqż|H2VR+l]9=*oS)v!YҸΏn_tS=8݊atm5os3p4N[ /ID$oT*H %9?-܇$gT QdFЃӨP=%?'}틎#8\$ӎbOsk\N,:?U+4d*̓uW7+HHP6U;srqI6OE!Vݙ˹8d}wI\F4'T 5n1쥤X6YAOz,hj&v._FXY*3(~!NN ICK6 Ff3Iz'*fW'4W kEk3=^^khM{Maj@e&doO2-n_N,V+9_MHA2)R_pUf_ VB5Vf:'}O68~iRJ–TTAHQu/5FhqpV"*\Jp#yiզeKQGGYKQmH-&zc!} ~KyX/N{`҅MmǪR@oGu^o3[+ZVkxfPã06r| ՝wǧXYc3HRkRK*}T4ːR=Q/54;E~- U0h&Ѩete `hdr9wu_b`^tjZϲ~ļQKaYtS8f|?UdQA"='DR%h6q7x#/){g–YrM(7;^"װl[7ь2m`Uy+5rJBr[F2]ȞSr-lfw` Is! hɹXrؿK@h\wvNW^RQt :(LJ߷)4s2.y]ɛLy>?a/ZFZsqEf0=wȈW(xZϞў~M@^._ Gྜྷ;‹,tOJ3/C_#V:UK5S|i82׮,Cp.Bfz&B J=5Hs}d p^=We6JBu2<-TU^DNաLhJd^*q;&$Y˂Ϲ'=$o F<爽qh)"z˶:݇Wj0ԓCR' tfòB̆gSD$b>!_m@^` =7^,4+[hV3C&*_hWQ*%Z hۈr MVV? \IkL`-iSH]Z,*|bڼK?s.'oױĸiJ\ٛJ p{6a; jkD_G)~}JV+[vnOm4dCeˆ( x wHڞSq|hjp#Gԣ3d0*~Þ<[gUEx?AS mڵ\~g^YYj=k96rEnNFN=%aSf Ďٗ;9}vGױms]R;Bdm-ٚMH] ;,ZF^&CǩO0 Y]jk5/awXI1"Ds GU*YzCǎJqtjJYzsb|\oLK{\Mi*()E@\Y̏*\쏕 [XHuuyJ_*/F>QxG-FXFK|{뎷B%1lKo w=4tԀp y`~S|pJ;.BY Y-8QQ%J[1`4;ݣq(HO\=ǐC@  4rUh}:B1o 0-$ͯyݓ82k ϝ٨̆C r&3Ed&"n_y+CHoU/QkT>?m)Zz&9~yu(X 0q7Q0 B=:1ta+jL_Z1B'źK΅PDL5v%qd3ߔtϮІ avC.g}M%V>ߛzί2Tq]+"糜{3H? E"ëB8zJ MFt/nRYG6zhK`zoxMXo>#eWzV>m*/NRLq` \_2$`)`DEh>05sTyx(  iY,U[޳{J{z!:hd})O1P=Y6HX{7E@*̾](a{GaTr ~ΕTt+Ϭ"|r?󗲹?81GDVm;[ZHA!{cZ;P&9ESMpܚvAy7N@Mq`ig1Tt.z-5/Agv4}[k85 3p\/rv=9MEf$EY]Tϩӎ\v旮;PhS!DVXQ)<ǼjV- 3J}UK& O^[]j83T'APqCi~*b3o c$99g/.l~Ʉh ,忐߰k59}< |y0|s߉F8'H<6`5Ƙda&GfS0s+']DcB#|eS+|APäm hGM\Ә\ƬΤ-VO22D''[;k|q>u `g䑌BuhM| cZVWц"47h:)W0ЊGq*io*|CȒ<%r\TX}I0))ݞ󪝇.uav P,{vT+ A%Dڢk.,;Y:lZ#i*gL:v4[?NF9.5c0ǙC[ N!d><,-8W[Ynmu!g#f_cBοϰ..^J0:;+/xrf & ( w$/8 a]<&',w W6fy}`9㞎7y1è4O[àTIp9orhaX[>,~EiEYwv,ȴ*?(,VaYxuN'1 /|*nHۮox켆VvbljpkB+GRǸ3n6<;^$|z_4,~p<9T_0 1(>g[H&㮤@uR\ʑꟼn ] ޟvXicX~7Ixj[ R%[yʘI Qn7 ]0Ç`gE/!E#fHD7;A,Wh"owJR "$ɉq凵T,yEnDFmkV ^Z/ܜK9z)"0 oPw"OJ?j ivjKLܚ-C!w(~Ṟ+p_5wj0£U vZYK)@;f֍ caۏAy!"37D1rLPhfcWL{[fblXqީ3J#~hjXmk ;kw }ZXYM p4%zl-N9-+ǎ)+G-zw}2eQdjy4ֈԊUR~YrTro;Tl {?P , G +kT*frN`lQv\ *^< V8 +_+=o{#y`" ?t6h2(:'nB3zҋ剴Ǖ8 5h-)1z%RPK M\!F)b1Y)!FJ!r//z?n@+p0"YE|-z12f.B+6':7s6ܯR)xZKH~ƆM[F&Jr8Ͽ1/DrFe~ّ͵P@"9ah$T; K~n l「(ay M<96lj*@Mz %R7qb{=,)]aGw͎4ne:zd?_=R?+)R!A|BlUPmmgN+nm䍷J*)' M)SBб f~,yVqrEՒQ _R 7?hܛ-o"Ǡ9^n3WsZ~/Ѯ*yJ|do@<<5"Hq1~⑳,5,ͦk^ħ7*E82/ߺJWU#Z/Ly?ՃGʂb^e̯6#p-XCA"nk99 L(+/cn2B!n}IR} ^Dwxy27x:׍W\EAX9pT~ȚĞ3\bvR)y5Z/Rh׮[akvS߰jJx]U}@qSϒ{``{GIJ0-L4b펢4L`oSi$ͩh 漮I!E ?:=.Cd[^b n#Jӑ \6Q#gxRPVSz>ۂݠ - ߶d&@p,Qۧ@W"0a Նإ$Yq/C^-oV&QLgի7IrFrSas"j ߃">9oa\QDA2rA32չyh螱BP5&r55[t}aM 3%Eh:~]=ry7'(Z54E rOKEp zKh4/U?nrw?"DKeyjJE6XtY w?w4؆h"ieN>rCdy {X'Go .L]yS"x.ڎ4Q~ o2Y9Ӻg'F8k,P(wdR\"&̺݀`3Lf4 bCإ{kӺ:^pP8ʒ(UWu~dJzЋ iS̷Jy\3=\2%>70~0H=j_v;b(p0LVaU {@ W>ES=U`P]$a 5*E.^ [v3Nsq"YqcA$=gFD:mցhc\j[A<=W7€ϭ/ʛT-r4ާõ'#x 5")=ksq ^>Ziʭ֜VduY;ڒz.i!$gМ$k+>)OAS1FeKFtܵTw`Pr/hڹ\(_[_sX)eth;3#SDSczp ~7@_NZQ%PƵ6޺J#S&Vh( Z3~ђpe]e_cAh-2k{i`>7>SSrs"z5GZ(Ν!í!p+~xmܜbɒꏞy y8K6vlAťD0N_=ധ,^Ikbk=_LpĜ&⠑rzĤU,XR^SwMbd7uVs\z!{`Z^qC{\?K2%1[Aت;k*C$tMx\A{J5(H&r{5Oh2&i Wԡ *erT. tHEx|eM\еzF| _* 偎wKՅoI&eLb.M5/d?7l3Oú" #Gg֊6d:}3_XҭQipwb.D?yKan:v]mj<\6 PƟ@u0pyv7n'W&% L;|ReIǚh$^ em1,SK^|Q5j>9Ph-|~q滺s3j,ߵկJ\Na4[ 7>I_մ ^iNˋj۱k<Ͱa~mKnB飋LY->QmHE)h98@ E΋6?8kz1j8B2Ѥ6,[Xp#E vtK!bPL !.aeEk`V6[-0G8÷C8lPLIב;ҵU5,SUs@GѼc8c_9GTMB+/<rV>=IwCoGο9v`$ȀP_|gsB<% u*ߡ ax¼4هWb#3bNM\6\!R"L+EAN"}>3t&r.Ŧ.js`nvž yx#QC*X Gx _y2fm4I&R+Pi!R t 脫:'m>ah^@ ZܑH€[}DPP$X 4I5NAX?Ƿ1o_X0ߋ|:LQΗX+#?Zҽbb\!޺ <@E+역p :<=r'&j7H 'pw&c9uezE"p.geK!|iR-䒡U%Ju5{#&f9bc$<%x 9*c)C7)֬zR:E*eⴔBtm 'H@F.x5lNH}I1Za+HJds ij0v6xk~RUnqn~fcSmYA",NX/шpjdЕǗ)zɜ85֞b98dujrۖO " F.;g-cRO;"ၔ.A`ͨ/w/ymojJv'79_a |񘹏j]ʙM?015n44zu 0뜴&StEM(l AE_ &*]Rlg]MQxs1n g2F/*D;0d ۞ԭHN$<F2Fw qsY2ug|r&mRfY%`u&ؕ i3,`SvO١z0IjJ3Au?IxᬡrY*r>J`.A)Uu, wek\dY[ẁļZd~熮/]վy~lSٜ؞">9T,ig`AD^ #:,oY]0( &5J N!URh(#5yp @Dq B'Lj~["jgM/Vy4^U1wzlp|׎={ӃU_;]ǻ[e[j ]_H'$mmՑmJQ :DI\J'Y4@Z)'>swv5*ƨ6 XͪuSyR#a u.& !LI.@5 㠘 MSg) [˚r0pzy'ET#ٔMjz*Q,jMq]h]43nDNTg>39e-TvNoZ>&!92w d5&p%qp&ElXCm ` ^57BF}pK^.a6eJ״$"53ZguQ}3n",YbVQw~ZUrV\+bp|zB`J"(0ڒd{7`xԲm}0]Udy'~HK6F.:[$ދ#0>Ѧ1[QĦDXIzU!%2E8aJ̥38ӽ ?sKOkQ[OR HBcr2} +7b@~/>H?NٶIsSh[Te#kn BϛSظ݅< .Ǡwx@p ߤڃ7t|T\`xkd&e-ܲm8eE@NHA$Q(1՗C +_Y7Y\ 6:_bO?! _64~4U\PH8,l/(*ZVo,z h4Ɓ{Y>^~܆}*i^e"o؁M-{aDumC RD/b 1B[ԯXF9)䳇69޲vxʹ^W{ ;}iXc$~_XJb6!7i6EĞO{yy+,y,!7UQfaG,a . \c$\FNx$4C 9'"g(+]1a8%]'K c/Z㹏FL+(|˾UМF +}ϭJtS=?Etb1 - G" "@2sR845&w C9>L#"b@ \1r$Pd4x|T*0RnO1AeA!"=r@;-QٟӶ[C--57&9 1<Wkʬ/Ŀ>>x<6 Eh~Z{ojv[¾oLi8*![2,[)8]ʽawF7D,Ņw\ Tu]N_ϳJXe#4aDZ ƫmO&ăנo4W{7ae^ݛh tz=+tr1g4ݡp 3 pGzΫ˄h  Èyad eD Ne6OR3+W+c}'2$A19)9_،zG 2/ Lw$%llR$29咖r*c?و}b&G]6Hh%I!l7#/_WIo*e&SK=[?e-b+Yh P\:޹"'ia:橨4 *FOLXu1@`ٳJ@U:n]0AK3:AN^Kv6ᄌU2mY8@ p $GӰ^S ]i#2g!2J0d f[5R?@M4$:fn)􍔝!Lz"N3Ujdܼlѭ)[@.Ff2Dy$PsSHJndI2|2YW;9SRa˪g?5mILW&Y%vftFVSYe6.ZErO2;T<[sNvLn~=`|!"?{#3r`8;| @ֻjWSlx7}α~%32\V{+އjҪ>z?1D]!K5DxHG/q7Ӯ~ܤ2뷍 ':'oO R%z&cz09@7cVKHs}eGIo]&C ~4@|G 9gNkV/M$O)d:*:qknihse~L{08 жTW+.T$~"oIe?DJEh;ȠZtc'IK1Wɝ$}xﰘG㸬V;VXyA%kscTy$3v*dHI$eRFIR|ϕJטޖ:`/\jBsmV1\ō(`t[n4#;yC2ί^Ga3͡ȆHvjae>%J G߼b%AziU cR{6.=8~5,)[7 1`rwXt$:e}cxaZoՑgҗL^xRP"KFU ;,ϸzQ4>MfT@ʤjj5e-1[7Bi)hj<ۖcbv (9ړsn_7 }2@v!XÔQnW_*Y7֎_z=vi=zNG4ܘ>S5w9}ԺGq5Qb*ǹxx= .LrRWL]Cj?&|)(4Ú]d0x{|"J9)E]APJoz_mu'f1*,Ci12|\,n^e/99 TE劣7OU8NK,80j@mQZh=7!,@iu@'t9 1O%MHAC<=cB$8 QS&'9=+Ǭh]D=n_>,34mOnOd c+UYbiUvc5cr*(켚w#ofa赾Sztז/s::[{'PԾ-56q!m_+zCF}w`ǝU,NFePQTwK蝭tғD{u5~F8 %C=lqF_TB~xm{w=Kאp΂65uUz6Y=-H&)M+?yz m~鞁^JSH-U>U_j$U2Q3 "I:30};ru.U(0+B^,+H7aOjQK؊42٠rۈi&Px.سuewckI"Bh()<+ ~CeF4M0,#Ž D4k,DH^՛>0Yd>$uau3$*5mƄ}=uyrﶲ`Mp~Gjg"G;# l6tUtzMSvXMcE9q`3^OvLlfjҼowu}< j7+' d^@ҾayL qفqЪuu)W L`e|ecw̖'$#ުz|BZf߁?V.ыdNz/2Qp֛(Rl٩M}lT8"n>ꯒƽh{H?|]w{% SV}S֎tyA?:EpZ4l$r ,wpY'buQ!l)tūGIQџF.Lς&fҢ&]*3'؊V[<>oِN]&`uÔ/%_d C.X:Ђ*CdjmA FM@HPkas>[G3N_Q; }w('K GBGs^Tš8IJ *g`>IBhgm+o!jn>Al###4&tGlc8)BU;(YGZVޓ&oƥvD}Z40HeXz5y\%e vFO[[˘=Qm3mz%:^Cy;EZpvb)~-LF޶90߳fD$X2Jsk~4㭭܆j3팗DS qYfx<ҎOW7c`hpwFr[5GjS/$W.SMӧB!Lm&nwOU}_FS7[:THD@ ʡq/O)ۙP1o[>VI*0;5]8qDJLUlΙ\bZi6K,q Ѵ-U\sC8xő;`T ||#7TXLH,#A*5niM⩤<tuGf#sbho 5 Pc:.jAS!~w)]>*gCy-W"lY^)ev|lϯ{IΞ3n|LG;4gowu}esj+Zy?ci\n:jc ,RUapz@'7MZiM %ԶBαE.֎* *TV˻1`] X8.Zo՗PвKʪ,i F)g瘸n2ӥWC׈5sC !3 /A-캈LLjy&$[r|Ѱ}44=7wsӇ mVhDe9*FhsY C'|Z/"3*N+Ja__?.c;ԬdMbM\]q/`ŏqAYJ D_J;k<˵7gVsE";_y) c?MǍSGJݸYk37(y.j?WQ^6\BXfvH (w&|{vXC(亨B~뻙("XmQAIaia:.z tRq__4eSf=g;IF[# ê-x (+@ QWu5RjxP0q0Ʌ\d^ )ʡ~NOYKt?Q܀)a5{Хb)rߖWFt0AH[?yR `~,aX:"ҥ/_Yxr[0?WWA %−T|׿$+`E}uDlw}&T eLcW#l\n$^2+m =V^H$~7}5C"L*ʂUIu>]=䘯qT CChr&\)i7%~r-`w ҝ>v_B08 Q6(ݝBnU:hџUAiW1毿m@ɘÑ߲ȇp; 0Fn;OvKB{rn[`ց: Ğ u)kh:B h@#<yA#񯎡Yr,v S BJtu<dž׺U 莆L(~#7# @:ڏ4ό5 {ЛGvAyAa!$KQV?vh®FQZA `^ 5{nerN^r-}[Y0b%X͘kב 2i ͍ Cz~j|,q UQx?뷙G.Y$ס6%C ItQE)U6}*$y$ڵ7U2$N]?2fΉE7p5neAeXQﶏ^ _[0M|Vw3uF8;op FK;P7 vƱ^{DM7o@ OBt͜aD:zx-rݣ ("yuِڈ(֮=!GƀJ{+K+dp !J~1tdjٿ !-7ſ5DxIyëT2ljp^G8} ey7eȋN6RVKW^G12΋]JWR7ƭ*`|pd!LAAK3 t8z9Z#FYNuo*Ɓ)gd[Bw V'' mCBusŷw~5 Μ\Y+Lo16p/"Yyϵj²Ƚ;NHZKr.EVY@@0ʕC;Tgf}Zd2HZj|}_DAhc֘H֧ME'mtdUCkrNDR.[*L9&V&y[*'5`;.jW@v ɵ:c(74{RpIY*X j\X`]5'Bu'.orLʦ̍0G5a{˦u_tI}] n(bW{9wQ$z!*Hp6;P|=2-i3ܩ9ZȜq=JO_;{@6 ^mS-jCuQg̋w}jU}T7洛JlpN$n?t5 }pƭօ;pz~ӏ\<#tIӚ_%(^ڦ T@$f_J4dG`kA@e,kY8zhHCUы?aԆķ!o;Hr<`T |TlV 4#D0N^OfDhp *۳!ʛ,@`aZ󫡲>iEy*Rcy;%DoH_ŭH)@\ Pe0++!vc=ISTvňp£ր̫䷉-|[&8f~<B+ ThWRL;`Ƶ+ƾ\`OJn gKBYzd>bw^:#0 D&CuǦ3}|^$.=Ufwb^%<]9ҵY,Zӛ& AP2huYKi[t@@$`: I\v7 wsiG#%&r  C'8N `x9yHnԀ2HZ%t/5X4 ;;`a>-Upv~Ѥ kQd=/B*1i0?YESyu.˛GH|N#+jE24<]aAMLUz۔8dD"7+ua,caK}/?뀪^߳~dyo<@:V 4 AbiwᆾO mK[d `3aH!\e@GrU+4$K-P4LLP|dˀ$d 4 }FxRo78/ڂsMBc=5:uUJG&~WLM=-4!d, 24uۆ\(=EMaڬs7~trHCKVmy3 !S06 ?|[py0Mr_'&O=V3oWdVدĂǪR[F 0w5 s25<"P1b3ТxGŶ7f,~ٮL6R ɶM^|ˡ,lPP]# ƿjVL6u_1wtcVU\> 2h- Ҥ9eZh"uxĻIhf@?yR&03 $Dnlhri"}(pGg@DWƏft~S15zIb}Yf(.y$/ZG= p KKE9KuyHhrtq7w&CAbqxNIo,\mEr$.Yl8Wуh.!g];^M]V~7H^ )4WR䍺'Aay)m3fŭ c JW 5%4N|z'o|Ձw"7~_|N O ʦ 75l@'xbG#Kv z\D:v7f0pN&)YU[ d`E(VA(~].Rf^+XLjW5c3}q,U ˕>هݼ9Z}b_wR ;F`14-NOXl`,@AVIfĪ'A䦩RG"a8]9tksi|WAp+";KUk^\CmgnTKc'w8Nw?BۉGcG 2˹;ktld#x9I*Jӎ_(,shV*1{ l;i@(l 6*H[N/ݹnW##[Ӷ3$xt Ph dY;q1,'KϚ;8^blu/ X~E֡ꂊ8~y]pAITXu&wTo{H8lĝ(*urDPh3y#ﳟN_wy[n2z nڛjUlOd^sؿNf%v`,l+?;0E U)W"";X*9˄lR7mޖY7[u>pH-Cu$-\ 9"FZ2 +%?|_#r \$O=m|8^efJ,FC']۾zD+Q D63JG7]6_gLhjB]|M'9|V G(ʦ)o~6j3yTB6NRE5c9v =mmgqF5Jc Kz'KMni-1Hv&k1QGi ^ s9+D#!UYoTG%˧:ޏwmlIE /3"K Tv,A;\fsܭK GTeQ;Mӓ %g>z \l{]eiZ,6c^O܀{dF 1Ϛb'^t۽zKn\ beȄr3q~?rr%618]1ued4 iZm}.:iթ'Lq;G)[T{q*Gڒ+(gD< Ped,֨}x[CR낉l( ̃>@p2`dٍs*;n5=o{%R8&' WvqNr܋qȎ_jRƾ|&^ycքHtZ!RRz\3U'mK|O1'_w9dAOwmŶ] 3LYE%:۷0!GH`36 K=o7 D 8C\V?CU"3nWBh?`9lS^ fI{S'9CLnVar)bT(P1HiA \f+ |(L2- /P5>ǷLZ3)/ nӄ{?PKsO\|Li;Ӏ9G Q 0,w1=*e]YwgO^1es?I+L(|}uHk(:Pk` /h&.n9 ~qR6&ѓ6C jb*asyomKAsVh f$+ ǪܐpĂیICV4z,96"βj]I =$i 6Sȇ2>NœybÄ+@"́RV9;8Kk12r,S⡿I<K*RHN(T@vfy|m'y`.v)kURb8*MѬt2.f|9/Zc-P8*宣tIgMu2?Yb{l sS֐]^dee);'fӪzGMwC^"#{m*qz6W~pgiU?V]%:fHw_f#b!5?Z}4ڒ*eJ}3*Ѿ %N7k\Lr`Ci?1abx {kS%)<;\Lkn璺a@iJ  +0xrA?+K@ fj(ԦLA$[\ nÊjjwynB㶁{%fc[E{6dE#텃}TOGIݓjUsWgt]IZG b]} "|3&?4(g%ƦҢj~&֝9(%T6`eɝds`i"%:q'9n_,Tq(qr[zVԩYK47O.ȸ i+jAvaT d2bf 7%!?; e:_*:;,d䊆ibOP~|/(Y(A$]wdl;Q9żG$  ^WeLUʷ U^ !&G}C^ }s /uݓ M9z$klSk DȯSkdؗy{ŷڙaVQn cҝd(oݙv钆e#m c zr%qvtS1UM(Kѕ&uq_]g[huy=H5uvS_Q70[ie;[lyܲ؀Vr9jjS;srZ&w[&g O 6 O$M"'P3դvuG kޗپiIAkPU`D0G±zId>@|@QMYR% cO]qCl~QQz>ie7ecL4ōAg\|/|" F9D?q֥31 2N[f6 LuXXӖOh'>9D|^Z]PKYYL4QfSJ'U_߳P_6~ %mR ,~j$۶"7OQ)t7cT Yl=}h0*۪QG1;Y(>1 <[dV?6}'UnNKnR4K|_߆Yrkz%o(qv­U/wh~9jHB-+7d|N難XvR:VV',*n¿kQ邿j}g ل$*4R B鎯LZYZn]A[g͂.j[&/=,0EG7Vӗ5lQBz-r r^ZeVn; ŕ7ϮG jWⴠ.LZ),hl Ǻ?+1iT3fO:Üw7)Au:g^RRWmsx jgDf-xhfH(]XTWŸ涅|_=1up;/IZЀ?p-<|t3bnGWF*Is ~8`2FGHwN]xf~Xݣ'G,^wmE(O%ҋyP-?7)'`" k6Q`PRxaeEN򤉤!cjdǛQW<`;eSho^P71?Ztkwauh֍h(VSa0޷HGqNq@'?kMdB# xR6;lQv(8 ' zZU Quw][=ҍ=n" ~ ݝS0+k.Êlс_yL\ f-cOBȅC?{~z{ĩ8h شb(9w,dP,wsSY$YP,D ;W[*2J">Q%r&%Ҹ x!ȹj41 xfW7;jm4yBwȱ#2spEׇ|6%EWA?.{C 6ş OE"Lه5)3nMv*q2״|&X#1`>Sys *kKK%Db\:?+k9^D!Ie5ȏ_Zk₺+#L˜BVݏ/;O$[.7kho1knx-QFyxW]h%1(5qǞu@oe A6pw͞ M z[i n\raW^^tz 3|tRG}:8 E(SNkQ j;jg h%;$Q=ǀ2 QEiv(lcǹ'!}W0,\BB#Jix2A˕N֫{KPi`fYmͽM:z5Ra[Y?CȧVS pĞ3%ObSn'/ x299 @3odkȃ}d#&$7ɲxV>FW[F fħy d|SDyJ5Za@Dd\' KB=Eo| It N)T5\\坔HBE. a ,ˋN۪D;pN)ARMNۏJe?{}c_?֪=07e6z2OɉQ$}Hx6M"w~cJ jq`^ `}dSڮtqsYXK˟\!2B)Ҷ?A,&iX' "pۗkCR.B̖md#T!3Mup+屙VV2\ ho>KRgǷT;n zó0i+QeҬDZv0 |NDc>yK눫= JӿђRH6; MQ7:xhczʑeV H^=;!$R)ٜQ[̬eB:9(Ը|WLs lܦ5vg>4FN v܍gtvBqѴy3wGA&2zը)hb(kt@7Ŕ(S|:H]}u|->a_^?Ή|bM8NU .L`hlIr]_s3/l%db_'أ$F:LaR@hڠ@$Xuh.cxˤ̉Y0aw@7נ^~NĢ"$Nqv%%п|~aCkۥqv>{>? F+ergG qç[>3"1ܫJs^~Aggæo-%%)_li_dhܒC%R^M`ӦO 0pآ 0 f?=HFA˞5SFT^]& !ϲ8?CvpLˆ HϙjZmE7._A@glDo!Vb?Zͅ޴ywχy+8]3lϖa p Ji-wEדnmifqC<Sv}nՄKy6KZ0@M>7Gਙ4q fk#c=LS3Vc{׾R3Rn:wQQ8薌L֎ѺFqD us5TLY;20m^ĕdd1$O}63(u~8,,1 L0WKw1uqS ]M%^=r;YT[$ΓGjKn=4a[@-۩ c?a\@b@P$Ÿ;b,>bo&GFR-fkuafeyX -GAoPp0$g,_cttrZ~-,ס*_e\C/bj[܍6i12t,M,p &)HZ &;e0䊻fQx Ҧ xiFBf7g2y`OiTa,:/40Ty+ 3 ߽U5\k4Zfe-2!Rnc_d 2ޮxȬ$7#4z =,LRX0:J]ezq&,E`~6O?/kF2;ԭ,@Gr_|q?!תCG grCL 19-E5| jٳty2h4EIDΰc,!/o)AQuQ[7b2-np=]:cţ2C^6إ;פIu5wa-\D͊x5J3؞W e( Z gliO6-tfX_aGbďZMO9wnI+>5ZMk6Ճ;jƟ>Ę?2TOUϠJ71dFŇ|@UgXәTU9.r9åJ/g7A.SN1߾Zܑ KWTm;eVT=4l 9j hTwu C4L` 8Uٗ?ZzmIQiw=#exݿDMy6SɗFhL*X^'+!+lHIPh{qDT=klDVW({|AX|TRt@8g,TTkj?kfFRnK&qGKǽ4+ /&=5f8Q8jioҺRG;L/ 6`}+- &3u-R&C| !?- 0\ج{ *86f'r-ܳd6bY9ıS1>[5j+Scأ"L`$Ě'隌*궢*#+wGВO}EQ:wm WΝym(x~ έ9=jCZ 58|),%2UR{<U1,U< QYL&9­l)hϬq -{BAQ1`LrBny2_jݧ̋۫V]C8W̤^C/:W/ d/70SE[ʞھq _qfD(`]1B>\<n sI2\u0=?S*g/P hsr?)*~ޙ_@1DFO ի \CAn,G.B+DDPHQNJaXg8^]2?eٞjUyG"u=23+Áׯyx"9`V  . E[Xo*YcTLl`Wzb37& = לumΪ La痍uWA݌l/P-%R̀ DXzQ\`Tx杷4ԛG*)yP(Lw,TKILz󿥪BjR +{~ͮBr㍐of U #"CK^w* 3'0M Id(N۱9/Y<*P88ov;Щ @&I$@%mk%nDHo+Fr7(`ٿ/ֹUxpah80bQAF~3"b)cHۢes 1\MC*Yan}'\_]ϵ.`31gN?ݶzΜTB` H9x[ 'Y=BS;Q( #)JѤއ#i bCNܺWdW 1`_[?t|/zJXtMrd1xo͌Ez7I{z)+C}ـAJP2fj !XKbV.1#sdcƄzic؈k 3"m$.5~Nw E -`jU} ŋVR)ΛZ nJdGQE͙٤2 r*:k-~r{~r4jxqWG6*\GӢС;<罈~:4tW$G /'{^Ozym'z˦NWϩ&(޵~G+ «Lx\C?KGywQA6qG`F 9p(B1L&kgKŶ)oŵ4̾j/l_83hI[h/sY9lM@c T̉+;8~P gLwgSG(27{KoǫJSPA՘8FL*d#WQPֺ]e?ܫnMKC.Qm[˥jS 5] rUq+.rZhŪþĶ찴/+dv٢kK>%If_^ah ''X{6?nMyT*ٳ ivD7 HawDIع9WŘ.BpT<}rs_t5sl@[LE CŃ@ iXJKfo3"b6#k C6qZaN|@\6l}}C:"-ɻ'Zs8SrJh*aZ;yir1Br;p,5.vl{(R /|9BE7]DV`Z TQCR #< j+*oF٨"drLe'; qOm_Ϧr&EGp(yN c2o>fЕ[1u(Mn)t$tC G@Xa=yHtt*5+J#|Xh!PL18@а;׹%U MIxy/_/=I/f; μȔS203z{)ٌAgq^1q]l{kGЎbFؤC,6!tIG5Q`>b{ ; wQ?.hvjX|{J: UnXJS c-Y<ւ=blZ|O>Yg(f{e/3+".>N†xR ]u)uAIFzDը⃜[l6zN׮!3Y+:lx3ܤl*©rKȌV W {#ߌUv0BxuxH~4ljvǫ垂Ƌk !cnQf;>$lV8`F.p+ߎN4JO>8nܴʬw+Ϡy8aoL"1LHocSϩSZ> =};p6G7!  \l2}g\hSK3ռn׹+,\&Rd B=Ґ.vZb>d'G+NeyҎق2;ۼ &FqR姏iZk2TX7zR9UgM׊> @챊%e<b| hnIs4OOb 7*Miǚi"jOp@Ňg7KKEtيr1]L83HqU`k;Xh*uf]\סpW z#Mš0#o'|= Wl.->z2\?r K^^t4L!vcb Uf@rs`n#ExKj¦MƐ cΠ$1E2Gqy ]:>p苿?}}Xʰ2U"UmDvÔ b +a0ṞXq*`8%O w!e&ޟR x3lnP_:&r`k9EcV6Vs̉mΏIbJp bDΒr7Eiq?!lJ=Rp5'impˢ"XքfDY}/2J"Hdx#ؾS<ӆH}7e4AҼ.|WÝTL+ yZ,7\u$ˣx{ǀuQv4)q+MHE:[P~˫8'ڏ~s2; ]4{E=I- -4|{ssU(2;YTɝ޴Q٨qiBdnBΏ&j:T2;Yv]p}gfhr,u}\1 U.)vK2| 0eHT2Mjah> V6E*<֎Ds⺉Te'Fۭe29:,Ɠuq N0wǯ `C& 0${ \O {wVήbkOt7V7 ! ;jJqE)(X.GO ` +: 􆭶넀.GvUӱ]KݍD&X`ki nPJXbh0%.@'p ?FWQJ^s2*|=! &5FS= (!5ђe_ujQ$9\y^ؔMS3fϒǎq)}@40%Xskk\FpɔMlݔc#]&#c$п4l[Ø05S @ LEd2x<̐)D=C.;xrZ8A,nI:EBAj*DҬJb (mMF?mt^ y>'Py(B fsqOP o]eis8-=֤#5 g^sq)}yɪ2 p g`YYt&+}w@iF)堚޾&$1e"{b=[f:lIl+)wb.)guJq&;z4BږdM"A _`.1A HZքL =M\P})jcgN҈d;$ 5V!, F5/P^oz?"M0o*vU՚6-Hlk=g7=d{Ž> C#2LjI&x>K5{wY^|5']TW1jԧkJuVH{ ͟lI@ 'zH9ӷ7 Ӎ񆞜 s߄zV"Bo: ۰ۚ<<@"pͤ%dxlò`tgtT$ՙ\POi*t9Z<{[:@;e5܂8%* UlBF)>j`}C w*͂ rrmOFgmX=?fQ(Y;˕4jm?90ZRo%&a ;X%,Z~*;Suf(W?i/ӯ,H:BOtV+Kbt̖EXl/@+@50p fy>&;P;':c% ΋]Y hӣ#>nhOqNE'`bVE'`ێw!{r:ݝS6Pa4 {z=ϕSPNZ`p)kqeQ\ݺHw%]805Y[9йv= ygIMcN)ݍHa^z]T)?}{NPiowPë%V~9ʘekAf7eDB?Π GC,V"z/PBj[Sewo.4[3c+&”^>^1nضLs1>F΢̧l8!ʃ=3(#/UX!&n L3Or>L+]؟zw$ ;N,zs( **R@axZ/iE0b''̌CږCofկW4$ >3{;.Ct:qY\> [@";rZV3>5[Iu;37QSaoƪK{l6L4Z{b:y.>κ$5K7?B2!P:`+!k O)~ȠO'51*.""̶S,XT. alCu#}QW,Z <$4;zvEHc '5Đ7vɒMp_aen<8.ߊDgӲGSJɉ'9,M]2S#ɟd` _m[@僁" a窾iL&m qTcm9M(cfpUtrڸ?5:lzwY",mMp!f&(RTb{#s4Ǫd]"\_:'SjX=]q#!U ;ᔅ Lxh,H JJ$q2@& 6&ܪ}")kаVR1V@zsxcp_k^\u3Ϫ.4Y_:)'([Lr>XNQGf'?ro3}T]yS n?_*@f-Ft1Pv4\UrБp  ;/B>PK&zbpZP1=)4#x?Pn=CὬf`ɑi*VEuWQI[yd߀6FcGkyZL{Bk;:3-ukqG^~ }n#;V~[LC|ˎX2 SOHr%G9Kn JP%'Y!}8 4>}B|71l-ŧR&TO+ڠa J܌2wSAjyaǔ|_67$\s :+UMou;~?uNմPYw1*ݣ4\go1N9 {;f T۩[vlF ltO0%D?E8$&sI9!: ]dڢٸɓ#E|Tjo\щ0hWYAMKx7PeݠzpOKϩǰ3sXt#8# |`đo1gneef:܃ &Y/zܽ~gFi7x4*w L-o"K浒 I@\F8'5?#Fv_ޙ(G9l<=aJuT,`1|2AD,-\" ,OWv4#ž_1 V8##HRYfp伪y'M*]~-^J_E;V2F&Ew+ԅ,f k G v@|;cZ$}*!r^suf)\ E^ P2J f`α'',6Y)hc#zHw{A\բmC V+)IP8{|^/Q/>k5[NLMbER`G-9iDPjid(E8Do0#he!O0'z(>^}w slnDW*7ب+u:뷪4""/[@kEuN̖c6)A@ڿ~oYpOP$GmWVxo y{Q屟(-op5WpAϦ[^Y1@|6`+HNVU{ xx'ڲzꍨw=5# %8|#%;d*b#o] /1h.qJ. :>l(@ LէS5K$E .`Ҙ'TV/~QLJ G.Iy!4Kޚ <|#i5] <:[jT2<"7n{?ϛj™-7+GT Kȯ:cT2n~y:u-=^/Df^+li ezqkB^xV}'9%ex|J 063ح Λ~DP!T/QG~1vE-g&_ֈ`?ıCY©y Ǟ\Lh)k ;$@5MO_GZo#^Ak@c~ XSƒ=AlF-LTN`őz<0#y?OˉRNvGm}ߊ(+"v;X6yu/amXw)^eHdCAZXE A<u+FKP({JKUkxOoZoi M}"puƜ#Z>ؽ\}+Z T:4e6q|>DݽdZg11KsfQ#`lE ((Yɘ;jߘfiB2LER,,mł{XѭI@3TTDsl,B֖=7fPEn3ocY3;B9r?XkcQEMM,mH2 jC@:cl_/Ms6(&nĂ?':>T ,A:H 8H2~[Z~A&GX;ފ Ĝᯮ,q4Q[n A.% kQlB=%9aanycw"w[oIb?kOcKJDPسGA}T7gUjEzHETSKDqHFv؎EM݃E3\#|55D\o,Э H84-{b<)]BBeP{u, 噻,qC<ޢ6q$-O*jOb$xW<$:]?,B2Y5‰mx3f xSYҮfkό|tӥԃ)la=2bNd!'X3:@2iEKqjNMl#8PjKq_c,,Ob/x emGu^m&D(󳶳5*-JKGl( 9K&k c09娔ఴBr1m,@( Ek\Q%I[z 2!* ~h׽waE%`t16ژ4x ac#rJ{ ͍iz#Enx?fSQ [k,`QJUs9elIn 4W*@{<p0вJ`+4YĀpfTP$iǴRi3j9"#/΁8#Y>з"AN(:IbCl!4@edW0 PWnhT3W(4CYfl6=?ly6=չ"}JV:G#~3V]ogdrC$f`jJb.Г0źt6|;W-L0XL*9=(b>qN@XSujO) >o[ s/єzWTÀa.)3iK<5 sԀ> k%!,I؟a htYH}7~iL)Db0\AgcpҘ:E_kF/#̩:s???q}$I`|oVy L>=G:m功 O M,Pl@8+C% p >K=um:'p,dbMWÖѰqESaWmu2?@ǟJ+5"kSYw?RV*%?DСjYP ?(/ ޗM?ۯໜÉוЈ~D12=alY0bNkST%=>$B-4l%0 ј0 |{@I߱ 0>'/CC6[NI ^COI3T5E񺀩TȜd2]WaE^|,5\ AF\?GN ,`tfO#kCrGEiZ=Aħ١ޕ07VlD&O`F'{[Ѕoe&֌m (j«ZHaXz"Zʟ׭j?z/Z5bf-ӆG".1Gz H犧eVUY<lc&޸'CQ5fxޑ1B-(=H , ΗY HtdBf|2|94G4򂒖F}\28/6N;+bKa Ri1b~/Le]?D'~֩5hlY*{\7^Um ASa[߃mflXJ7~~2ҏi+^(C礪'1I}ЧľJ%&oz@p_?*i-;DvsC@@Eܱ,M*"6,=A,Ekg%tJ|g8wzA{^, _5|G~BEwХ(~qSɠ4WN&?];a+sz]}ӓ"0oa Ӌ+MfIVm>N!XK:>Ӥ맱f{V \h5Kj*6 HAy޴o^|tB$#$IŒx`D[$Q685mGOɭ3V*G\dV۪Un)~ ^󖩶2BB faJna[zަ(ƿq vd8IBӬKiZL MUg!?}RD*V٭\Nrǫ},XFcYtGɒќfE ebNM8 $l #Z*7[zn(;']Nk7ܾJ Ěi7^%٩gX(i zċWfB J,aM<]̔32UsN ?M7se!Fz- g#4(_);_]>ijubc!Ŷak-`yHiAplJ~7g!8( Nx0cP$tڻr "y! F Fve*+ǒg',hzޏ …JKU6 `x0cED.}jL8TY"X79A0,2[L{)#l?QHOruIڟ;8,%27կжw,4pt0d QLl<~34\WC4Bu6"'%k} "C*{gWb~i!bfc d+e+X$"GAY Sg>gy(;؀ ?iOr<ͻF9pBp8@ Ԏ:`)kX|*isDCǍ}oK:"u/sЕНGnȆ5Viqf835 R7^֯]AD>tH^[̻~u!dݣuQ9}Dj?cClRH: Xߢp؍BE,dhz9B!w0Ş&pKiKY_;b)T Ŷ&(-VKh[a\}Jd EsyH U+xx`3 FP^1=Ld~mԆrzbM^P @Px[axS$*:g3H +:_2&ͦWK̻zOBn*ј}/fɭ},bYClNE rVar_p5Ta =׬:&*txt)^AT>qϰB 1 <䕿ji;ի‰b65,羥$^lul|[S'Fs6YK_$ Thh*kw]H}1 E DuP|mIS517*/k$7]J~oR<O[g t>%U;Hp[.ɨ|4߶~VsEAL$į\QfQqlfߑ {oM:ù}6A6(D H T%~./JG)mp[%/;@"eUu@~x DllAWk^zƭӵ3 [/JqM N϶SC1qz΂; h;Qc$"P8*ļGE/~H\9E=hLj'7̝΂  !X@2$AњZ׹^_{:bQ/ *ŭ28|+0NM?yy;fC4*a> _[T TCTaQF +b}MuW s[7CzR{W)1ՄT*Gɘ)ĕ鳎&bYZ>r9 M5baZ֒l\lH  KARS'‚$kQXV;:;|푰Paϑ'?@،2ZUwtXV[gU%Q&|o301Ԡ(gtOL *r@$ɽ`ѵtt&% "WH}uf !ձ 4OJЛ!5{_.GsmtsS4R4 ?U3urȗX댨0qB>,|(,-W/M8qpHHtH 7λiPҎ;]2j\ٍ{D8cpv([cݗFTact~P-d(Iz,u yaFIWّyAQaSx߷ȩf85gmZE,{ɀx!wۆD9Y[֊o2 A`4'nۗŷи5qdPm"Ӧ_eGA9 oBA ̜b r}-V=JwgqM$ f% Ǧt6;БLe1+Q/zs!Y 2]qxyuKU#vl՞u j; ÊR!  .FbZx:ڑX(nY"f"5Jq}IZѪSa 7bDZ֙2m4(L}y1 hv&4RN8:ǩB,  TNUC$I_q &LCSdM:"Kf >}O垱3?_ п}AׯG6%xJ0ЏlVܬg98(5 q͸?=GOv6N܎ob8;&Li?<G|6yjD4*.UPȏKhmQz)ܛP9֫ 2 ድ,]Z+}Jة%9JMʵڿ(`?needm"-nz#r8;=egSTGi8O> V;%#ߞ/\)^Z.10ݹL_&R,~VLloK:ImN[ HKGT"̑hLب98E9s_~T|Šduh 9 haZa0.$egʯ?6#kAXI )`@EO:ž呬+;o|N#ƒ=T|ra{6#e{+_*L8im.؂ }Ho^1g6ѣO[K?;on"Xtޝh{XqnyjpCMz#ۑ ™\:'rKA#.)Gܣ fҞMZJ<04ܒRE%!+W{>S1e/@u_ĝA+*Te|bBX-ޯ@G@UkRRce5wYh52EؽCwD_$gܛjٺW8~9(>nU 2:qz#Y,(I0~J& ]Ϸms4E=q8>0>o ˜]CKtS릶-N,~˃X\G[1\[OK]&QY dt2'Bq"Y[^?_ ]~cvU"%'5A0?981L{8pnkȬ#YJ5;h#]3SNA/HF$ܓaz{YOIUC!At Z $oelZUkz|rLy)gÀw7CMe[_Af崟Amm:8Ii,vT/pgA,7QJ -teUq[3'h?F 'x m)l}br(`ZBIr 0 ƢyS#6nsgK~I TvC%ެ mCcH)#WyD=drg/;-X I r@5 /ox;PO_4Ϡ؝tL f8~B] _,))h.MS9}{-XO5ptmwۆ~:ExTL9_HU}6e'Fr3ᢿUa% FFr~Z!Βhlk}ѱL?R EN&7{p6_x.JdKVZJtI=g7YaM1a'&Poo<,y`#8vgF&e7HP-F4)ЎNu3 O,=l;=𭛷t8GYikCܓgolŬ ڗ-\Cv;X:W 9H[zߑqC&n{/xIfg4uhP E4_󣇢l>ɖ~(j@- u{}*co=N.im^"Am%>\sẌ́Lwnn웺2- nϏ;85|Ao %tuZɾj=w. /dl~dݨsRZØLD *Wu`Qݪ{f@PdO#x5Yھ1=P}a 8]4*Xo_1Q;W8lp(p@_3BU{t"@@p 3߼Rs7]嗂tY:F $#qy1$ Ō3E{{݀ƙ)3,ѩgtLWAߨ6Q}z(Bg m+HQ(pLvm5WT="1g/PҮ'c %붗Ju;dq1_ѐ+)e ?Z,ؒZ$xtFbbfQvw)pIOv`HÉo~G`oչMX+'ȖPvVK%Brg+,Qʋ#A3e4A#-H=zbr6v v&YP푿4&֣ćESzqɾGʡ A揟%̋RmETmḁU.LWWU,%vGs4REr"T(w:їk_ )EzNad'?YG\3=V),M8~o~,Ff־g(`(R.k_` AA GV$T xU)^9c!@" Y,|m:&h{àML ;J7_9 Vv]%9Z(C.ݦ9/ZGϦ!n"KUk: El~ ]vS:,fjOA$@wJ)Ŀ54qL饼UeViy|9_iw"F bIW]{UW=Wp[/6FօBAt?>EcW~Pu T0%ܐgz;{W wcxX&3EY}9g?ph,LS ;nu˪)"10LBg fqt克6fwHʸ])ht3AN<,Ra/e@S'97p!kԏ7 Yf{ שUVvVvecHӷC ѲaJrGg]Q2LK52,FŞs,cJ2i\8jŇY6\;$_n<ѫ AqgBR2zօgLL@}8D8bhͯfM q&Ù9'M\EBpgiO+gGu{&)ź:@S1Cz7B. 6I|wb9]~)xpfRYcxb\RϬ~uPг>7a[<^)]i2#-%Kp::^8&‚;&*Q>9`"⍗iqԩҺ !\c>d7ƃȋ ]ix aJP]t+~zȜeCDŽZtݤZۉ3yxynMݱg8皧.Ju@G۪8xȎ,\D gy ݉BK?h+;/men6.{oӗyPkO0r򧃲ݻ|eۼ|9Oj;k~.}cp?pdą6#j4o4Ȣ{iE+4G73SbEs)Ƣ{8*${}0*SדF)kɆ(UPB]'cɛW›ړ\"OɓYC>p] Y*9'<4@8UUV.}@sq* -Y~\?5ZQCZjx~*sG狠32 yeȋkN%mYm4X%5 #nx`@f+k< z.Y%6DD!E9fCåG (.d HA`C2Y D C`)[ѤCshmrTPK.΃ 49J[w<߉~<de?F=,,Y $/6`v0OGx8MQ~lluLNׯ%?5֍^V5R< T?zIPfGciNQi )/jJ$9玲nb9cR]s6%$BuXam-Up0=5|erg`);][/nyynV~ šhgϡlaA?ǯeIAg $j#˗Z'_DzF )~S6%y8@ͩ9%ny Elyр`~U&6jK Ƹx-ٯ( N Už}D_ :OdL6vKV5REPy=Oh Zt=Έ.=LX^ܳ\Zy|+ >ީ?2 T1 39VNJTRt8 L?N8<,z${˴3uOJK2w/v,.-@[9V_:nGɹ`ƬC pvN4>Mo%9t-yJ譽 հ0hZ\ܦo:6cժCxF%ם"epHL Y!-64/edVih]J⣙`ʿVn`JNu^tlsQ \c@rL&{^5{ՙ\plMk- 2uv-2GT^B\ƕ]ak#29W]ߟtW, xs4RA i(P  a^@tMwk (^X!m~B<~SE9wF~ߵp_r@[;F.{{f 댫^WDyHa?e<7;/ܵhUS%gKM7 b>U PzFU6s8ye_how=[YRIJRi?V^zǶ(l Tzb<03P:uv. /LAB⧚`܎^h r5f9ֱnڑMC{[v=:^ԍ_p5S FT?x1Υi WO4[ʢCCL=BF{GSbDݞ8R,|Zb-Q*uVβ^TsW*By[/v A(:1I6iIqH22𜲬{?6pihxu8|jMW8lAlrR"^JF ۭ)}+~?ŸIݠ _WO)_ +̻U^8=B UV>dH9?B$ .)qCt}F!ux=h@m;%{1R8](*2kRӠPU1 D_ssD|X9 /?#בd]2)MI-m CT0eC3B?H@O핍OW 4waǻ\Ψk{g fM#_\DXN H2,A*I>}('h:ZBD`,dI-:iM$az V{A &ḛdζ8")*?i2_F-IoO`(y64F٧yPPdgj""-Unvrn6l{Nܣk/55}\צSȵl,G>ǜ4Α(M<D~ʅSAY3UO4"db$ԝrEPq>ap!ٚZ;~2x0= veCٽOxnLƛlϠbQ_9b6~,5GEery 3͚w JǫK}[jTPi'sD-qJ 54O[̵[rBlDkNbInwo&ߗJD3WpH.}.׭6>@DZ gu jzU}Q"s,2~Q&;I vPK I=Hc7I>bQk''ޡti+vvƵ卵c=zr3.ZsHڏz~;[TƗ@A|R.g?Ta*љw2$iK03"Mo.cSJ՘.>9C<:#-Z~y=ERV=v|h*i{ }p6V.7b3o+/>#p2njCa ;Z8o{U \*ߎijE{I/ШdsuWȆꍙN_*{O=Ug)sJ64]J2wPJ;/@>6ݭPY%":zOQM 0 0; " @A'9#Vp|*9܏ٿ+ޏ2*&H|U"ܹm} k=Ua2*V4Y|v,=Jjse8>1bM@b?Hrv5*HTPbm~=O3Nߣ%>"ʁ쌧ntI@CO_JLF+A!2T/01Z;Fi3fh <* BopvBM8[seU+2*?M4&cf'$GҙP+X;GQ )ްje mۉ PdMzcUVeGa=q\ڇ2r7+v;low$xh1(s90WcX`\<@a2 k.7f1YB K <]鲒5}anzKpА"ʘAI 3(Kө`{ߊMgKRNTC0mx} }HکzÜ-ih@vz8v ?ÜyrUhxM()6.Tj7I ~G?dхL6^_?6ߴ]GpͨWc0NDUՌ鈸iࠐ32jx=D-HKe*CyW^.G_Kqc9&4@8#uKWdg 8W -GxAe@3/ov10s]x f<"*?|N-[Q 1b~?==X 'ArT7C#:k(黳{'hw6oZSk~u!FOnP֥beBRw{4*"~T|&D${0ᗂ 6XH )ڬIut@1oXk;@+8K $šf)6F>ec*@RD?^s>HLCOdKWެuYBWIUg],e'?ֹb(g_@R&"""H("?]aTwހe`pgg[OL`t?@|\@^Y59/53ߘx/5iH?i @'gy>Y) e_#h5?p[}>zN QF 0.{!iժTFoyep wJע1~Õ?do .\#KÇ6ۊ.{횤I%,(Q!lԽ4\r0]g)=QaeR.[:fҜCdS?2HV7|nS=*[O-AuV\n{nArh4eG7]\sk0 j xLi(7wג:f$/qWc0?xY-hf>Ol1&8LG!hr汜 (](~vZI`JowDYFpeGt(`_pY7b}eۢ.IruC^['yyYK.sܩj vKE hSBՅ`@ zUl?)B F:kYz dm/㊷81P(/CA`q̈́7Wti1a!hG9=1ȪWɔ"'w5.Z&ſ >S|[y%o~3iK1tߒ_#Aex^~ AX_㊟ZVTEGűe7XP'N%D9]}M]iG?xxQ# 8Fo_\gMY2Do.8o㟣v9q%~Q9=L*^ ۫lAQ~6G%ޥf;jΜNا3,nc@R#Z1 IRH^GCDk`_г*RE8.wnB6UeԩN8˭ !(D^p2i>4(#fǩxfǓ\hlMORAvi ,#j?M7'ͯӞYV2څ,DXwӨ}휻 ϲbev7}I0ggmYQs#-{NrLkq (9_g 2$LbQP>×B"5 eK9Gh-)_^o@t4/lt2Y#(32JM]#2߿9{s7N7f9Z_1 hw ЬG>{8¥d嶜+4c}%Cr.JOcrrBi Φn${7 Py^2O\ҫ4yĶ1-E(G%Cp,K4KxvtG }ESn[AEJ?S|WX]-Ο֧Di)Ơh(yˉGH'Α.`$u-<Cgת0LߓQ?Xћ@r%6%x :R4Ap@Ev}("bF6HE:vrxW$-9$5(Z e PC! +nw{2jd פzL-).ٺ0]/w 5 Fa2@[3:Mt^_[CX3_UgCs}5PI|CVc"'w]O8hYY(e98EJFpH~Qy$*2Nr'cH_o>)Jp~_T<;&BLJI+ l2T<$}\]4b 7Hpj|ic`/5LNݤ&d\:L$\&uZ@7/]hv9q }\.BDyK O"k<߲X!D(G5(MEm7!9FahT+VI C] -V>/dd gQ8#؁YD c;^:򨊟-zNiVTޚf )SgW*3GemI[V c#Ud)=]rh5XT‹-Ns pS]&~x $l8srnTbAK[JL34;%;3#ZXPJTG{C/Zx uEf7͹F+MRzjQ˜ m%ʘSGՏ(̨g؂s~u0vBjr9[24͠Ҫ ~Us$d7p1;f~ 9Pmerii5'`f'y*懧GsၙL^rah!w{cl-)9&& 0 k֘tCK+z TYk9X י(veO降Ppjl):75=jhЄ]M,v95rFAsHȎb㓮#f񬫈~B/g:D,? ?Il7 WMu@KդRCUKy mz ~W;{^Һ䤁ӱt5Wjy{e " AKzbD:I@SdxywJ Υ* jhixlzց^9Nu2( f%ZYd-pAv털x#+5g]1Wn.2t-JN*7c9 2yOn{^ ov jо38C~bN6yN,WڭOɔBJ 1Jt220sւ(i>$N 4Júvb;z^eWpRy'>?}}d<^nQyGJ3 >+ᰈ2Az9|{A;O:tz >&/q;Rv08J6mmŽS/_>_yv8e?˷&.!M2BW0HnwJSȩAGKd`R1^ň?5aw*B e]CBRjZhlbK;Z27a  ʙ]n ~v}m~mmL][]I)BIF ߡ 0&]D+Ű 6\ ~˨>,gA/.ECGek)!a~eQOݼcڔBmb~=z񥬟ڎvYh? ~jdkqD ޽ Jlr>E@$d<{ &B8ZJدnt 'T8 bQތ% `MZ0C$Ʉ8<ɐ řU$J\N~+O^0nC!~ST<`6WaSMuZtF1Ie2?AkG!}i]h>LeL/lt\UQ͞xw!^BZ  z1e{Ő<#][돥ǎFl>Naz7M7r{+[xE/S"i 1 UoYp34/B k!yɬؿ2s>_n9[erbj h$ ȣN)G{;N ehfM]P5hs,6IR)ܤ+7]dXy8* KFɬ smŕZԗ(?)wyOG,n5Yi`0N2ݻRł|&Wqcב(: ;(i+gθ[y0Iܞ%7Ĕ_+>l5l7).兌SeB3{ `cmRX, =|! ?|"E%{$\RUrE N^+Ie~5\_OBLaX)Grg<4!t7b5_\Cdv&tҟ7H2i;~l`O5,m;{Νy_>2.}yq2A߾mc1G1;=y-~_.0]hZg}/n~s_U˒4F?!XG%Mo޴04pTdvg[$ &V$ͧ9īN0`!ëྗFA<ϱrѠX@'ARg/"{鞣4}zb21NI6Ư!o&`ٵ $R/􏗩 k~ߠgGcǦ熧]/X U%©); 7l$ݓS*2\?ʐ3*>#]6@$xX^\b{EKW*n. eqJA_+Ϲx7jSM9Uɩ2WSQEa/S' 6`{#`8oՈw0_4ȟ:Xj7xN/jtX*YF"?avy/FbKWp/d;%d^ Gj9InBvɠzia(;l\_][^m|8kIR[ef\-5*z".kD13z ,|!Y2cKU %]&bb qpY|~?BP E8 3![Zb8y<ޡyb ёwB}!oDA+]B.l Nl`fQݭP;oT6gL'@C\&;MaSny,zWa*|$Ok#..<N 4T$ Ne2d!|utu|ibBqKY!Rh,fSg8'ғci7b87N>D 9DR[Ǹ fർ.BLd_wP v]Y8t _8͇A H R\I֋ %lÀ잌Ms #+:㣗QP ]5`~{/&/"ZɆA,Dwzcٯc4mԘwӾ AF"a]wPK(ɱRQyb)eqp]tTlh葊|EͧkߛG;"ȬPdeke6x 4eK^.Vp;j9Mz)xYͣm|5UuM=,cbLYe hyB3.ƵN/޶d(^2[jGݱ?XK!BzN!VVT_"?Q|Jh{B6[&r!Pt˜[ d|Y7)<5; mAu 8F,nZ.8 ,G"nÔ7z\(L;$LW9>pf/YLV `XmP VI $23\Ö8.s;Z⟖NHW 91e35L$PoRd]]Cnoѡo]|wļ/Pvf99X;6&5󻎸HՆ`6U3dK߶Z؎>4R,HrbC({]!zEGQJ@p 8pq0I]N ~96ȅԿn {8 ôg!iM+,N @h:A U_oM8A|U}Am>EP&uŌUIPߞg}ZJ Abla;>c ˮ3B10wpzOh TRu)ъ}f-{NZW"PAJauG2>E.%:.2pY{cLIKOvDݔ$%NuO.|R,,Jz‡18.1& :L߳ߓLThSa*x@D 3p˰AJ'Sl >!7uy;R56XQnL,p+SZ=n\d̛_o2SA y<)axG0Kki @xS-^fV/v t}djZ"nR+H(>e{+ݳC7U\7I]{Xt~(ixk73ݟGw'綟k*t,;2sClQF$tvՁDێĉ%;ci Dgq U^:6#%[b jx{S b|Ib/$ Roa[ :WN0tćF2j^("1 B:) :n@J,@8 3η\>TA T4n:w e%D9$ /FJ r .K>%)"쿛QƤEbUݍ0 eEdM:ZF 9C 49վۛBUwͿۇw ϵitt!: Rr#/\d[U{X=G*D`-<`!l7[GHe&e)zX <9"'3?LKyO]Dg%hR!Zjٻu ĆGN"gn}hH`hwHb2``#p^bhU+JROu_'XJ"vr-aQ<>f"y&GJu?`rNx[P-vۼտ 'FBa@G^7yޫ'27A8X0ҥoq-On)Z08Ht@\w_e,9'K&9i';b@P=9k h*^$Sr*Ah%ʤ[y,PiN1^)p`J(iϪAAȇ%BP4xw8+f5n!9fz6!UZj'#t" nM$:%$}VG}ļpI` 0il4:y &s@kNT\(r+ggrk.'>PiѤaoDbOHH`a{G<\QVAs_ks66[A|*x?O*L 'h0T_73=yMθTWajFq$0O?0^bbYs91XȪ! nqnOELk7^$\hCx$׌x}7Džo b~XẂ QzUI Y]BV7v݆=2@ɋQFX6duvH>+tLgGs@ɟHm fS A_hAcYypa[m}ex;Ld$?$M#e[*1r{a]u<<((DAJv/~dc?ދgϙPk tz,Y]d Ϊ0<ϒr\'gD~Z3E&ʭCC䉏1$~N#VE `| J)%{ɴ`ڿGR\[!3{bgX6bve-:3 ۞P6,*_p^p<&s+`J>OvK_F~bu- eFjgX|A^x71/^79TA7x7K:BWqHɝ=+x!%^ݟ˗n=#`6&5G>̕JA/^4{hvk9O Q-E3(6|JM A m\DD~71kz:'2/,WuCNqX-a]|R5пJBR]&@ާ;ƪ"8XlccA&u9gpB^77Bjq3nbjپŠ(+Ehr>ǽ#$Ad1v~Ꙉ)0G3dvſ!JI.ALҢ;td1<}~2$JB.Hdj6:!"2NUrTǝhi~TL7qm (iN5( D <נ)-؂_qw%lx~:; B] ӂ[c|"[|Hm716WFo}A8f>!'_!2E!ɒ>@_~QM?4#ZN-lap1?/˽qwrvܹwaм>a\nF5եը71 c}s2}|} rFi;|ķNGmAe 3,JR~5홤`W׹#o ltOfӢ-͠W M%T1*_}OjėI}?^\xy2DWb@߯5EA0Rp&aI@ >~2,wq?SFW>n}5] _y(6Ɂ;8nc@85|DZx#/,ZdD1;)^~%% Ȧ>2?5 Ron)Mg!ZNJyɮ؝4VĞ;߼YgruR  -7,ín7L4 %hWvju.*ƺ/ZQytx KɩҔue:\ꃁHTwT]W"8 |CFD ⥚mXyw|x0j}`ǟN8{Sۻl|,wOڻ_!_U7?g֙y6,.8yArQjoLk \U.Nk -TݑEנ)`@aR7^BG1p:%TRD^ۿ[˦_K?DE*.9EYld$4DϮ}r#Ik̘jjaфv ~7! mM\hꚑ$(m>q3gLr~:0ho62:Nڶd^P gX&NQƫ 2R [RE7#gYRHDo&9aOȉ+ uecW7c\$hy7Qx>U (! hI8$!}>>NY3(Ƈih{s8>H ̾o4:\w7l/ee6uը23" פ@A*l,SL}(D{<2pO(e Cq(ł iNeYbTNvZou?/d{|oU'dI,_ExxX@ ?M Ћrjjҡ,vm'Be''<xi!-lu}݇RJ}5DXqZc1l!QӊP yQ:ٝzß>4:#Z@.~SEײ14XHb` 1n OkTپxaҧ^[1Afk\<+>$뫤e`ppZww |Pؓm2&F:`^cף[=G/`gif@49] Ys7Iv+)y= *eɜ /)c)̭/1 D}%])ԅ f"Uo9)*"yDj.N!c669x+`yN qE?Nq9gZN2j/w?,{;ѦC ?FTs;|sUe330 BI~LFJ id[d5vsۋ|CCvI:~ ?D9VHH(6,(򉕿$L}6qXꎅ;!۹JC0&,"/\厾c# ZϢ烙Pt)2>YI6ћ\~\H3W.ȾV\~І $R0 (y.Rz+H]{͔bZ,a{ a='<nBS& $DU7vyWZjOMzv7z:yn7(D2F.{wW:1$M͜;)zjN'flbFbiU2Rd3en. Q?ϯTk>q٫0w 5zV_y_o#c||S$_{mS4q#_MYTS_@ .9H`%J_R>a\y$ (8[0-a)]uGQ襼t:`sU4J,dF:/wS,z9$.Vg@槜Lѕ/gz+bOɅe*AoYa;"ؽdfjۃahfkXfl-[uR Vw-Ip9 0(B4_Mw pmfԵS&1_:7lhhkLJ-<ϿUsiؔĖ-EeDǣE~R|8?\Us%1FKmm2I4ǟHb" "bd?@"dAoXCn1Dm= F ŰKtzuM9}4Pci$_3RMw{N}Ÿ=qcRaD9sC\>\r#|w0Ba~Qu֙IЯFMdiMS_jR_˰Z箐 `T_'?E`P":}ϒmL ;3>?Jxx^dUߵ}\TlwC!}NvPMv独V#T r0fF L@+]#Htܬwh连+Njy-NYYEr^+<~VeıZEB;=I4kGDQ& `v 弽?ݏ*&FV&@)/jh7JKUHαKDҦԬ?þL7 Jdn oO`+GNjtM91R; [W6~QYk4t\%3q9-a{`/*̹I6Zy%䞞ʝ:?0fHr(ѳRrM%ﭘYLPFt q;F+-ctZ v"ݕjIN-~U"iOA]%ז#OxdjXoI/NZGraRa"d;&|rw+!(N55 aV%sQGoZ km ;x_K&R Fs?E#0%ibtܳyotpŸ'Ӹ ՙE$&U*޻q\ւứ#6چfA9)U}n`$P|^uZvT둤$IEbtDDU^)@@;%n7\l z龴KJR:FbA5VTVK}~ct(:˟*/}G ӭH"S*vGѾ1Z: TKZ1cHWEX.C{|~ul]_@""X,:;!W^6;3#%H9~zxPZo y.չXخSiP/yU'+fSk2fZGUd*qtlzG` yP@BD"8w Unr}vhD|s>8k=5~Ԙv"9I?v~"ڷe<0/HO2о"k4i+݈l+K4Fbe?Y Y%54'Z V4׷N,25 Vw;oqMϒUz鰂tewp*FIN֭cBx7ѯ^E{|56F2m3Kz?wV0vk=.G Wfw;](" G!Ibh-Wl}U{]؃"i?5Z⾞`l=1<$29]QBX`p(fԇ󴈓xpIhɟ֎ڥܱZ+2" Qt.xS?XZEW?>Cïd4}7A&3#F;61'`UM %aU$ Vx[>.a9 +u0dH.$b(F0%,_w?x)Y7Ç=sE[i,KqƟiβ]A_ @o(Jf8ˠ#ڃr Y h{Zj#D)kV.t&$UYECHq&d 66m$o2b`#pѫFNJ  Y)t]Iu>sEf>DQ`3rC4o-Ώ\b7yLfNq>m<^[٧#-4beE2i!SLrvM"_y05|g"iW:β=6‰"c`QyzTy@ Q}ۿߔvtoĜ .^AmK+ѻ2ȉ/d R/x!mz?ãL_\$jP 11 Zƽq󮧫{%HGo3ocaz\:=kkRudj6%] MՒ}D>xTF K8gy1ZOLMYq?oI cODi򚒇k+ b )aJ{J u%U -{X^err[Pb6NF".6ș;BQ7k^ENPY=2{.k?aoZgMcJd!:%U!,v+ߞAYvV8\#݃c(<BFnS$W{S6I5龕%X#mmlhСM)#)4Cwع^@٢(.pu*qHW:3PpbԪa@ x9<Hv(9Z)DyX.gȃZO]H0a#[NK0Fp |u{dGk{z5nrїսF?uPmo| &M$ir46cw{vJwN~6E vAL֔8m9זlgOY%K>%Pa{JSDx%GL :YX)qMaL?ɷ4tB@sq$~[qrCcȁT~4C>d;`k7#Ds>N<(V$wRnZMwL#8i=*C(jΡ@ q9wÝ"UGf|g,2'W_ wŔ\6@EsfZO]e탾PП6{Ik,q:JrCT;&a)k2$Xy&eE;ݜJuy{Y]uZX,ˮ}'MM @ -tg@*\ԡ1% n@rde9&!l e3+?O8$HCa Q$V4ۥ:Ul΅Nm@|Zn$8K(`z{)>f'{x"ioc+%M?I>;Th\SCi Mgcf(ųdht{Ӌj=6mY> d>hrL4$3(CYJ|& 9 $pLས[_%廏HpَN[lvU'K,I(|rٿ1ہi#^y%iQx̑퓜=1}_<D8TZ="s@[L3Qel/߾# &("Ep^UqCVgB$79bl as&iIGi%l=?bdA:O 4h|p۵<ee)|xGh&<_նodr/)ɐg̑ EW2eVxU>K#'^\ʗ `a;Y$$wЬp^^tQq \!=w9#_UJ*UL/d-oP5ӷy?Xi%ku b7#g:a Lj*Z]GG},xXhYь WH"rbgr,.haVudR@8;*W9/[|ݒ7.pԘ02 M7P,hbh)\J 26}x'kjVmy$dlVB{1ꬡJB<_#&i_{Ɨ3M%㙸b:KQE|#C򊏸[pZ PCI,4$h19%2ȢDq>x_R0}iݍ`Rct.sfu 'CYj0zr3mu>ʀSeXXWNe jLje,ᯥ~ @YG/|)R(=W؉?Ձ1R=qٙiU.H>qc3IƷ&JqI\!~@{@0#"L q X@D8x:Wحų"( h$yl:#i7F׿W"Gw\})J)?Ȑlעǝ>TNS,nQYѬR 4PSuqvvd!7 !WJhMtDݓOGb7!DN]c[C@eX-nySȥPƞ HBn ]Bl~ɷ @9RY ߲nKSM /B *̝B!QY!*nҢ<3\-hYʇ(d g^sSrİcI,P?űU (R DrnYGKAí}j.vEmݴ+m6CJΠ@;YLl|zܥŰ!  TyX[615Ѹ"=rk5|=`z*|]7~#wY)$kFSэlCu-TqH15%>pL/dԵ';2C; ˸7n[Aq~drixG൧GLAj`sgPq5}gy{q]n\7G}5>^\,Bˉn6vcPihmBs:jELN?W}xơ_M]I Zr%lq~mt;,kzׯ+VsNel&9sqY3|wN'U{?=p>Z!Z a9lafD{@{hEAz {/'& ra4?y(w3pEvt<";-;}c֕oQ~,2)IQ :dn<iRuZ ֑s>V3V .Ifkwr%!P@WY},͠02pc%Ӹ "=ֻj樛W>>⪹3zE~$ϳlkԞv}^Xs5/l=},?3jimLklmࢱ2Ȫe [!g57;:1o~ N( (t̵ 1Rzh`yp ]D5z=ZwM 4܅|MߜĢ $dʴBZ SśvskʮO~Gw%)h4'ݻ|C=2ø3]X( n8̧-VOw`f,fkX  WϞ9-0ޞR1e4\Z^Ld_G|ď\ďoczW>-+cS}އLR_X\9MyUsK3/w tb+Vphqt[TV%v{׹cKHJ`BYL}@aA3?~/,Uy}y2jP0r;95Gb?ЦQ>-\XieI'^/O҇=lECG;9Z}@s ƪ~AN aI2e!qOv}qSV iK!lmymyˊ.պe2oNPtn$Ez>ih?;V#1<7y*rwS:#-Wrqk+*oQk4gL4okDב%s_%)>]d 0'3 XD!I<]c^wK4Xy =nO76όDbqrk%XJ=cL9*cKoW@~qLP8,Amr鑾N?IY;kcxeڜ55qm(3I'ދvjF*}i _t4ދLAJ$j;x@4|N9 Y%JS%`޲HVjkI VMeQGŕְitnL@BRaҮe;*:v֖vuFN:ZD͙ElmԒxVor`61NRٓe%Cu~KCmukﮝ= LKp`)[/x~#5il.WQNN٠?oslc cAM;#0fGo\Өy6ǥzd}v&5QxUqLǢKv XDeA/qSK{(|NP.:)'} ! FVE@HE-J4x;GkgRm{zz+ ,>i_n-w+z6[ ҺڛA8[g릺,vY@ԧc{q&ۉ5"R#hvZR/V55Q/1^`8F XrϥDKښʨg7%)P*`{"9tސ쐕V,} ёj<-߄))  ~p0 `p GT"ѱ-h,0#}JHV +^t(0 Jz6}N,ct.Qk R߃K/.b六3/J_5(ye;kڙ#56oQruZcΥ*с+qPRCztz Sb2[y@ 96N-sةI(7$6n{WJg֙Р5t5VK k0/rbDL—&靸,6N"K\q7xj2c\0C&ddHWB*A^roMVaОWׄD7d$,'{LcIZН% A\kNc[F"LOF,CeMNmPguD*7+LkBU/8B`Dr9רӯۑ,P/Q90P3Xu*5Fy.BFNS4i0Xl=v-=ygmM+WLUOܚ~ZMimӻ( ܉V-i8ܜ kŒQ`zJa} +֟F7*ھΖ~O^DG07D#GI֨v`$DQbsbQLGl2̯op]B>չN ɲ10ͦ2躐` p0[ O9|A$K؇$0̯`+' ~5+h„fHkpQv'-^>ݎwcmY{ψg4oOĒ(eٞ%L;ǰ rq.Q.2ɍJA R~8R-$S؝z$ [7G> SU)O [d2ATҼvYhwZl)B.wq9*meYMb>z=5fykݠvI 6 pf:ONP)/^_#%;hsK b6t<9  Ya;o4 `e%.RdNhsy[OڣP'UfÎ U~A'_Y{c1$)g$YFc٨ߣAA&o é&E' ~{`7<_E;whhVVTg1TP·HnnW+}]K k`SJ|+{3Dճh< +R֡[^e!W>Dp=Yl6&⻅ЮPL ̐"=\bwl#ٰ Q:. !KΥH滥^aJ.S|cY]j~q8C\c0biAhJdlc^B@g{SXʤԣMS'Oѻ ljB=VOyy?+ ˜I a )=?o2.CeU* <wk+k]'G:*% >AR;އxH`?9p iMMٝUN//cX!u&Ag:0| 8SxEeR^•t6'+ 6CȧK䐭Ń,\Hqо,6p}Jy|_c {FByq+z߿^\:b>x9\EG)K4@l"THi]uk-#$!1 ^32WwhK@sd/}oh$`НΪ[ȝ, ݖX4hua[ #5q)^ZW&益k,eg*[[bӦ`y<"itzc{ABGH?uտC,CB` s1Z>@|()4ś>FŢ#H\q3J5Ayz9VsA]i7; [އ̽e65ۢDdA\&ܛҹ}Ht2ù;=Or:/_nŚlWy`^gW_3<(wg4Ɍtا s% p#HH'ʎ]t<,V[qc&3o3[^xrSV]BBfL#Q@XQ_yװ#XX.=rvybzP=Gտ@Hs&՞veNcz2$K/tG!.u=OˌڣoԳ lĂ%R/g vn\0(v[D~NuY#&DP1k9EZhO.^Wg)`K$=CT RmEWה_s.M<:y3l~BgF:+k\bC?Cs'\Aq+U&Ϲ{701Эc^+]rvZE#_x68y.uFh=HA $D2I~Q"k_ qEU!9^',e9w)DˮR3 rFX(rM+GXB:b,)cWTN;@_\A~]7mhe5kB913Tѕ;1ќ.9D4(oF(hCʖ 5}ivHj0ͬd6҈(%V'btk*s]mY^k9ô-;fJc[ĈJz$c\%0Y(4΅(WIJ%،b#]=?A;ᢝ4CZnj 0AEhno?뿊Ӆ'ʯ2̂W!k/H%Kix? %XI-zEl,c=嚦cǦqVVK)uEa\KEC5-(cZX̲7*szWx_(N)y)7z)yH<#Z#`"lQ3\'߸zmzw!K *),DqEWaQ,򵍥H *k @R6 3E㍄l;OUi)UVԙ%S`@wn!Y?ynw !6n$za\8ci 7mMΌGsld;>}eJOcq#XhH[ݳ.P3H+r5>5c%~@e*Hһc.u}Y`@rECZ)}fJ%Vq2gI.¹{`v[,W Td/>3ib Th/et4}GC7:= KbL.< i+.RuZqh89?g2nk_d%Ѩ wkQǣ& ;]{ܙ9.%1^BJz-$PQ }vxPZwI r/?z~weCYXZ2~?rZedB)M<+tFN[E/K~y{kcb]2sc e dwHDB&D 1TS&u'r Ѻl/D1>ǂRFuIό @-"Bu2IC06gw?R͂ĉjlf,y2},~'OV-*GdbK>+ Ϥ<=&?Sʒ=:]X5&*3ϢĞU[ɺ5zrt;x%S5Q4牑^~%n/AhuZٱ Hd]z:r%fuŴ($P3s?۩? 7v)yHA'7Ƞ&N ̘MOػzE+m&J! lt1X:QՋdi:fa7j=B !#O"M ą;h#Z[d b_{E{ht("Mn׬ӭ=(̤7vh2oP\$/ lcTwc08 7"v;<\M\1BIa]-,_/ɦهVn1J?^ƺoAK fg%D.}+)aWĽ*";!R*~x}OfN6MP֍tf"sʶӨ3 ɻ.]6a9HxV^4TK$Q4RYy%nFf)I'e2*$]|]i }JڳMtziS KtdSg_@8 B[}BAq*A D ?TzIl8:d"[%5[IuaGJ vѵif段O!^5s%@`৯>{IP Т'3) h|QCL6}Sa:t~U[i=|3y _0txR-5107Ⱥl'xrck 9Z̐əJV׷;IHɸ CN{kt:ѩ$ @̍7-")x؆F9]/gYz &L\/#ZVNU3CbCvi ?FBV vdAC+R(طl/TAdy0[)/xmPS͉# T0bA`kCjpҘ$6ڎԯƫnޔ:Fy, TV?.N5vmG&k.Z}-z#E4lnJn$1n=vBgNE*"_n:6A_eCwSU {^93sX?%8"[Y޿{wS.gh ,~ 2{x##(ִ|>X &#@u}֩777YK %],IWVdlk_hP` zFDžY/N~BУ*a8B#%8qH`d=Q0ND㙸>mv$04F <]aC5<6wkqf*A=f̛'`. k35n|dVڥF-2xr=( W'2 Ѻ\\<&wQNL?W^LYnC@K~y,?WF :*Y{Q!sԸJx' mR:"UC!.jP>{Y"+Iy5'ur8R"*F 01%zxhIJg5X핊57:]'竑驫)Iަw,EX'7WzZH7%pykPݓ 0{6E/L~ [s%ʜP pfUV~Xg 7ˆ]ڢhR&'PSp?BӽL@9#) bbuaOj6Ǥ l.#)?O PC +6_c7xUb,r)mmIX}r64f>5wx\v4i].HlRF/HKMur~" ;9kG ,@ vnVT Ba x+j hyYI]Zw'rs},1!\n>֠l2(PpoIFK)+e1V;=T6q34؅x{EzHbkǵ+zCA DzY):~'x JG+^)>?^%s!D)F7({e8b[V}cMT:33Z&7"iy`]Oy T% ſ(c+=@9|RoCh뭭\ Kk BSs„E##dޔSͮq3'>~YeNߐo4pP#H6ܩ[3{#DBDed%ď>R?Hc%62<ՏL2 -S4xUf#wsFʛ*p(l܉iR1mB, ghu&tWg_1޺c=  0e6(ƛNH(MoXZgoJ1 ߠ&㵐n bQ'8<b90@Om@cȫ/N7y#J."a4 qIZaͬpz& #(V S:e^Vu&킿anIl&8&v#? X+,=pa+vZcnX=:w0.BF#uu4y2?w IrVDmqU >ڦ7纟) :>Ŏ\aϥhjH TZ"?n"=3 qe1bvM,\̓f)EuډTg>-kQb[)^a@ 7\,4BpaٿnQ;"P۩fŷ*cNKs5g!9DX$&W$]u|m5WF`U#D~XDY,5`>}nC=>^ʍ2uPf߹]s^![`йU0 MC&ԯvm/[S`m0+7 fDi|y^;Եߔ:t߻6nQ>~r*)<LLJv~?6N7\ (ܫgr G \YfO;/kҗP0JR8zICD]/QJ D+eY c4$/P^/##sU 1lE7ΌI\ghCώsJʿ'_YkJ+vJ>;<fhn{9ldώPS9`#6{Y:U}_'[ʸFLo$I*$ 55xflYi))ȋ llL#.yqBEعq+RdUlJug;,UFY53/.=g\[%;bJ&ۦ  }.!1ARj d{)M#m HwcT|_Vg%'/K%lM 9\6HK`#ꦦ _ < `WVmА`Ǡ\ :TLfy'9`|4Eγ~4 %"Rz1^0h_GCߧ09Pg :yhZ{7A99YUۯI◎yAٷ|8G0eW271ukS%fg',F!*?\[h!k]XIкuQy;Q^܍4_)b*~r)xX 1`F)HFN57Fїڷqh$puǃTbZޚty/$/]//NL~c& AO|t[ʊ5>~pVp!asI6GltzNaɚvص=4)ը^ GצYҎ)5][(i;a[\h)_6u{.vXp|y5X8s#IQi ?0^(X"izT5z1|!_@uGJY< ĤCx@IDw= |gM "5LPn7ȏ.X׼=-z K:H*&Q>5YVU"30 g`x`h5Y*,+y~ V `@aUt>b\M5QAܝ)Ri-Q ߶x{C-LrD>YQE aF; O+`AaWn; Ra^NK8"h%lh &}T U\ LJ1J KIzBu]dQSw|Y>HeI-I+|?Ajx3!輷/זּlWym$(9F&ѸBSUu=&$kvtV_ߪi\^+M7*p2WJ4l9̌,) :)oSlΨ t:~n)~ƯE]a~-ox?٬S,Ln7]>O!.|cO79>n*.TROcg@q9\ =}̓\5.z1t&3\ "Yλ׺>][}M$Ԙ&[UX־(`Iëg\ ,'Jf <#XH@-oz6~$X*dGJqI7B$,?b,×c3|BTSm wbolc :Pg`qnp;T;$ҞSfO.41AES_PJ&$ FG-P]+iS3`M?N],p&'.rڵJ.֡Zi<ղtV\}~"KxV4%$Vky<%W|g12'Ih?thk&X$|u4w:DlX/f$Wvs/ꉷL_V6Bw2!<)Ymy:EEVp檈g(`{еHal kNfqmW9s릓@~Aзe^׬×My,Z1_p@{Hk’\K=i$@/3  VENǻ$u*])U"s1_9=[i^Y'蓇,u@(%{r* t{M[L9]jWL1(kNKt Y/<~Qb.}>H!Eߴ;rMo3 7jṃ+,Bυ;=_%۝' Wܾqk5@1xcWNI 0te9Ŷ:DznSoBHmK`Φ'Tg~00\lqAPKm>` \yj-@s /3JJ;S0X%o 10a\KPw;N( ?{ۏ>n8̔dK:z< kh, R*:<9=r>5o56۴XY&rN+ςv lGpǭW~QSL)L wſ4jMNҙe0 ~ )Tr}z+TPU=E5`v@pb =Ee`{IeZXZkYoJMnQR|~n=u@#Xh>z/f>&C%_L^jڷ!kQ'!`2@xk8Jڇ,M( C*ս$=~Jsq͟CNǵ=dIY&2T,VgtX7^#KdKE[;v=7U(w |fU\<7},et8D@ qx4.8e&1U(xdh8wn]ur8VH/66fcyP[,+~=/y[] 2Kv,nc25&Ffd jzL E/(]_١UxCJ=Mwqγck乷+zd`G861Ďlov TT%6~hˏW37+*dZ-yapuK\Ps:E96P33PFIm+Wx4B kl*QyQonf2 %}5EzLʎ.8U1b{sMp>rkkpONU(4.(GodWb=jыT.cXvL:]߀Ƴ~@DntS {ؘp6PFJj›oAL_T OxnvֈXD@Qxm9G׉6k;bxh+2%?k7_ tVa+egTYT疛Kꪓ2hOq^3! RQ7TO(e w|&T.ya$YfdG7mHP(SNދ NVD2TsܼwA·k43)eLÐ|Ênm;r$P`N33A"K e'-璒X=ŭ&յ%M̴[+`Ӳ9Sl>j>\x 9RncOV"*_tᚋ(RgqJdPi xre$U 4Vb0?hJ(ܻMٳJcVpbPD?n2ACTH79" 5#vM?h:N, <9[EAO(gҜfg%_9ZkM0}sXxfdɒQ6u$ ueZ#0ik'ENtc ¬®ujYa8e`Ƌ{wco^P-vq_>(}In^dJꆾ 㝽١[lc<CC#&>KKƵ.KVxKt6OGwqxfʸӶ:f|/M+%+ w)oU=G_۠J9%G`7d`&Id>_01G‘_lpɚ;͈FѠ5W@^ʖJ jy["P>h> 9)HmJczϞ1p7Ovy9{pĠIO0y=2,j?ѬB Msj5AS+wL[>2AsC :egZ,W;o# AkN1[{!X0pLSvRyy/  >0]'*֕"m 3,wx(/H "0 /K~KRtsT[Ba}"3i?]xQ,2oA?X@InިM"v$=4C+p&R$>x.[}{Y=ʫ ǽɨy#U:tϲtS+nBLvWD(H?[+ES |p=MGsE cDnjr9B2,sxBȗ8^z4 L1D"LȸQ !H,dе. O7Y_</iXx + >\K#{nWlO}ⶍͽEE*sy4\?> F4^ջw1z0 9  $hźn0rV8[S_xcX7 ) ilDYSJD"JE*-v"Hm~}}lx#]881{% 7 zKy]Lzˋ;rUsMJ0ϖ3ۈ[S)dO>dLRhT\fݭ58rSJWw3o8{pp-aµQ)zkz;y ]9X#BK4/$sxpT羯4C'^@O4A"#\DFeRBw0it46`6U SD>ȋ S~bd,YFJo7 \GKUxvdAio1xw b=AbnT;BGKiPB{Ta^?E?bRJQ:C#ךH˽/~C^~$>(yM&E &QNp0aOR4M RZՅ]yɏ:5Zcx[aq|"0纐J$ psqTS`':HxP%+ޙO!:e ϱ^C]ҏ_w>R,GCE&j\1GjBn†b@ d\#Lq6\E@x뇧/K$Ŗr/: tTo@hf,?1n]჊Dx3!ᆤ88?q/{NlOyƟY1FܟpHj L[u:Eb#!ϽuMӃ~t@%({(oiUe U[+*LEڡ4KvG8:V9H:B&w@ 4  &?'df.&d<;tCy`fIк%NMM$41@; ;JeIZ j-.%24\-\N\9 OAhTӇK PcNJ(xw~%lf< = GgqtV7=IlbF@w?صBn,ec 6?s' OufşՖthyӘ̞?Ɩaȷ@pBd"aoJPцQ(ȅ,V`ha?Ւ F0"1t۲Qn-W \> gHED ~iYshwF!yJVkD4 ]y}lkDZ@x=S9mob~/ R2Qt+ucQ*lL#ohH_Fz*;jw(գ\`>{pku7gebܖOݝ;)2(7> Kk9@ *?}Q4ڴ/Y=rD$")"'|W^dE.2 >ƻI˛3B}Q *>o7 ѨtuRs,Sg3AE&*@I/BH5'.\I[ѦyaA}YzvzJC ǽLwMK*(.Ș 72/| ?lg\w͙i3^I'zx@P{*G'"wOA[*L1Se!~{0+$V92 ^A(+翻}B/w"pNh@|R,f ղQYLt #3 *&UI HC:1mG^{=FrJ7XجM [AYpeaG:Vdpc8Fe3YHƛ%`y([eTm6*2ץ'Cc@uE 5wYar#ow*{=Ծhmgg<s{}Z}xr:]83֓cQ ^'Ȫ'NPТ/4~JQmHwjmY+PV 1@r 8z0Q EO ~-^5ʤ/3 NsbY[2/}̞2ip1 &~|8C:E(qS;v =Mj=@/UngXb]4( ",(E_ Œ{OiT ڴ U<:\.Z-Z:-vk\HF<3J{k5srҔvaNFS Q)`ܢ^[[d.^0$>s5])/ZfVizXk1zP Lc<#)soi?a/ǥ%kns:0V>m@4ᛁ ^8v "Ot~F!nv߹t\zj^\887/4`%ĩX?ӾPTp5jcKN.%'OO4 y= Rs@to_݃du ޤ(]m,Лy O%a\ 4 lڧ czu{n܇}`~Pדz}pM^zwGveMg-.TEˮw{b_OhJzʓO)]֎&"M)-=dלڛ͚Nq2ۢĬ`rqn?*!qA"/D>_ԝ35>p VB. *~JGĶ-4$x 5#p]t =қFʡ\yk*U@3( @vlv}Vў "A`B'-uQex._h@qH(n f"Gos>>S504ϲ X`umV3Z' }v^?Mlc ;G7ZAL8>sI&k22 "m{`s3HM]>ex@|Q呸c (h*)OQiJ%k2)p 7!-{W2 CFvX>|ȱehf `SAzח|JDֺyi hLY*gP^7G) ZRdn`T BoRX}shiXp#N%D `Ȃ T%!Z&r`:95'v95 DVE>f$TtA nH"s~wm\{/`~CCn36]I%~,0$?{7ڵ[M3L1?u-ԗ p^cܬqeT`POA6a={!{.#?0.ȝW{bnd0d"rDQgFL,u0%qn1B`zkM[iٮzm&nWr-%q4䣦='\_ϖe7$]D~ex>]ekw˪P8uȠ /GtaȘw[R &'8\NA얙'LHUhC+jg!2bcjڨ,PqGt!=1@he=J-b` 9SQҷUcf)53b~hpĨKȓ1y0 Ĉ{2]FCW@x)fݕ 60cJiHDhFOYxE}_6x92kDL!sv)jXHzO[230&` #Q¾QYJ_Ы8&8/JO .ċtI+󟈂{"3$WcWudU3oX1Y#< bNa_0XЃZ%='6e^~2Fj3jnԴN (vxG`Q.*ҔW4󑟿QA đ6$0k0sEVFT֙mX 111N7äDR%I <4ZAJѡ7)X Oqh&]ÜdfBlH6wYƞ.PSC1A#<aiAs"#!vṵLƒ!&42\@V3@{M;ѡ(h ?,h#"S&E^몚0ӡ`mo%2\YEE¨jRhr8mZ3d6nƽ#PcYscx{"&XȊI¤WMRdH%>U9Q ſp`fLtj\6/@,oT\0cl "zlyt>c{P`30jE}rOT{&{mc2bn9py!37Kv~$$k.ru\{Pl.6`戌RX!3 ?]7FmV3neQ1!P[=n*t; Ss0$ȋcOc`N<~] Dz]"$@{;~\a95VzI6QxI @L?+][ykynֿ2aS9)V"qa_T/;'x~+!y ZDLyLJpYT@'a` c*5>S ex [~tEJ B0 ` J ţL*Ô`Hh I7#B-i] Ćp [by'B]5FT~0*q-ހ01BD[8-d7^Ej<ɻ=Xz9 1@\8zj/_᯿aG]<24#8:$ [˞jT˷Vwy'">-~0od~WЧӾ Ƕ$ŚVwV=Mp \Gml"  t6u1{Q YM_G !|߃ȯu0w贇rtP6{{<ᙺ9z-+KLE^+>jRm]')>K찟=tN:S'ivʟy| t W8!ځ.rb@fe&3ŷpKx{~FJ݈7., 76|2B:#vCR/NSVJ&m_3:zZuLgXl$S^ac>zblV^d%w; d |UNtQvsk. qňd` α\Cz {&J{VQu'p//: `A(^g5L!~43^&)Pvª&TdNx=BdFx dR&yOkì /Ww$]O4e'Jж_ZjC%hB OUۍh0 g8L Q@`}ξ{K =t=W_S*qy P-Y˴gܴ#l9^QvHތ&V/T{D; S$X0Dt ,/'"^D*}uaRN,݄Y>87 IGrF5t;8]]ȇ)ECNkXz]CZA0r Dd0\cG|UڇM 19!r6:~R"_hۛʦ` (-Y'TfVᘊ^U7$i/D=Tk<' :Fh%>ZXprB0%]1(g /6¦8Ur5j>+Y y)JVq7Q+MfEb! ԫ" S2-EU:g[TM8l2)" <}$bI{\M'J4kH]B hĦ巋c$cS״(?} ݣ ۺhnMB!E/3st:pߥc_YR?GX DNq5tKpNvgD D |i}F/7FG

y8b PM |#oUzcV(9?$UE\ `hE9]4zB%{ɚʧc ~'A1V)4&1EttVde jI_ki>蛗RHn/I4Mا' rΥ x~ܖ'A{pnS7hj@V,=ZD\/3+cm-QJ;ڵ] ) k<̛۠FLi!1ĞښzlkhH=žgOxqZ\ܸtL%Jq¾_l<L(J(:5`A3x_/ժeG ]jqA̩)hP/^oOfdOӛB{P*-k[J{vM a$ҾK`C+fB^Gpm^.a,JZjL;&_9-"R 2qZeӓ} &'Rں U (.@O4ؐcxS^ryc<PG|~*Vۺr=A1?]l0j(ZX:'hXzx.N,2wn<% w!?/+[W^#/dGA _`8@bC.+ #mZGn\BT~> m&Ջlj'tq.vmSC-c;P v 1 W¤ d@Be;GωgMH)0ml|8{EKpw/weZi9g݇7Ja]Ҵc=N!lfOZa{Z1uծ ##gL* G躑'L@/R6 !Vl'rL^xjɧc=@A 9_cDw|y#Fsm7c$]c5xO yV|8k|{j|wD=WtwZa/H޼_OoǍ 6 9VRa[+])}ԷB9RRkU+jxB0*(GS@ '-˦ U3}q8@{ GW#Qp~IHLv|9BY~ jl"CVwT='tA t.HҖE3SXA2UHXDZ[F6Ti$҆dj>#񾮑Og~8Pnʦ#H߹IUH߸@ޮ4kmUwb-;I5/q+4?y7LPwJ&TJ ۉ&99;ʇ}1 $3tVn,ciLT)796ݒ>5S P?5t,I4t9k^ή؟a˰?~ -`U5?mAxszu:q"|XM{>_WfPmK᳖դ\Xԗw !yGa g sp)}b !ʠp!˔'KHZ!ݫLmKvfS),ZmrqNw-\`bL5> TDQ&Q+0YTĖ5qۗіfzBбHƘ'# &Rcu.FMXg݇It(ʘsŚ%xbw(rn;_K޶7hYZKwEAg&@e"zUX_cy:qFxly~(Ͽs.ٰx9(K}O3w,Ig.rm>7r_zʣCW]h}X9W^;å/jc`Op׊e|CUقZ"FH>&U4Fg$;\#s`w{qc'RX%?@6 ANh5W^ė/@% `@"Pe&͢x ۲;7߰U-0V=6X$g\@ž&X|%v2w+/me|gu|<%FGʼfDеF:`3;ir{+栓+0$ZC,+%"8*e4ꘓZ*\Ş9wKח$}8?PD/xi-|II#m1"IEj%MڧPX3YV{,invEjtg p{B/*4EAJp mI~̙/RU!DVߣMcr an.S-mT#"h+Dʗ{kkMBԼOV9zrLcH+E~1 8u$y K"~frف1\XCyJ2|C%@6o3qnS<3{ƝD!^TӀܪﳊP^iCSϳ~-f dB<_R\UJT4i_Ć$+%d ⚞+<%}+8Y»U$ :c5ns䄈8kU ǟ݄1f xҪ iQ~W.@׫emh׀NTM쉃U$Uy V``;T90\M^z8)#m2y1+ՄBdzb337umοQVx5eZ'FO?qT5xZ2`?̐Õ*&V;1ͳo.;zQJ6Mj!b({ޖr|r\O$̄qצfrG)̋5W]$iu $d!ȹi}PD5PG4fb39}G=B,T_b+Hsk\uc)ŤOu H*H.ߔ}7Gy=޼.RgkzK]eX?1a ء s#!LJp"i5[Zc5)\^ٞSyٞ/}`>ArrZ"arrc'ywsѦҜ'mY3|mjݲ3\##ȅ{p{ܪ`#@N7&`X`;.]/b X2E08rhfwj Eäp}fs٠)zVnSW }jd\GGV6UN(j?לma3H(Ql 8TAvƾ?kcf&IxŹr7?ţ'7;jbq6+$0pBӔj8p>[Rpx&v3>F),vx7tYߘ7bwۍ&,}SM>K~G5xoa,)(ʵ/}crϧhP6"01/%MR*\[ 3YGBw+̋Ւ$L0,!6Eh[ہj+cF,`Jj+[9r/`髫<z3I ?,OV~=5~ +AFasy㝢v}tn} . 9y2LS #>6Z> KOˀpr]z[dqNj4Ҡ=Zz#Fkؐ5hh3߶˝Yյ$__a#n"9,=niiUg_<}/ݪN+q%YoP&7j҂ԛtw5TZC`޺k|2*ʕ*:\y) 8=}Tg5qC!/vkcQP*=ev>Q(xbJ`]wW_/! '~hƛq6dw,2ɞTuOǂ6 z6l=-(CT%>[1OOk8<[q_=}0` Z.#'tQjpRw#7iHʷw+l J"jr#뱸咉W=’.+s, /8ˎ'WX7x:A(\0 697'VHdKe{Z$ܓ ۝g~k#̮ ߜvW!W<$g.P]D [*B_d!iO S¯t ˫~6.։ 9CӦ_YߧԿZ:֮*O&}Yv-^_Hv _^2zhMQݙKW,"7Gc_qG@2F}W!>cM/xqD9tvk餁(ӀA>cUv{j(  a>K0ʵRk<TE,@w!ȣVmenoIG9pm.TVy7f?m~ [GzVVEzD4沮 0 6Rt'Uj}"A}ɘj{~!dfɥ?IpWjDq2S!/G MA$6Ē(s!?z?E[8xo",<=$q2xI6?E\'[`ˬtLlE9Y,p2G Qws衳\H>qA{Dً{ip)C@/)mom.9BUGkInsݰԡ2E۠M"7[R3,%&_w/jAKvk>.TK _j{OaGv34_@RUsH/\ -?o;ނvZlU=bmD#],0OP9)g 3N]!!tER&7{c$8 %YUĠddTa;m}M>6G񐾹i@$HaԒJB" .bi-dS7`Cハ$OinηD{CȢ=D3NFhj^G(:A;Ul;GoM7僑Bmziq5ևUwHx?ӗ˰33ڿO˽%;.xqX(PBM:[ &쏕z;ҡש?Mõz8+mQ88HY`qnUVC?zwŒt߰Xo .-w"5BZq?j2RJyʍ)9 n OxC'oWW(\)4Jh!&/$2>ƕbe$;B~l]Djα03uādJ>Dّ}}ܫtd>؍yS[HGoG/ϝ1auxC`k\@AL4UOL"Ӝ"hgNAץ8.nd`u-!](#(a#Ex;Rm vX#!2c!V/K.bN@+"o )9S5yW>`dusO\N(Mx\$;{hr)27v1v1>,*Bk |Ɩ_&;?t$OUq`F&AfA,oaEcm3EwήM ԡ>6ݡQ坖B9?PNèYY ^DJ6pu%e\$ m#Yey8{q?3w'C>Έ`$s,ה*'>[ Tj{ cȌ\tp&I?&bXDөe~ʤdžc-hA b4rfjږ?܊*ڟp-2b\zj0ԗmYw>a.{fC1!=+. 'Q')~1ʸCIgG-5Vk;4_zܱR=vWPnaߞ 硢"#Wz> W"z̋*,W$ ڴ5L~/peUZx$m&8 ,|Hz=* WoEa`2Ԗ㕷֊G*)߄(aK@84f2EZur.$a>GM&{ZܫF"] *g(Snx[\̄/ l3m!KVD/]H:g}C򨖒4n龒ADCY8ݱ:$16_jگ۪ _MP8cg 4@v|"H9v1NǤA(uz0TjOg Yw`.w˫"m$]CQ9?W:O>Lk LJQ h k^(.HM  bhq,;hǂ\I󧎞RqlBB|,uGWK2X; "rR*LU5K_K揄xҎޛrJ6$MD{~@2}9[>;g"FMC(K@$L©dj s.:H10W5}?_k{ D-iV0r%aTFz&ъQ6{*^?oxKRXȶxpXI^5z XI#@ƽ2TLI's-@#!?z,7&.L"{q{NÓ>"B#YgxmkrC*Q_*3XIꨚ&Ӿ[Kppӑp'؂e\0-g?߸|~Ws.&` _ ֟+՞O-ϲGӉ7ǪJ@ [+ڹUe )2I$ FRg*Ѱ4fd͝! >dm[&Hyd\ncMCoR4@>췑BWkʇ`vZ =o}rnα)')LYD#(JVTI>ng׃=,Ϡ Ѣ!sEJn#Eoo=AƸ{edQMLc~^Xw$efz>jA Y6WΦ?ATl6c_\doDK\@uRv3uUX(~ 1WE?-={s׷8Hd> fe>ʎs)eoC KSjP.Xq~2Ԍ.niF?0đu\ܕw)g؄`OF \dWECzQ&"[wg>$߲qMeAٜk84TNP8I#wQzㅋo&=*]`!n2]FA^yoҐ,+C,~(_n-xr{nW„UXbԸ 9]@tij4|5 wH"c:® /-I\ฒftdǝX/kOj/ *slr}E/r)_B$z9S65#z\sEs fK)'9YC|fJDCߏHBK˴gs)ԦmsA/#5fW+l "*U+SE,_90nCr*|beyFV2y(I @ 4M<504+ F3?O&!:{z.icVq p:AԼE5> ;.Cb~Lu@x؏‰;hz.#,S4#;&p>9ki?Q"X2>QzC^Ϧ-\{C;Fs6H7KJ *gZSfYuޗB[b'8ˆSzE"3" 21k7XJ1xyFN!unME#d0 JicK1Gx_4m!Z }å['ĭ^G|jw#XC}+koc fӷJ򸣾C $z fJYTu|{{}e2~Z \+vͅ@P{7~adU%Odw!Y+Vkf{Kn2O>'yf_l`DD{Ʋ*dN]wYMF%1*L[RR"Y?uJyu:AAvy%YWv`1bnhr4錊twMś?ұnFW*@nm`w}QN糺c^6gW`DGMA=&շ0TQзqS {~8mJpS#0At$ɖ4 D`]:dcY^\*2غ,%J߆[0V#LQwaBu=0QƖ-r^k;kٻYWWD.@^|?̢.f)4(Z46bGݾ T,˾ٛbj!t HRvb+hX]r 3 9a:iqr%iշVeM +c|P)% Ȝ\u.?MsF8DčEd0ǥw׆Ie»\{cJFnf8J" J%)NVcDEB[w e<A%a1e+Or/}5rd-ϥ@Q3R#%iIC[_<9o7o/V# Jz1aj#jH*70)555} 1ф@s_-qB#/vY|oϺK= lB%ZuR)PbiJ2J}:U*iֻp>/cqL֌tWܾaP?uOc2o08 d`kQv}~ J; _=IW5XGRٟhrΈ C 3S죩 =ax}$<5#Ʒzn^-?}zz2g<ӷ .xljTY1ك  KaC1١%d2v8Vg^;K#̫'A*~R}HҳOzs>(l 5ɑN' Ҏ{ŧ5g* ͽ9> [{BDXXФ1wƿIݧ֣F@i^3[70nzSv>d*"AL6^:" Ð!2B_(}w9U>Ij ,ٕ"ѤvP&{Ԙ\Eތ {Va8rߵ׋=1TZ1άJǢߙ X%Vwtr9>y*"9đ9L/ԵC拖Hj:`PZvL q&6#3y^9hA܀w]sѢqRb"H$>Y7Disi1%\?% B`d>֍m 6iyՈϭ?zΤahU#jvׅl[j[?@z|˴zeaA#=6L`C oj? Ð 6Ns:Qwfu T,I!b0q:=;fݯwzB]DzD+ 8MZoV>ht4gfP_m{5 n0ȁcMT#闋~s8> 2^7-dcXR&nKhXW߯[aHkI@YqU VAQ+<$a,Tmݾ89W<9Li)~:AV'b7:|0W(GDV/\~=oEG'FCq|jx`w@CI[ټ`R__,LYqaޔ'O'^_1HAP[l-UW:w} cr+WIg( ZpywB]nE%|JX֖'\xIZHlAy}-?džpGv7Y"Je}lH#V` SiHt,xDm+m峼7ĒI^ @ sj?0Rٚ5Z40nR;0OAw-M$;6WG)&1L`c :0+ ĭ&Su9{: ]aVLjP`WZm+TP0 "*7yY4_}%Ao?zx{KAsR^(_HzY7{tP"(4kLzO HSl_ut.)k;EBuVzyLH*4H= ܖHGz{x0W_Xӝ=U{Ꝯa[&\ }l䨾qhVŬX*̼DJ2G%rs-;YɺӅ7h7ԥ@@Eکgz 8=?ufo˔pvI $+Zt(Ej":r}Fi B4^)&]E [G\IF?w/~tk\ЛO&IfGTdⶓC;>씁7&FtM׾I5[l'H/Cu\^ן{Y!Hia,z%7dV(3> I3*AUMLSb1#nkY![<ćo՗ ozWRJ0gCXyC+Ѝi0{RݘÎzƨe53[?B&k$( &_&/|ĥ{.3Xʩ@O#<@#cǜ4DV %hb.Z[il'}Igg6@lN;UR[_?K + KI8X{4\0 |_ e^4 eI29 po(DRB 'odi9Xr\t( 9 L:g9*8CcN_v[#vyut揲 sl؈S !@>W}g#G'}J!fsԘ N UؔOcgtRۚ= SyR%3|b'dK>١M> ,ZDicFysGM?t@mK>̠ %NgVz,_<4b-՟aG'WM\AB'Cu\PwV9%}I{.L5*UVˊw҄axT'9d5XhmeM/ڋ . $%MO$1ߛq"S{ zx-t$C6kNK !̓~$|1<}xgAe)z1Z+[R qmP4'b ~D",i,2$ۅ(8m\|цcgM1*{qUHaJ9MT,cA|~N,NQ(HOX~L߈w;^'KRvr.cx*!X;N爱S^:?j367r{lr&{γ~p]vv،0f`b6G(Kz6*E|f޹8 WauTe9*-\.Og!xگ.Bh2nMC%'9:LGZc{ϹROU4o o[UcW0Mr},c٢L^r g r7X=oU0L y#2DZ0*Hf݄Et8VյojiB^l]թ,%h猸,G _J49Y/pu; y]~!|o t2<v1d__d|T;hŢ5Kʹ:oͬv9'10}@h f-DBn|?+S hDM\_7y9֕4B.O9&E)^?ij ԥ!;WC0keν*6vsJ B&ae(C [7 ZZur3k?JD]pK{}8vrp՛/h;.BΠMtj88%8)`Q[&ڿL5z->L;t?Ut:QvEV@ga_o.dELr{c!;NIõazQ ̓҉umڞjCjT뽘Ve8 "Ň] H7)i JT(7**vAAdDQY^ `,! 8f/op;X {_,^u>Ww_C~;R֫}uzds7#lA*,)@/HHT1yk)pQ|1/`߃3ZO vqO V- Y(:Ї k,,Քq|ZĽ*Z'ݲHVdQ"l;Ff@ 9fvx $V,e ? ~{@D_!`a܏ۇ Zd/?Z.0zAeEO‘HQX26 6Y#̽}PU &SR|wl$_Jke~fq~ð/wmpz#)IZUVH ,B"Ë}j}vdݨ|(Kuį@ZbqCK@g?wwQRŲ曔K>n{]ԫS4w <6qo(W-DKfe9q'3;W%6 eӹMb^J[\UN2p`L7S)4]mWapuq9G|a|$EyK\I:/$Ͱ cYe'=ƈTYQ c<1^*NA!"hN:qgqeLɳvzѿ @A|/.u}g½iy\$ DF!woWykzFXce\D 'Smxb5?"4lP $4r={%U }S FLB{z'fYEgJ/d"r=dl/~Tq#V,e0tBL2w◯{n[=UUB蜟yx~q۽91P^^& /t۽ EJSqJQ 'w%T5I -*+WvǀrG cwE*u}H5`qtna5י60lǫ,Uk 1$D9AYP rTo'!:]]sS5'. Gb;s۱J}+#m)1#6Y +H(|[f.Lt'ӷ'jxg PiƝ$v"0(kD ,ؤK~Τ\&^Hl5[08Y{|q(eOOfx 2@,0}@)dgmѶ |d9:{daw"VL."!*.7 <,NUi+V8[bѤ//|aT_+5`OB܅䧭NoO1Y+гW?..mwl "a6-rnΥP|tȸ<@~+dks@ XoO9s璍7A`Ca4>:JŢ6Zi|DʬWC湎as9y.ѣމDjm̊S9/=DGkz(Р&aFR7-ղZy'ًiAl}7FslVG&/*IxkR0A jH2sz۔Ey| oMf^srz`nAg)ְmA.6Y響h?/oA/`mGud hSf5;DUG&^a>.(ˎYFW x!a%"3l5zhaԅ = _9OG"JA6N] n!S]48%"،Bb0E&6#-[W[u8uSy13q5{'}A`2H1k pJ'geٴpA@l95nDb: %vx f8;06@rknc:ijZ{5ve-7wG߅pȑ኿k Vp=Y&&>u` ȃ,[/ݦupߞ}L,I@|h_D!jyz@ :<+V=('h6̝>~?Bܙq>tlM \'Ф,0-fR\?Qs~ )TlK(o^>TR&E|OrsA$)Ђ?KUI7 Gxɑ x.뒜G5T+qҠf/8O!DtyWNJRYqʪ)8֌0"Y@lq409Kwo<|K+5APof0MWyʔ{D7Go*w &30L[tex%WBѵ$Zߓ$>J;O^o2$zYRr ܃Cc̝jpf9U v"\[C0A WHO[lEr'h4rQqOWIX87`n/4VKE:׶_`]N[y.k[y&r#@`6 tl88Q* %Bw"<ŶV#IxX~ku ~DOA@P̅xH;!R;\1Vt9}+D۔"_zny8Ӂ/J\AԌC6~+𫫼RȔbc.N$IM]0Qidkِ 9!#9:V@6(捖3^_Vhv\'T&Mg|탟"&8X'WI7H0) Hc?RhaH)Vt fWv q.sj/Q5Y'B͒!zmneWNp)ZZ Of+sB= 7笁r96{\HmgS#[[gӅcs7(b C0o M,p?5/? QBӗa<h@D,W)RJW$2Kl՜]iE]x4 ޕqɍЄX޳W9nwC(X`PΈ&['5ͬ?쬌Hn-đqqm}jM&LjZlut^[4jS 'R"dogM+hHhR+8 ~čտsX^k~~XjMfgc #6[~4Qx( ? :h5LCO>v'.fuiahL.xiWSVIeWdv'̼n:aD] e j01IB/+ WEU@TL :yYq7,yjUƢ "+UE*nEc3cȡ/\6n#&p8CVd/8{]f5aɦWy$W@PySG}.3ts9NR V%C5kiCuD;J8iQfVtZR؏DU׸F^. eHr`Z@[OEBr\hL Nȼ/_{I8pL()hIB (@{)+PݜzOǞ @ 5QAL'lVe7s@{IULG73ei so|R۪]z5Q>ޮʩDD=2bin/h7RUNUgCO7bWTpܺ VzVȁ;c!o&VghX{͞*P*&ouf` $K'.|fH$w'g5U * C<UzͿŗX/wĥD9x6e_*8v4hpjbc:e5W6r# dܫ?sk|{Mc90/{\Fh Gr0V!囮U:鯎{E?_fP:1-Y$jNϐqU?!BmOӼOorKG?I]gdOֵur?=M$;R D3!T3ڲL7RR[_v{M7 ҝtI|iRx1տ{ gX @V(s!^`@0YN,A؋r~ k>L3J)îA!RŲωxJ"N,3/\c?o X^ChgK7y%|!Իoӄ;l@j$vXz_Yw3P;Iz-ڐݥ=L!L>3 ev. 鳋T<P~\mDqk|3Fт+̖ryڊGo&c4!Sebњy߅l͙O(ߺ h^nQ䧽"| KfckZ6D{੓è_?5P_N7pWSC (lK&7P`/}9z2԰ߎdp4&,-ykdV]'L] }Ha( ,RSj|, Bz/)!4 }4kܞs Iy!jl̈yt7 !Tr=Ԙ런`8b]T*M_|I0!v>]*|OQM Eh@GRzi"?KdګlkDҐ $PN!҈ &vĀe /@ǷVvbװO}E~$& bI'a/7UM~͘ΈFɩ*bXDbdo~89zwp-Cǖ#CW_5dm1gESQ?(W2Q$˖,Kt+sd#ݐ>2`)PLs%ڴ|jPf)4./+,zgߕ y I:$Rkǒ\x}]QC&+o*: +a~]v0É/l!㡥@gyb6B g%;˖n ߖ cK5mb#2r%ZiRDb=QqZQ59(Xgbt$}fcɚTޞ:+$ qfi=5(&sRm:F%/]z$ 6CTy jʫ.AqrME|rZkfAPILTv}#ƾ)xd<*RvسXT86,5/67Y'*# T&p@X[E..ik]!~1c":VQi1݆?U;xFjFAˏGK#G?Vqp|gKGvU[eﺜ8*!i1դjkDc1au9_;Z=YY$_.#[ߋ"]Yܽ='nC| 8h>M>lRBC Z=y^Xh!z/TѮ\yIЁ|QeT^bXؕ.[;j2W>?jf`76%y#g T8qs o ϰkS]/O>dk 3D'#߻a8$S.Ej lF/H0oNSx ׾zRKK]4QsBehWFWS 9QϾ@%ϮAyKSq46CXMlIᕔyԉFYP7dFYu9EPPXw܁G[}Xh `޽>1j+'ZDvY\- 4<+nœwI~"i-:G'֩ G<χ!F]ɗQR32xw'q~hՊAɀ8Fv?^dLO_{ŸS: UpaN"r b)_3ɪjXjY'^o.`ѧhܯ]\ e83[Odei=Mw-P-A0|8%jcѡoJ3|/^*5lū|6?-ʋ~7@Snox$%J l?iI9ndZ)Cc9GhtI<5\1rôeOM{rmkj",ӷyhZp=6![YFkhjEhA4ձ~fu|.\bcI-Q;煄v(pNΰ7l`*9q/aq4wqO| vF V/9]Ԥ 0Þ5NrWTc}KXڤQR8 +18S]_=ŤmɎD..gpK!tvg?[h/:Og3-[t AlHt(k2ܷ`irCas=uQɁl#/tISfmRO4ܖvfbN!gFSݝ~ڟ SRB֕7 uGrwV9coFűbvJMYZ+ Damz+7>'gpDF1jN] R>tnȘy_W *֮G3ĩJWf[p>RL;=Ͳʍ7I"N Q1}p?,dC8s w$,f3ʧ$[[Q#U 8@IObf0mqy_»bYI~ V` 0T'BqgLy}kkyu;3srO%l E6> L.g'< !h}Ns+]bK"E%\OLe9-`qAA4jJd~pP'ߊ!eqA V%טҔ\$D2o]`Z?K:n̘.Q;t$6&=NK1Ct[ݠ:R"٬aYX\_r_1O=Q_!VNrp:4 l]x8*OaZ{{KGO'hx+sx-r&1Lٳ*,ELlrǞ&w vq\E *<>6kؒ 1PZ.wATJl!⹚~ݎz#~txW K|=X4Ctb߫Q1\:oIUuf,eMtZ3Ga:Ř}l1C(ECH_R귟)%  #clO6GP1v3 a*-Qu枥pߒjS pgf)jh~IH~@LثW^ OS$ܢl`:P>{=p1cVw5UDb)YQrx\Jdr,WpJ|<]Kx7(`k}hw3&all0:U}rP ;DPHU唅q`dD/ԾK)_2_ ˣYOtT^4jEMiE|Lj8WK.ǎ\oUpOTA&j9(\3T,8q bSg%! omA\ "ZYAZ~T;ܫd@Qrbo&q3ud3Ī;ğ>bQj:ۿ;H{gk&) Ѣ[yyz+w .?Ok +>orJ5WTy 1`, ,#@mg:!oPPLj&u1kZ@sJٿ=hg;xujps{ XY_f &Kd 4i|m`a٭Ĕs(竚^7Rl&3Gmdb`,WBoΪ3S\¼jFdp;U/ SSIOJ爯-!IzA5)y~^ h90 4]~QsfL_5qݱ5bsvx;2/ sy^K2!dn˚Mo#pnWktTg=aD5;&)6H<:P޹wQJRf6_Yr=JZ7-;]Lv^3 rTR\ALً ]BSau쓥R5 5BZ=9b+İ)ɾ ofy\&DUiL)W[?gSV8/d/̽vDk_khzz Aa#f zF?빪/FoȖYܭ>Saf; …yeodMV# Wfw`F@+Hiҍ$_͜ PqJ F7F2p#d{`+=nӍtoPy{ FeVe{xNPy ?y = $luӏ #js7rVs5>stj tsP%D=_q%Z,o߃̤}ԺcwPax 2)wڸmUl!zPډK0P8l|[nӖ>J(Zbm\8}8P9>J͎?03hɬ4 UL!iq j6?yƋ\rÅto[odg*4.ϺL*at*m^#2YI*\ (Jfi!*8:@$̈8&,]r_9P"ǛXKohr _t:V^K^! Pauh6 0ABɑ[PZm>lgZЮ~WcZy-NveIX=y.|%ↈwf@ya<Æ47D.# )Z<%o{eh6,C˵;^AG>D ^\vq9\#x7K\mR`e*;%)(I~|q m¹kt*c@Vq/NHI.y JcLb Fa-'S#R\/IK;2ښP_^ ̇!֭jVTM?7>Vֽ:rڟ ƻTFc ,z<(ܘ+{ rlr9Xg`8B~czj޿wzdbgrU =GYj>y5#j²ZDBݓ62qi-.f@OYҦ|iݓ/Bw5/J Nl_:a5O뛎Wa$h ~V'ʏ3DV{ӾlU%`Z&Q153Fp:XW}e=εCD#2X0!sf(leڐ~Fv3jHcqn`x15ٮ9Uf vK6ou>`7 |H#/f'qbXs"Cf[uXNb9 0@2KGذفn|d*XPZʁmha܇EI+R}z)RP .KD{kL\n X#3[|spڻqWc0."8yZTL0MAx}g`3eб E $O=id ׅ|c!8ӧ$] t9P!7LKLh<蝖{dnr}_iGދeR@j_0S\yPϺ^>- .Ebbr$MY!?pn3Pkq TP:r*k !Wc.|+Ac^ {K~J:YJ?btx߰O1\gl"äz2JSVU+/Y#ˡHV(cۖW8Ҳ$+2ׅ)fNzYz3_ ݆fP2cĆe2t&/*H/Y ?}/BS=ׄpbEdpo֧-M&,~:vhee[ZLne ¿4i672l5t2:e'4wy [q:q` {fz=w;IOUiԥ}gi'^hK[盲,pGW?Ĝu^[[_vLVXh{POws~'3E&!('BH|y4w +V2SmkT@ 5Cezv5Q`@=_ΛX ̩T:m򥷖ZF-z")hQ t*&CSN]F/ kru1E|,{7>;SEDt9Dl`k9$:. aP˻ zIqTg[vvm=_` k%˪6oZ\!UﺠX+B7p(P+Y0{ntMvYyNYC`6^Y`*(Ҙ6B]ٞo$MsHy~XjCjpd uwl\bGfi$+ipPD(a zX ! 4 v CcaD=#nV ăd.aȯ͎ާ_xZv@kd< ^>]뒤Ƌ###6?`4 !yfZ s!9(U @IR2p)~\yZ{f#$p8 KwU2bȇ&L1$'Wv(t8i)z1Cm{-<R19ߜDXiQjim%2"o;R);r0 tƒm]*#F{39I*t-]lm mI~\D8BR <Ig2 %0o]c?mIS ϾX7emdp{! )U9L" sZUO[xeTIOEmʮ~t% $2(?WGĝ&q )7J݋1O-mT #DF1{FѤ֭i]17Ԣ27-y^fG T4^LG#%] eK4|3.eAG= J ! W,} R_^Yc/q,!{8ꢁM -LW#=ӎ<<(G ;^L #ie dhߣk1\z~t L12!%F~1Me ŬL/zqaL:l8|Er/d@@-ECJ J+/Ct9Nvpй˧GYNЧK)xa%QE<"tPƫJ-~ } iZM=q-:M`Sꐩh/ixvqz.\B?krz[8#'*.8] Dg2-7-KlXH)d+2?l#mJqZq,oK^dAiǦW{+wR }DC u]֖)fؐBeIsvr9V эFV8/(Mފ#+&-ݷ?sWDk%(!q-a5{w=}NaϞRTqlxD wjuŝXV:/J$,[\EɘuD[χE;\ͥ_Y|EX6 :H[s<~g )wTbiZនˇWb ,3z3f$v4Kc]Uъ4r2Yb)ˑ$/<:/5Ađ_+IwimEA&!#^(lvNW:wÿg!YC2w c Sb=v4 1Y)c$䜛0h]8lPX91ŶDGS߿)X%'{M^Dn>|r3kqAG+n =5.%\E!°ivW8w 'QD$#hIT,|[,tSC#֒h<,).0:XapWOu<+Aq*ĥb̲"wiTmzh8$OKSn1Tw<)l;PR^s;Q1ðXwK^w4?j8AOd2à<wcMCv7΃!fT<G؟f .Hxs"ɴPQhX>i -o1Xvטz0J*F+)xq#]o0a{&(sҹ<7yd {KڢPaZ!|u.)3~sWP~}iTmU݊a起?NU&$+tӇ9(~Oh@F+S1#fվORog[挥+Z7@KUACeT!g4W8HK~~?0r=ls3>%{%qv`y? S |5, ?Uif2DPKKYvOjcΗZ`~\Ǜq3y7@U 6vM%T>9;T'xqۧVg/S󨔤˩.s[g=R5=s;eLRjxKN2La.38Xo+6uPA9sCH;o.-ph݁C ?u/@'ԭmo/=Rbrܽ0t#MHM!.SSGHcL*A8_KPGWlэ.3r}>zN(qB%'jhhKh=c9P-e,?25K̉0@/U7 b6o\rtKI$b {kωDΒtΜn@OGPw[8@.UЩ@IEdxJW3ޯ =4yPLՃnVФ t/ןed)D!gf#";"+OZ$7$w7S5g Zy3e 1A@#*,Ud|H_"q!Z3<-^y76{rj$!g8ϱ^R_>#h=g;H.FR9yؐQ=ky)yO92UC&VJdwQ$,z.mMY5EA9SUv@ [Ru, c; D7գt$  4Eܕ[ ] |9*9GK(uX.ID(0򖪵3پ|>,&@gߴd*Q%F7M|Thf/Jn; װ69g @pެG66/"4~(gn17XSu% "9mXldqr[SFģA@CO6W;6=_F•ز6(F<_8>P\"`|O|LԤY Wƥ.%C}]FVLj)d`db3(:"JE aD?Zp^KZNdJ*s_5%#b' %.w3sNνb|~PZ.̪]\8é)>\ 0EFZƾϸUFX|vZm9-Cc_(mdZMGoXF/d ̡u( FUcsjzksj/G>!Z2}ϻg[qp?i& cڢ z[3pxB}$>'Nk 29nS3efOo"]Ά|#$Lg7D@L(9Ӎ( z-` #!WVhn\CPWfq2GSgB˫fr*h($x)Δ P7MVV?RMޡnqDP_g.y|`W~IWst@:V__ mКVF Ib%]f/m)SNs(t "@UdXsٮ[Db1=)e5U}: s%@{ ¬5Nt-w А7 amJNϠY;uE+,mo K#e,rGcᐆ1Wqo`'v<|6(pBKbӵaw6 ؎fN ;TҲW"T炥rbvbk϶푑wQ5!޷ޓaUW-}ldxWQP~t/,߉˸ARWU+#Fi2&rvwiblPzM"xcͧ<7[=:4ػ|Iw:a?Ѕ˞xٛx3 v<$gQazD|2mܠ> ރBnUZxCPDpp\Aɧ ٺ'D!!_䭨8=cMz]>u0ofIķP@T/! GB2"@Vs&l_Kp EI_ Tr3qRs]7Igt˱M<[YiMRy%.nB/n @dVJdDIL k؞,A _oCnAD2ut{U!{SPTLoiadp Y:YOL0m>d%(v`/A(=7Y1'h V-gHէ7%7e%nXg"O74X-dwJZ4@с78딪tCm`a0090>{]/9%MM%IޢW^(}}geTC;=6Y(b-;~hp=>K3y u= 8z+kY5y1UOHxߗ(ƟcU-=|{e/7O7ARCK5*MQ:Ѥ(l{옉%-u2 sl;ܗWmyBy< Z# Ԁ)^4z~a Mgܣ߷4'۬V%3Lz|>oͥ{l%.PC!X=8c r֎Ht1$'3D B _rϼ=5i04鄎\9;T`"HA%)8Ao35}Әluʁҭ4R/'&5 2½1xy)1 鶆EÅ^"ûe/rOGЏ3|:O[6hò=P`ƲmUEdS)gCaUkн&:1>}Jر3]տi`]F:GoM4-d܇-=e~p9nS~&ҒĵXoqՙ멩]bO"_ ɷz`G{ԩWƢ&n4)XQbqTWs 77X]bT/rG ߠ0K?e2z\J'ESdGMV)vb)JPx{W-ܗyƒ[v6Qw& Ob,@32 MD(tJ9V $n|;7hn+{_yY=BnvU Qv:xޖm_nDWWD%7OG^4\ :Q,' Nq &TZݲan(#Y4wv +"{%$@wLใOIB^C64E hM\ycУ-MO?aEs/꒙}HWԑTZ'4J|R>u*["9-Tځ7^|yx &4~-Tl΅Xx4$ejط-/}zӚ4T׋*ʱ`$%Bvd, #i<0 \懘]}IMYrG`Lsؑ<&m! .!Z L ={9DRV6GD]a MLG9n GOh[>; dY 7K`6jNrh[ZA^UJa(\MR~{wO#ǨD:5),-iP YIIO'rH"e‰dW|u QsNmȘYlnGQ*(s¶ue]x{lYݦNEqsY2Zpj-Mإ'8*yFd-Y?L7 hл }݄-hvO.L@=!0P7r-Kq>!Bd~tAp/%7IsncTq .~Jrp[ly7 (R^}ZXI΃5V+%D7>ߌm/1 .}Xb]b"Zo׫i=3i>Yb8 2s^p>~Y/Oy3-: ;%0 |N_\/GGX]VZP.Tߩ&~ͭ1E0(6Vu01 1K2_BO8ɚ~ HGIcFSY54\W_{(#6 Dem\"qrh9by: C Tx #{ A]ALyc)3 q݋AZhYkasLq70 ap3{0\B7?WjDq_VnG1 fs)tC ^'NB'&(a"pil#!*&ΝBXuKUp ?2`\ԪKkXQ@5߽IQ(">N3TEjҍ]ʬ9#$p|e^?NSrRVFkXLuy 僚v i+?⋯+L@pBmTDg.shMTr aQn$\k{眢=4( 9|P2skVF({QWE(k 9CYemq1܎ 48 8h_1]#PCٝz,>b߾ȾL3ZlfȹoxaGk%-Jz)ׂm\E3\ĵ;Di[@l+q.s( ZxY؏UC3m|5&)IpeR[I)1lٺT_IKO$P 2 ح]68'԰;-;f\+vZ67B2W{_Ad7=4}K3[Bx (A[66馴J]R tVzgC{~-d #pxF0L;%Žmwhg.WEvJ~9oer{lך(|KV&8z.P3(k_mu<"kJwr¾ dd)MT櫵8 rRRRb Ġ}ޔRjEm9s=\ j< ?S^seoҗeP1VkX(>ߨ,M?+GJۭI hgh'\W8M1G>^ O[5R|S#,>`1ƵRccL=WT<|P H#{Ew^PE Y5)?Y4yf0 :n Ù#|XUlO%]7Lo[f͠E",Jz!@03a#] _>&ݗo mbZ_@Z,}Ujwm{*˄LyPҲՉE`Pp%viBL8U>"K ^ČxvZ*X g|.H"&vk hoŕ =Փ'YzW( [ybJ_v$<\ Yeūm(kPy܉dj0ܖ]{)@_L,]/"l\-X^|ʦ[)MPx=%S䢲|Լ'/?9ū{̄R]zpfB\<>?&_u]S#qi8h;)cd\?(0Ϗ@ %U8 ptˈ5M4cA: i 1v咺T4)n^Ч^:Kv4"F %14cT?}>̇zV\?Ekcg'ҕ4g|c"YKJ9nѹ;eΫ9d(Ycu' ـ%I"_ /O=. fw>GU 0;G#bu;z,Opst.F/V,YY(4#$Ͽy({ T }%@CK;hlM__= 0c]BLS."'4Qľf:3Kpޟ'5mR0ZukJ Z$x൵εLp(HWqy ˁu`1ff2a†L>(|0RrO87rKˏVC)1p p\+O,\NIѼb#:S# A2dś8@[,Ϩ#?HQ5PZHMR nMoڥP@?ŻQ |6ܯ#E~&i] *#ev]vڐXSRQ$6, k]2]^Yg)!>>GK͹UAc &i 4]f7vLhiU`^9CHFgQ0ƢPxfEZx +(l 3S%dsB1~np-1K"]:`%MeVGRw=_EKx 7#2W8= tQ⡌l"`@pne.y!|.1YĊ&zpa_Q 1EFnm, TF_g5H6ec*"_L{]zx;tY$RkYjB)Y ,שɩ™=2*ڋ}W$o1wep~Ƭ]ȩGw|EK2G[zXs dߙj1h,[e3_'|ډƤ\6W:z^ jJPh}@ȏP*e*8o.,;A2C:ՉKq,# RxNPմA@1ihͫ` i@[VE|eNP`\u 7 6yBڝ15#7+%f>~|s/<qce'E?ؤ2.Aܑwvd@71&iq)f { 4mhc]Z=scXBQU< K fiA6LmsgoH fJ ]wʌ82R= ů H0$-TaGCЎׁOviս|؂l0^;[dt[Lx-h_-=Uh[ J|(v^"(6Ke:A֌D,tޫ ФzP LY:X70$Iy8MG-]h!ryj b漨 '5xO\3dG0FvQ&t tOF9i!)2eH2%4& ֠Mp6^TA{j(sh?X>37pv'i;if2Bd0_.J v'*FI183t8f˰ޘxN?fI8kmKbwxUf S_gxP7AAcΪ'quʸ nE\#̫=rQ,LK~5m?.gVo GN7}ӓ,$jb\+U@~93 :rITߚBkfs;λ&98yvPATDxe2! l.|m?ރDuE4UT4LG@ Y Y6$MO3HRHA};Xz8Hh"M&$ʌ|lYy 5U)&d7"em($?.E𼀡Ju}7DOg#RY c:ZaP\ITrOX2գRWW:_ Hw;?%ANƇZAe"D skgL!ب3+Hu!`Q=7z2detYჰ* w8>:aiEDŽH Z᠊EL[ui΃˱wIt'J%am؄#~V'YbY6ؿ֢̊N0%l>L}at1̕+ J#m/'_6ub(F-:WEܾ6+ !Cx@`Cv19W_(E6ݝCkW1TYRzZȮpE~.;]"m6]iiQ&Er6\5]GΣ΋-/^iټ8STk ء(@)HxUR*HnShoJ&?^N,k^yZΨ(c=,7=?5+u5ї-Acut2l_9D=ܒs=$5'E"Rf]X@5rcWs`S58By_{hT~ E9y뎞TnD(uۡ#KŧH/o1X3 Ou{5ϺEU~g "")7ynlM96 EѻN,"K"pyY|%*bDjazD J`UXU=OL˔єELѕ!=(:C"L<5\3S gY.!0# 9Κ'>[>Kq* NEZ?GzVȸ|g }HA/א/N@TEX(~3l{󲃵"OsjqK1Ӯ'2kbq 3SVI>d Nr( ^;|VՉxHiEB^^“TBjA#1mj4/$ <@Q1t:Q;KNzs@W$P9SEm5۠keț5nY1ʋtmUfasH\WKdVv# QI/- ܎wyִWbw, >-dZR ͓WױJ4ĉ1jprV&B- $N=-{=rjrͿsFf^ Tg#)!Z ቇSiI+7w!.Fs= _CF7}r3' mp ܝHLr iK/_G3鿞"\40+>sR쩬"rFlpsoؤ=`vql7>" LGM霵RQ@Ѐ?dlj$zw{oXґ0ycQJrh7\Z^V[r]}bg%H\%a !rɅ3<5 o<7QrȂ @CWTj\O.~n:upWd4 UWc2Z2Bs$IBh:YfpGp4g|Y/f қ{/} z–'w2#Ƈo)6жǎS{U{"Q4(<>u}:"t"{4lb:%sy6ph]ʎ& ˮW#0H)GV=vvm}OAr! 4Ė*7MGqk a"KmRCOg,6aA4Y(ѺiɧrE1wC78}~{=k?J/͐Vgd|à\ :v .jVUw\٠"/?_Mx?BxCZo0;{9xB{oMM]vw3'lʨYN 4fHC4gЮVMvs3l1H;\h'0 H:&da\% ,{J/8 iVDF-~b*7n[׾9cGY4OdGi%`#Y D t &Op*)Vu)L&f]${ ⽋!25HDXYDsgOouj_%vΡjb+)|Glh3ezii20$b0;Rq6~Rzh3T[>S8o X 8ی:h>Lf >%m_\$,V"}JSA(0 b >/ 8U#e2}Q0+f^0(tL"[m.&Tx 0e1ʽ43 ao4#FVj;JBuir1#( #,ΣrVaYpu&w8dQ 46o,JQY677:G۶y I)ސUD_ _`ިzxL?Ze !Уxis$.ku' uc@#/4δ:.mb$!8pMdk!e652R VҲ]?IփTwro ;mYzG 썿&yNZP:6В3mmÐw.8>h ; T'pSxHX:LP6kIL0PL[?\zh+z17x(o;$V3M-EBL/vU>k$=ոϏZb:Xzx <+赉y^UF"doH0O C];ˍ8RdE\f'NFYb|oUޡ:3P_Lp]9CyJL>8P7q#S|pp8'YYx8Ybֻw0 7`;w@i k.ʻJ>"U}N ٥{uC[fmcxX'gӠ?= ߻ظQ=:QVH@oe:t1 L& 0Ӟ6tYcmUpP5&uHXƪyjUtH#j xSG C$=lX]9E-x =eۘoF!9xZVW @5^<9`&,S8NҺ3rr!Gz*/٫<Ɛ7^?cs:cBJIMNxr}'`TmlO$/`]qyCRDɳpKc$i}?7?kg s_H|BʬKfAeoSJVYak?<dYS* B^~`PN'|^){ 7܇a@ %z=q]_f[ C͎^PҼ/e$()W_ `X|2p1B.3ԡɳb-'ԑZO!($z~&*^>I-6e5#>P hxd~fg-0uiϳpOnW*OFx@P3ԭ(GOJa*q+M?A\m(@V;+f,:Ϫ'?O>%CmzץuYݗn(a6O@RN0y}CqLv7L|ӂƵ12>t+ޭiEw;!%F"l5i0*Uac>__ <R8*(\ U oڶTbV hnËÈ%Mp1Js"t~\$j Vߍz{IbWIRÖsL_yXjikۏN+%1HHmL32NP<#-wO3 uST;6.jbg-F,)_x`ʵ~sn]5z?.ۈr+8ӼGRٷm u%ѵ$h;"J#ኔw- xs7b$Hf][ m/t%pOl|n:fLQSWF6fz$9 ǥklǬ;c@ń8@5Yxg>[ J ق۪eW \lF,v@?E:~W೸_M;TdCZ߉q?=ښZbr){[zԾEUڠtnivY=w߯IɘZ0 .(m 0 6nu,e۟g IG2v_w䐲7F ZIΔ-Et-k5hRx /!_V{^lc? h#qXjLQ Co}*1_5m?؄ ˰z6)֟~AB)L mT4cC(.BaiSA_T'߾c[ 0ҔC',"mNOkb`\C$I'GëG 6na*: CVo E}S1+`g^X ɒ#yÍ8T앫ɥ`֙MBsTyl#֗tlڛ2X'N%Roq `DbRgfVoʬAJ(9 TZN#_ raΜA]R[])#6TĂ_RhHhgj{r,^y|_ $)rnW& +X$~%`|1;gˉsL|N$xZac'}'eȍ's_~γt!U#:k_yBCwt싷3!7{A鋏ZhvRjxgY_rNBƉ~'8^-F"neUj-gY[0uҩ#<n ؃A%)-6NL.Ԯt$aL>O~`0|:kv@eTl>^8rHڭmJ3ZA/JbtaU&ЧY[8-Ktl@(B44(@GcC h_>-p Qba^>޺gu7n]YXi7Twi8(-+VᬰܡT۟X*Uetd*;jc>SI7Ix orl_3\`^»_>Up~M)|<$>Gbj_*M?^"ZK+-7?T ilã#NRfl$!=Ճ=c׷NB6Ͷ-ʉ>]U;lߖLro,r'$V ĸz2sw CUqU #_0_n/'?qcgK{PX~xx#P1~\J-ۄ֟藛\osy#TiW=B41FJx"1lRnޜt~ HV(l'l}mnvsaUF61H_u3EXPxxm Y /k *4oџi͆@bi*t(@5U(亊iՊ+ǔi W,h|y9;}Y^c{5L'|F~{ Y# g [ "ƋfM&[WYy|<6+"(:qidۑ+`X~=kFcrjWѬbAF7qltdھjA oyOo|qoꬴ/%#:ҫe6'^Dڸ:NSP<0'uI0*B?ؓz>858Q߾A_|[sb_pS$@Bn '# *WbfGG/hNA:ol8<ԫG( ^AgKk&ˈ&$DG`?z)u+Lrx߄ez)t#b 5 [5eW:)I٥!_EGBT IQI#H,=s9\&Y4[GuPq@BƅR-[ "f040ʢfE]zgNYl=Xd'3U?p=7'59,H)a us+G"͟IY1\1芿%>`T%ywI4R-ͩj4SY^fa}S5)!ﷄԥeDW~DEF— ]QU u@MN49pe&tE6ٷʰ IAcSKH8pFt(.>44+^g]t+os[VrՕD&>07th4;L܄zDӑG^rѿVxYpif1Rbū*/>[4A=OzЍ( L2} w$K00̡ӗHg/%@@dhvY3Zk \QГ\si}0~Ulk#gg#< ɫ™-j677,rq@E!BZ'`S_35Z:E#%Zq7[dtJu%7:*_ipi(u^~gww E | GcPs$rU\[&$EITI__hB x7`JJmD@\0_2Klb8Q2 e{?p s c9*.uMÍ索(O7"0c.:u~qĹI4F?`iۃ?.NȺ²)u 6EpڌӿcGM,S8^$L L P)Gss{#tp9$򔨟1/ _$eਞ@02F;&2T,lJDzlZa)γN?5S)zvx)i x9#88 hՀr(/;x܈c;8q:;IՑ 0{9d1%.dr97OH-d|EŻ?p-x+X߿K`3c3`RڧC”HtJ+Bm*Q3mOe|ϛl\Nb0zH\#0?{Fe k(Cзj2˅dX}lR9>>"ihݽ@ *>|_֙"\B$?͠[r!dgOwGQZ]~"l*UXc:fUQ.z#WY vԥI 9p+?J?XrC p-xlwU᛾.=SWڼ·Uvyt6 G7;Եd p.mG.~ϢT*oH` rUѼ*B?:6?_?Y1 C!>RсԱ&\K~ {nbH'&x=N0%rcA_m U]Y=E#'?Z5'f4tu*GnХ 110(F$KtVQ_ZVXKTM4UcgKg/$-&lx/#2}g_[f=fCԖ0Kə)yAF\Y $"u[(e2#hCqvw0r7hP>2?"+_0MN/`OG-yٯyJXS3;$~{Ik߃E5ڎSǨ>I#Dnqq ?vpǙOZ_&ӫR34ފz2m#d'+d/H3E Ͱ5w`uI]Ea*#HƘ[W- /;mKyL ůF{ɀc l!EQ1d%;RVAqpZ8_LRXڞ3y^s*M[Qb}`"|کìacXg X!T12˝`?Dݎ٥ٓhbmCkcå5su?8ʌQbn|eHv9q}n˴:/8'CIݺY`!h]gs"F#Y_HQ1PWb=b0Mѣ['N];=Z"⃰6cBKytcMכ4yxIxLy_0v KZ{/ F]f/YA;ǴI5Q?IP^`hvWWrؐoئC}xrL쮄4boU갴w?{1zmܸF\)\A=RP>4|sќx)"To둘6Ï8lۃTM6j~]k4Ov<.|H.δ-[~7Tdoldcwfzb9B b sp>[So$Ӛ'ۨ/P ]8qa^A|`ɫ&7n(~?jTƣYq 1R4kڞua[,}e`(}ѢR*}C ?`C2ߖ.x,$Bڠ9J8!.ȯwz[wp!'f5V%PY7iv3goGrb6clުƣ96wᓴj ɺE+Eqj?^GD}o"I|/0MηQnYnxSddzun5]\:ZW騉[!&V0ϺnݻR[ \ԫ'ʼ 4iR࿓+ӚOIVXOU]nHz{ʓS;1'LFB%$I^̯h(4ʈMyrYpKQv־LDx>\#L0qIon:YH R =ӣ B\b_uڸfh"n%MbqpN9=Gk[JJLGx rǜߥJ=~9FWH 34g=W C 5BIg2_'XW%)FzU cC`k- -/ +jxc_'RT sbyfޕ"F'c58Z4m{\8.Ӕ~6)Cv7F%m0Y,v}=C7nN UB{v3IۿϤfl4+ggeK4@%1GyDiȧ> W?|ҿokvj8mwDZ&;bH{_'5ۧ1*̦JЖ_sbS~b$Jk}srY*tEVBڍi3%LJrS Tyƺ'@qs2. r#y#'bmn}'+"H E3֢4jz>_Xփz@ܘ`"$NF[aC\*.5{2/vO7y@B B߅ҜLuje*}9bn6^4쉝 K?o[>|Ն[>wF۹QjCcG}ru>轕R?3Z惥nNѬ>VE='2)ZLa`cT^=;?¶~W5S+`tx0`QP$̴s=}`Grs3IGN rPYWa*_XDZv0Plvi#taEZfλ3^B춯͐"uE-z)1 "E;#h/|Uga2˶;͍RM !C[ҏHx= d0N%§ʉê3Bc"dQ2v9;jzϧ GϕzɣЪ)49/t7e9Ųr_hwƲURCOWqh7VaGff+'=Qu׏Ϣĸ` UE>FUÕgNd1fZy*ZZ/1gjɚl]wF[9NJlȖ-MSNKGŀMNZt.,z^ʒ^"0~1лxp{Fd"Gd&>i&Bj? 0ϚE‚8vrҔiJ\k,+[S4g̒nfSM(v֛СwF7H .5L/?{i&BAZR(iF ()b z^ϡ\uy 'Mdq,*dT?xV=A8Q .44~;#_q+ܫu9_a$ ;}Zc¸ 5;Z|eަԪy',F7JLKRP!xJzz`Ŵu–J6I53Bbg 0{eWY a|g^*ې7K!:'qpqʀص?|x|_ɔɞM=4}u2ZbFl{J6R-"z6&~ldsaX/v`8Į0:뤮U5B/8It0'BT䀿ø+D)/׳SNz>xoxk^e&Lm292 /[Ebm@L]O.mk&pE Ļ𙖫<:# 8}xTuMtka⁐:X*!hDP=a{Nii2k=,tv&e)hΓ#=ژ l5(;6$S[ #ڪqiO6Ї]M{e:'␠"XѲ3{e'Kvݹ(@Ć zkgC?0ST3)t&nˤ:, G١*p6b:s4fŀRC!I$˿QDKRR y:R?RkJǯY#;v@hbY,S0KV*+0 ۴0b 0*a >Մ&Mdϴti[ 6n%IRe}t0Jy8!LH˪QKelANsGEZVBZ dWKt `%@2#+qA# i(-osS#˸0a4=lQ 1FD1+?].[܎r%ٗ`Q:{.w1,tCb ,zO)}F \Yo'~&U -βQvᕇ(&?Da>qI1Ta5ԶHGNHAi%ZIHS-$Vg"?I1]0V4Ir\5zF"4 }G(wj{K>dG+*d^1US$%B{:4p-w7wI^n7 /Ja˶MyP}ϷД\FM 3۶4h)͓G hsoRpvfw *5%&| _8{tF-I8 (5x|Zi%^(M"r.:s*O{6|=X%X,Y=| =I8h=cERGb)TfK;r33sqGCȎ;C"y ;;66IEMA8?e Mr/2Bg1^mTUL*+=hE: h7}ZW^< 8gً/NI ̃LF]񴟛 |>-gNg{\Rj7[OOҠ"8ZLnN2'8ChH'*UAaCeW44 2jh,l4-%btMQ뾗;TD˕\y5Qad#p?:oAڑvD4r1|M'D.k*im`4JSoeS="P?jzܐF n0sA}ˠN[_P禺xf+[C~_DA!J/]PZ+Dž]>qnQ@RM^R;N}R&fՎ Zmf)$P R׻EW܊jT' pI1.4XY 9Y]!obTV*huTVbAW$*ZzuY0liCp?L~m!) +Y(E-O$'`LkA XÇ]Dx|:ͭk\tC`z)$,o®e.c5!Q<`{M9r~Sǣ|=f䔙Tctn%a"mo"d3 "ɨ _ѐ+$_9.G'e(c> A:xXdv /A} œa4FÅ*p0V8ڿ'T`fld 6BJaIǫ'7 -JEM|8M&mK =~NCq+qł`m\qc<_~VdsJ,#-?LHG>}v\zK6nb|BmFa$XM ]q]ݗG9ȭXUz}h4.3޺co2,nREp8ck+7af#&P= iQ>8f$Һ:ȍ}b@oC=郧6@0~pmpix7W)I# xG[Nc;Qby}蜕H ;%rYB% r>%jE~I?Jn!R22^\N鉹' ]|Ab=b]Ѩa@=*|(-27γa>W1Q E^P6q?վEJA<# ٗ޴Fe,ToSVPS_9!((]xu#޿} `D-+﫲'QJ.o}`qW6^&ǀ*~^[v=LGnݲa׆1 &$^T-ൗSTTWEsyk, 1> ^T޶X||Cq |6ۉ!RkZ`nxdǑY5}P.^0_ Pn2ZE1i??~5-l$뉠*, ܃8@4o\ې)֙ U=Һ}bi>% 8fʹmHc *aq@]YayNEc/smk(i~l~)ʚX'V`JI@`w%r|-sҔ E ~̬asQDׂ,WΪP^ N{4t:f[&e3A[PIřVOvv㨽tXiQв1i_3U8&iVFs3arMکV:/L/=u"9afLJMm((p~F{ c]\uR[e}p% П JbKP/X0,|P EP 8E]:q@D(?iyC`>! tRs&@l c&<x}tG Y Wb\03ؕqYSx5F/ڢ9~9\F9-C ±EՐGcWEWdGl- LDoH\] )*Jr̆?KgȘ#RYUOT&pdǹUӪxYleFkc53=F0"dg]6*4Ptbb2h`umMvİ?=1gC)v_C)5ֺ&,rt1@{}V :NJWʐ$*+%*NpmFy0P=!) ovUP{ 6;tn1LPΨ%eDbF( <-i~;dŶɗy/ E?Rmlؿs=cY ?>N|Yq^e1q]\#ˋjPp5з~& b0av|zFhv3B*'GPiNaݯY uqؠ$qTa|"Gь×+Dɐ7b hԁ7}2t=UgɉH: `c>`ۻEPr4x/8Żo-N{0` i: SvU4PdγSBcosc ]W.TN#⏣a<| (5!ScM'5%Nڌwoz@lHLs ^YF>%lY 4U/庼)%֯P*'DyI>VHX~Al$-ѷ&!]Ү?`N1JT65T5~J <ȳ!Zp8zWhJ "yIB:!۲d>$-4Tlrٖr̀@qGCfDf:;بµ>>-20-%}! _t @$MMhYN s1:Cr|Cp9:ju;ZCd|~  `Ltr?]