diff options
Diffstat (limited to 'phpBB/phpbb')
170 files changed, 13167 insertions, 2252 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 .= '&' . rawurlencode($captcha_var) . '=' . request_var($captcha_var, (int) $config[$captcha_var]); + $variables .= '&' . 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 2771369e57..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( @@ -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 . "&configure=1&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..fc30f4789d --- /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/data/v310/acp_prune_users_module.php b/phpBB/phpbb/db/migration/data/v310/acp_prune_users_module.php index 0ca4f2f19c..725c57ca86 100644 --- a/phpBB/phpbb/db/migration/data/v310/acp_prune_users_module.php +++ b/phpBB/phpbb/db/migration/data/v310/acp_prune_users_module.php @@ -13,7 +13,7 @@ namespace phpbb\db\migration\data\v310; -class acp_prune_users_module extends \phpbb\db\migration\migration +class acp_prune_users_module extends \phpbb\db\migration\container_aware_migration { public function effectively_installed() { @@ -70,12 +70,7 @@ class acp_prune_users_module extends \phpbb\db\migration\migration $acp_cat_users_id = (int) $this->db->sql_fetchfield('module_id'); $this->db->sql_freeresult($result); - if (!class_exists('\acp_modules')) - { - include($this->phpbb_root_path . 'includes/acp/acp_modules.' . $this->php_ext); - } - $module_manager = new \acp_modules(); - $module_manager->module_class = 'acp'; - $module_manager->move_module($acp_prune_users_id, $acp_cat_users_id); + $module_manager = $this->container->get('module.manager'); + $module_manager->move_module($acp_prune_users_id, $acp_cat_users_id, 'acp'); } } diff --git a/phpBB/phpbb/db/migration/data/v310/dev.php b/phpBB/phpbb/db/migration/data/v310/dev.php index f037191c2a..250258eea7 100644 --- a/phpBB/phpbb/db/migration/data/v310/dev.php +++ b/phpBB/phpbb/db/migration/data/v310/dev.php @@ -13,7 +13,7 @@ namespace phpbb\db\migration\data\v310; -class dev extends \phpbb\db\migration\migration +class dev extends \phpbb\db\migration\container_aware_migration { public function effectively_installed() { @@ -204,18 +204,13 @@ class dev extends \phpbb\db\migration\migration $language_management_module_id = $this->db->sql_fetchfield('module_id'); $this->db->sql_freeresult($result); - if (!class_exists('acp_modules')) - { - include($this->phpbb_root_path . 'includes/acp/acp_modules.' . $this->php_ext); - } // acp_modules calls adm_back_link, which is undefined at this point if (!function_exists('adm_back_link')) { include($this->phpbb_root_path . 'includes/functions_acp.' . $this->php_ext); } - $module_manager = new \acp_modules(); - $module_manager->module_class = 'acp'; - $module_manager->move_module($language_module_id, $language_management_module_id); + $module_manager = $this->container->get('module.manager'); + $module_manager->move_module($language_module_id, $language_management_module_id, 'acp'); } public function update_ucp_pm_basename() 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..69ac71abb7 100644 --- a/phpBB/phpbb/db/migration/tool/module.php +++ b/phpBB/phpbb/db/migration/tool/module.php @@ -13,6 +13,8 @@ namespace phpbb\db\migration\tool; +use phpbb\module\exception\module_exception; + /** * Migration module management tool */ @@ -27,6 +29,9 @@ class module implements \phpbb\db\migration\tool\tool_interface /** @var \phpbb\user */ protected $user; + /** @var \phpbb\module\module_manager */ + protected $module_manager; + /** @var string */ protected $phpbb_root_path; @@ -42,15 +47,17 @@ class module implements \phpbb\db\migration\tool\tool_interface * @param \phpbb\db\driver\driver_interface $db * @param \phpbb\cache\service $cache * @param \phpbb\user $user + * @param \phpbb\module\module_manager $module_manager * @param string $phpbb_root_path * @param string $php_ext * @param string $modules_table */ - public function __construct(\phpbb\db\driver\driver_interface $db, \phpbb\cache\service $cache, \phpbb\user $user, $phpbb_root_path, $php_ext, $modules_table) + public function __construct(\phpbb\db\driver\driver_interface $db, \phpbb\cache\service $cache, \phpbb\user $user, \phpbb\module\module_manager $module_manager, $phpbb_root_path, $php_ext, $modules_table) { $this->db = $db; $this->cache = $cache; $this->user = $user; + $this->module_manager = $module_manager; $this->phpbb_root_path = $phpbb_root_path; $this->php_ext = $php_ext; $this->modules_table = $modules_table; @@ -171,6 +178,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; @@ -186,7 +195,6 @@ class module implements \phpbb\db\migration\tool\tool_interface $basename = (isset($data['module_basename'])) ? $data['module_basename'] : ''; $module = $this->get_module_info($class, $basename); - $result = ''; foreach ($module['modes'] as $mode => $module_info) { if (!isset($data['modes']) || in_array($mode, $data['modes'])) @@ -237,13 +245,6 @@ class module implements \phpbb\db\migration\tool\tool_interface return; } - if (!class_exists('acp_modules')) - { - include($this->phpbb_root_path . 'includes/acp/acp_modules.' . $this->php_ext); - $this->user->add_lang('acp/modules'); - } - $acp_modules = new \acp_modules(); - $module_data = array( 'module_enabled' => (isset($data['module_enabled'])) ? $data['module_enabled'] : 1, 'module_display' => (isset($data['module_display'])) ? $data['module_display'] : 1, @@ -254,19 +255,14 @@ class module implements \phpbb\db\migration\tool\tool_interface 'module_mode' => (isset($data['module_mode'])) ? $data['module_mode'] : '', 'module_auth' => (isset($data['module_auth'])) ? $data['module_auth'] : '', ); - $result = $acp_modules->update_module_data($module_data, true); - // update_module_data can either return a string or an empty array... - if (is_string($result)) - { - // Error - throw new \phpbb\db\migration\exception('MODULE_ERROR', $result); - } - else + try { + $this->module_manager->update_module_data($module_data); + // 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']) @@ -316,6 +312,11 @@ class module implements \phpbb\db\migration\tool\tool_interface $this->db->sql_query($sql); } } + catch (module_exception $e) + { + // Error + throw new \phpbb\db\migration\exception('MODULE_ERROR', $e->getMessage()); + } // Clear the Modules Cache $this->cache->destroy("_modules_$class"); @@ -415,21 +416,9 @@ class module implements \phpbb\db\migration\tool\tool_interface $module_ids[] = (int) $module; } - if (!class_exists('acp_modules')) - { - include($this->phpbb_root_path . 'includes/acp/acp_modules.' . $this->php_ext); - $this->user->add_lang('acp/modules'); - } - $acp_modules = new \acp_modules(); - $acp_modules->module_class = $class; - foreach ($module_ids as $module_id) { - $result = $acp_modules->delete_module($module_id); - if (!empty($result)) - { - return; - } + $this->module_manager->delete_module($module_id, $class); } $this->cache->destroy("_modules_$class"); @@ -472,13 +461,7 @@ class module implements \phpbb\db\migration\tool\tool_interface */ protected function get_module_info($class, $basename) { - if (!class_exists('acp_modules')) - { - include($this->phpbb_root_path . 'includes/acp/acp_modules.' . $this->php_ext); - $this->user->add_lang('acp/modules'); - } - $acp_modules = new \acp_modules(); - $module = $acp_modules->get_module_infos($basename, $class, true); + $module = $this->module_manager->get_module_infos($class, $basename, true); if (empty($module)) { 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 a214356ac3..c9adbe7d63 100644 --- a/phpBB/phpbb/di/container_builder.php +++ b/phpBB/phpbb/di/container_builder.php @@ -13,392 +13,502 @@ namespace phpbb\di; +use phpbb\filesystem\filesystem; +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\Filesystem\Exception\IOException; +use Symfony\Component\HttpKernel\DependencyInjection\MergeExtensionConfigurationPass; class container_builder { - /** @var string phpBB Root Path */ - protected $phpbb_root_path; - - /** @var string php file extension */ - protected $php_ext; - /** - * The container under construction - * - * @var ContainerBuilder - */ - protected $container; + * @var string The environment to use. + */ + protected $environment; /** - * @var \phpbb\db\driver\driver_interface - */ - protected $dbal_connection = null; + * @var string phpBB Root Path + */ + protected $phpbb_root_path; /** - * @var array the installed extensions - */ - protected $installed_exts = null; + * @var string php file extension + */ + protected $php_ext; /** - * Indicates whether the php config file should be injected into the container (default to true). - * - * @var bool - */ - protected $inject_config = true; + * The container under construction + * + * @var ContainerBuilder + */ + protected $container; /** - * Indicates whether extensions should be used (default to true). - * - * @var bool - */ + * Indicates whether extensions should be used (default to true). + * + * @var bool + */ protected $use_extensions = true; /** - * Defines a custom path to find the configuration of the container (default to $this->phpbb_root_path . 'config') - * - * @var string - */ + * Defines a custom path to find the configuration of the container (default to $this->phpbb_root_path . 'config') + * + * @var string + */ protected $config_path = null; /** - * Indicates whether the phpBB compile pass should be used (default to true). - * - * @var bool - */ - protected $use_custom_pass = true; + * Indicates whether the container should be dumped to the filesystem (default to true). + * + * If DEBUG_CONTAINER is set this option is ignored and a new container is build. + * + * @var bool + */ + protected $use_cache = true; /** - * Indicates whether the kernel compile pass should be used (default to true). - * - * @var bool - */ - protected $use_kernel_pass = true; + * Indicates if the container should be compiled automatically (default to true). + * + * @var bool + */ + protected $compile_container = true; /** - * Indicates whether the container should be dumped to the filesystem (default to true). - * - * If DEBUG_CONTAINER is set this option is ignored and a new container is build. - * - * @var bool - */ - protected $dump_container = true; + * Custom parameters to inject into the container. + * + * Default to: + * array( + * 'core.root_path', $this->phpbb_root_path, + * 'core.php_ext', $this->php_ext, + * ); + * + * @var array + */ + protected $custom_parameters = null; /** - * Indicates if the container should be compiled automatically (default to true). - * - * @var bool - */ - protected $compile_container = true; + * @var \phpbb\config_php_file + */ + protected $config_php_file; /** - * Custom parameters to inject into the container. - * - * Default to true: - * array( - * 'core.root_path', $this->phpbb_root_path, - * 'core.php_ext', $this->php_ext, - * ); - * - * @var array - */ - protected $custom_parameters = null; + * @var string + */ + protected $cache_dir; /** - * @var \phpbb\config_php_file - */ - protected $config_php_file; + * @var array + */ + private $container_extensions; /** - * Constructor - * - * @param \phpbb\config_php_file $config_php_file - * @param string $phpbb_root_path Path to the phpbb includes directory. - * @param string $php_ext php file extension - */ - function __construct(\phpbb\config_php_file $config_php_file, $phpbb_root_path, $php_ext) + * Constructor + * + * @param string $phpbb_root_path Path to the phpbb includes directory. + * @param string $php_ext php file extension + */ + function __construct($phpbb_root_path, $php_ext) { - $this->config_php_file = $config_php_file; $this->phpbb_root_path = $phpbb_root_path; $this->php_ext = $php_ext; } /** - * Build and return a new Container respecting the current configuration - * - * @return \phpbb_cache_container|ContainerBuilder - */ + * Build and return a new Container respecting the current configuration + * + * @return \phpbb_cache_container|ContainerBuilder + */ 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->use_cache && $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)); + $this->container_extensions = array(new 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); + $this->load_extensions(); } - if ($this->inject_config) + // Inject the config + if ($this->config_php_file) { - $container_extensions[] = new \phpbb\di\extension\config($this->config_php_file); + $this->container_extensions[] = new extension\config($this->config_php_file); } - $this->container = $this->create_container($container_extensions); + $this->container = $this->create_container($this->container_extensions); - if ($this->use_custom_pass) - { - // Symfony Kernel Listeners - $this->container->addCompilerPass(new \phpbb\di\pass\collection_pass()); - $this->container->addCompilerPass(new RegisterListenersPass('dispatcher', 'event.listener_listener', 'event.listener')); + // Easy collections through tags + $this->container->addCompilerPass(new pass\collection_pass()); - if ($this->use_kernel_pass) - { - $this->container->addCompilerPass(new RegisterListenersPass('dispatcher')); - } - } + // Event listeners "phpBB style" + $this->container->addCompilerPass(new RegisterListenersPass('dispatcher', 'event.listener_listener', 'event.listener')); + + // Event listeners "Symfony style" + $this->container->addCompilerPass(new RegisterListenersPass('dispatcher')); + + $filesystem = new 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) { $this->container->compile(); - } - if ($this->dump_container && !defined('DEBUG_CONTAINER')) - { - $this->dump_container($container_filename); + if ($this->use_cache) + { + $this->dump_container($config_cache); + } } } - $this->container->set('config.php', $this->config_php_file); - - if ($this->compile_container) + if ($this->compile_container && $this->config_php_file) { - $this->inject_dbal(); + $this->container->set('config.php', $this->config_php_file); } return $this->container; } /** - * Set if the extensions should be used. - * - * @param bool $use_extensions - */ - public function set_use_extensions($use_extensions) + * Enable the extensions. + * + * @param string $environment The environment to use + * @return $this + */ + public function with_environment($environment) + { + $this->environment = $environment; + + return $this; + } + + /** + * Enable the extensions. + * + * @return $this + */ + public function with_extensions() { - $this->use_extensions = $use_extensions; + $this->use_extensions = true; + + return $this; } /** - * Set if the phpBB compile pass have to be used. - * - * @param bool $use_custom_pass - */ - public function set_use_custom_pass($use_custom_pass) + * Disable the extensions. + * + * @return $this + */ + public function without_extensions() { - $this->use_custom_pass = $use_custom_pass; + $this->use_extensions = false; + + return $this; } /** - * Set if the kernel compile pass have to be used. - * - * @param bool $use_kernel_pass - */ - public function set_use_kernel_pass($use_kernel_pass) + * Enable the caching of the container. + * + * If DEBUG_CONTAINER is set this option is ignored and a new container is build. + * + * @return $this + */ + public function with_cache() { - $this->use_kernel_pass = $use_kernel_pass; + $this->use_cache = true; + + return $this; } /** - * Set if the php config file should be injecting into the container. - * - * @param bool $inject_config - */ - public function set_inject_config($inject_config) + * Disable the caching of the container. + * + * @return $this + */ + public function without_cache() { - $this->inject_config = $inject_config; + $this->use_cache = false; + + return $this; } /** - * Set if a dump container should be used. - * - * If DEBUG_CONTAINER is set this option is ignored and a new container is build. - * - * @var bool $dump_container - */ - public function set_dump_container($dump_container) + * Set the cache directory. + * + * @param string $cache_dir The cache directory. + * @return $this + */ + public function with_cache_dir($cache_dir) { - $this->dump_container = $dump_container; + $this->cache_dir = $cache_dir; + + return $this; } /** - * Set if the container should be compiled automatically (default to true). - * - * @var bool $dump_container - */ - public function set_compile_container($compile_container) + * Enable the compilation of the container. + * + * @return $this + */ + public function with_compiled_container() { - $this->compile_container = $compile_container; + $this->compile_container = true; + + return $this; + } + + /** + * Disable the compilation of the container. + * + * @return $this + */ + public function without_compiled_container() + { + $this->compile_container = false; + + return $this; } /** - * Set a custom path to find the configuration of the container - * - * @param string $config_path - */ - public function set_config_path($config_path) + * Set a custom path to find the configuration of the container. + * + * @param string $config_path + * @return $this + */ + public function with_config_path($config_path) { $this->config_path = $config_path; + + return $this; } /** - * Set custom parameters to inject into the container. - * - * @param array $custom_parameters - */ - public function set_custom_parameters($custom_parameters) + * Set custom parameters to inject into the container. + * + * @param array $custom_parameters + * @return $this + */ + public function with_custom_parameters($custom_parameters) { $this->custom_parameters = $custom_parameters; + + return $this; } /** - * Dump the container to the disk. - * - * @param string $container_filename The name of the file. - */ - protected function dump_container($container_filename) + * Set custom parameters to inject into the container. + * + * @param \phpbb\config_php_file $config_php_file + * @return $this + */ + public function with_config(\phpbb\config_php_file $config_php_file) { - $dumper = new PhpDumper($this->container); - $cached_container_dump = $dumper->dump(array( - 'class' => 'phpbb_cache_container', - 'base_class' => 'Symfony\\Component\\DependencyInjection\\ContainerBuilder', - )); + $this->config_php_file = $config_php_file; - file_put_contents($container_filename, $cached_container_dump); + return $this; } /** - * Inject the connection into the container if one was opened. - */ - protected function inject_dbal() + * Returns the path to the container configuration (default: root_path/config) + * + * @return string + */ + protected function get_config_path() { - if ($this->dbal_connection !== null) - { - $this->container->get('dbal.conn')->set_driver($this->dbal_connection); - } + return $this->config_path ?: $this->phpbb_root_path . 'config'; } /** - * Get DB connection. - * - * @return \phpbb\db\driver\driver_interface - */ - protected function get_dbal_connection() + * Returns the path to the cache directory (default: root_path/cache/environment). + * + * @return string Path to the cache directory. + */ + protected function get_cache_dir() { - if ($this->dbal_connection === null) - { - $dbal_driver_class = $this->config_php_file->convert_30_dbms_to_31($this->config_php_file->get('dbms')); - $this->dbal_connection = new $dbal_driver_class(); - $this->dbal_connection->sql_connect( - $this->config_php_file->get('dbhost'), - $this->config_php_file->get('dbuser'), - $this->config_php_file->get('dbpasswd'), - $this->config_php_file->get('dbname'), - $this->config_php_file->get('dbport'), - defined('PHPBB_DB_NEW_LINK') && PHPBB_DB_NEW_LINK - ); - } - - return $this->dbal_connection; + return $this->cache_dir ?: $this->phpbb_root_path . 'cache/' . $this->get_environment() . '/'; } /** - * Get enabled extensions. - * - * @return array enabled extensions - */ - protected function get_installed_extensions() + * Load the enabled extensions. + */ + protected function load_extensions() { - $db = $this->get_dbal_connection(); - $extension_table = $this->config_php_file->get('table_prefix') . 'ext'; + if ($this->config_php_file !== null) + { + // Build an intermediate container to load the ext list from the database + $container_builder = new container_builder($this->phpbb_root_path, $this->php_ext); + $ext_container = $container_builder + ->without_cache() + ->without_extensions() + ->with_config($this->config_php_file) + ->with_environment('production') + ->without_compiled_container() + ->get_container() + ; + + $ext_container->register('cache.driver', '\\phpbb\\cache\\driver\\dummy'); + $ext_container->compile(); + + $extensions = $ext_container->get('ext.manager')->all_enabled(); + + // Load each extension found + foreach ($extensions as $ext_name => $path) + { + $extension_class = '\\' . str_replace('/', '\\', $ext_name) . '\\di\\extension'; - $sql = 'SELECT * - FROM ' . $extension_table . ' - WHERE ext_active = 1'; + if (!class_exists($extension_class)) + { + $extension_class = '\\phpbb\\extension\\di\\extension_base'; + } - $result = $db->sql_query($sql); - $rows = $db->sql_fetchrowset($result); - $db->sql_freeresult($result); + $this->container_extensions[] = new $extension_class($ext_name, $path); - $exts = array(); - foreach ($rows as $row) + // Load extension autoloader + $filename = $path . 'vendor/autoload.php'; + if (file_exists($filename)) + { + require $filename; + } + } + } + else { - $exts[$row['ext_name']] = $this->phpbb_root_path . 'ext/' . $row['ext_name'] . '/'; + // To load the extensions we need the database credentials. + // Automatically disable the extensions if we don't have them. + $this->use_extensions = false; } + } + + /** + * Dump the container to the disk. + * + * @param ConfigCache $cache The config cache + */ + protected function dump_container($cache) + { + try + { + $dumper = new PhpDumper($this->container); + $cached_container_dump = $dumper->dump(array( + 'class' => 'phpbb_cache_container', + 'base_class' => 'Symfony\\Component\\DependencyInjection\\ContainerBuilder', + )); - return $exts; + $cache->write($cached_container_dump, $this->container->getResources()); + } + catch (IOException $e) + { + // Don't fail if the cache isn't writeable + } } /** - * Create the ContainerBuilder object - * - * @param array $extensions Array of Container extension objects - * @return ContainerBuilder object - */ + * Create the ContainerBuilder object + * + * @param array $extensions Array of Container extension objects + * @return ContainerBuilder object + */ 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->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; } /** - * Get the filename under which the dumped container will be stored. - * - * @return string Path for dumped container - */ + * Get the filename under which the dumped container will be stored. + * + * @return string Path for dumped container + */ protected function get_container_filename() { - return $this->phpbb_root_path . 'cache/container_' . md5($this->phpbb_root_path) . '.' . $this->php_ext; + return $this->get_cache_dir() . 'container_' . md5($this->phpbb_root_path) . '.' . $this->php_ext; + } + + /** + * Return the name of the current environment. + * + * @return string + */ + protected function get_environment() + { + return $this->environment ?: 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 02c2a1b9d6..e042d0a5d1 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( @@ -266,7 +266,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) @@ -280,7 +280,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"; @@ -371,7 +371,6 @@ class md_exporter { $files_list = array( 'prosilver' => array(), - 'subsilver2' => array(), 'adm' => array(), ); @@ -391,10 +390,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/bbcode.php b/phpBB/phpbb/help/controller/bbcode.php new file mode 100644 index 0000000000..e16f99023d --- /dev/null +++ b/phpBB/phpbb/help/controller/bbcode.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\help\controller; + +/** + * BBCode help page + */ +class bbcode extends controller +{ + /** + * @return string The title of the page + */ + public function display() + { + $this->language->add_lang('help/bbcode'); + + $this->manager->add_block( + 'HELP_BBCODE_BLOCK_INTRO', + false, + array( + 'HELP_BBCODE_INTRO_BBCODE_QUESTION' => 'HELP_BBCODE_INTRO_BBCODE_ANSWER', + ) + ); + $this->manager->add_block( + 'HELP_BBCODE_BLOCK_TEXT', + false, + array( + 'HELP_BBCODE_TEXT_BASIC_QUESTION' => 'HELP_BBCODE_TEXT_BASIC_ANSWER', + 'HELP_BBCODE_TEXT_COLOR_QUESTION' => 'HELP_BBCODE_TEXT_COLOR_ANSWER', + 'HELP_BBCODE_TEXT_COMBINE_QUESTION' => 'HELP_BBCODE_TEXT_COMBINE_ANSWER', + ) + ); + $this->manager->add_block( + 'HELP_BBCODE_BLOCK_QUOTES', + false, + array( + 'HELP_BBCODE_QUOTES_TEXT_QUESTION' => 'HELP_BBCODE_QUOTES_TEXT_ANSWER', + 'HELP_BBCODE_QUOTES_CODE_QUESTION' => 'HELP_BBCODE_QUOTES_CODE_ANSWER', + ) + ); + $this->manager->add_block( + 'HELP_BBCODE_BLOCK_LISTS', + false, + array( + 'HELP_BBCODE_LISTS_UNORDERER_QUESTION' => 'HELP_BBCODE_LISTS_UNORDERER_ANSWER', + 'HELP_BBCODE_LISTS_ORDERER_QUESTION' => 'HELP_BBCODE_LISTS_ORDERER_ANSWER', + ) + ); + $this->manager->add_block( + 'HELP_BBCODE_BLOCK_LINKS', + true, + array( + 'HELP_BBCODE_LINKS_BASIC_QUESTION' => 'HELP_BBCODE_LINKS_BASIC_ANSWER', + ) + ); + $this->manager->add_block( + 'HELP_BBCODE_BLOCK_IMAGES', + false, + array( + 'HELP_BBCODE_IMAGES_BASIC_QUESTION' => 'HELP_BBCODE_IMAGES_BASIC_ANSWER', + 'HELP_BBCODE_IMAGES_ATTACHMENT_QUESTION' => 'HELP_BBCODE_IMAGES_ATTACHMENT_ANSWER', + ) + ); + $this->manager->add_block( + 'HELP_BBCODE_BLOCK_OTHERS', + false, + array( + 'HELP_BBCODE_OTHERS_CUSTOM_QUESTION' => 'HELP_BBCODE_OTHERS_CUSTOM_ANSWER', + ) + ); + + return $this->language->lang('BBCODE_GUIDE'); + } +} diff --git a/phpBB/phpbb/help/controller/controller.php b/phpBB/phpbb/help/controller/controller.php new file mode 100644 index 0000000000..29494205a9 --- /dev/null +++ b/phpBB/phpbb/help/controller/controller.php @@ -0,0 +1,76 @@ +<?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; + +/** + * BBCode help page + */ +abstract class controller +{ + /** @var \phpbb\controller\helper */ + protected $helper; + + /** @var \phpbb\help\manager */ + protected $manager; + + /** @var \phpbb\template\template */ + protected $template; + + /** @var \phpbb\language\language */ + protected $language; + + /** @var string */ + protected $root_path; + + /** @var string */ + protected $php_ext; + + /** + * Constructor + * + * @param \phpbb\controller\helper $helper + * @param \phpbb\help\manager $manager + * @param \phpbb\template\template $template + * @param \phpbb\language\language $language + * @param string $root_path + * @param string $php_ext + */ + public function __construct(\phpbb\controller\helper $helper, \phpbb\help\manager $manager, \phpbb\template\template $template, \phpbb\language\language $language, $root_path, $php_ext) + { + $this->helper = $helper; + $this->manager = $manager; + $this->template = $template; + $this->language = $language; + $this->root_path = $root_path; + $this->php_ext = $php_ext; + } + + /** + * @return string + */ + abstract protected function display(); + + public function handle() + { + $title = $this->display(); + + $this->template->assign_vars(array( + 'L_FAQ_TITLE' => $title, + 'S_IN_FAQ' => true, + )); + + make_jumpbox(append_sid("{$this->root_path}viewforum.{$this->php_ext}")); + return $this->helper->render('faq_body.html', $title); + } +} diff --git a/phpBB/phpbb/help/controller/faq.php b/phpBB/phpbb/help/controller/faq.php new file mode 100644 index 0000000000..5e45cfe667 --- /dev/null +++ b/phpBB/phpbb/help/controller/faq.php @@ -0,0 +1,165 @@ +<?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; + +/** + * FAQ help page + */ +class faq extends controller +{ + /** + * @return string The title of the page + */ + public function display() + { + $this->language->add_lang('help/faq'); + + $this->manager->add_block( + 'HELP_FAQ_BLOCK_LOGIN', + false, + array( + 'HELP_FAQ_LOGIN_REGISTER_QUESTION' => 'HELP_FAQ_LOGIN_REGISTER_ANSWER', + 'HELP_FAQ_LOGIN_COPPA_QUESTION' => 'HELP_FAQ_LOGIN_COPPA_ANSWER', + 'HELP_FAQ_LOGIN_CANNOT_REGISTER_QUESTION' => 'HELP_FAQ_LOGIN_CANNOT_REGISTER_ANSWER', + 'HELP_FAQ_LOGIN_REGISTER_CONFIRM_QUESTION' => 'HELP_FAQ_LOGIN_REGISTER_CONFIRM_ANSWER', + 'HELP_FAQ_LOGIN_CANNOT_LOGIN_QUESTION' => 'HELP_FAQ_LOGIN_CANNOT_LOGIN_ANSWER', + 'HELP_FAQ_LOGIN_CANNOT_LOGIN_ANYMORE_QUESTION' => 'HELP_FAQ_LOGIN_CANNOT_LOGIN_ANYMORE_ANSWER', + 'HELP_FAQ_LOGIN_LOST_PASSWORD_QUESTION' => 'HELP_FAQ_LOGIN_LOST_PASSWORD_ANSWER', + 'HELP_FAQ_LOGIN_AUTO_LOGOUT_QUESTION' => 'HELP_FAQ_LOGIN_AUTO_LOGOUT_ANSWER', + 'HELP_FAQ_LOGIN_DELETE_COOKIES_QUESTION' => 'HELP_FAQ_LOGIN_DELETE_COOKIES_ANSWER', + ) + ); + $this->manager->add_block( + 'HELP_FAQ_BLOCK_USERSETTINGS', + false, + array( + 'HELP_FAQ_USERSETTINGS_CHANGE_SETTINGS_QUESTION' => 'HELP_FAQ_USERSETTINGS_CHANGE_SETTINGS_ANSWER', + 'HELP_FAQ_USERSETTINGS_HIDE_ONLINE_QUESTION' => 'HELP_FAQ_USERSETTINGS_HIDE_ONLINE_ANSWER', + 'HELP_FAQ_USERSETTINGS_TIMEZONE_QUESTION' => 'HELP_FAQ_USERSETTINGS_TIMEZONE_ANSWER', + 'HELP_FAQ_USERSETTINGS_SERVERTIME_QUESTION' => 'HELP_FAQ_USERSETTINGS_SERVERTIME_ANSWER', + 'HELP_FAQ_USERSETTINGS_LANGUAGE_QUESTION' => 'HELP_FAQ_USERSETTINGS_LANGUAGE_ANSWER', + 'HELP_FAQ_USERSETTINGS_AVATAR_QUESTION' => 'HELP_FAQ_USERSETTINGS_AVATAR_ANSWER', + 'HELP_FAQ_USERSETTINGS_AVATAR_DISPLAY_QUESTION' => 'HELP_FAQ_USERSETTINGS_AVATAR_DISPLAY_ANSWER', + 'HELP_FAQ_USERSETTINGS_RANK_QUESTION' => 'HELP_FAQ_USERSETTINGS_RANK_ANSWER', + 'HELP_FAQ_USERSETTINGS_EMAIL_LOGIN_QUESTION' => 'HELP_FAQ_USERSETTINGS_EMAIL_LOGIN_ANSWER', + ) + ); + $this->manager->add_block( + 'HELP_FAQ_BLOCK_POSTING', + false, + array( + 'HELP_FAQ_POSTING_CREATE_QUESTION' => 'HELP_FAQ_POSTING_CREATE_ANSWER', + 'HELP_FAQ_POSTING_EDIT_DELETE_QUESTION' => 'HELP_FAQ_POSTING_EDIT_DELETE_ANSWER', + 'HELP_FAQ_POSTING_SIGNATURE_QUESTION' => 'HELP_FAQ_POSTING_SIGNATURE_ANSWER', + 'HELP_FAQ_POSTING_POLL_CREATE_QUESTION' => 'HELP_FAQ_POSTING_POLL_CREATE_ANSWER', + 'HELP_FAQ_POSTING_POLL_ADD_QUESTION' => 'HELP_FAQ_POSTING_POLL_ADD_ANSWER', + 'HELP_FAQ_POSTING_POLL_EDIT_QUESTION' => 'HELP_FAQ_POSTING_POLL_EDIT_ANSWER', + 'HELP_FAQ_POSTING_FORUM_RESTRICTED_QUESTION' => 'HELP_FAQ_POSTING_FORUM_RESTRICTED_ANSWER', + 'HELP_FAQ_POSTING_NO_ATTACHMENTS_QUESTION' => 'HELP_FAQ_POSTING_NO_ATTACHMENTS_ANSWER', + 'HELP_FAQ_POSTING_WARNING_QUESTION' => 'HELP_FAQ_POSTING_WARNING_ANSWER', + 'HELP_FAQ_POSTING_REPORT_QUESTION' => 'HELP_FAQ_POSTING_REPORT_ANSWER', + 'HELP_FAQ_POSTING_DRAFT_QUESTION' => 'HELP_FAQ_POSTING_DRAFT_ANSWER', + 'HELP_FAQ_POSTING_QUEUE_QUESTION' => 'HELP_FAQ_POSTING_QUEUE_ANSWER', + 'HELP_FAQ_POSTING_BUMP_QUESTION' => 'HELP_FAQ_POSTING_BUMP_ANSWER', + ) + ); + $this->manager->add_block( + 'HELP_FAQ_BLOCK_FORMATTING', + false, + array( + 'HELP_FAQ_FORMATTING_BBOCDE_QUESTION' => 'HELP_FAQ_FORMATTING_BBOCDE_ANSWER', + 'HELP_FAQ_FORMATTING_HTML_QUESTION' => 'HELP_FAQ_FORMATTING_HTML_ANSWER', + 'HELP_FAQ_FORMATTING_SMILIES_QUESTION' => 'HELP_FAQ_FORMATTING_SMILIES_ANSWER', + 'HELP_FAQ_FORMATTING_IMAGES_QUESTION' => 'HELP_FAQ_FORMATTING_IMAGES_ANSWER', + 'HELP_FAQ_FORMATTING_GLOBAL_ANNOUNCE_QUESTION' => 'HELP_FAQ_FORMATTING_GLOBAL_ANNOUNCE_ANSWER', + 'HELP_FAQ_FORMATTING_ANNOUNCEMENT_QUESTION' => 'HELP_FAQ_FORMATTING_ANNOUNCEMENT_ANSWER', + 'HELP_FAQ_FORMATTING_STICKIES_QUESTION' => 'HELP_FAQ_FORMATTING_STICKIES_ANSWER', + 'HELP_FAQ_FORMATTING_LOCKED_QUESTION' => 'HELP_FAQ_FORMATTING_LOCKED_ANSWER', + 'HELP_FAQ_FORMATTING_ICONS_QUESTION' => 'HELP_FAQ_FORMATTING_ICONS_ANSWER', + ) + ); + $this->manager->add_block( + 'HELP_FAQ_BLOCK_GROUPS', + true, + array( + 'HELP_FAQ_GROUPS_ADMINISTRATORS_QUESTION' => 'HELP_FAQ_GROUPS_ADMINISTRATORS_ANSWER', + 'HELP_FAQ_GROUPS_MODERATORS_QUESTION' => 'HELP_FAQ_GROUPS_MODERATORS_ANSWER', + 'HELP_FAQ_GROUPS_USERGROUPS_QUESTION' => 'HELP_FAQ_GROUPS_USERGROUPS_ANSWER', + 'HELP_FAQ_GROUPS_USERGROUPS_JOIN_QUESTION' => 'HELP_FAQ_GROUPS_USERGROUPS_JOIN_ANSWER', + 'HELP_FAQ_GROUPS_USERGROUPS_LEAD_QUESTION' => 'HELP_FAQ_GROUPS_USERGROUPS_LEAD_ANSWER', + 'HELP_FAQ_GROUPS_COLORS_QUESTION' => 'HELP_FAQ_GROUPS_COLORS_ANSWER', + 'HELP_FAQ_GROUPS_DEFAULT_QUESTION' => 'HELP_FAQ_GROUPS_DEFAULT_ANSWER', + 'HELP_FAQ_GROUPS_TEAM_QUESTION' => 'HELP_FAQ_GROUPS_TEAM_ANSWER', + ) + ); + $this->manager->add_block( + 'HELP_FAQ_BLOCK_PMS', + false, + array( + 'HELP_FAQ_PMS_CANNOT_SEND_QUESTION' => 'HELP_FAQ_PMS_CANNOT_SEND_ANSWER', + 'HELP_FAQ_PMS_UNWANTED_QUESTION' => 'HELP_FAQ_PMS_UNWANTED_ANSWER', + 'HELP_FAQ_PMS_SPAM_QUESTION' => 'HELP_FAQ_PMS_SPAM_ANSWER', + ) + ); + $this->manager->add_block( + 'HELP_FAQ_BLOCK_FRIENDS', + false, + array( + 'HELP_FAQ_FRIENDS_BASIC_QUESTION' => 'HELP_FAQ_FRIENDS_BASIC_ANSWER', + 'HELP_FAQ_FRIENDS_MANAGE_QUESTION' => 'HELP_FAQ_FRIENDS_MANAGE_ANSWER', + ) + ); + $this->manager->add_block( + 'HELP_FAQ_BLOCK_SEARCH', + false, + array( + 'HELP_FAQ_SEARCH_FORUM_QUESTION' => 'HELP_FAQ_SEARCH_FORUM_ANSWER', + 'HELP_FAQ_SEARCH_NO_RESULT_QUESTION' => 'HELP_FAQ_SEARCH_NO_RESULT_ANSWER', + 'HELP_FAQ_SEARCH_BLANK_QUESTION' => 'HELP_FAQ_SEARCH_BLANK_ANSWER', + 'HELP_FAQ_SEARCH_MEMBERS_QUESTION' => 'HELP_FAQ_SEARCH_MEMBERS_ANSWER', + 'HELP_FAQ_SEARCH_OWN_QUESTION' => 'HELP_FAQ_SEARCH_OWN_ANSWER', + ) + ); + $this->manager->add_block( + 'HELP_FAQ_BLOCK_BOOKMARKS', + false, + array( + 'HELP_FAQ_BOOKMARKS_DIFFERENCE_QUESTION' => 'HELP_FAQ_BOOKMARKS_DIFFERENCE_ANSWER', + 'HELP_FAQ_BOOKMARKS_TOPIC_QUESTION' => 'HELP_FAQ_BOOKMARKS_TOPIC_ANSWER', + 'HELP_FAQ_BOOKMARKS_FORUM_QUESTION' => 'HELP_FAQ_BOOKMARKS_FORUM_ANSWER', + 'HELP_FAQ_BOOKMARKS_REMOVE_QUESTION' => 'HELP_FAQ_BOOKMARKS_REMOVE_ANSWER', + ) + ); + $this->manager->add_block( + 'HELP_FAQ_BLOCK_ATTACHMENTS', + false, + array( + 'HELP_FAQ_ATTACHMENTS_ALLOWED_QUESTION' => 'HELP_FAQ_ATTACHMENTS_ALLOWED_ANSWER', + 'HELP_FAQ_ATTACHMENTS_OWN_QUESTION' => 'HELP_FAQ_ATTACHMENTS_OWN_ANSWER', + ) + ); + $this->manager->add_block( + 'HELP_FAQ_BLOCK_ISSUES', + false, + array( + 'HELP_FAQ_ISSUES_WHOIS_PHPBB_QUESTION' => 'HELP_FAQ_ISSUES_WHOIS_PHPBB_ANSWER', + 'HELP_FAQ_ISSUES_FEATURE_QUESTION' => 'HELP_FAQ_ISSUES_FEATURE_ANSWER', + 'HELP_FAQ_ISSUES_LEGAL_QUESTION' => 'HELP_FAQ_ISSUES_LEGAL_ANSWER', + 'HELP_FAQ_ISSUES_ADMIN_QUESTION' => 'HELP_FAQ_ISSUES_ADMIN_ANSWER', + ) + ); + + return $this->language->lang('FAQ_EXPLAIN'); + } +} 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/help/manager.php b/phpBB/phpbb/help/manager.php new file mode 100644 index 0000000000..d6991c0733 --- /dev/null +++ b/phpBB/phpbb/help/manager.php @@ -0,0 +1,136 @@ +<?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; + +/** + * Class help page manager + */ +class manager +{ + /** @var \phpbb\event\dispatcher */ + protected $dispatcher; + + /** @var \phpbb\language\language */ + protected $language; + + /** @var \phpbb\template\template */ + protected $template; + + /** @var bool */ + protected $switched_column; + + /** + * Constructor + * + * @param \phpbb\event\dispatcher $dispatcher + * @param \phpbb\language\language $language + * @param \phpbb\template\template $template + */ + public function __construct(\phpbb\event\dispatcher $dispatcher, \phpbb\language\language $language, \phpbb\template\template $template) + { + $this->dispatcher = $dispatcher; + $this->language = $language; + $this->template = $template; + } + + /** + * Add a new faq block + * + * @param string $block_name Name or language key with the name of the block + * @param bool $switch_column Switch the column of the menu + */ + public function add_block($block_name, $switch_column = false, $questions = array()) + { + /** + * You can use this event to add a block before the current one. + * + * @event core.help_manager_add_block_before + * @var string block_name Language key of the block headline + * @var bool switch_column Should we switch the menu column before this headline + * @var array questions Array with questions + * @since 3.2.0-a1 + */ + $vars = array('block_name', 'switch_column', 'questions'); + extract($this->dispatcher->trigger_event('core.help_manager_add_block_before', compact($vars))); + + $this->template->assign_block_vars('faq_block', array( + 'BLOCK_TITLE' => $this->language->lang($block_name), + 'SWITCH_COLUMN' => !$this->switched_column && $switch_column, + )); + + foreach ($questions as $question => $answer) + { + $this->add_question($question, $answer); + } + + $this->switched_column = $this->switched_column || $switch_column; + + /** + * You can use this event to add a block after the current one. + * + * @event core.help_manager_add_block_after + * @var string block_name Language key of the block headline + * @var bool switch_column Should we switch the menu column before this headline + * @var array questions Array with questions + * @since 3.2.0-a1 + */ + $vars = array('block_name', 'switch_column', 'questions'); + extract($this->dispatcher->trigger_event('core.help_manager_add_block_after', compact($vars))); + } + + /** + * Add a new faq question + * + * @param string $question Question or language key with the question of the block + * @param string $answer Answer or language key with the answer of the block + */ + public function add_question($question, $answer) + { + /** + * You can use this event to add a question before the current one. + * + * @event core.help_manager_add_question_before + * @var string question Language key of the question + * @var string answer Language key of the answer + * @since 3.2.0-a1 + */ + $vars = array('question', 'answer'); + extract($this->dispatcher->trigger_event('core.help_manager_add_question_before', compact($vars))); + + $this->template->assign_block_vars('faq_block.faq_row', array( + 'FAQ_QUESTION' => $this->language->lang($question), + 'FAQ_ANSWER' => $this->language->lang($answer), + )); + + /** + * You can use this event to add a question after the current one. + * + * @event core.help_manager_add_question_after + * @var string question Language key of the question + * @var string answer Language key of the answer + * @since 3.2.0-a1 + */ + $vars = array('question', 'answer'); + extract($this->dispatcher->trigger_event('core.help_manager_add_question_after', compact($vars))); + } + + /** + * Returns whether the block titles switched side + * @return bool + */ + public function switched_column() + { + return $this->switched_column; + } +} 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..b2b9f5ce12 --- /dev/null +++ b/phpBB/phpbb/language/language.php @@ -0,0 +1,566 @@ +<?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(); + $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 + * + * 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..359202fd63 --- /dev/null +++ b/phpBB/phpbb/language/language_file_loader.php @@ -0,0 +1,206 @@ +<?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 language_file_not_found When the path to the file cannot be resolved + */ + protected function get_language_file_path($path, $filename, $locales) + { + $language_file_path = $filename; + + // 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) + { + // Do not suppress error if in DEBUG mode + if (defined('DEBUG')) + { + include $path; + } + else + { + @include $path; + } + } +} 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 3d995b4e4a..1b02d98b82 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/module/exception/module_exception.php b/phpBB/phpbb/module/exception/module_exception.php new file mode 100644 index 0000000000..8ad75112bc --- /dev/null +++ b/phpBB/phpbb/module/exception/module_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\module\exception; + +class module_exception extends \phpbb\exception\runtime_exception +{ + +} diff --git a/phpBB/phpbb/module/exception/module_not_found_exception.php b/phpBB/phpbb/module/exception/module_not_found_exception.php new file mode 100644 index 0000000000..2d485e7b35 --- /dev/null +++ b/phpBB/phpbb/module/exception/module_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\module\exception; + +class module_not_found_exception extends module_exception +{ + +} diff --git a/phpBB/phpbb/module/module_manager.php b/phpBB/phpbb/module/module_manager.php new file mode 100644 index 0000000000..a812d06736 --- /dev/null +++ b/phpBB/phpbb/module/module_manager.php @@ -0,0 +1,564 @@ +<?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\module; + +use phpbb\module\exception\module_exception; +use phpbb\module\exception\module_not_found_exception; + +class module_manager +{ + /** + * @var \phpbb\cache\driver\driver_interface + */ + protected $cache; + + /** + * @var \phpbb\db\driver\driver_interface + */ + protected $db; + + /** + * @var \phpbb\extension\manager + */ + protected $extension_manager; + + /** + * @var string + */ + protected $modules_table; + + /** + * @var string + */ + protected $phpbb_root_path; + + /** + * @var string + */ + protected $php_ext; + + /** + * Constructor + * + * @param \phpbb\cache\driver\driver_interface $cache Cache driver + * @param \phpbb\db\driver\driver_interface $db Database driver + * @param \phpbb\extension\manager $ext_manager Extension manager + * @param string $modules_table Module database table's name + * @param string $phpbb_root_path Path to phpBB's root + * @param string $php_ext Extension of PHP files + */ + public function __construct(\phpbb\cache\driver\driver_interface $cache, \phpbb\db\driver\driver_interface $db, \phpbb\extension\manager $ext_manager, $modules_table, $phpbb_root_path, $php_ext) + { + $this->cache = $cache; + $this->db = $db; + $this->extension_manager = $ext_manager; + $this->modules_table = $modules_table; + $this->phpbb_root_path = $phpbb_root_path; + $this->php_ext = $php_ext; + } + + /** + * Get row for specified module + * + * @param int $module_id ID of the module + * @param string $module_class Class of the module (acp, ucp, mcp etc...) + * + * @return array Array of data fetched from the database + * + * @throws \phpbb\module\exception\module_not_found_exception When there is no module with $module_id + */ + public function get_module_row($module_id, $module_class) + { + $module_id = (int) $module_id; + + $sql = 'SELECT * + FROM ' . $this->modules_table . " + WHERE module_class = '" . $this->db->sql_escape($module_class) . "' + AND module_id = $module_id"; + $result = $this->db->sql_query($sql); + $row = $this->db->sql_fetchrow($result); + $this->db->sql_freeresult($result); + + if (!$row) + { + throw new module_not_found_exception('NO_MODULE'); + } + + return $row; + } + + /** + * Get available module information from module files + * + * @param string $module_class Class of the module (acp, ucp, mcp etc...) + * @param string $module ID of module + * @param bool $use_all_available Use all available instead of just all + * enabled extensions + * + * @return array Array with module information gathered from module info files. + */ + public function get_module_infos($module_class, $module = '', $use_all_available = false) + { + $directory = $this->phpbb_root_path . 'includes/' . $module_class . '/info/'; + $fileinfo = array(); + + $finder = $this->extension_manager->get_finder($use_all_available); + + $modules = $finder + ->extension_suffix('_module') + ->extension_directory("/$module_class") + ->core_path("includes/$module_class/info/") + ->core_prefix($module_class . '_') + ->get_classes(true); + + foreach ($modules as $cur_module) + { + // Skip entries we do not need if we know the module we are + // looking for + if ($module && strpos(str_replace('\\', '_', $cur_module), $module) === false && $module !== $cur_module) + { + continue; + } + + $info_class = preg_replace('/_module$/', '_info', $cur_module); + + // If the class does not exist it might be following the old + // format. phpbb_acp_info_acp_foo needs to be turned into + // acp_foo_info and the respective file has to be included + // manually because it does not support auto loading + $old_info_class_file = str_replace("phpbb_{$module_class}_info_", '', $cur_module); + $old_info_class = $old_info_class_file . '_info'; + + if (class_exists($old_info_class)) + { + $info_class = $old_info_class; + } + else if (!class_exists($info_class)) + { + $info_class = $old_info_class; + + // need to check class exists again because previous checks triggered autoloading + if (!class_exists($info_class) && file_exists($directory . $old_info_class_file . '.' . $this->php_ext)) + { + include($directory . $old_info_class_file . '.' . $this->php_ext); + } + } + + if (class_exists($info_class)) + { + $info = new $info_class(); + $module_info = $info->module(); + + $main_class = (isset($module_info['filename'])) ? $module_info['filename'] : $cur_module; + + $fileinfo[$main_class] = $module_info; + } + } + + ksort($fileinfo); + + return $fileinfo; + } + + /** + * Get module branch + * + * @param int $module_id ID of the module + * @param string $module_class Class of the module (acp, ucp, mcp etc...) + * @param string $type Type of branch (Expected values: all, parents or children) + * @param bool $include_module Whether or not to include the specified module with $module_id + * + * @return array Returns an array containing the modules in the specified branch type. + */ + public function get_module_branch($module_id, $module_class, $type = 'all', $include_module = true) + { + $module_id = (int) $module_id; + + switch ($type) + { + case 'parents': + $condition = 'm1.left_id BETWEEN m2.left_id AND m2.right_id'; + break; + + case 'children': + $condition = 'm2.left_id BETWEEN m1.left_id AND m1.right_id'; + break; + + default: + $condition = 'm2.left_id BETWEEN m1.left_id AND m1.right_id OR m1.left_id BETWEEN m2.left_id AND m2.right_id'; + break; + } + + $rows = array(); + + $sql = 'SELECT m2.* + FROM ' . $this->modules_table . ' m1 + LEFT JOIN ' . $this->modules_table . " m2 ON ($condition) + WHERE m1.module_class = '" . $this->db->sql_escape($module_class) . "' + AND m2.module_class = '" . $this->db->sql_escape($module_class) . "' + AND m1.module_id = $module_id + ORDER BY m2.left_id DESC"; + $result = $this->db->sql_query($sql); + + while ($row = $this->db->sql_fetchrow($result)) + { + if (!$include_module && $row['module_id'] == $module_id) + { + continue; + } + + $rows[] = $row; + } + $this->db->sql_freeresult($result); + + return $rows; + } + + /** + * Remove modules cache file + * + * @param string $module_class Class of the module (acp, ucp, mcp etc...) + */ + public function remove_cache_file($module_class) + { + // Sanitise for future path use, it's escaped as appropriate for queries + $cache_class = str_replace(array('.', '/', '\\'), '', basename($module_class)); + $this->cache->destroy('_modules_' . $cache_class); + $this->cache->destroy('sql', $this->modules_table); + } + + /** + * Update/Add module + * + * @param array &$module_data The module data + * + * @throws \phpbb\module\exception\module_not_found_exception When parent module or the category is not exist + */ + public function update_module_data(&$module_data) + { + if (!isset($module_data['module_id'])) + { + // no module_id means we're creating a new category/module + if ($module_data['parent_id']) + { + $sql = 'SELECT left_id, right_id + FROM ' . $this->modules_table . " + WHERE module_class = '" . $this->db->sql_escape($module_data['module_class']) . "' + AND module_id = " . (int) $module_data['parent_id']; + $result = $this->db->sql_query($sql); + $row = $this->db->sql_fetchrow($result); + $this->db->sql_freeresult($result); + + if (!$row) + { + throw new module_not_found_exception('PARENT_NOT_EXIST'); + } + + // Workaround + $row['left_id'] = (int) $row['left_id']; + $row['right_id'] = (int) $row['right_id']; + + $sql = 'UPDATE ' . $this->modules_table . " + SET left_id = left_id + 2, right_id = right_id + 2 + WHERE module_class = '" . $this->db->sql_escape($module_data['module_class']) . "' + AND left_id > {$row['right_id']}"; + $this->db->sql_query($sql); + + $sql = 'UPDATE ' . $this->modules_table . " + SET right_id = right_id + 2 + WHERE module_class = '" . $this->db->sql_escape($module_data['module_class']) . "' + AND {$row['left_id']} BETWEEN left_id AND right_id"; + $this->db->sql_query($sql); + + $module_data['left_id'] = (int) $row['right_id']; + $module_data['right_id'] = (int) $row['right_id'] + 1; + } + else + { + $sql = 'SELECT MAX(right_id) AS right_id + FROM ' . $this->modules_table . " + WHERE module_class = '" . $this->db->sql_escape($module_data['module_class']) . "'"; + $result = $this->db->sql_query($sql); + $row = $this->db->sql_fetchrow($result); + $this->db->sql_freeresult($result); + + $module_data['left_id'] = (int) $row['right_id'] + 1; + $module_data['right_id'] = (int) $row['right_id'] + 2; + } + + $sql = 'INSERT INTO ' . $this->modules_table . ' ' . $this->db->sql_build_array('INSERT', $module_data); + $this->db->sql_query($sql); + + $module_data['module_id'] = $this->db->sql_nextid(); + } + else + { + $row = $this->get_module_row($module_data['module_id'], $module_data['module_class']); + + if ($module_data['module_basename'] && !$row['module_basename']) + { + // we're turning a category into a module + $branch = $this->get_module_branch($module_data['module_id'], $module_data['module_class'], 'children', false); + + if (sizeof($branch)) + { + throw new module_not_found_exception('NO_CATEGORY_TO_MODULE'); + } + } + + if ($row['parent_id'] != $module_data['parent_id']) + { + $this->move_module($module_data['module_id'], $module_data['parent_id'], $module_data['module_class']); + } + + $update_ary = $module_data; + unset($update_ary['module_id']); + + $sql = 'UPDATE ' . $this->modules_table . ' + SET ' . $this->db->sql_build_array('UPDATE', $update_ary) . " + WHERE module_class = '" . $this->db->sql_escape($module_data['module_class']) . "' + AND module_id = " . (int) $module_data['module_id']; + $this->db->sql_query($sql); + } + } + + /** + * Move module around the tree + * + * @param int $from_module_id ID of the current parent module + * @param int $to_parent_id ID of the target parent module + * @param string $module_class Class of the module (acp, ucp, mcp etc...) + * + * @throws \phpbb\module\exception\module_not_found_exception If the module specified to move modules from does not + * have any children. + */ + public function move_module($from_module_id, $to_parent_id, $module_class) + { + $moved_modules = $this->get_module_branch($from_module_id, $module_class, 'children'); + + if (empty($moved_modules)) + { + throw new module_not_found_exception(); + } + + $from_data = $moved_modules[0]; + $diff = sizeof($moved_modules) * 2; + + $moved_ids = array(); + for ($i = 0; $i < sizeof($moved_modules); ++$i) + { + $moved_ids[] = $moved_modules[$i]['module_id']; + } + + // Resync parents + $sql = 'UPDATE ' . $this->modules_table . " + SET right_id = right_id - $diff + WHERE module_class = '" . $this->db->sql_escape($module_class) . "' + AND left_id < " . (int) $from_data['right_id'] . ' + AND right_id > ' . (int) $from_data['right_id']; + $this->db->sql_query($sql); + + // Resync righthand side of tree + $sql = 'UPDATE ' . $this->modules_table . " + SET left_id = left_id - $diff, right_id = right_id - $diff + WHERE module_class = '" . $this->db->sql_escape($module_class) . "' + AND left_id > " . (int) $from_data['right_id']; + $this->db->sql_query($sql); + + if ($to_parent_id > 0) + { + $to_data = $this->get_module_row($to_parent_id, $module_class); + + // Resync new parents + $sql = 'UPDATE ' . $this->modules_table . " + SET right_id = right_id + $diff + WHERE module_class = '" . $this->db->sql_escape($module_class) . "' + AND " . (int) $to_data['right_id'] . ' BETWEEN left_id AND right_id + AND ' . $this->db->sql_in_set('module_id', $moved_ids, true); + $this->db->sql_query($sql); + + // Resync the righthand side of the tree + $sql = 'UPDATE ' . $this->modules_table . " + SET left_id = left_id + $diff, right_id = right_id + $diff + WHERE module_class = '" . $this->db->sql_escape($module_class) . "' + AND left_id > " . (int) $to_data['right_id'] . ' + AND ' . $this->db->sql_in_set('module_id', $moved_ids, true); + $this->db->sql_query($sql); + + // Resync moved branch + $to_data['right_id'] += $diff; + if ($to_data['right_id'] > $from_data['right_id']) + { + $diff = '+ ' . ($to_data['right_id'] - $from_data['right_id'] - 1); + } + else + { + $diff = '- ' . abs($to_data['right_id'] - $from_data['right_id'] - 1); + } + } + else + { + $sql = 'SELECT MAX(right_id) AS right_id + FROM ' . $this->modules_table . " + WHERE module_class = '" . $this->db->sql_escape($module_class) . "' + AND " . $this->db->sql_in_set('module_id', $moved_ids, true); + $result = $this->db->sql_query($sql); + $row = $this->db->sql_fetchrow($result); + $this->db->sql_freeresult($result); + + $diff = '+ ' . (int) ($row['right_id'] - $from_data['left_id'] + 1); + } + + $sql = 'UPDATE ' . $this->modules_table . " + SET left_id = left_id $diff, right_id = right_id $diff + WHERE module_class = '" . $this->db->sql_escape($module_class) . "' + AND " . $this->db->sql_in_set('module_id', $moved_ids); + $this->db->sql_query($sql); + } + + /** + * Remove module from tree + * + * @param int $module_id ID of the module to delete + * @param string $module_class Class of the module (acp, ucp, mcp etc...) + * + * @throws \phpbb\module\exception\module_exception When the specified module cannot be removed + */ + public function delete_module($module_id, $module_class) + { + $module_id = (int) $module_id; + + $row = $this->get_module_row($module_id, $module_class); + + $branch = $this->get_module_branch($module_id, $module_class, 'children', false); + + if (sizeof($branch)) + { + throw new module_exception('CANNOT_REMOVE_MODULE'); + } + + // If not move + $diff = 2; + $sql = 'DELETE FROM ' . $this->modules_table . " + WHERE module_class = '" . $this->db->sql_escape($module_class) . "' + AND module_id = $module_id"; + $this->db->sql_query($sql); + + $row['right_id'] = (int) $row['right_id']; + $row['left_id'] = (int) $row['left_id']; + + // Resync tree + $sql = 'UPDATE ' . $this->modules_table . " + SET right_id = right_id - $diff + WHERE module_class = '" . $this->db->sql_escape($module_class) . "' + AND left_id < {$row['right_id']} AND right_id > {$row['right_id']}"; + $this->db->sql_query($sql); + + $sql = 'UPDATE ' . $this->modules_table . " + SET left_id = left_id - $diff, right_id = right_id - $diff + WHERE module_class = '" . $this->db->sql_escape($module_class) . "' + AND left_id > {$row['right_id']}"; + $this->db->sql_query($sql); + } + + /** + * Move module position by $steps up/down + * + * @param array $module_row Array of module data + * @param string $module_class Class of the module (acp, ucp, mcp etc...) + * @param string $action Direction of moving (valid values: move_up or move_down) + * @param int $steps Number of steps to move module + * + * @return string Returns the language name of the module + * + * @throws \phpbb\module\exception\module_not_found_exception When the specified module does not exists + */ + public function move_module_by($module_row, $module_class, $action = 'move_up', $steps = 1) + { + /** + * Fetch all the siblings between the module's current spot + * and where we want to move it to. If there are less than $steps + * siblings between the current spot and the target then the + * module will move as far as possible + */ + $sql = 'SELECT module_id, left_id, right_id, module_langname + FROM ' . $this->modules_table . " + WHERE module_class = '" . $this->db->sql_escape($module_class) . "' + AND parent_id = " . (int) $module_row['parent_id'] . ' + AND ' . (($action == 'move_up') ? 'right_id < ' . (int) $module_row['right_id'] . ' ORDER BY right_id DESC' : 'left_id > ' . (int) $module_row['left_id'] . ' ORDER BY left_id ASC'); + $result = $this->db->sql_query_limit($sql, $steps); + + $target = array(); + while ($row = $this->db->sql_fetchrow($result)) + { + $target = $row; + } + $this->db->sql_freeresult($result); + + if (!sizeof($target)) + { + // The module is already on top or bottom + throw new module_not_found_exception(); + } + + /** + * $left_id and $right_id define the scope of the nodes that are affected by the move. + * $diff_up and $diff_down are the values to substract or add to each node's left_id + * and right_id in order to move them up or down. + * $move_up_left and $move_up_right define the scope of the nodes that are moving + * up. Other nodes in the scope of ($left_id, $right_id) are considered to move down. + */ + if ($action == 'move_up') + { + $left_id = (int) $target['left_id']; + $right_id = (int) $module_row['right_id']; + + $diff_up = (int) ($module_row['left_id'] - $target['left_id']); + $diff_down = (int) ($module_row['right_id'] + 1 - $module_row['left_id']); + + $move_up_left = (int) $module_row['left_id']; + $move_up_right = (int) $module_row['right_id']; + } + else + { + $left_id = (int) $module_row['left_id']; + $right_id = (int) $target['right_id']; + + $diff_up = (int) ($module_row['right_id'] + 1 - $module_row['left_id']); + $diff_down = (int) ($target['right_id'] - $module_row['right_id']); + + $move_up_left = (int) ($module_row['right_id'] + 1); + $move_up_right = (int) $target['right_id']; + } + + // Now do the dirty job + $sql = 'UPDATE ' . $this->modules_table . " + SET left_id = left_id + CASE + WHEN left_id BETWEEN {$move_up_left} AND {$move_up_right} THEN -{$diff_up} + ELSE {$diff_down} + END, + right_id = right_id + CASE + WHEN right_id BETWEEN {$move_up_left} AND {$move_up_right} THEN -{$diff_up} + ELSE {$diff_down} + END + WHERE module_class = '" . $this->db->sql_escape($module_class) . "' + AND left_id BETWEEN {$left_id} AND {$right_id} + AND right_id BETWEEN {$left_id} AND {$right_id}"; + $this->db->sql_query($sql); + + $this->remove_cache_file($module_class); + + return $target['module_langname']; + } +} 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 7c5c18aa47..0e2924e29c 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 96bfc86322..2d7925c1b4 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 d2f34f95d0..5d4f7fe0c6 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 e25fdcd808..f6b3136a21 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="(.+?)"#'; - - /** * 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 749cfe0b8e..e7d385f59f 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 aed31e8642..8289a04cf1 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 fb08a9eee1..f0681b9eb8 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..5af005769f --- /dev/null +++ b/phpBB/phpbb/routing/router.php @@ -0,0 +1,460 @@ +<?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\DependencyInjection\ContainerInterface; +use Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException; +use Symfony\Component\DependencyInjection\Exception\RuntimeException; +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; + + /** + * @var ContainerInterface + */ + protected $container; + + /** + * Construct method + * + * @param ContainerInterface $container DI container + * @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 $extension_manager Extension manager + * @param array $routing_files Array of strings containing paths to YAML files holding route information + */ + public function __construct(ContainerInterface $container, \phpbb\filesystem\filesystem_interface $filesystem, $phpbb_root_path, $php_ext, $environment, manager $extension_manager = null, $routing_files = array()) + { + $this->container = $container; + $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)); + } + } + + $this->resolveParameters($this->route_collection); + + return $this; + } + + /** + * Replaces placeholders with service container parameter values in: + * - the route defaults, + * - the route requirements, + * - the route path, + * - the route host, + * - the route schemes, + * - the route methods. + * + * @param RouteCollection $collection + */ + private function resolveParameters(RouteCollection $collection) + { + foreach ($collection as $route) + { + foreach ($route->getDefaults() as $name => $value) + { + $route->setDefault($name, $this->resolve($value)); + } + + $requirements = $route->getRequirements(); + unset($requirements['_scheme']); + unset($requirements['_method']); + + foreach ($requirements as $name => $value) + { + $route->setRequirement($name, $this->resolve($value)); + } + + $route->setPath($this->resolve($route->getPath())); + $route->setHost($this->resolve($route->getHost())); + + $schemes = array(); + foreach ($route->getSchemes() as $scheme) + { + $schemes = array_merge($schemes, explode('|', $this->resolve($scheme))); + } + + $route->setSchemes($schemes); + $methods = array(); + foreach ($route->getMethods() as $method) + { + $methods = array_merge($methods, explode('|', $this->resolve($method))); + } + + $route->setMethods($methods); + $route->setCondition($this->resolve($route->getCondition())); + } + } + + /** + * Recursively replaces placeholders with the service container parameters. + * + * @param mixed $value The source which might contain "%placeholders%" + * + * @return mixed The source with the placeholders replaced by the container + * parameters. Arrays are resolved recursively. + * + * @throws ParameterNotFoundException When a placeholder does not exist as a container parameter + * @throws RuntimeException When a container value is not a string or a numeric value + */ + private function resolve($value) + { + if (is_array($value)) + { + foreach ($value as $key => $val) + { + $value[$key] = $this->resolve($val); + } + + return $value; + } + + if (!is_string($value)) + { + return $value; + } + + $container = $this->container; + $escapedValue = preg_replace_callback('/%%|%([^%\s]++)%/', function ($match) use ($container, $value) + { + // skip %% + if (!isset($match[1])) + { + return '%%'; + } + + $resolved = $container->getParameter($match[1]); + if (is_string($resolved) || is_numeric($resolved)) + { + return (string) $resolved; + } + + throw new RuntimeException(sprintf( + 'The container parameter "%s", used in the route configuration value "%s", '. + 'must be a string or numeric, but it is of type %s.', + $match[1], + $value, + gettype($resolved) + ) + ); + }, $value); + + return str_replace('%%', '%', $escapedValue); + } + + /** + * 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 e7ef64826c..9f1aaec7c2 100644 --- a/phpBB/phpbb/search/fulltext_mysql.php +++ b/phpBB/phpbb/search/fulltext_mysql.php @@ -196,8 +196,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; } @@ -851,7 +851,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 c2cae3b37e..43f399eb13 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 @@ -99,7 +106,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 * @param \phpbb\event\dispatcher_interface $phpbb_dispatcher Event dispatcher object @@ -118,10 +125,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); @@ -1285,9 +1288,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; @@ -1532,7 +1535,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; } @@ -1567,7 +1570,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 . ' @@ -1583,7 +1586,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); } /** @@ -1654,8 +1657,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') { @@ -1682,12 +1683,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: @@ -1780,9 +1778,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 d17b26be8f..8fe80a39a3 100644 --- a/phpBB/phpbb/search/fulltext_postgres.php +++ b/phpBB/phpbb/search/fulltext_postgres.php @@ -852,7 +852,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 cd7add72f0..937292fd38 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; @@ -143,12 +143,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'; @@ -219,7 +220,7 @@ class fulltext_sphinx } // Move delta to main index each hour - set_config('search_gc', 3600); + $this->config->set('search_gc', 3600); return false; } @@ -462,6 +463,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)) { @@ -609,7 +612,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())); @@ -763,7 +766,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..7ad728e6cc --- /dev/null +++ b/phpBB/phpbb/textformatter/s9e/factory.php @@ -0,0 +1,556 @@ +<?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 lang={IDENTIFIER;optional}]{TEXT}[/CODE]', + 'color' => '[COLOR={COLOR}]{TEXT}[/COLOR]', + 'email' => '[EMAIL={EMAIL;useContent} subject={TEXT;optional;postFilter=rawurlencode} body={TEXT;optional;postFilter=rawurlencode}]{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> + <xsl:attribute name="href"> + <xsl:text>mailto:</xsl:text> + <xsl:value-of select="@email"/> + <xsl:if test="@subject or @body"> + <xsl:text>?</xsl:text> + <xsl:if test="@subject">subject=<xsl:value-of select="@subject"/></xsl:if> + <xsl:if test="@body"><xsl:if test="@subject">&</xsl:if>body=<xsl:value-of select="@body"/></xsl:if> + </xsl:if> + </xsl:attribute> + <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..838c211e56 --- /dev/null +++ b/phpBB/phpbb/textformatter/s9e/parser.php @@ -0,0 +1,396 @@ +<?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); + } + } + + // Deduplicate error messages. array_unique() only works on strings so we have to serialize + if (!empty($errors)) + { + $errors = array_map('unserialize', array_unique(array_map('serialize', $errors))); + } + + return $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..51bc44f339 --- /dev/null +++ b/phpBB/phpbb/textformatter/s9e/renderer.php @@ -0,0 +1,295 @@ +<?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); + + /** + * 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; + } + + /** + * {@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..803c71a5a2 --- /dev/null +++ b/phpBB/phpbb/textformatter/s9e/utils.php @@ -0,0 +1,126 @@ +<?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); + } + + /** + * Return given string between quotes + * + * Will use either single- or double- quotes depending on whichever requires less escaping. + * Quotes and backslashes are escaped with backslashes where necessary + * + * @param string $str Original string + * @return string Escaped string within quotes + */ + protected function enquote($str) + { + $singleQuoted = "'" . addcslashes($str, "\\'") . "'"; + $doubleQuoted = '"' . addcslashes($str, '\\"') . '"'; + + return (strlen($singleQuoted) < strlen($doubleQuoted)) ? $singleQuoted : $doubleQuoted; + } + + /** + * {@inheritdoc} + */ + public function generate_quote($text, array $attributes = array()) + { + $text = trim($text); + $quote = '[quote'; + if (isset($attributes['author'])) + { + // Add the author as the BBCode's default attribute + $quote .= '=' . $this->enquote($attributes['author']); + unset($attributes['author']); + } + foreach ($attributes as $name => $value) + { + $quote .= ' ' . $name . '=' . $this->enquote($value); + } + $quote .= ']'; + $newline = (strlen($quote . $text . '[/quote]') > 80 || strpos($text, "\n") !== false) ? "\n" : ''; + $quote .= $newline . $text . $newline . '[/quote]'; + + return $quote; + } + + /** + * 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..41a6ba2345 --- /dev/null +++ b/phpBB/phpbb/textformatter/utils_interface.php @@ -0,0 +1,68 @@ +<?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); + + /** + * Create a quote block for given text + * + * Possible attributes: + * - author + * + * @param string $text Quote's text + * @param array $attributes Quote's attributes + * @return string Quote block to be used in a new post/text + */ + public function generate_quote($text, array $attributes = array()); + + /** + * 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/textreparser/base.php b/phpBB/phpbb/textreparser/base.php new file mode 100644 index 0000000000..87a4268d0d --- /dev/null +++ b/phpBB/phpbb/textreparser/base.php @@ -0,0 +1,211 @@ +<?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\textreparser; + +abstract class base implements reparser_interface +{ + /** + * {@inheritdoc} + */ + abstract public function get_max_id(); + + /** + * Return all records in given range + * + * @param integer $min_id Lower bound + * @param integer $max_id Upper bound + * @return array Array of records + */ + abstract protected function get_records_by_range($min_id, $max_id); + + /** + * {@inheritdoc} + */ + abstract protected function save_record(array $record); + + /** + * Add fields to given record, if applicable + * + * The enable_* fields are not always saved to the database. Sometimes we need to guess their + * original value based on the text content or possibly other fields + * + * @param array $record Original record + * @return array Complete record + */ + protected function add_missing_fields(array $record) + { + if (!isset($record['enable_bbcode'], $record['enable_smilies'], $record['enable_magic_url'])) + { + $record += array( + 'enable_bbcode' => $this->guess_bbcodes($record), + 'enable_smilies' => $this->guess_smilies($record), + 'enable_magic_url' => $this->guess_magic_url($record), + ); + } + + // Those BBCodes are disabled based on context and user permissions and that value is never + // stored in the database. Here we test whether they were used in the original text. + $bbcodes = array('flash', 'img', 'quote', 'url'); + foreach ($bbcodes as $bbcode) + { + $field_name = 'enable_' . $bbcode . '_bbcode'; + $record[$field_name] = $this->guess_bbcode($record, $bbcode); + } + + // Magic URLs are tied to the URL BBCode, that's why if magic URLs are enabled we make sure + // that the URL BBCode is also enabled + if ($record['enable_magic_url']) + { + $record['enable_url_bbcode'] = true; + } + + return $record; + } + + /** + * Guess whether given BBCode is in use in given record + * + * @param array $record + * @param string $bbcode + * @return bool + */ + protected function guess_bbcode(array $record, $bbcode) + { + if (!empty($record['bbcode_uid'])) + { + // Look for the closing tag, e.g. [/url] + $match = '[/' . $bbcode . ':' . $record['bbcode_uid']; + if (strpos($record['text'], $match) !== false) + { + return true; + } + } + + if (substr($record['text'], 0, 2) == '<r') + { + // Look for the closing tag inside of a e element, in an element of the same name, e.g. + // <e>[/url]</e></URL> + $match = '<e>[/' . $bbcode . ']</e></' . strtoupper($bbcode) . '>'; + if (strpos($record['text'], $match) !== false) + { + return true; + } + } + + return false; + } + + /** + * Guess whether any BBCode is in use in given record + * + * @param array $record + * @return bool + */ + protected function guess_bbcodes(array $record) + { + if (!empty($record['bbcode_uid'])) + { + // Test whether the bbcode_uid is in use + $match = ':' . $record['bbcode_uid']; + if (strpos($record['text'], $match) !== false) + { + return true; + } + } + + if (substr($record['text'], 0, 2) == '<r') + { + // Look for a closing tag inside of an e element + return (bool) preg_match('(<e>\\[/\\w+\\]</e>)', $match); + } + + return false; + } + + /** + * Guess whether magic URLs are in use in given record + * + * @param array $record + * @return bool + */ + protected function guess_magic_url(array $record) + { + // Look for <!-- m --> or for a URL tag that's not immediately followed by <s> + return (strpos($record['text'], '<!-- m -->') !== false || preg_match('(<URL [^>]++>(?!<s>))', $record['text'])); + } + + /** + * Guess whether smilies are in use in given record + * + * @param array $record + * @return bool + */ + protected function guess_smilies(array $record) + { + return (strpos($record['text'], '<!-- s') !== false || strpos($record['text'], '<E>') !== false); + } + + /** + * {@inheritdoc} + */ + public function reparse_range($min_id, $max_id) + { + foreach ($this->get_records_by_range($min_id, $max_id) as $record) + { + $this->reparse_record($record); + } + } + + /** + * Reparse given record + * + * @param array $record Associative array containing the record's data + */ + protected function reparse_record(array $record) + { + $record = $this->add_missing_fields($record); + $flags = ($record['enable_bbcode']) ? OPTION_FLAG_BBCODE : 0; + $flags |= ($record['enable_smilies']) ? OPTION_FLAG_SMILIES : 0; + $flags |= ($record['enable_magic_url']) ? OPTION_FLAG_LINKS : 0; + $unparsed = array_merge( + $record, + generate_text_for_edit($record['text'], $record['bbcode_uid'], $flags) + ); + + // generate_text_for_edit() and decode_message() actually return the text as HTML. It has to + // be decoded to plain text before it can be reparsed + $text = html_entity_decode($unparsed['text'], ENT_QUOTES, 'UTF-8'); + $bitfield = $flags = null; + generate_text_for_storage( + $text, + $unparsed['bbcode_uid'], + $bitfield, + $flags, + $unparsed['enable_bbcode'], + $unparsed['enable_magic_url'], + $unparsed['enable_smilies'], + $unparsed['enable_img_bbcode'], + $unparsed['enable_flash_bbcode'], + $unparsed['enable_quote_bbcode'], + $unparsed['enable_url_bbcode'] + ); + + // Save the new text if it has changed + if ($text !== $record['text']) + { + $record['text'] = $text; + $this->save_record($record); + } + } +} diff --git a/phpBB/phpbb/textreparser/plugins/contact_admin_info.php b/phpBB/phpbb/textreparser/plugins/contact_admin_info.php new file mode 100644 index 0000000000..8910f2256b --- /dev/null +++ b/phpBB/phpbb/textreparser/plugins/contact_admin_info.php @@ -0,0 +1,69 @@ +<?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\textreparser\plugins; + +class contact_admin_info extends \phpbb\textreparser\base +{ + /** + * @var \phpbb\config\db_text + */ + protected $config_text; + + /** + * Constructor + * + * @param \phpbb\config\db_text $config_text + */ + public function __construct(\phpbb\config\db_text $config_text) + { + $this->config_text = $config_text; + } + + /** + * {@inheritdoc} + */ + public function get_max_id() + { + return 1; + } + + /** + * {@inheritdoc} + */ + protected function get_records_by_range($min_id, $max_id) + { + $values = $this->config_text->get_array(array( + 'contact_admin_info', + 'contact_admin_info_uid', + 'contact_admin_info_flags', + )); + + return array(array( + 'id' => 1, + 'text' => $values['contact_admin_info'], + 'bbcode_uid' => $values['contact_admin_info_uid'], + 'enable_bbcode' => $values['contact_admin_info_flags'] & OPTION_FLAG_BBCODE, + 'enable_magic_url' => $values['contact_admin_info_flags'] & OPTION_FLAG_LINKS, + 'enable_smilies' => $values['contact_admin_info_flags'] & OPTION_FLAG_SMILIES, + )); + } + + /** + * {@inheritdoc} + */ + protected function save_record(array $record) + { + $this->config_text->set('contact_admin_info', $record['text']); + } +} diff --git a/phpBB/phpbb/textreparser/plugins/forum_description.php b/phpBB/phpbb/textreparser/plugins/forum_description.php new file mode 100644 index 0000000000..7798e4b20b --- /dev/null +++ b/phpBB/phpbb/textreparser/plugins/forum_description.php @@ -0,0 +1,37 @@ +<?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\textreparser\plugins; + +class forum_description extends \phpbb\textreparser\row_based_plugin +{ + /** + * {@inheritdoc} + */ + public function get_columns() + { + return array( + 'id' => 'forum_id', + 'text' => 'forum_desc', + 'bbcode_uid' => 'forum_desc_uid', + ); + } + + /** + * {@inheritdoc} + */ + public function get_table_name() + { + return FORUMS_TABLE; + } +} diff --git a/phpBB/phpbb/textreparser/plugins/forum_rules.php b/phpBB/phpbb/textreparser/plugins/forum_rules.php new file mode 100644 index 0000000000..57c666a556 --- /dev/null +++ b/phpBB/phpbb/textreparser/plugins/forum_rules.php @@ -0,0 +1,37 @@ +<?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\textreparser\plugins; + +class forum_rules extends \phpbb\textreparser\row_based_plugin +{ + /** + * {@inheritdoc} + */ + public function get_columns() + { + return array( + 'id' => 'forum_id', + 'text' => 'forum_rules', + 'bbcode_uid' => 'forum_rules_uid', + ); + } + + /** + * {@inheritdoc} + */ + public function get_table_name() + { + return FORUMS_TABLE; + } +} diff --git a/phpBB/phpbb/textreparser/plugins/group_description.php b/phpBB/phpbb/textreparser/plugins/group_description.php new file mode 100644 index 0000000000..ddd0e1d1c5 --- /dev/null +++ b/phpBB/phpbb/textreparser/plugins/group_description.php @@ -0,0 +1,37 @@ +<?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\textreparser\plugins; + +class group_description extends \phpbb\textreparser\row_based_plugin +{ + /** + * {@inheritdoc} + */ + public function get_columns() + { + return array( + 'id' => 'group_id', + 'text' => 'group_desc', + 'bbcode_uid' => 'group_desc_uid', + ); + } + + /** + * {@inheritdoc} + */ + public function get_table_name() + { + return GROUPS_TABLE; + } +} diff --git a/phpBB/phpbb/textreparser/plugins/pm_text.php b/phpBB/phpbb/textreparser/plugins/pm_text.php new file mode 100644 index 0000000000..4d06a2878b --- /dev/null +++ b/phpBB/phpbb/textreparser/plugins/pm_text.php @@ -0,0 +1,40 @@ +<?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\textreparser\plugins; + +class pm_text extends \phpbb\textreparser\row_based_plugin +{ + /** + * {@inheritdoc} + */ + public function get_columns() + { + return array( + 'id' => 'msg_id', + 'enable_bbcode' => 'enable_bbcode', + 'enable_smilies' => 'enable_smilies', + 'enable_magic_url' => 'enable_magic_url', + 'text' => 'message_text', + 'bbcode_uid' => 'bbcode_uid', + ); + } + + /** + * {@inheritdoc} + */ + public function get_table_name() + { + return PRIVMSGS_TABLE; + } +} diff --git a/phpBB/phpbb/textreparser/plugins/poll_option.php b/phpBB/phpbb/textreparser/plugins/poll_option.php new file mode 100644 index 0000000000..7b803146c4 --- /dev/null +++ b/phpBB/phpbb/textreparser/plugins/poll_option.php @@ -0,0 +1,74 @@ +<?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\textreparser\plugins; + +class poll_option extends \phpbb\textreparser\base +{ + /** + * @var \phpbb\db\driver\driver_interface + */ + protected $db; + + /** + * Constructor + * + * @param \phpbb\db\driver\driver_interface $db Database connection + */ + public function __construct(\phpbb\db\driver\driver_interface $db) + { + $this->db = $db; + } + + /** + * {@inheritdoc} + */ + public function get_max_id() + { + $sql = 'SELECT MAX(topic_id) AS max_id FROM ' . POLL_OPTIONS_TABLE; + $result = $this->db->sql_query($sql); + $max_id = (int) $this->db->sql_fetchfield('max_id'); + $this->db->sql_freeresult($result); + + return $max_id; + } + + /** + * {@inheritdoc} + */ + protected function get_records_by_range($min_id, $max_id) + { + $sql = 'SELECT o.topic_id, o.poll_option_id, o.poll_option_text AS text, p.bbcode_uid + FROM ' . POLL_OPTIONS_TABLE . ' o, ' . TOPICS_TABLE . ' t, ' . POSTS_TABLE . ' p + WHERE o.topic_id BETWEEN ' . $min_id . ' AND ' . $max_id .' + AND t.topic_id = o.topic_id + AND p.post_id = t.topic_first_post_id'; + $result = $this->db->sql_query($sql); + $records = $this->db->sql_fetchrowset($result); + $this->db->sql_freeresult($result); + + return $records; + } + + /** + * {@inheritdoc} + */ + protected function save_record(array $record) + { + $sql = 'UPDATE ' . POLL_OPTIONS_TABLE . " + SET poll_option_text = '" . $this->db->sql_escape($record['text']) . "' + WHERE topic_id = " . $record['topic_id'] . ' + AND poll_option_id = ' . $record['poll_option_id']; + $this->db->sql_query($sql); + } +} diff --git a/phpBB/phpbb/textreparser/plugins/poll_title.php b/phpBB/phpbb/textreparser/plugins/poll_title.php new file mode 100644 index 0000000000..b447004527 --- /dev/null +++ b/phpBB/phpbb/textreparser/plugins/poll_title.php @@ -0,0 +1,50 @@ +<?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\textreparser\plugins; + +class poll_title extends \phpbb\textreparser\row_based_plugin +{ + /** + * {@inheritdoc} + */ + public function get_columns() + { + return array( + 'id' => 'topic_id', + 'text' => 'poll_title', + ); + } + + /** + * {@inheritdoc} + */ + protected function get_records_by_range_query($min_id, $max_id) + { + $sql = 'SELECT t.topic_id AS id, t.poll_title AS text, p.bbcode_uid + FROM ' . TOPICS_TABLE . ' t, ' . POSTS_TABLE . ' p + WHERE t.topic_id BETWEEN ' . $min_id . ' AND ' . $max_id .' + AND t.poll_max_options > 0 + AND p.post_id = t.topic_first_post_id'; + + return $sql; + } + + /** + * {@inheritdoc} + */ + public function get_table_name() + { + return TOPICS_TABLE; + } +} diff --git a/phpBB/phpbb/textreparser/plugins/post_text.php b/phpBB/phpbb/textreparser/plugins/post_text.php new file mode 100644 index 0000000000..4a07c98cea --- /dev/null +++ b/phpBB/phpbb/textreparser/plugins/post_text.php @@ -0,0 +1,40 @@ +<?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\textreparser\plugins; + +class post_text extends \phpbb\textreparser\row_based_plugin +{ + /** + * {@inheritdoc} + */ + public function get_columns() + { + return array( + 'id' => 'post_id', + 'enable_bbcode' => 'enable_bbcode', + 'enable_smilies' => 'enable_smilies', + 'enable_magic_url' => 'enable_magic_url', + 'text' => 'post_text', + 'bbcode_uid' => 'bbcode_uid', + ); + } + + /** + * {@inheritdoc} + */ + public function get_table_name() + { + return POSTS_TABLE; + } +} diff --git a/phpBB/phpbb/textreparser/plugins/user_signature.php b/phpBB/phpbb/textreparser/plugins/user_signature.php new file mode 100644 index 0000000000..f657a45d38 --- /dev/null +++ b/phpBB/phpbb/textreparser/plugins/user_signature.php @@ -0,0 +1,73 @@ +<?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\textreparser\plugins; + +class user_signature extends \phpbb\textreparser\row_based_plugin +{ + /** + * @var array Bit numbers used for user options + * @see \phpbb\user + */ + protected $keyoptions; + + /** + * {@inheritdoc} + */ + protected function add_missing_fields(array $row) + { + if (!isset($this->keyoptions)) + { + $this->save_keyoptions(); + } + + $options = $row['user_options']; + $row += array( + 'enable_bbcode' => phpbb_optionget($this->keyoptions['sig_bbcode'], $options), + 'enable_smilies' => phpbb_optionget($this->keyoptions['sig_smilies'], $options), + 'enable_magic_url' => phpbb_optionget($this->keyoptions['sig_links'], $options), + ); + + return parent::add_missing_fields($row); + } + + /** + * {@inheritdoc} + */ + public function get_columns() + { + return array( + 'id' => 'user_id', + 'text' => 'user_sig', + 'bbcode_uid' => 'user_sig_bbcode_uid', + 'user_options' => 'user_options', + ); + } + + /** + * {@inheritdoc} + */ + public function get_table_name() + { + return USERS_TABLE; + } + + /** + * Save the keyoptions var from \phpbb\user + */ + protected function save_keyoptions() + { + $class_vars = get_class_vars('phpbb\\user'); + $this->keyoptions = $class_vars['keyoptions']; + } +} diff --git a/phpBB/phpbb/textreparser/reparser_interface.php b/phpBB/phpbb/textreparser/reparser_interface.php new file mode 100644 index 0000000000..9ea1732870 --- /dev/null +++ b/phpBB/phpbb/textreparser/reparser_interface.php @@ -0,0 +1,32 @@ +<?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\textreparser; + +interface reparser_interface +{ + /** + * Return the highest ID for all existing records + * + * @return integer + */ + public function get_max_id(); + + /** + * Reparse all records in given range + * + * @param integer $min_id Lower bound + * @param integer $max_id Upper bound + */ + public function reparse_range($min_id, $max_id); +} diff --git a/phpBB/phpbb/textreparser/row_based_plugin.php b/phpBB/phpbb/textreparser/row_based_plugin.php new file mode 100644 index 0000000000..d3ca334591 --- /dev/null +++ b/phpBB/phpbb/textreparser/row_based_plugin.php @@ -0,0 +1,117 @@ +<?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\textreparser; + +abstract class row_based_plugin extends base +{ + /** + * @var \phpbb\db\driver\driver_interface + */ + protected $db; + + /** + * Constructor + * + * @param \phpbb\db\driver\driver_interface $db Database connection + */ + public function __construct(\phpbb\db\driver\driver_interface $db) + { + $this->db = $db; + } + + /** + * Return the name of the column that correspond to each field + * + * @return array + */ + abstract public function get_columns(); + + /** + * Return the name of the table used by this plugin + * + * @return string + */ + abstract public function get_table_name(); + + /** + * {@inheritdoc} + */ + public function get_max_id() + { + $columns = $this->get_columns(); + + $sql = 'SELECT MAX(' . $columns['id'] . ') AS max_id FROM ' . $this->get_table_name(); + $result = $this->db->sql_query($sql); + $max_id = (int) $this->db->sql_fetchfield('max_id'); + $this->db->sql_freeresult($result); + + return $max_id; + } + + /** + * {@inheritdoc} + */ + protected function get_records_by_range($min_id, $max_id) + { + $sql = $this->get_records_by_range_query($min_id, $max_id); + $result = $this->db->sql_query($sql); + $records = $this->db->sql_fetchrowset($result); + $this->db->sql_freeresult($result); + + return $records; + } + + /** + * Generate the query that retrieves all records for given range + * + * @param integer $min_id Lower bound + * @param integer $max_id Upper bound + * @return string SQL query + */ + protected function get_records_by_range_query($min_id, $max_id) + { + $columns = $this->get_columns(); + $fields = array(); + foreach ($columns as $field_name => $column_name) + { + if ($column_name === $field_name) + { + $fields[] = $column_name; + } + else + { + $fields[] = $column_name . ' AS ' . $field_name; + } + } + + $sql = 'SELECT ' . implode(', ', $fields) . ' + FROM ' . $this->get_table_name() . ' + WHERE ' . $columns['id'] . ' BETWEEN ' . $min_id . ' AND ' . $max_id; + + return $sql; + } + + /** + * {@inheritdoc} + */ + protected function save_record(array $record) + { + $columns = $this->get_columns(); + + $sql = 'UPDATE ' . $this->get_table_name() . ' + SET ' . $columns['text'] . " = '" . $this->db->sql_escape($record['text']) . "' + WHERE " . $columns['id'] . ' = ' . $record['id']; + $this->db->sql_query($sql); + } +} diff --git a/phpBB/phpbb/user.php b/phpBB/phpbb/user.php index 3bc1af2899..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) === 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 5ce8ca2d4d..cdd28329db 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() @@ -189,12 +189,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'], 'USER_AVATAR', false, $lazy); + return phpbb_get_avatar($row, 'USER_AVATAR', false, $lazy); } /** 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; } |