diff options
Diffstat (limited to 'phpBB/phpbb/db/driver')
-rw-r--r-- | phpBB/phpbb/db/driver/driver.php | 1078 | ||||
-rw-r--r-- | phpBB/phpbb/db/driver/driver_interface.php | 453 | ||||
-rw-r--r-- | phpBB/phpbb/db/driver/factory.php | 443 | ||||
-rw-r--r-- | phpBB/phpbb/db/driver/mssql.php | 476 | ||||
-rw-r--r-- | phpBB/phpbb/db/driver/mssql_base.php | 71 | ||||
-rw-r--r-- | phpBB/phpbb/db/driver/mssql_odbc.php | 377 | ||||
-rw-r--r-- | phpBB/phpbb/db/driver/mssqlnative.php | 442 | ||||
-rw-r--r-- | phpBB/phpbb/db/driver/mysql.php | 485 | ||||
-rw-r--r-- | phpBB/phpbb/db/driver/mysql_base.php | 138 | ||||
-rw-r--r-- | phpBB/phpbb/db/driver/mysqli.php | 475 | ||||
-rw-r--r-- | phpBB/phpbb/db/driver/oracle.php | 807 | ||||
-rw-r--r-- | phpBB/phpbb/db/driver/postgres.php | 490 | ||||
-rw-r--r-- | phpBB/phpbb/db/driver/sqlite.php | 378 | ||||
-rw-r--r-- | phpBB/phpbb/db/driver/sqlite3.php | 405 |
14 files changed, 6518 insertions, 0 deletions
diff --git a/phpBB/phpbb/db/driver/driver.php b/phpBB/phpbb/db/driver/driver.php new file mode 100644 index 0000000000..01dd66cd6e --- /dev/null +++ b/phpBB/phpbb/db/driver/driver.php @@ -0,0 +1,1078 @@ +<?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\driver; + +/** +* Database Abstraction Layer +*/ +abstract class driver implements driver_interface +{ + var $db_connect_id; + var $query_result; + var $return_on_error = false; + var $transaction = false; + var $sql_time = 0; + var $num_queries = array(); + var $open_queries = array(); + + var $curtime = 0; + var $query_hold = ''; + var $html_hold = ''; + var $sql_report = ''; + + var $persistency = false; + var $user = ''; + var $server = ''; + var $dbname = ''; + + // Set to true if error triggered + var $sql_error_triggered = false; + + // Holding the last sql query on sql error + var $sql_error_sql = ''; + // Holding the error information - only populated if sql_error_triggered is set + var $sql_error_returned = array(); + + // Holding transaction count + var $transactions = 0; + + // Supports multi inserts? + var $multi_insert = false; + + /** + * Current sql layer + */ + var $sql_layer = ''; + + /** + * Wildcards for matching any (%) or exactly one (_) character within LIKE expressions + */ + var $any_char; + var $one_char; + + /** + * Exact version of the DBAL, directly queried + */ + var $sql_server_version = false; + + /** + * Constructor + */ + function __construct() + { + $this->num_queries = array( + 'cached' => 0, + 'normal' => 0, + 'total' => 0, + ); + + // Fill default sql layer based on the class being called. + // This can be changed by the specified layer itself later if needed. + $this->sql_layer = substr(get_class($this), strlen('phpbb\db\driver\\')); + + // Do not change this please! This variable is used to easy the use of it - and is hardcoded. + $this->any_char = chr(0) . '%'; + $this->one_char = chr(0) . '_'; + } + + /** + * {@inheritdoc} + */ + public function get_sql_layer() + { + return $this->sql_layer; + } + + /** + * {@inheritdoc} + */ + public function get_db_name() + { + return $this->dbname; + } + + /** + * {@inheritdoc} + */ + public function get_any_char() + { + return $this->any_char; + } + + /** + * {@inheritdoc} + */ + public function get_one_char() + { + return $this->one_char; + } + + /** + * {@inheritdoc} + */ + public function get_db_connect_id() + { + return $this->db_connect_id; + } + + /** + * {@inheritdoc} + */ + public function get_sql_error_triggered() + { + return $this->sql_error_triggered; + } + + /** + * {@inheritdoc} + */ + public function get_sql_error_sql() + { + return $this->sql_error_sql; + } + + /** + * {@inheritdoc} + */ + public function get_transaction() + { + return $this->transaction; + } + + /** + * {@inheritdoc} + */ + public function get_sql_time() + { + return $this->sql_time; + } + + /** + * {@inheritdoc} + */ + public function get_sql_error_returned() + { + return $this->sql_error_returned; + } + + /** + * {@inheritdoc} + */ + public function get_multi_insert() + { + return $this->multi_insert; + } + + /** + * {@inheritdoc} + */ + public function set_multi_insert($multi_insert) + { + $this->multi_insert = $multi_insert; + } + + /** + * {@inheritDoc} + */ + function sql_return_on_error($fail = false) + { + $this->sql_error_triggered = false; + $this->sql_error_sql = ''; + + $this->return_on_error = $fail; + } + + /** + * {@inheritDoc} + */ + function sql_num_queries($cached = false) + { + return ($cached) ? $this->num_queries['cached'] : $this->num_queries['normal']; + } + + /** + * {@inheritDoc} + */ + function sql_add_num_queries($cached = false) + { + $this->num_queries['cached'] += ($cached !== false) ? 1 : 0; + $this->num_queries['normal'] += ($cached !== false) ? 0 : 1; + $this->num_queries['total'] += 1; + } + + /** + * {@inheritDoc} + */ + function sql_close() + { + if (!$this->db_connect_id) + { + return false; + } + + if ($this->transaction) + { + do + { + $this->sql_transaction('commit'); + } + while ($this->transaction); + } + + foreach ($this->open_queries as $query_id) + { + $this->sql_freeresult($query_id); + } + + // Connection closed correctly. Set db_connect_id to false to prevent errors + if ($result = $this->_sql_close()) + { + $this->db_connect_id = false; + } + + return $result; + } + + /** + * {@inheritDoc} + */ + function sql_query_limit($query, $total, $offset = 0, $cache_ttl = 0) + { + if (empty($query)) + { + return false; + } + + // Never use a negative total or offset + $total = ($total < 0) ? 0 : $total; + $offset = ($offset < 0) ? 0 : $offset; + + return $this->_sql_query_limit($query, $total, $offset, $cache_ttl); + } + + /** + * {@inheritDoc} + */ + function sql_fetchrowset($query_id = false) + { + if ($query_id === false) + { + $query_id = $this->query_result; + } + + if ($query_id !== false) + { + $result = array(); + while ($row = $this->sql_fetchrow($query_id)) + { + $result[] = $row; + } + + return $result; + } + + return false; + } + + /** + * {@inheritDoc} + */ + function sql_rowseek($rownum, &$query_id) + { + global $cache; + + if ($query_id === false) + { + $query_id = $this->query_result; + } + + if ($cache && $cache->sql_exists($query_id)) + { + return $cache->sql_rowseek($rownum, $query_id); + } + + if ($query_id === false) + { + return false; + } + + $this->sql_freeresult($query_id); + $query_id = $this->sql_query($this->last_query_text); + + if ($query_id === false) + { + return false; + } + + // We do not fetch the row for rownum == 0 because then the next resultset would be the second row + for ($i = 0; $i < $rownum; $i++) + { + if (!$this->sql_fetchrow($query_id)) + { + return false; + } + } + + return true; + } + + /** + * {@inheritDoc} + */ + function sql_fetchfield($field, $rownum = false, $query_id = false) + { + global $cache; + + if ($query_id === false) + { + $query_id = $this->query_result; + } + + if ($query_id !== false) + { + if ($rownum !== false) + { + $this->sql_rowseek($rownum, $query_id); + } + + if ($cache && !is_object($query_id) && $cache->sql_exists($query_id)) + { + return $cache->sql_fetchfield($query_id, $field); + } + + $row = $this->sql_fetchrow($query_id); + return (isset($row[$field])) ? $row[$field] : false; + } + + return false; + } + + /** + * {@inheritDoc} + */ + function sql_like_expression($expression) + { + $expression = utf8_str_replace(array('_', '%'), array("\_", "\%"), $expression); + $expression = utf8_str_replace(array(chr(0) . "\_", chr(0) . "\%"), array('_', '%'), $expression); + + return $this->_sql_like_expression('LIKE \'' . $this->sql_escape($expression) . '\''); + } + + /** + * {@inheritDoc} + */ + function sql_not_like_expression($expression) + { + $expression = utf8_str_replace(array('_', '%'), array("\_", "\%"), $expression); + $expression = utf8_str_replace(array(chr(0) . "\_", chr(0) . "\%"), array('_', '%'), $expression); + + return $this->_sql_not_like_expression('NOT LIKE \'' . $this->sql_escape($expression) . '\''); + } + + /** + * {@inheritDoc} + */ + public function sql_case($condition, $action_true, $action_false = false) + { + $sql_case = 'CASE WHEN ' . $condition; + $sql_case .= ' THEN ' . $action_true; + $sql_case .= ($action_false !== false) ? ' ELSE ' . $action_false : ''; + $sql_case .= ' END'; + return $sql_case; + } + + /** + * {@inheritDoc} + */ + public function sql_concatenate($expr1, $expr2) + { + return $expr1 . ' || ' . $expr2; + } + + /** + * {@inheritDoc} + */ + function sql_buffer_nested_transactions() + { + return false; + } + + /** + * {@inheritDoc} + */ + function sql_transaction($status = 'begin') + { + switch ($status) + { + case 'begin': + // If we are within a transaction we will not open another one, but enclose the current one to not loose data (preventing auto commit) + if ($this->transaction) + { + $this->transactions++; + return true; + } + + $result = $this->_sql_transaction('begin'); + + if (!$result) + { + $this->sql_error(); + } + + $this->transaction = true; + break; + + case 'commit': + // If there was a previously opened transaction we do not commit yet... + // but count back the number of inner transactions + if ($this->transaction && $this->transactions) + { + $this->transactions--; + return true; + } + + // Check if there is a transaction (no transaction can happen if + // there was an error, with a combined rollback and error returning enabled) + // This implies we have transaction always set for autocommit db's + if (!$this->transaction) + { + return false; + } + + $result = $this->_sql_transaction('commit'); + + if (!$result) + { + $this->sql_error(); + } + + $this->transaction = false; + $this->transactions = 0; + break; + + case 'rollback': + $result = $this->_sql_transaction('rollback'); + $this->transaction = false; + $this->transactions = 0; + break; + + default: + $result = $this->_sql_transaction($status); + break; + } + + return $result; + } + + /** + * {@inheritDoc} + */ + function sql_build_array($query, $assoc_ary = false) + { + if (!is_array($assoc_ary)) + { + return false; + } + + $fields = $values = array(); + + if ($query == 'INSERT' || $query == 'INSERT_SELECT') + { + foreach ($assoc_ary as $key => $var) + { + $fields[] = $key; + + if (is_array($var) && is_string($var[0])) + { + // This is used for INSERT_SELECT(s) + $values[] = $var[0]; + } + else + { + $values[] = $this->_sql_validate_value($var); + } + } + + $query = ($query == 'INSERT') ? ' (' . implode(', ', $fields) . ') VALUES (' . implode(', ', $values) . ')' : ' (' . implode(', ', $fields) . ') SELECT ' . implode(', ', $values) . ' '; + } + else if ($query == 'MULTI_INSERT') + { + trigger_error('The MULTI_INSERT query value is no longer supported. Please use sql_multi_insert() instead.', E_USER_ERROR); + } + else if ($query == 'UPDATE' || $query == 'SELECT' || $query == 'DELETE') + { + $values = array(); + foreach ($assoc_ary as $key => $var) + { + $values[] = "$key = " . $this->_sql_validate_value($var); + } + $query = implode(($query == 'UPDATE') ? ', ' : ' AND ', $values); + } + + return $query; + } + + /** + * {@inheritDoc} + */ + function sql_in_set($field, $array, $negate = false, $allow_empty_set = false) + { + if (!sizeof($array)) + { + if (!$allow_empty_set) + { + // Print the backtrace to help identifying the location of the problematic code + $this->sql_error('No values specified for SQL IN comparison'); + } + else + { + // NOT IN () actually means everything so use a tautology + if ($negate) + { + return '1=1'; + } + // IN () actually means nothing so use a contradiction + else + { + return '1=0'; + } + } + } + + if (!is_array($array)) + { + $array = array($array); + } + + if (sizeof($array) == 1) + { + @reset($array); + $var = current($array); + + return $field . ($negate ? ' <> ' : ' = ') . $this->_sql_validate_value($var); + } + else + { + return $field . ($negate ? ' NOT IN ' : ' IN ') . '(' . implode(', ', array_map(array($this, '_sql_validate_value'), $array)) . ')'; + } + } + + /** + * {@inheritDoc} + */ + function sql_bit_and($column_name, $bit, $compare = '') + { + if (method_exists($this, '_sql_bit_and')) + { + return $this->_sql_bit_and($column_name, $bit, $compare); + } + + return $column_name . ' & ' . (1 << $bit) . (($compare) ? ' ' . $compare : ''); + } + + /** + * {@inheritDoc} + */ + function sql_bit_or($column_name, $bit, $compare = '') + { + if (method_exists($this, '_sql_bit_or')) + { + return $this->_sql_bit_or($column_name, $bit, $compare); + } + + return $column_name . ' | ' . (1 << $bit) . (($compare) ? ' ' . $compare : ''); + } + + /** + * {@inheritDoc} + */ + function cast_expr_to_bigint($expression) + { + return $expression; + } + + /** + * {@inheritDoc} + */ + function cast_expr_to_string($expression) + { + return $expression; + } + + /** + * {@inheritDoc} + */ + function sql_lower_text($column_name) + { + return "LOWER($column_name)"; + } + + /** + * {@inheritDoc} + */ + function sql_multi_insert($table, $sql_ary) + { + if (!sizeof($sql_ary)) + { + return false; + } + + if ($this->multi_insert) + { + $ary = array(); + foreach ($sql_ary as $id => $_sql_ary) + { + // If by accident the sql array is only one-dimensional we build a normal insert statement + if (!is_array($_sql_ary)) + { + return $this->sql_query('INSERT INTO ' . $table . ' ' . $this->sql_build_array('INSERT', $sql_ary)); + } + + $values = array(); + foreach ($_sql_ary as $key => $var) + { + $values[] = $this->_sql_validate_value($var); + } + $ary[] = '(' . implode(', ', $values) . ')'; + } + + return $this->sql_query('INSERT INTO ' . $table . ' ' . ' (' . implode(', ', array_keys($sql_ary[0])) . ') VALUES ' . implode(', ', $ary)); + } + else + { + foreach ($sql_ary as $ary) + { + if (!is_array($ary)) + { + return false; + } + + $result = $this->sql_query('INSERT INTO ' . $table . ' ' . $this->sql_build_array('INSERT', $ary)); + + if (!$result) + { + return false; + } + } + } + + return true; + } + + /** + * Function for validating values + * @access private + */ + function _sql_validate_value($var) + { + if (is_null($var)) + { + return 'NULL'; + } + else if (is_string($var)) + { + return "'" . $this->sql_escape($var) . "'"; + } + else + { + return (is_bool($var)) ? intval($var) : $var; + } + } + + /** + * {@inheritDoc} + */ + function sql_build_query($query, $array) + { + $sql = ''; + switch ($query) + { + case 'SELECT': + case 'SELECT_DISTINCT'; + + $sql = str_replace('_', ' ', $query) . ' ' . $array['SELECT'] . ' FROM '; + + // Build table array. We also build an alias array for later checks. + $table_array = $aliases = array(); + $used_multi_alias = false; + + foreach ($array['FROM'] as $table_name => $alias) + { + if (is_array($alias)) + { + $used_multi_alias = true; + + foreach ($alias as $multi_alias) + { + $table_array[] = $table_name . ' ' . $multi_alias; + $aliases[] = $multi_alias; + } + } + else + { + $table_array[] = $table_name . ' ' . $alias; + $aliases[] = $alias; + } + } + + // We run the following code to determine if we need to re-order the table array. ;) + // The reason for this is that for multi-aliased tables (two equal tables) in the FROM statement the last table need to match the first comparison. + // DBMS who rely on this: Oracle, PostgreSQL and MSSQL. For all other DBMS it makes absolutely no difference in which order the table is. + if (!empty($array['LEFT_JOIN']) && sizeof($array['FROM']) > 1 && $used_multi_alias !== false) + { + // Take first LEFT JOIN + $join = current($array['LEFT_JOIN']); + + // Determine the table used there (even if there are more than one used, we only want to have one + preg_match('/(' . implode('|', $aliases) . ')\.[^\s]+/U', str_replace(array('(', ')', 'AND', 'OR', ' '), '', $join['ON']), $matches); + + // If there is a first join match, we need to make sure the table order is correct + if (!empty($matches[1])) + { + $first_join_match = trim($matches[1]); + $table_array = $last = array(); + + foreach ($array['FROM'] as $table_name => $alias) + { + if (is_array($alias)) + { + foreach ($alias as $multi_alias) + { + ($multi_alias === $first_join_match) ? $last[] = $table_name . ' ' . $multi_alias : $table_array[] = $table_name . ' ' . $multi_alias; + } + } + else + { + ($alias === $first_join_match) ? $last[] = $table_name . ' ' . $alias : $table_array[] = $table_name . ' ' . $alias; + } + } + + $table_array = array_merge($table_array, $last); + } + } + + $sql .= $this->_sql_custom_build('FROM', implode(' CROSS JOIN ', $table_array)); + + if (!empty($array['LEFT_JOIN'])) + { + foreach ($array['LEFT_JOIN'] as $join) + { + $sql .= ' LEFT JOIN ' . key($join['FROM']) . ' ' . current($join['FROM']) . ' ON (' . $join['ON'] . ')'; + } + } + + if (!empty($array['WHERE'])) + { + $sql .= ' WHERE ' . $this->_sql_custom_build('WHERE', $array['WHERE']); + } + + if (!empty($array['GROUP_BY'])) + { + $sql .= ' GROUP BY ' . $array['GROUP_BY']; + } + + if (!empty($array['ORDER_BY'])) + { + $sql .= ' ORDER BY ' . $array['ORDER_BY']; + } + + break; + } + + return $sql; + } + + /** + * {@inheritDoc} + */ + function sql_error($sql = '') + { + global $auth, $user, $config; + + // Set var to retrieve errored status + $this->sql_error_triggered = true; + $this->sql_error_sql = $sql; + + $this->sql_error_returned = $this->_sql_error(); + + if (!$this->return_on_error) + { + $message = 'SQL ERROR [ ' . $this->sql_layer . ' ]<br /><br />' . $this->sql_error_returned['message'] . ' [' . $this->sql_error_returned['code'] . ']'; + + // Show complete SQL error and path to administrators only + // Additionally show complete error on installation or if extended debug mode is enabled + // The DEBUG constant is for development only! + if ((isset($auth) && $auth->acl_get('a_')) || defined('IN_INSTALL') || defined('DEBUG')) + { + $message .= ($sql) ? '<br /><br />SQL<br /><br />' . htmlspecialchars($sql) : ''; + } + else + { + // If error occurs in initiating the session we need to use a pre-defined language string + // This could happen if the connection could not be established for example (then we are not able to grab the default language) + if (!isset($user->lang['SQL_ERROR_OCCURRED'])) + { + $message .= '<br /><br />An sql error occurred while fetching this page. Please contact an administrator if this problem persists.'; + } + else + { + if (!empty($config['board_contact'])) + { + $message .= '<br /><br />' . sprintf($user->lang['SQL_ERROR_OCCURRED'], '<a href="mailto:' . htmlspecialchars($config['board_contact']) . '">', '</a>'); + } + else + { + $message .= '<br /><br />' . sprintf($user->lang['SQL_ERROR_OCCURRED'], '', ''); + } + } + } + + if ($this->transaction) + { + $this->sql_transaction('rollback'); + } + + if (strlen($message) > 1024) + { + // We need to define $msg_long_text here to circumvent text stripping. + global $msg_long_text; + $msg_long_text = $message; + + trigger_error(false, E_USER_ERROR); + } + + trigger_error($message, E_USER_ERROR); + } + + if ($this->transaction) + { + $this->sql_transaction('rollback'); + } + + return $this->sql_error_returned; + } + + /** + * {@inheritDoc} + */ + function sql_report($mode, $query = '') + { + global $cache, $starttime, $phpbb_root_path, $phpbb_path_helper, $user; + global $request; + + if (is_object($request) && !$request->variable('explain', false)) + { + return false; + } + + if (!$query && $this->query_hold != '') + { + $query = $this->query_hold; + } + + switch ($mode) + { + case 'display': + if (!empty($cache)) + { + $cache->unload(); + } + $this->sql_close(); + + $mtime = explode(' ', microtime()); + $totaltime = $mtime[0] + $mtime[1] - $starttime; + + echo '<!DOCTYPE html> + <html dir="ltr"> + <head> + <meta charset="utf-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <title>SQL Report</title> + <link href="' . htmlspecialchars($phpbb_path_helper->update_web_root_path($phpbb_root_path) . $phpbb_path_helper->get_adm_relative_path()) . 'style/admin.css" rel="stylesheet" type="text/css" media="screen" /> + </head> + <body id="errorpage"> + <div id="wrap"> + <div id="page-header"> + <a href="' . build_url('explain') . '">Return to previous page</a> + </div> + <div id="page-body"> + <div id="acp"> + <div class="panel"> + <span class="corners-top"><span></span></span> + <div id="content"> + <h1>SQL Report</h1> + <br /> + <p><b>Page generated in ' . round($totaltime, 4) . " seconds with {$this->num_queries['normal']} queries" . (($this->num_queries['cached']) ? " + {$this->num_queries['cached']} " . (($this->num_queries['cached'] == 1) ? 'query' : 'queries') . ' returning data from cache' : '') . '</b></p> + + <p>Time spent on ' . $this->sql_layer . ' queries: <b>' . round($this->sql_time, 5) . 's</b> | Time spent on PHP: <b>' . round($totaltime - $this->sql_time, 5) . 's</b></p> + + <br /><br /> + ' . $this->sql_report . ' + </div> + <span class="corners-bottom"><span></span></span> + </div> + </div> + </div> + <div id="page-footer"> + Powered by <a href="https://www.phpbb.com/">phpBB</a>® Forum Software © phpBB Limited + </div> + </div> + </body> + </html>'; + + exit_handler(); + + break; + + case 'stop': + $endtime = explode(' ', microtime()); + $endtime = $endtime[0] + $endtime[1]; + + $this->sql_report .= ' + + <table cellspacing="1"> + <thead> + <tr> + <th>Query #' . $this->num_queries['total'] . '</th> + </tr> + </thead> + <tbody> + <tr> + <td class="row3"><textarea style="font-family:\'Courier New\',monospace;width:99%" rows="5" cols="10">' . preg_replace('/\t(AND|OR)(\W)/', "\$1\$2", htmlspecialchars(preg_replace('/[\s]*[\n\r\t]+[\n\r\s\t]*/', "\n", $query))) . '</textarea></td> + </tr> + </tbody> + </table> + + ' . $this->html_hold . ' + + <p style="text-align: center;"> + '; + + if ($this->query_result) + { + if (preg_match('/^(UPDATE|DELETE|REPLACE)/', $query)) + { + $this->sql_report .= 'Affected rows: <b>' . $this->sql_affectedrows() . '</b> | '; + } + $this->sql_report .= 'Before: ' . sprintf('%.5f', $this->curtime - $starttime) . 's | After: ' . sprintf('%.5f', $endtime - $starttime) . 's | Elapsed: <b>' . sprintf('%.5f', $endtime - $this->curtime) . 's</b>'; + } + else + { + $error = $this->sql_error(); + $this->sql_report .= '<b style="color: red">FAILED</b> - ' . $this->sql_layer . ' Error ' . $error['code'] . ': ' . htmlspecialchars($error['message']); + } + + $this->sql_report .= '</p><br /><br />'; + + $this->sql_time += $endtime - $this->curtime; + break; + + case 'start': + $this->query_hold = $query; + $this->html_hold = ''; + + $this->_sql_report($mode, $query); + + $this->curtime = explode(' ', microtime()); + $this->curtime = $this->curtime[0] + $this->curtime[1]; + + break; + + case 'add_select_row': + + $html_table = func_get_arg(2); + $row = func_get_arg(3); + + if (!$html_table && sizeof($row)) + { + $html_table = true; + $this->html_hold .= '<table cellspacing="1"><tr>'; + + foreach (array_keys($row) as $val) + { + $this->html_hold .= '<th>' . (($val) ? ucwords(str_replace('_', ' ', $val)) : ' ') . '</th>'; + } + $this->html_hold .= '</tr>'; + } + $this->html_hold .= '<tr>'; + + $class = 'row1'; + foreach (array_values($row) as $val) + { + $class = ($class == 'row1') ? 'row2' : 'row1'; + $this->html_hold .= '<td class="' . $class . '">' . (($val) ? $val : ' ') . '</td>'; + } + $this->html_hold .= '</tr>'; + + return $html_table; + + break; + + case 'fromcache': + + $this->_sql_report($mode, $query); + + break; + + case 'record_fromcache': + + $endtime = func_get_arg(2); + $splittime = func_get_arg(3); + + $time_cache = $endtime - $this->curtime; + $time_db = $splittime - $endtime; + $color = ($time_db > $time_cache) ? 'green' : 'red'; + + $this->sql_report .= '<table cellspacing="1"><thead><tr><th>Query results obtained from the cache</th></tr></thead><tbody><tr>'; + $this->sql_report .= '<td class="row3"><textarea style="font-family:\'Courier New\',monospace;width:99%" rows="5" cols="10">' . preg_replace('/\t(AND|OR)(\W)/', "\$1\$2", htmlspecialchars(preg_replace('/[\s]*[\n\r\t]+[\n\r\s\t]*/', "\n", $query))) . '</textarea></td></tr></tbody></table>'; + $this->sql_report .= '<p style="text-align: center;">'; + $this->sql_report .= 'Before: ' . sprintf('%.5f', $this->curtime - $starttime) . 's | After: ' . sprintf('%.5f', $endtime - $starttime) . 's | Elapsed [cache]: <b style="color: ' . $color . '">' . sprintf('%.5f', ($time_cache)) . 's</b> | Elapsed [db]: <b>' . sprintf('%.5f', $time_db) . 's</b></p><br /><br />'; + + // Pad the start time to not interfere with page timing + $starttime += $time_db; + + break; + + default: + + $this->_sql_report($mode, $query); + + break; + } + + return true; + } + + /** + * {@inheritDoc} + */ + function get_estimated_row_count($table_name) + { + return $this->get_row_count($table_name); + } + + /** + * {@inheritDoc} + */ + function get_row_count($table_name) + { + $sql = 'SELECT COUNT(*) AS rows_total + FROM ' . $this->sql_escape($table_name); + $result = $this->sql_query($sql); + $rows_total = $this->sql_fetchfield('rows_total'); + $this->sql_freeresult($result); + + return $rows_total; + } +} diff --git a/phpBB/phpbb/db/driver/driver_interface.php b/phpBB/phpbb/db/driver/driver_interface.php new file mode 100644 index 0000000000..8b487c5d42 --- /dev/null +++ b/phpBB/phpbb/db/driver/driver_interface.php @@ -0,0 +1,453 @@ +<?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\driver; + +interface driver_interface +{ + /** + * Gets the name of the sql layer. + * + * @return string + */ + public function get_sql_layer(); + + /** + * Gets the name of the database. + * + * @return string + */ + public function get_db_name(); + + /** + * Wildcards for matching any (%) character within LIKE expressions + * + * @return string + */ + public function get_any_char(); + + /** + * Wildcards for matching exactly one (_) character within LIKE expressions + * + * @return string + */ + public function get_one_char(); + + /** + * Gets the time spent into the queries + * + * @return int + */ + public function get_sql_time(); + + /** + * Gets the connect ID. + * + * @return mixed + */ + public function get_db_connect_id(); + + /** + * Indicates if an error was triggered. + * + * @return bool + */ + public function get_sql_error_triggered(); + + /** + * Gets the last faulty query + * + * @return string + */ + public function get_sql_error_sql(); + + /** + * Indicates if we are in a transaction. + * + * @return bool + */ + public function get_transaction(); + + /** + * Gets the returned error. + * + * @return array + */ + public function get_sql_error_returned(); + + /** + * Indicates if multiple insertion can be used + * + * @return bool + */ + public function get_multi_insert(); + + /** + * Set if multiple insertion can be used + * + * @param bool $multi_insert + */ + public function set_multi_insert($multi_insert); + + /** + * Gets the exact number of rows in a specified table. + * + * @param string $table_name Table name + * @return string Exact number of rows in $table_name. + */ + public function get_row_count($table_name); + + /** + * Gets the estimated number of rows in a specified table. + * + * @param string $table_name Table name + * @return string Number of rows in $table_name. + * Prefixed with ~ if estimated (otherwise exact). + */ + public function get_estimated_row_count($table_name); + + /** + * Run LOWER() on DB column of type text (i.e. neither varchar nor char). + * + * @param string $column_name The column name to use + * @return string A SQL statement like "LOWER($column_name)" + */ + public function sql_lower_text($column_name); + + /** + * Display sql error page + * + * @param string $sql The SQL query causing the error + * @return mixed Returns the full error message, if $this->return_on_error + * is set, null otherwise + */ + public function sql_error($sql = ''); + + /** + * Returns whether results of a query need to be buffered to run a + * transaction while iterating over them. + * + * @return bool Whether buffering is required. + */ + public function sql_buffer_nested_transactions(); + + /** + * Run binary OR operator on DB column. + * + * @param string $column_name The column name to use + * @param int $bit The value to use for the OR operator, + * will be converted to (1 << $bit). Is used by options, + * using the number schema... 0, 1, 2...29 + * @param string $compare Any custom SQL code after the check (e.g. "= 0") + * @return string A SQL statement like "$column | (1 << $bit) {$compare}" + */ + public function sql_bit_or($column_name, $bit, $compare = ''); + + /** + * Version information about used database + * + * @param bool $raw Only return the fetched sql_server_version + * @param bool $use_cache Is it safe to retrieve the value from the cache + * @return string sql server version + */ + public function sql_server_info($raw = false, $use_cache = true); + + /** + * Return on error or display error message + * + * @param bool $fail Should we return on errors, or stop + * @return null + */ + public function sql_return_on_error($fail = false); + + /** + * Build sql statement from an array + * + * @param string $query Should be on of the following strings: + * INSERT, INSERT_SELECT, UPDATE, SELECT, DELETE + * @param array $assoc_ary Array with "column => value" pairs + * @return string A SQL statement like "c1 = 'a' AND c2 = 'b'" + */ + public function sql_build_array($query, $assoc_ary = array()); + + /** + * Fetch all rows + * + * @param mixed $query_id Already executed query to get the rows from, + * if false, the last query will be used. + * @return mixed Nested array if the query had rows, false otherwise + */ + public function sql_fetchrowset($query_id = false); + + /** + * SQL Transaction + * + * @param string $status Should be one of the following strings: + * begin, commit, rollback + * @return mixed Buffered, seekable result handle, false on error + */ + public function sql_transaction($status = 'begin'); + + /** + * Build a concatenated expression + * + * @param string $expr1 Base SQL expression where we append the second one + * @param string $expr2 SQL expression that is appended to the first expression + * @return string Concatenated string + */ + public function sql_concatenate($expr1, $expr2); + + /** + * Build a case expression + * + * Note: The two statements action_true and action_false must have the same + * data type (int, vchar, ...) in the database! + * + * @param string $condition The condition which must be true, + * to use action_true rather then action_else + * @param string $action_true SQL expression that is used, if the condition is true + * @param mixed $action_false SQL expression that is used, if the condition is false + * @return string CASE expression including the condition and statements + */ + public function sql_case($condition, $action_true, $action_false = false); + + /** + * Build sql statement from array for select and select distinct statements + * + * Possible query values: SELECT, SELECT_DISTINCT + * + * @param string $query Should be one of: SELECT, SELECT_DISTINCT + * @param array $array Array with the query data: + * SELECT A comma imploded list of columns to select + * FROM Array with "table => alias" pairs, + * (alias can also be an array) + * Optional: LEFT_JOIN Array of join entries: + * FROM Table that should be joined + * ON Condition for the join + * Optional: WHERE Where SQL statement + * Optional: GROUP_BY Group by SQL statement + * Optional: ORDER_BY Order by SQL statement + * @return string A SQL statement ready for execution + */ + public function sql_build_query($query, $array); + + /** + * Fetch field + * if rownum is false, the current row is used, else it is pointing to the row (zero-based) + * + * @param string $field Name of the column + * @param mixed $rownum Row number, if false the current row will be used + * and the row curser will point to the next row + * Note: $rownum is 0 based + * @param mixed $query_id Already executed query to get the rows from, + * if false, the last query will be used. + * @return mixed String value of the field in the selected row, + * false, if the row does not exist + */ + public function sql_fetchfield($field, $rownum = false, $query_id = false); + + /** + * Fetch current row + * + * @param mixed $query_id Already executed query to get the rows from, + * if false, the last query will be used. + * @return mixed Array with the current row, + * false, if the row does not exist + */ + public function sql_fetchrow($query_id = false); + + /** + * Returns SQL string to cast a string expression to an int. + * + * @param string $expression An expression evaluating to string + * @return string Expression returning an int + */ + public function cast_expr_to_bigint($expression); + + /** + * Get last inserted id after insert statement + * + * @return string Autoincrement value of the last inserted row + */ + public function sql_nextid(); + + /** + * Add to query count + * + * @param bool $cached Is this query cached? + * @return null + */ + public function sql_add_num_queries($cached = false); + + /** + * Build LIMIT query + * + * @param string $query The SQL query to execute + * @param int $total The number of rows to select + * @param int $offset + * @param int $cache_ttl Either 0 to avoid caching or + * the time in seconds which the result shall be kept in cache + * @return mixed Buffered, seekable result handle, false on error + */ + public function sql_query_limit($query, $total, $offset = 0, $cache_ttl = 0); + + /** + * Base query method + * + * @param string $query The SQL query to execute + * @param int $cache_ttl Either 0 to avoid caching or + * the time in seconds which the result shall be kept in cache + * @return mixed Buffered, seekable result handle, false on error + */ + public function sql_query($query = '', $cache_ttl = 0); + + /** + * Returns SQL string to cast an integer expression to a string. + * + * @param string $expression An expression evaluating to int + * @return string Expression returning a string + */ + public function cast_expr_to_string($expression); + + /** + * Connect to server + * + * @param string $sqlserver Address of the database server + * @param string $sqluser User name of the SQL user + * @param string $sqlpassword Password of the SQL user + * @param string $database Name of the database + * @param mixed $port Port of the database server + * @param bool $persistency + * @param bool $new_link Should a new connection be established + * @return mixed Connection ID on success, string error message otherwise + */ + public function sql_connect($sqlserver, $sqluser, $sqlpassword, $database, $port = false, $persistency = false, $new_link = false); + + /** + * Run binary AND operator on DB column. + * Results in sql statement: "{$column_name} & (1 << {$bit}) {$compare}" + * + * @param string $column_name The column name to use + * @param int $bit The value to use for the AND operator, + * will be converted to (1 << $bit). Is used by + * options, using the number schema: 0, 1, 2...29 + * @param string $compare Any custom SQL code after the check (for example "= 0") + * @return string A SQL statement like: "{$column} & (1 << {$bit}) {$compare}" + */ + public function sql_bit_and($column_name, $bit, $compare = ''); + + /** + * Free sql result + * + * @param mixed $query_id Already executed query result, + * if false, the last query will be used. + * @return null + */ + public function sql_freeresult($query_id = false); + + /** + * Return number of sql queries and cached sql queries used + * + * @param bool $cached Should we return the number of cached or normal queries? + * @return int Number of queries that have been executed + */ + public function sql_num_queries($cached = false); + + /** + * Run more than one insert statement. + * + * @param string $table Table name to run the statements on + * @param array $sql_ary Multi-dimensional array holding the statement data + * @return bool false if no statements were executed. + */ + public function sql_multi_insert($table, $sql_ary); + + /** + * Return number of affected rows + * + * @return mixed Number of the affected rows by the last query + * false if no query has been run before + */ + public function sql_affectedrows(); + + /** + * DBAL garbage collection, close SQL connection + * + * @return mixed False if no connection was opened before, + * Server response otherwise + */ + public function sql_close(); + + /** + * Seek to given row number + * + * @param mixed $rownum Row number the curser should point to + * Note: $rownum is 0 based + * @param mixed $query_id ID of the query to set the row cursor on + * if false, the last query will be used. + * $query_id will then be set correctly + * @return bool False if something went wrong + */ + public function sql_rowseek($rownum, &$query_id); + + /** + * Escape string used in sql query + * + * @param string $msg String to be escaped + * @return string Escaped version of $msg + */ + public function sql_escape($msg); + + /** + * Correctly adjust LIKE expression for special characters + * Some DBMS are handling them in a different way + * + * @param string $expression The expression to use. Every wildcard is + * escaped, except $this->any_char and $this->one_char + * @return string A SQL statement like: "LIKE 'bertie_%'" + */ + public function sql_like_expression($expression); + + /** + * Correctly adjust NOT LIKE expression for special characters + * Some DBMS are handling them in a different way + * + * @param string $expression The expression to use. Every wildcard is + * escaped, except $this->any_char and $this->one_char + * @return string A SQL statement like: "NOT LIKE 'bertie_%'" + */ + public function sql_not_like_expression($expression); + + /** + * Explain queries + * + * @param string $mode Available modes: display, start, stop, + * add_select_row, fromcache, record_fromcache + * @param string $query The Query that should be explained + * @return mixed Either a full HTML page, boolean or null + */ + public function sql_report($mode, $query = ''); + + /** + * Build IN or NOT IN sql comparison string, uses <> or = on single element + * arrays to improve comparison speed + * + * @param string $field Name of the sql column that shall be compared + * @param array $array Array of values that are (not) allowed + * @param bool $negate true for NOT IN (), false for IN () + * @param bool $allow_empty_set If true, allow $array to be empty, + * this function will return 1=1 or 1=0 then. + * @return string A SQL statement like: "IN (1, 2, 3, 4)" or "= 1" + */ + public function sql_in_set($field, $array, $negate = false, $allow_empty_set = false); +} diff --git a/phpBB/phpbb/db/driver/factory.php b/phpBB/phpbb/db/driver/factory.php new file mode 100644 index 0000000000..fb3a826254 --- /dev/null +++ b/phpBB/phpbb/db/driver/factory.php @@ -0,0 +1,443 @@ +<?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\driver; + +use \Symfony\Component\DependencyInjection\ContainerInterface; + +/** +* Database Abstraction Layer +*/ +class factory implements driver_interface +{ + /** + * @var driver_interface + */ + protected $driver = null; + + /** + * @var ContainerInterface + */ + protected $container; + + /** + * Constructor. + * + * @param ContainerInterface $container A ContainerInterface instance + */ + public function __construct(ContainerInterface $container) + { + $this->container = $container; + } + + /** + * Return the current driver (and retrieved it from the container if necessary) + * + * @return driver_interface + */ + protected function get_driver() + { + if ($this->driver === null) + { + $this->driver = $this->container->get('dbal.conn.driver'); + } + + return $this->driver; + } + + /** + * Set the current driver + * + * @param driver_interface $driver + */ + public function set_driver(driver_interface $driver) + { + $this->driver = $driver; + } + + /** + * {@inheritdoc} + */ + public function get_sql_layer() + { + return $this->get_driver()->get_sql_layer(); + } + + /** + * {@inheritdoc} + */ + public function get_db_name() + { + return $this->get_driver()->get_db_name(); + } + + /** + * {@inheritdoc} + */ + public function get_any_char() + { + return $this->get_driver()->get_any_char(); + } + + /** + * {@inheritdoc} + */ + public function get_one_char() + { + return $this->get_driver()->get_one_char(); + } + + /** + * {@inheritdoc} + */ + public function get_db_connect_id() + { + return $this->get_driver()->get_db_connect_id(); + } + + /** + * {@inheritdoc} + */ + public function get_sql_error_triggered() + { + return $this->get_driver()->get_sql_error_triggered(); + } + + /** + * {@inheritdoc} + */ + public function get_sql_error_sql() + { + return $this->get_driver()->get_sql_error_sql(); + } + + /** + * {@inheritdoc} + */ + public function get_transaction() + { + return $this->get_driver()->get_transaction(); + } + + /** + * {@inheritdoc} + */ + public function get_sql_time() + { + return $this->get_driver()->get_sql_time(); + } + + /** + * {@inheritdoc} + */ + public function get_sql_error_returned() + { + return $this->get_driver()->get_sql_error_returned(); + } + + /** + * {@inheritdoc} + */ + public function get_multi_insert() + { + return $this->get_driver()->get_multi_insert(); + } + + /** + * {@inheritdoc} + */ + public function set_multi_insert($multi_insert) + { + $this->get_driver()->set_multi_insert($multi_insert); + } + + /** + * {@inheritdoc} + */ + public function get_row_count($table_name) + { + return $this->get_driver()->get_row_count($table_name); + } + + /** + * {@inheritdoc} + */ + public function get_estimated_row_count($table_name) + { + return $this->get_driver()->get_estimated_row_count($table_name); + } + + /** + * {@inheritdoc} + */ + public function sql_lower_text($column_name) + { + return $this->get_driver()->sql_lower_text($column_name); + } + + /** + * {@inheritdoc} + */ + public function sql_error($sql = '') + { + return $this->get_driver()->sql_error($sql); + } + + /** + * {@inheritdoc} + */ + public function sql_buffer_nested_transactions() + { + return $this->get_driver()->sql_buffer_nested_transactions(); + } + + /** + * {@inheritdoc} + */ + public function sql_bit_or($column_name, $bit, $compare = '') + { + return $this->get_driver()->sql_bit_or($column_name, $bit, $compare); + } + + /** + * {@inheritdoc} + */ + public function sql_server_info($raw = false, $use_cache = true) + { + return $this->get_driver()->sql_server_info($raw, $use_cache); + } + + /** + * {@inheritdoc} + */ + public function sql_return_on_error($fail = false) + { + return $this->get_driver()->sql_return_on_error($fail); + } + + /** + * {@inheritdoc} + */ + public function sql_build_array($query, $assoc_ary = array()) + { + return $this->get_driver()->sql_build_array($query, $assoc_ary); + } + + /** + * {@inheritdoc} + */ + public function sql_fetchrowset($query_id = false) + { + return $this->get_driver()->sql_fetchrowset($query_id); + } + + /** + * {@inheritdoc} + */ + public function sql_transaction($status = 'begin') + { + return $this->get_driver()->sql_transaction($status); + } + + /** + * {@inheritdoc} + */ + public function sql_concatenate($expr1, $expr2) + { + return $this->get_driver()->sql_concatenate($expr1, $expr2); + } + + /** + * {@inheritdoc} + */ + public function sql_case($condition, $action_true, $action_false = false) + { + return $this->get_driver()->sql_case($condition, $action_true, $action_false); + } + + /** + * {@inheritdoc} + */ + public function sql_build_query($query, $array) + { + return $this->get_driver()->sql_build_query($query, $array); + } + + /** + * {@inheritdoc} + */ + public function sql_fetchfield($field, $rownum = false, $query_id = false) + { + return $this->get_driver()->sql_fetchfield($field, $rownum, $query_id); + } + + /** + * {@inheritdoc} + */ + public function sql_fetchrow($query_id = false) + { + return $this->get_driver()->sql_fetchrow($query_id); + } + + /** + * {@inheritdoc} + */ + public function cast_expr_to_bigint($expression) + { + return $this->get_driver()->cast_expr_to_bigint($expression); + } + + /** + * {@inheritdoc} + */ + public function sql_nextid() + { + return $this->get_driver()->sql_nextid(); + } + + /** + * {@inheritdoc} + */ + public function sql_add_num_queries($cached = false) + { + return $this->get_driver()->sql_add_num_queries($cached); + } + + /** + * {@inheritdoc} + */ + public function sql_query_limit($query, $total, $offset = 0, $cache_ttl = 0) + { + return $this->get_driver()->sql_query_limit($query, $total, $offset, $cache_ttl); + } + + /** + * {@inheritdoc} + */ + public function sql_query($query = '', $cache_ttl = 0) + { + return $this->get_driver()->sql_query($query, $cache_ttl); + } + + /** + * {@inheritdoc} + */ + public function cast_expr_to_string($expression) + { + return $this->get_driver()->cast_expr_to_string($expression); + } + + /** + * {@inheritdoc} + */ + public function sql_connect($sqlserver, $sqluser, $sqlpassword, $database, $port = false, $persistency = false, $new_link = false) + { + throw new \Exception('Disabled method.'); + } + + /** + * {@inheritdoc} + */ + public function sql_bit_and($column_name, $bit, $compare = '') + { + return $this->get_driver()->sql_bit_and($column_name, $bit, $compare); + } + + /** + * {@inheritdoc} + */ + public function sql_freeresult($query_id = false) + { + return $this->get_driver()->sql_freeresult($query_id); + } + + /** + * {@inheritdoc} + */ + public function sql_num_queries($cached = false) + { + return $this->get_driver()->sql_num_queries($cached); + } + + /** + * {@inheritdoc} + */ + public function sql_multi_insert($table, $sql_ary) + { + return $this->get_driver()->sql_multi_insert($table, $sql_ary); + } + + /** + * {@inheritdoc} + */ + public function sql_affectedrows() + { + return $this->get_driver()->sql_affectedrows(); + } + + /** + * {@inheritdoc} + */ + public function sql_close() + { + return $this->get_driver()->sql_close(); + } + + /** + * {@inheritdoc} + */ + public function sql_rowseek($rownum, &$query_id) + { + return $this->get_driver()->sql_rowseek($rownum, $query_id); + } + + /** + * {@inheritdoc} + */ + public function sql_escape($msg) + { + return $this->get_driver()->sql_escape($msg); + } + + /** + * {@inheritdoc} + */ + public function sql_like_expression($expression) + { + return $this->get_driver()->sql_like_expression($expression); + } + + /** + * {@inheritdoc} + */ + public function sql_not_like_expression($expression) + { + return $this->get_driver()->sql_not_like_expression($expression); + } + + /** + * {@inheritdoc} + */ + public function sql_report($mode, $query = '') + { + return $this->get_driver()->sql_report($mode, $query); + } + + /** + * {@inheritdoc} + */ + public function sql_in_set($field, $array, $negate = false, $allow_empty_set = false) + { + return $this->get_driver()->sql_in_set($field, $array, $negate, $allow_empty_set); + } +} diff --git a/phpBB/phpbb/db/driver/mssql.php b/phpBB/phpbb/db/driver/mssql.php new file mode 100644 index 0000000000..f9ea884ce2 --- /dev/null +++ b/phpBB/phpbb/db/driver/mssql.php @@ -0,0 +1,476 @@ +<?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\driver; + +/** +* MSSQL Database Abstraction Layer +* Minimum Requirement is MSSQL 2000+ +*/ +class mssql extends \phpbb\db\driver\driver +{ + var $connect_error = ''; + + /** + * {@inheritDoc} + */ + function sql_connect($sqlserver, $sqluser, $sqlpassword, $database, $port = false, $persistency = false, $new_link = false) + { + if (!function_exists('mssql_connect')) + { + $this->connect_error = 'mssql_connect function does not exist, is mssql extension installed?'; + return $this->sql_error(''); + } + + $this->persistency = $persistency; + $this->user = $sqluser; + $this->dbname = $database; + + $port_delimiter = (defined('PHP_OS') && substr(PHP_OS, 0, 3) === 'WIN') ? ',' : ':'; + $this->server = $sqlserver . (($port) ? $port_delimiter . $port : ''); + + @ini_set('mssql.charset', 'UTF-8'); + @ini_set('mssql.textlimit', 2147483647); + @ini_set('mssql.textsize', 2147483647); + + $this->db_connect_id = ($this->persistency) ? @mssql_pconnect($this->server, $this->user, $sqlpassword, $new_link) : @mssql_connect($this->server, $this->user, $sqlpassword, $new_link); + + if ($this->db_connect_id && $this->dbname != '') + { + if (!@mssql_select_db($this->dbname, $this->db_connect_id)) + { + @mssql_close($this->db_connect_id); + return false; + } + } + + return ($this->db_connect_id) ? $this->db_connect_id : $this->sql_error(''); + } + + /** + * {@inheritDoc} + */ + function sql_server_info($raw = false, $use_cache = true) + { + global $cache; + + if (!$use_cache || empty($cache) || ($this->sql_server_version = $cache->get('mssql_version')) === false) + { + $result_id = @mssql_query("SELECT SERVERPROPERTY('productversion'), SERVERPROPERTY('productlevel'), SERVERPROPERTY('edition')", $this->db_connect_id); + + $row = false; + if ($result_id) + { + $row = @mssql_fetch_assoc($result_id); + @mssql_free_result($result_id); + } + + $this->sql_server_version = ($row) ? trim(implode(' ', $row)) : 0; + + if (!empty($cache) && $use_cache) + { + $cache->put('mssql_version', $this->sql_server_version); + } + } + + if ($raw) + { + return $this->sql_server_version; + } + + return ($this->sql_server_version) ? 'MSSQL<br />' . $this->sql_server_version : 'MSSQL'; + } + + /** + * {@inheritDoc} + */ + public function sql_concatenate($expr1, $expr2) + { + return $expr1 . ' + ' . $expr2; + } + + /** + * SQL Transaction + * @access private + */ + function _sql_transaction($status = 'begin') + { + switch ($status) + { + case 'begin': + return @mssql_query('BEGIN TRANSACTION', $this->db_connect_id); + break; + + case 'commit': + return @mssql_query('COMMIT TRANSACTION', $this->db_connect_id); + break; + + case 'rollback': + return @mssql_query('ROLLBACK TRANSACTION', $this->db_connect_id); + break; + } + + return true; + } + + /** + * {@inheritDoc} + */ + function sql_query($query = '', $cache_ttl = 0) + { + if ($query != '') + { + global $cache; + + // EXPLAIN only in extra debug mode + if (defined('DEBUG')) + { + $this->sql_report('start', $query); + } + else if (defined('PHPBB_DISPLAY_LOAD_TIME')) + { + $this->curtime = microtime(true); + } + + $this->query_result = ($cache && $cache_ttl) ? $cache->sql_load($query) : false; + $this->sql_add_num_queries($this->query_result); + + if ($this->query_result === false) + { + if (($this->query_result = @mssql_query($query, $this->db_connect_id)) === false) + { + $this->sql_error($query); + } + + if (defined('DEBUG')) + { + $this->sql_report('stop', $query); + } + else if (defined('PHPBB_DISPLAY_LOAD_TIME')) + { + $this->sql_time += microtime(true) - $this->curtime; + } + + 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) + { + $this->open_queries[(int) $this->query_result] = $this->query_result; + } + } + else if (defined('DEBUG')) + { + $this->sql_report('fromcache', $query); + } + } + else + { + return false; + } + + return $this->query_result; + } + + /** + * Build LIMIT query + */ + function _sql_query_limit($query, $total, $offset = 0, $cache_ttl = 0) + { + $this->query_result = false; + + // Since TOP is only returning a set number of rows we won't need it if total is set to 0 (return all rows) + if ($total) + { + // We need to grab the total number of rows + the offset number of rows to get the correct result + if (strpos($query, 'SELECT DISTINCT') === 0) + { + $query = 'SELECT DISTINCT TOP ' . ($total + $offset) . ' ' . substr($query, 15); + } + else + { + $query = 'SELECT TOP ' . ($total + $offset) . ' ' . substr($query, 6); + } + } + + $result = $this->sql_query($query, $cache_ttl); + + // Seek by $offset rows + if ($offset) + { + $this->sql_rowseek($offset, $result); + } + + return $result; + } + + /** + * {@inheritDoc} + */ + function sql_affectedrows() + { + return ($this->db_connect_id) ? @mssql_rows_affected($this->db_connect_id) : false; + } + + /** + * {@inheritDoc} + */ + function sql_fetchrow($query_id = false) + { + global $cache; + + if ($query_id === false) + { + $query_id = $this->query_result; + } + + if ($cache && $cache->sql_exists($query_id)) + { + return $cache->sql_fetchrow($query_id); + } + + if ($query_id === false) + { + return false; + } + + $row = @mssql_fetch_assoc($query_id); + + // I hope i am able to remove this later... hopefully only a PHP or MSSQL bug + if ($row) + { + foreach ($row as $key => $value) + { + $row[$key] = ($value === ' ' || $value === null) ? '' : $value; + } + } + + return $row; + } + + /** + * {@inheritDoc} + */ + function sql_rowseek($rownum, &$query_id) + { + global $cache; + + if ($query_id === false) + { + $query_id = $this->query_result; + } + + 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; + } + + /** + * {@inheritDoc} + */ + function sql_nextid() + { + $result_id = @mssql_query('SELECT SCOPE_IDENTITY()', $this->db_connect_id); + if ($result_id) + { + if ($row = @mssql_fetch_assoc($result_id)) + { + @mssql_free_result($result_id); + return $row['computed']; + } + @mssql_free_result($result_id); + } + + return false; + } + + /** + * {@inheritDoc} + */ + function sql_freeresult($query_id = false) + { + global $cache; + + if ($query_id === false) + { + $query_id = $this->query_result; + } + + if ($cache && !is_object($query_id) && $cache->sql_exists($query_id)) + { + return $cache->sql_freeresult($query_id); + } + + if (isset($this->open_queries[(int) $query_id])) + { + unset($this->open_queries[(int) $query_id]); + return @mssql_free_result($query_id); + } + + return false; + } + + /** + * {@inheritDoc} + */ + function sql_escape($msg) + { + return str_replace(array("'", "\0"), array("''", ''), $msg); + } + + /** + * {@inheritDoc} + */ + function sql_lower_text($column_name) + { + return "LOWER(SUBSTRING($column_name, 1, DATALENGTH($column_name)))"; + } + + /** + * Build LIKE expression + * @access private + */ + function _sql_like_expression($expression) + { + return $expression . " ESCAPE '\\'"; + } + + /** + * Build NOT LIKE expression + * @access private + */ + function _sql_not_like_expression($expression) + { + return $expression . " ESCAPE '\\'"; + } + + /** + * return sql error array + * @access private + */ + function _sql_error() + { + if (function_exists('mssql_get_last_message')) + { + $error = array( + 'message' => @mssql_get_last_message(), + 'code' => '', + ); + + // Get error code number + $result_id = @mssql_query('SELECT @@ERROR as code', $this->db_connect_id); + if ($result_id) + { + $row = @mssql_fetch_assoc($result_id); + $error['code'] = $row['code']; + @mssql_free_result($result_id); + } + + // Get full error message if possible + $sql = 'SELECT CAST(description as varchar(255)) as message + FROM master.dbo.sysmessages + WHERE error = ' . $error['code']; + $result_id = @mssql_query($sql); + + if ($result_id) + { + $row = @mssql_fetch_assoc($result_id); + if (!empty($row['message'])) + { + $error['message'] .= '<br />' . $row['message']; + } + @mssql_free_result($result_id); + } + } + else + { + $error = array( + 'message' => $this->connect_error, + 'code' => '', + ); + } + + return $error; + } + + /** + * Build db-specific query data + * @access private + */ + function _sql_custom_build($stage, $data) + { + return $data; + } + + /** + * Close sql connection + * @access private + */ + function _sql_close() + { + return @mssql_close($this->db_connect_id); + } + + /** + * Build db-specific report + * @access private + */ + function _sql_report($mode, $query = '') + { + switch ($mode) + { + case 'start': + $html_table = false; + @mssql_query('SET SHOWPLAN_TEXT ON;', $this->db_connect_id); + if ($result = @mssql_query($query, $this->db_connect_id)) + { + @mssql_next_result($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); + + if ($html_table) + { + $this->html_hold .= '</table>'; + } + break; + + case 'fromcache': + $endtime = explode(' ', microtime()); + $endtime = $endtime[0] + $endtime[1]; + + $result = @mssql_query($query, $this->db_connect_id); + while ($void = @mssql_fetch_assoc($result)) + { + // Take the time spent on parsing rows into account + } + @mssql_free_result($result); + + $splittime = explode(' ', microtime()); + $splittime = $splittime[0] + $splittime[1]; + + $this->sql_report('record_fromcache', $query, $endtime, $splittime); + + break; + } + } +} diff --git a/phpBB/phpbb/db/driver/mssql_base.php b/phpBB/phpbb/db/driver/mssql_base.php new file mode 100644 index 0000000000..514df9eaca --- /dev/null +++ b/phpBB/phpbb/db/driver/mssql_base.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\db\driver; + +/** +* MSSQL Database Base Abstraction Layer + */ +abstract class mssql_base extends \phpbb\db\driver\driver +{ + /** + * {@inheritDoc} + */ + public function sql_concatenate($expr1, $expr2) + { + return $expr1 . ' + ' . $expr2; + } + + /** + * {@inheritDoc} + */ + function sql_escape($msg) + { + return str_replace(array("'", "\0"), array("''", ''), $msg); + } + + /** + * {@inheritDoc} + */ + function sql_lower_text($column_name) + { + return "LOWER(SUBSTRING($column_name, 1, DATALENGTH($column_name)))"; + } + + /** + * Build LIKE expression + * @access private + */ + function _sql_like_expression($expression) + { + return $expression . " ESCAPE '\\'"; + } + + /** + * Build NOT LIKE expression + * @access private + */ + function _sql_not_like_expression($expression) + { + return $expression . " ESCAPE '\\'"; + } + + /** + * Build db-specific query data + * @access private + */ + function _sql_custom_build($stage, $data) + { + return $data; + } +} diff --git a/phpBB/phpbb/db/driver/mssql_odbc.php b/phpBB/phpbb/db/driver/mssql_odbc.php new file mode 100644 index 0000000000..8e5d4c7a4c --- /dev/null +++ b/phpBB/phpbb/db/driver/mssql_odbc.php @@ -0,0 +1,377 @@ +<?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\driver; + +/** +* Unified ODBC functions +* Unified ODBC functions support any database having ODBC driver, for example Adabas D, IBM DB2, iODBC, Solid, Sybase SQL Anywhere... +* Here we only support MSSQL Server 2000+ because of the provided schema +* +* @note number of bytes returned for returning data depends on odbc.defaultlrl php.ini setting. +* If it is limited to 4K for example only 4K of data is returned max, resulting in incomplete theme data for example. +* @note odbc.defaultbinmode may affect UTF8 characters +*/ +class mssql_odbc extends \phpbb\db\driver\mssql_base +{ + var $last_query_text = ''; + var $connect_error = ''; + + /** + * {@inheritDoc} + */ + function sql_connect($sqlserver, $sqluser, $sqlpassword, $database, $port = false, $persistency = false, $new_link = false) + { + $this->persistency = $persistency; + $this->user = $sqluser; + $this->dbname = $database; + + $port_delimiter = (defined('PHP_OS') && substr(PHP_OS, 0, 3) === 'WIN') ? ',' : ':'; + $this->server = $sqlserver . (($port) ? $port_delimiter . $port : ''); + + $max_size = @ini_get('odbc.defaultlrl'); + if (!empty($max_size)) + { + $unit = strtolower(substr($max_size, -1, 1)); + $max_size = (int) $max_size; + + if ($unit == 'k') + { + $max_size = floor($max_size / 1024); + } + else if ($unit == 'g') + { + $max_size *= 1024; + } + else if (is_numeric($unit)) + { + $max_size = floor((int) ($max_size . $unit) / 1048576); + } + $max_size = max(8, $max_size) . 'M'; + + @ini_set('odbc.defaultlrl', $max_size); + } + + if ($this->persistency) + { + if (!function_exists('odbc_pconnect')) + { + $this->connect_error = 'odbc_pconnect function does not exist, is odbc extension installed?'; + return $this->sql_error(''); + } + $this->db_connect_id = @odbc_pconnect($this->server, $this->user, $sqlpassword); + } + else + { + if (!function_exists('odbc_connect')) + { + $this->connect_error = 'odbc_connect function does not exist, is odbc extension installed?'; + return $this->sql_error(''); + } + $this->db_connect_id = @odbc_connect($this->server, $this->user, $sqlpassword); + } + + return ($this->db_connect_id) ? $this->db_connect_id : $this->sql_error(''); + } + + /** + * {@inheritDoc} + */ + function sql_server_info($raw = false, $use_cache = true) + { + global $cache; + + if (!$use_cache || empty($cache) || ($this->sql_server_version = $cache->get('mssqlodbc_version')) === false) + { + $result_id = @odbc_exec($this->db_connect_id, "SELECT SERVERPROPERTY('productversion'), SERVERPROPERTY('productlevel'), SERVERPROPERTY('edition')"); + + $row = false; + if ($result_id) + { + $row = @odbc_fetch_array($result_id); + @odbc_free_result($result_id); + } + + $this->sql_server_version = ($row) ? trim(implode(' ', $row)) : 0; + + if (!empty($cache) && $use_cache) + { + $cache->put('mssqlodbc_version', $this->sql_server_version); + } + } + + if ($raw) + { + return $this->sql_server_version; + } + + return ($this->sql_server_version) ? 'MSSQL (ODBC)<br />' . $this->sql_server_version : 'MSSQL (ODBC)'; + } + + /** + * SQL Transaction + * @access private + */ + function _sql_transaction($status = 'begin') + { + switch ($status) + { + case 'begin': + return @odbc_exec($this->db_connect_id, 'BEGIN TRANSACTION'); + break; + + case 'commit': + return @odbc_exec($this->db_connect_id, 'COMMIT TRANSACTION'); + break; + + case 'rollback': + return @odbc_exec($this->db_connect_id, 'ROLLBACK TRANSACTION'); + break; + } + + return true; + } + + /** + * {@inheritDoc} + */ + function sql_query($query = '', $cache_ttl = 0) + { + if ($query != '') + { + global $cache; + + // EXPLAIN only in extra debug mode + if (defined('DEBUG')) + { + $this->sql_report('start', $query); + } + else if (defined('PHPBB_DISPLAY_LOAD_TIME')) + { + $this->curtime = microtime(true); + } + + $this->last_query_text = $query; + $this->query_result = ($cache && $cache_ttl) ? $cache->sql_load($query) : false; + $this->sql_add_num_queries($this->query_result); + + if ($this->query_result === false) + { + if (($this->query_result = @odbc_exec($this->db_connect_id, $query)) === false) + { + $this->sql_error($query); + } + + if (defined('DEBUG')) + { + $this->sql_report('stop', $query); + } + else if (defined('PHPBB_DISPLAY_LOAD_TIME')) + { + $this->sql_time += microtime(true) - $this->curtime; + } + + 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) + { + $this->open_queries[(int) $this->query_result] = $this->query_result; + } + } + else if (defined('DEBUG')) + { + $this->sql_report('fromcache', $query); + } + } + else + { + return false; + } + + return $this->query_result; + } + + /** + * Build LIMIT query + */ + function _sql_query_limit($query, $total, $offset = 0, $cache_ttl = 0) + { + $this->query_result = false; + + // Since TOP is only returning a set number of rows we won't need it if total is set to 0 (return all rows) + if ($total) + { + // We need to grab the total number of rows + the offset number of rows to get the correct result + if (strpos($query, 'SELECT DISTINCT') === 0) + { + $query = 'SELECT DISTINCT TOP ' . ($total + $offset) . ' ' . substr($query, 15); + } + else + { + $query = 'SELECT TOP ' . ($total + $offset) . ' ' . substr($query, 6); + } + } + + $result = $this->sql_query($query, $cache_ttl); + + // Seek by $offset rows + if ($offset) + { + $this->sql_rowseek($offset, $result); + } + + return $result; + } + + /** + * {@inheritDoc} + */ + function sql_affectedrows() + { + return ($this->db_connect_id) ? @odbc_num_rows($this->query_result) : false; + } + + /** + * {@inheritDoc} + */ + function sql_fetchrow($query_id = false) + { + global $cache; + + if ($query_id === false) + { + $query_id = $this->query_result; + } + + if ($cache && $cache->sql_exists($query_id)) + { + return $cache->sql_fetchrow($query_id); + } + + return ($query_id !== false) ? @odbc_fetch_array($query_id) : false; + } + + /** + * {@inheritDoc} + */ + function sql_nextid() + { + $result_id = @odbc_exec($this->db_connect_id, 'SELECT @@IDENTITY'); + + if ($result_id) + { + if (@odbc_fetch_array($result_id)) + { + $id = @odbc_result($result_id, 1); + @odbc_free_result($result_id); + return $id; + } + @odbc_free_result($result_id); + } + + return false; + } + + /** + * {@inheritDoc} + */ + function sql_freeresult($query_id = false) + { + global $cache; + + if ($query_id === false) + { + $query_id = $this->query_result; + } + + if ($cache && !is_object($query_id) && $cache->sql_exists($query_id)) + { + return $cache->sql_freeresult($query_id); + } + + if (isset($this->open_queries[(int) $query_id])) + { + unset($this->open_queries[(int) $query_id]); + return @odbc_free_result($query_id); + } + + return false; + } + + /** + * return sql error array + * @access private + */ + function _sql_error() + { + if (function_exists('odbc_errormsg')) + { + $error = array( + 'message' => @odbc_errormsg(), + 'code' => @odbc_error(), + ); + } + else + { + $error = array( + 'message' => $this->connect_error, + 'code' => '', + ); + } + + return $error; + } + + /** + * Close sql connection + * @access private + */ + function _sql_close() + { + return @odbc_close($this->db_connect_id); + } + + /** + * Build db-specific report + * @access private + */ + function _sql_report($mode, $query = '') + { + switch ($mode) + { + case 'start': + break; + + case 'fromcache': + $endtime = explode(' ', microtime()); + $endtime = $endtime[0] + $endtime[1]; + + $result = @odbc_exec($this->db_connect_id, $query); + while ($void = @odbc_fetch_array($result)) + { + // Take the time spent on parsing rows into account + } + @odbc_free_result($result); + + $splittime = explode(' ', microtime()); + $splittime = $splittime[0] + $splittime[1]; + + $this->sql_report('record_fromcache', $query, $endtime, $splittime); + + break; + } + } +} diff --git a/phpBB/phpbb/db/driver/mssqlnative.php b/phpBB/phpbb/db/driver/mssqlnative.php new file mode 100644 index 0000000000..46a9b3a477 --- /dev/null +++ b/phpBB/phpbb/db/driver/mssqlnative.php @@ -0,0 +1,442 @@ +<?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. +* +*/ + +/** +* This is the MS SQL Server Native database abstraction layer. +* PHP mssql native driver required. +* @author Chris Pucci +* +*/ + +namespace phpbb\db\driver; + +class mssqlnative extends \phpbb\db\driver\mssql_base +{ + var $m_insert_id = null; + var $last_query_text = ''; + var $query_options = array(); + var $connect_error = ''; + + /** + * {@inheritDoc} + */ + function sql_connect($sqlserver, $sqluser, $sqlpassword, $database, $port = false, $persistency = false, $new_link = false) + { + // Test for driver support, to avoid suppressed fatal error + if (!function_exists('sqlsrv_connect')) + { + $this->connect_error = 'Native MS SQL Server driver for PHP is missing or needs to be updated. Version 1.1 or later is required to install phpBB3. You can download the driver from: http://www.microsoft.com/sqlserver/2005/en/us/PHP-Driver.aspx'; + return $this->sql_error(''); + } + + //set up connection variables + $this->persistency = $persistency; + $this->user = $sqluser; + $this->dbname = $database; + $port_delimiter = (defined('PHP_OS') && substr(PHP_OS, 0, 3) === 'WIN') ? ',' : ':'; + $this->server = $sqlserver . (($port) ? $port_delimiter . $port : ''); + + //connect to database + $this->db_connect_id = sqlsrv_connect($this->server, array( + 'Database' => $this->dbname, + 'UID' => $this->user, + 'PWD' => $sqlpassword + )); + + return ($this->db_connect_id) ? $this->db_connect_id : $this->sql_error(''); + } + + /** + * {@inheritDoc} + */ + function sql_server_info($raw = false, $use_cache = true) + { + global $cache; + + if (!$use_cache || empty($cache) || ($this->sql_server_version = $cache->get('mssql_version')) === false) + { + $arr_server_info = sqlsrv_server_info($this->db_connect_id); + $this->sql_server_version = $arr_server_info['SQLServerVersion']; + + if (!empty($cache) && $use_cache) + { + $cache->put('mssql_version', $this->sql_server_version); + } + } + + if ($raw) + { + return $this->sql_server_version; + } + + return ($this->sql_server_version) ? 'MSSQL<br />' . $this->sql_server_version : 'MSSQL'; + } + + /** + * {@inheritDoc} + */ + function sql_buffer_nested_transactions() + { + return true; + } + + /** + * SQL Transaction + * @access private + */ + function _sql_transaction($status = 'begin') + { + switch ($status) + { + case 'begin': + return sqlsrv_begin_transaction($this->db_connect_id); + break; + + case 'commit': + return sqlsrv_commit($this->db_connect_id); + break; + + case 'rollback': + return sqlsrv_rollback($this->db_connect_id); + break; + } + return true; + } + + /** + * {@inheritDoc} + */ + function sql_query($query = '', $cache_ttl = 0) + { + if ($query != '') + { + global $cache; + + // EXPLAIN only in extra debug mode + if (defined('DEBUG')) + { + $this->sql_report('start', $query); + } + else if (defined('PHPBB_DISPLAY_LOAD_TIME')) + { + $this->curtime = microtime(true); + } + + $this->last_query_text = $query; + $this->query_result = ($cache && $cache_ttl) ? $cache->sql_load($query) : false; + $this->sql_add_num_queries($this->query_result); + + if ($this->query_result === false) + { + if (($this->query_result = @sqlsrv_query($this->db_connect_id, $query, array(), $this->query_options)) === false) + { + $this->sql_error($query); + } + // reset options for next query + $this->query_options = array(); + + if (defined('DEBUG')) + { + $this->sql_report('stop', $query); + } + else if (defined('PHPBB_DISPLAY_LOAD_TIME')) + { + $this->sql_time += microtime(true) - $this->curtime; + } + + 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) + { + $this->open_queries[(int) $this->query_result] = $this->query_result; + } + } + else if (defined('DEBUG')) + { + $this->sql_report('fromcache', $query); + } + } + else + { + return false; + } + return $this->query_result; + } + + /** + * Build LIMIT query + */ + function _sql_query_limit($query, $total, $offset = 0, $cache_ttl = 0) + { + $this->query_result = false; + + // total == 0 means all results - not zero results + if ($offset == 0 && $total !== 0) + { + if (strpos($query, "SELECT") === false) + { + $query = "TOP {$total} " . $query; + } + else + { + $query = preg_replace('/SELECT(\s*DISTINCT)?/Dsi', 'SELECT$1 TOP '.$total, $query); + } + } + else if ($offset > 0) + { + $query = preg_replace('/SELECT(\s*DISTINCT)?/Dsi', 'SELECT$1 TOP(10000000) ', $query); + $query = 'SELECT * + FROM (SELECT sub2.*, ROW_NUMBER() OVER(ORDER BY sub2.line2) AS line3 + FROM (SELECT 1 AS line2, sub1.* FROM (' . $query . ') AS sub1) as sub2) AS sub3'; + + if ($total > 0) + { + $query .= ' WHERE line3 BETWEEN ' . ($offset+1) . ' AND ' . ($offset + $total); + } + else + { + $query .= ' WHERE line3 > ' . $offset; + } + } + + $result = $this->sql_query($query, $cache_ttl); + + return $result; + } + + /** + * {@inheritDoc} + */ + function sql_affectedrows() + { + return ($this->db_connect_id) ? @sqlsrv_rows_affected($this->query_result) : false; + } + + /** + * {@inheritDoc} + */ + function sql_fetchrow($query_id = false) + { + global $cache; + + if ($query_id === false) + { + $query_id = $this->query_result; + } + + if ($cache && $cache->sql_exists($query_id)) + { + return $cache->sql_fetchrow($query_id); + } + + if ($query_id === false) + { + return false; + } + + $row = @sqlsrv_fetch_array($query_id, SQLSRV_FETCH_ASSOC); + + if ($row) + { + foreach ($row as $key => $value) + { + $row[$key] = ($value === ' ' || $value === null) ? '' : $value; + } + + // remove helper values from LIMIT queries + if (isset($row['line2'])) + { + unset($row['line2'], $row['line3']); + } + } + return (sizeof($row)) ? $row : false; + } + + /** + * {@inheritDoc} + */ + function sql_nextid() + { + $result_id = @sqlsrv_query($this->db_connect_id, 'SELECT @@IDENTITY'); + + if ($result_id !== false) + { + $row = @sqlsrv_fetch_array($result_id); + $id = $row[0]; + @sqlsrv_free_stmt($result_id); + return $id; + } + else + { + return false; + } + } + + /** + * {@inheritDoc} + */ + function sql_freeresult($query_id = false) + { + global $cache; + + if ($query_id === false) + { + $query_id = $this->query_result; + } + + if ($cache && !is_object($query_id) && $cache->sql_exists($query_id)) + { + return $cache->sql_freeresult($query_id); + } + + if (isset($this->open_queries[(int) $query_id])) + { + unset($this->open_queries[(int) $query_id]); + return @sqlsrv_free_stmt($query_id); + } + + return false; + } + + /** + * return sql error array + * @access private + */ + function _sql_error() + { + if (function_exists('sqlsrv_errors')) + { + $errors = @sqlsrv_errors(SQLSRV_ERR_ERRORS); + $error_message = ''; + $code = 0; + + if ($errors != null) + { + foreach ($errors as $error) + { + $error_message .= "SQLSTATE: " . $error['SQLSTATE'] . "\n"; + $error_message .= "code: " . $error['code'] . "\n"; + $code = $error['code']; + $error_message .= "message: " . $error['message'] . "\n"; + } + $this->last_error_result = $error_message; + $error = $this->last_error_result; + } + else + { + $error = (isset($this->last_error_result) && $this->last_error_result) ? $this->last_error_result : array(); + } + + $error = array( + 'message' => $error, + 'code' => $code, + ); + } + else + { + $error = array( + 'message' => $this->connect_error, + 'code' => '', + ); + } + + return $error; + } + + /** + * Close sql connection + * @access private + */ + function _sql_close() + { + return @sqlsrv_close($this->db_connect_id); + } + + /** + * Build db-specific report + * @access private + */ + function _sql_report($mode, $query = '') + { + switch ($mode) + { + case 'start': + $html_table = false; + @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)) + { + $html_table = $this->sql_report('add_select_row', $query, $html_table, $row); + } + } + @sqlsrv_query($this->db_connect_id, 'SET SHOWPLAN_TEXT OFF;'); + @sqlsrv_free_stmt($result); + + if ($html_table) + { + $this->html_hold .= '</table>'; + } + break; + + case 'fromcache': + $endtime = explode(' ', microtime()); + $endtime = $endtime[0] + $endtime[1]; + + $result = @sqlsrv_query($this->db_connect_id, $query); + while ($void = @sqlsrv_fetch_array($result)) + { + // Take the time spent on parsing rows into account + } + @sqlsrv_free_stmt($result); + + $splittime = explode(' ', microtime()); + $splittime = $splittime[0] + $splittime[1]; + + $this->sql_report('record_fromcache', $query, $endtime, $splittime); + + break; + } + } + + /** + * Utility method used to retrieve number of rows + * Emulates mysql_num_rows + * Used in acp_database.php -> write_data_mssqlnative() + * Requires a static or keyset cursor to be definde via + * mssqlnative_set_query_options() + */ + function mssqlnative_num_rows($res) + { + if ($res !== false) + { + return sqlsrv_num_rows($res); + } + else + { + return false; + } + } + + /** + * Allows setting mssqlnative specific query options passed to sqlsrv_query as 4th parameter. + */ + function mssqlnative_set_query_options($options) + { + $this->query_options = $options; + } +} diff --git a/phpBB/phpbb/db/driver/mysql.php b/phpBB/phpbb/db/driver/mysql.php new file mode 100644 index 0000000000..e93c7239e8 --- /dev/null +++ b/phpBB/phpbb/db/driver/mysql.php @@ -0,0 +1,485 @@ +<?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\driver; + +/** +* MySQL4 Database Abstraction Layer +* Compatible with: +* MySQL 3.23+ +* MySQL 4.0+ +* MySQL 4.1+ +* MySQL 5.0+ +*/ +class mysql extends \phpbb\db\driver\mysql_base +{ + var $multi_insert = true; + var $connect_error = ''; + + /** + * {@inheritDoc} + */ + function sql_connect($sqlserver, $sqluser, $sqlpassword, $database, $port = false, $persistency = false, $new_link = false) + { + $this->persistency = $persistency; + $this->user = $sqluser; + $this->server = $sqlserver . (($port) ? ':' . $port : ''); + $this->dbname = $database; + + $this->sql_layer = 'mysql4'; + + if ($this->persistency) + { + if (!function_exists('mysql_pconnect')) + { + $this->connect_error = 'mysql_pconnect function does not exist, is mysql extension installed?'; + return $this->sql_error(''); + } + $this->db_connect_id = @mysql_pconnect($this->server, $this->user, $sqlpassword); + } + else + { + if (!function_exists('mysql_connect')) + { + $this->connect_error = 'mysql_connect function does not exist, is mysql extension installed?'; + return $this->sql_error(''); + } + $this->db_connect_id = @mysql_connect($this->server, $this->user, $sqlpassword, $new_link); + } + + if ($this->db_connect_id && $this->dbname != '') + { + if (@mysql_select_db($this->dbname, $this->db_connect_id)) + { + // Determine what version we are using and if it natively supports UNICODE + if (version_compare($this->sql_server_info(true), '4.1.0', '>=')) + { + @mysql_query("SET NAMES 'utf8'", $this->db_connect_id); + + // enforce strict mode on databases that support it + 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'])); + + // TRADITIONAL includes STRICT_ALL_TABLES and STRICT_TRANS_TABLES + if (!in_array('TRADITIONAL', $modes)) + { + if (!in_array('STRICT_ALL_TABLES', $modes)) + { + $modes[] = 'STRICT_ALL_TABLES'; + } + + if (!in_array('STRICT_TRANS_TABLES', $modes)) + { + $modes[] = 'STRICT_TRANS_TABLES'; + } + } + + $mode = implode(',', $modes); + @mysql_query("SET SESSION sql_mode='{$mode}'", $this->db_connect_id); + } + } + else if (version_compare($this->sql_server_info(true), '4.0.0', '<')) + { + $this->sql_layer = 'mysql'; + } + + return $this->db_connect_id; + } + } + + return $this->sql_error(''); + } + + /** + * {@inheritDoc} + */ + function sql_server_info($raw = false, $use_cache = true) + { + global $cache; + + 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); + + $this->sql_server_version = $row['version']; + + if (!empty($cache) && $use_cache) + { + $cache->put('mysql_version', $this->sql_server_version); + } + } + + return ($raw) ? $this->sql_server_version : 'MySQL ' . $this->sql_server_version; + } + + /** + * SQL Transaction + * @access private + */ + function _sql_transaction($status = 'begin') + { + switch ($status) + { + case 'begin': + return @mysql_query('BEGIN', $this->db_connect_id); + break; + + case 'commit': + return @mysql_query('COMMIT', $this->db_connect_id); + break; + + case 'rollback': + return @mysql_query('ROLLBACK', $this->db_connect_id); + break; + } + + return true; + } + + /** + * {@inheritDoc} + */ + function sql_query($query = '', $cache_ttl = 0) + { + if ($query != '') + { + global $cache; + + // EXPLAIN only in extra debug mode + if (defined('DEBUG')) + { + $this->sql_report('start', $query); + } + else if (defined('PHPBB_DISPLAY_LOAD_TIME')) + { + $this->curtime = microtime(true); + } + + $this->query_result = ($cache && $cache_ttl) ? $cache->sql_load($query) : false; + $this->sql_add_num_queries($this->query_result); + + if ($this->query_result === false) + { + if (($this->query_result = @mysql_query($query, $this->db_connect_id)) === false) + { + $this->sql_error($query); + } + + if (defined('DEBUG')) + { + $this->sql_report('stop', $query); + } + else if (defined('PHPBB_DISPLAY_LOAD_TIME')) + { + $this->sql_time += microtime(true) - $this->curtime; + } + + 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) + { + $this->open_queries[(int) $this->query_result] = $this->query_result; + } + } + else if (defined('DEBUG')) + { + $this->sql_report('fromcache', $query); + } + } + else + { + return false; + } + + return $this->query_result; + } + + /** + * {@inheritDoc} + */ + function sql_affectedrows() + { + if ($this->db_connect_id) + { + // We always want the number of matched rows + // instead of changed rows, when running an update. + // So when mysql_info() returns the number of matched rows + // we return that one instead of mysql_affected_rows() + $mysql_info = @mysql_info($this->db_connect_id); + if ($mysql_info !== false) + { + $match = array(); + preg_match('#^Rows matched: (\d)+ Changed: (\d)+ Warnings: (\d)+$#', $mysql_info, $match); + if (isset($match[1])) + { + return $match[1]; + } + } + + return @mysql_affected_rows($this->db_connect_id); + } + return false; + } + + /** + * {@inheritDoc} + */ + function sql_fetchrow($query_id = false) + { + global $cache; + + if ($query_id === false) + { + $query_id = $this->query_result; + } + + if ($cache && $cache->sql_exists($query_id)) + { + return $cache->sql_fetchrow($query_id); + } + + return ($query_id !== false) ? @mysql_fetch_assoc($query_id) : false; + } + + /** + * {@inheritDoc} + */ + function sql_rowseek($rownum, &$query_id) + { + global $cache; + + if ($query_id === false) + { + $query_id = $this->query_result; + } + + if ($cache && $cache->sql_exists($query_id)) + { + return $cache->sql_rowseek($rownum, $query_id); + } + + return ($query_id !== false) ? @mysql_data_seek($query_id, $rownum) : false; + } + + /** + * {@inheritDoc} + */ + function sql_nextid() + { + return ($this->db_connect_id) ? @mysql_insert_id($this->db_connect_id) : false; + } + + /** + * {@inheritDoc} + */ + function sql_freeresult($query_id = false) + { + global $cache; + + if ($query_id === false) + { + $query_id = $this->query_result; + } + + if ($cache && !is_object($query_id) && $cache->sql_exists($query_id)) + { + return $cache->sql_freeresult($query_id); + } + + if (isset($this->open_queries[(int) $query_id])) + { + unset($this->open_queries[(int) $query_id]); + return @mysql_free_result($query_id); + } + + return false; + } + + /** + * {@inheritDoc} + */ + function sql_escape($msg) + { + if (!$this->db_connect_id) + { + return @mysql_real_escape_string($msg); + } + + return @mysql_real_escape_string($msg, $this->db_connect_id); + } + + /** + * return sql error array + * @access private + */ + function _sql_error() + { + if ($this->db_connect_id) + { + $error = array( + 'message' => @mysql_error($this->db_connect_id), + 'code' => @mysql_errno($this->db_connect_id), + ); + } + else if (function_exists('mysql_error')) + { + $error = array( + 'message' => @mysql_error(), + 'code' => @mysql_errno(), + ); + } + else + { + $error = array( + 'message' => $this->connect_error, + 'code' => '', + ); + } + + return $error; + } + + /** + * Close sql connection + * @access private + */ + function _sql_close() + { + return @mysql_close($this->db_connect_id); + } + + /** + * Build db-specific report + * @access private + */ + function _sql_report($mode, $query = '') + { + static $test_prof; + + // current detection method, might just switch to see the existance of INFORMATION_SCHEMA.PROFILING + if ($test_prof === null) + { + $test_prof = false; + if (version_compare($this->sql_server_info(true), '5.0.37', '>=') && version_compare($this->sql_server_info(true), '5.1', '<')) + { + $test_prof = true; + } + } + + switch ($mode) + { + case 'start': + + $explain_query = $query; + if (preg_match('/UPDATE ([a-z0-9_]+).*?WHERE(.*)/s', $query, $m)) + { + $explain_query = 'SELECT * FROM ' . $m[1] . ' WHERE ' . $m[2]; + } + else if (preg_match('/DELETE FROM ([a-z0-9_]+).*?WHERE(.*)/s', $query, $m)) + { + $explain_query = 'SELECT * FROM ' . $m[1] . ' WHERE ' . $m[2]; + } + + if (preg_match('/^SELECT/', $explain_query)) + { + $html_table = false; + + // begin profiling + if ($test_prof) + { + @mysql_query('SET profiling = 1;', $this->db_connect_id); + } + + if ($result = @mysql_query("EXPLAIN $explain_query", $this->db_connect_id)) + { + while ($row = @mysql_fetch_assoc($result)) + { + $html_table = $this->sql_report('add_select_row', $query, $html_table, $row); + } + } + @mysql_free_result($result); + + if ($html_table) + { + $this->html_hold .= '</table>'; + } + + if ($test_prof) + { + $html_table = false; + + // get the last profile + if ($result = @mysql_query('SHOW PROFILE ALL;', $this->db_connect_id)) + { + $this->html_hold .= '<br />'; + while ($row = @mysql_fetch_assoc($result)) + { + // make <unknown> HTML safe + if (!empty($row['Source_function'])) + { + $row['Source_function'] = str_replace(array('<', '>'), array('<', '>'), $row['Source_function']); + } + + // remove unsupported features + foreach ($row as $key => $val) + { + if ($val === null) + { + unset($row[$key]); + } + } + $html_table = $this->sql_report('add_select_row', $query, $html_table, $row); + } + } + @mysql_free_result($result); + + if ($html_table) + { + $this->html_hold .= '</table>'; + } + + @mysql_query('SET profiling = 0;', $this->db_connect_id); + } + } + + break; + + case 'fromcache': + $endtime = explode(' ', microtime()); + $endtime = $endtime[0] + $endtime[1]; + + $result = @mysql_query($query, $this->db_connect_id); + while ($void = @mysql_fetch_assoc($result)) + { + // Take the time spent on parsing rows into account + } + @mysql_free_result($result); + + $splittime = explode(' ', microtime()); + $splittime = $splittime[0] + $splittime[1]; + + $this->sql_report('record_fromcache', $query, $endtime, $splittime); + + break; + } + } +} diff --git a/phpBB/phpbb/db/driver/mysql_base.php b/phpBB/phpbb/db/driver/mysql_base.php new file mode 100644 index 0000000000..5e0b359134 --- /dev/null +++ b/phpBB/phpbb/db/driver/mysql_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\db\driver; + +/** +* Abstract MySQL Database Base Abstraction Layer +*/ +abstract class mysql_base extends \phpbb\db\driver\driver +{ + /** + * {@inheritDoc} + */ + public function sql_concatenate($expr1, $expr2) + { + return 'CONCAT(' . $expr1 . ', ' . $expr2 . ')'; + } + + /** + * Build LIMIT query + */ + function _sql_query_limit($query, $total, $offset = 0, $cache_ttl = 0) + { + $this->query_result = false; + + // if $total is set to 0 we do not want to limit the number of rows + if ($total == 0) + { + // MySQL 4.1+ no longer supports -1 in limit queries + $total = '18446744073709551615'; + } + + $query .= "\n LIMIT " . ((!empty($offset)) ? $offset . ', ' . $total : $total); + + return $this->sql_query($query, $cache_ttl); + } + + /** + * {@inheritDoc} + */ + function get_estimated_row_count($table_name) + { + $table_status = $this->get_table_status($table_name); + + if (isset($table_status['Engine'])) + { + if ($table_status['Engine'] === 'MyISAM') + { + return $table_status['Rows']; + } + else if ($table_status['Engine'] === 'InnoDB' && $table_status['Rows'] > 100000) + { + return '~' . $table_status['Rows']; + } + } + + return parent::get_row_count($table_name); + } + + /** + * {@inheritDoc} + */ + function get_row_count($table_name) + { + $table_status = $this->get_table_status($table_name); + + if (isset($table_status['Engine']) && $table_status['Engine'] === 'MyISAM') + { + return $table_status['Rows']; + } + + return parent::get_row_count($table_name); + } + + /** + * Gets some information about the specified table. + * + * @param string $table_name Table name + * + * @return array + * + * @access protected + */ + function get_table_status($table_name) + { + $sql = "SHOW TABLE STATUS + LIKE '" . $this->sql_escape($table_name) . "'"; + $result = $this->sql_query($sql); + $table_status = $this->sql_fetchrow($result); + $this->sql_freeresult($result); + + return $table_status; + } + + /** + * Build LIKE expression + * @access private + */ + function _sql_like_expression($expression) + { + return $expression; + } + + /** + * Build NOT LIKE expression + * @access private + */ + function _sql_not_like_expression($expression) + { + return $expression; + } + + /** + * Build db-specific query data + * @access private + */ + function _sql_custom_build($stage, $data) + { + switch ($stage) + { + case 'FROM': + $data = '(' . $data . ')'; + break; + } + + return $data; + } +} diff --git a/phpBB/phpbb/db/driver/mysqli.php b/phpBB/phpbb/db/driver/mysqli.php new file mode 100644 index 0000000000..c0ddfbf76c --- /dev/null +++ b/phpBB/phpbb/db/driver/mysqli.php @@ -0,0 +1,475 @@ +<?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\driver; + +/** +* MySQLi Database Abstraction Layer +* mysqli-extension has to be compiled with: +* MySQL 4.1+ or MySQL 5.0+ +*/ +class mysqli extends \phpbb\db\driver\mysql_base +{ + var $multi_insert = true; + var $connect_error = ''; + + /** + * {@inheritDoc} + */ + function sql_connect($sqlserver, $sqluser, $sqlpassword, $database, $port = false, $persistency = false, $new_link = false) + { + if (!function_exists('mysqli_connect')) + { + $this->connect_error = 'mysqli_connect function does not exist, is mysqli extension installed?'; + return $this->sql_error(''); + } + + $this->persistency = $persistency; + $this->user = $sqluser; + + // If persistent connection, set dbhost to localhost when empty and prepend it with 'p:' prefix + $this->server = ($this->persistency) ? 'p:' . (($sqlserver) ? $sqlserver : 'localhost') : $sqlserver; + + $this->dbname = $database; + $port = (!$port) ? null : $port; + + // If port is set and it is not numeric, most likely mysqli socket is set. + // Try to map it to the $socket parameter. + $socket = null; + if ($port) + { + if (is_numeric($port)) + { + $port = (int) $port; + } + else + { + $socket = $port; + $port = null; + } + } + + $this->db_connect_id = mysqli_init(); + + if (!@mysqli_real_connect($this->db_connect_id, $this->server, $this->user, $sqlpassword, $this->dbname, $port, $socket, MYSQLI_CLIENT_FOUND_ROWS)) + { + $this->db_connect_id = ''; + } + + if ($this->db_connect_id && $this->dbname != '') + { + @mysqli_query($this->db_connect_id, "SET NAMES 'utf8'"); + + // enforce strict mode on databases that support it + 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) + { + $row = @mysqli_fetch_assoc($result); + + $modes = array_map('trim', explode(',', $row['sql_mode'])); + } + else + { + $modes = array(); + } + @mysqli_free_result($result); + + // TRADITIONAL includes STRICT_ALL_TABLES and STRICT_TRANS_TABLES + if (!in_array('TRADITIONAL', $modes)) + { + if (!in_array('STRICT_ALL_TABLES', $modes)) + { + $modes[] = 'STRICT_ALL_TABLES'; + } + + if (!in_array('STRICT_TRANS_TABLES', $modes)) + { + $modes[] = 'STRICT_TRANS_TABLES'; + } + } + + $mode = implode(',', $modes); + @mysqli_query($this->db_connect_id, "SET SESSION sql_mode='{$mode}'"); + } + return $this->db_connect_id; + } + + return $this->sql_error(''); + } + + /** + * {@inheritDoc} + */ + function sql_server_info($raw = false, $use_cache = true) + { + global $cache; + + 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) + { + $row = @mysqli_fetch_assoc($result); + + $this->sql_server_version = $row['version']; + + if (!empty($cache) && $use_cache) + { + $cache->put('mysqli_version', $this->sql_server_version); + } + } + @mysqli_free_result($result); + } + + return ($raw) ? $this->sql_server_version : 'MySQL(i) ' . $this->sql_server_version; + } + + /** + * SQL Transaction + * @access private + */ + function _sql_transaction($status = 'begin') + { + switch ($status) + { + case 'begin': + return @mysqli_autocommit($this->db_connect_id, false); + break; + + case 'commit': + $result = @mysqli_commit($this->db_connect_id); + @mysqli_autocommit($this->db_connect_id, true); + return $result; + break; + + case 'rollback': + $result = @mysqli_rollback($this->db_connect_id); + @mysqli_autocommit($this->db_connect_id, true); + return $result; + break; + } + + return true; + } + + /** + * {@inheritDoc} + */ + function sql_query($query = '', $cache_ttl = 0) + { + if ($query != '') + { + global $cache; + + // EXPLAIN only in extra debug mode + if (defined('DEBUG')) + { + $this->sql_report('start', $query); + } + else if (defined('PHPBB_DISPLAY_LOAD_TIME')) + { + $this->curtime = microtime(true); + } + + $this->query_result = ($cache && $cache_ttl) ? $cache->sql_load($query) : false; + $this->sql_add_num_queries($this->query_result); + + if ($this->query_result === false) + { + if (($this->query_result = @mysqli_query($this->db_connect_id, $query)) === false) + { + $this->sql_error($query); + } + + if (defined('DEBUG')) + { + $this->sql_report('stop', $query); + } + else if (defined('PHPBB_DISPLAY_LOAD_TIME')) + { + $this->sql_time += microtime(true) - $this->curtime; + } + + if ($cache && $cache_ttl) + { + $this->query_result = $cache->sql_save($this, $query, $this->query_result, $cache_ttl); + } + } + else if (defined('DEBUG')) + { + $this->sql_report('fromcache', $query); + } + } + else + { + return false; + } + + return $this->query_result; + } + + /** + * {@inheritDoc} + */ + function sql_affectedrows() + { + return ($this->db_connect_id) ? @mysqli_affected_rows($this->db_connect_id) : false; + } + + /** + * {@inheritDoc} + */ + function sql_fetchrow($query_id = false) + { + global $cache; + + if ($query_id === false) + { + $query_id = $this->query_result; + } + + if ($cache && !is_object($query_id) && $cache->sql_exists($query_id)) + { + return $cache->sql_fetchrow($query_id); + } + + if ($query_id !== false && $query_id !== null) + { + $result = @mysqli_fetch_assoc($query_id); + return $result !== null ? $result : false; + } + + return false; + } + + /** + * {@inheritDoc} + */ + function sql_rowseek($rownum, &$query_id) + { + global $cache; + + if ($query_id === false) + { + $query_id = $this->query_result; + } + + if ($cache && !is_object($query_id) && $cache->sql_exists($query_id)) + { + return $cache->sql_rowseek($rownum, $query_id); + } + + return ($query_id !== false) ? @mysqli_data_seek($query_id, $rownum) : false; + } + + /** + * {@inheritDoc} + */ + function sql_nextid() + { + return ($this->db_connect_id) ? @mysqli_insert_id($this->db_connect_id) : false; + } + + /** + * {@inheritDoc} + */ + function sql_freeresult($query_id = false) + { + global $cache; + + if ($query_id === false) + { + $query_id = $this->query_result; + } + + if ($cache && !is_object($query_id) && $cache->sql_exists($query_id)) + { + return $cache->sql_freeresult($query_id); + } + + return @mysqli_free_result($query_id); + } + + /** + * {@inheritDoc} + */ + function sql_escape($msg) + { + return @mysqli_real_escape_string($this->db_connect_id, $msg); + } + + /** + * return sql error array + * @access private + */ + function _sql_error() + { + if ($this->db_connect_id) + { + $error = array( + 'message' => @mysqli_error($this->db_connect_id), + 'code' => @mysqli_errno($this->db_connect_id) + ); + } + else if (function_exists('mysqli_connect_error')) + { + $error = array( + 'message' => @mysqli_connect_error(), + 'code' => @mysqli_connect_errno(), + ); + } + else + { + $error = array( + 'message' => $this->connect_error, + 'code' => '', + ); + } + + return $error; + } + + /** + * Close sql connection + * @access private + */ + function _sql_close() + { + return @mysqli_close($this->db_connect_id); + } + + /** + * Build db-specific report + * @access private + */ + function _sql_report($mode, $query = '') + { + static $test_prof; + + // current detection method, might just switch to see the existance of INFORMATION_SCHEMA.PROFILING + if ($test_prof === null) + { + $test_prof = false; + if (strpos(mysqli_get_server_info($this->db_connect_id), 'community') !== false) + { + $ver = mysqli_get_server_version($this->db_connect_id); + if ($ver >= 50037 && $ver < 50100) + { + $test_prof = true; + } + } + } + + switch ($mode) + { + case 'start': + + $explain_query = $query; + if (preg_match('/UPDATE ([a-z0-9_]+).*?WHERE(.*)/s', $query, $m)) + { + $explain_query = 'SELECT * FROM ' . $m[1] . ' WHERE ' . $m[2]; + } + else if (preg_match('/DELETE FROM ([a-z0-9_]+).*?WHERE(.*)/s', $query, $m)) + { + $explain_query = 'SELECT * FROM ' . $m[1] . ' WHERE ' . $m[2]; + } + + if (preg_match('/^SELECT/', $explain_query)) + { + $html_table = false; + + // begin profiling + if ($test_prof) + { + @mysqli_query($this->db_connect_id, 'SET profiling = 1;'); + } + + if ($result = @mysqli_query($this->db_connect_id, "EXPLAIN $explain_query")) + { + while ($row = @mysqli_fetch_assoc($result)) + { + $html_table = $this->sql_report('add_select_row', $query, $html_table, $row); + } + } + @mysqli_free_result($result); + + if ($html_table) + { + $this->html_hold .= '</table>'; + } + + if ($test_prof) + { + $html_table = false; + + // get the last profile + if ($result = @mysqli_query($this->db_connect_id, 'SHOW PROFILE ALL;')) + { + $this->html_hold .= '<br />'; + while ($row = @mysqli_fetch_assoc($result)) + { + // make <unknown> HTML safe + if (!empty($row['Source_function'])) + { + $row['Source_function'] = str_replace(array('<', '>'), array('<', '>'), $row['Source_function']); + } + + // remove unsupported features + foreach ($row as $key => $val) + { + if ($val === null) + { + unset($row[$key]); + } + } + $html_table = $this->sql_report('add_select_row', $query, $html_table, $row); + } + } + @mysqli_free_result($result); + + if ($html_table) + { + $this->html_hold .= '</table>'; + } + + @mysqli_query($this->db_connect_id, 'SET profiling = 0;'); + } + } + + break; + + case 'fromcache': + $endtime = explode(' ', microtime()); + $endtime = $endtime[0] + $endtime[1]; + + $result = @mysqli_query($this->db_connect_id, $query); + if ($result !== null) + { + while ($void = @mysqli_fetch_assoc($result)) + { + // Take the time spent on parsing rows into account + } + } + @mysqli_free_result($result); + + $splittime = explode(' ', microtime()); + $splittime = $splittime[0] + $splittime[1]; + + $this->sql_report('record_fromcache', $query, $endtime, $splittime); + + break; + } + } +} diff --git a/phpBB/phpbb/db/driver/oracle.php b/phpBB/phpbb/db/driver/oracle.php new file mode 100644 index 0000000000..6dcab5dd7d --- /dev/null +++ b/phpBB/phpbb/db/driver/oracle.php @@ -0,0 +1,807 @@ +<?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\driver; + +/** +* Oracle Database Abstraction Layer +*/ +class oracle extends \phpbb\db\driver\driver +{ + var $last_query_text = ''; + var $connect_error = ''; + + /** + * {@inheritDoc} + */ + function sql_connect($sqlserver, $sqluser, $sqlpassword, $database, $port = false, $persistency = false, $new_link = false) + { + $this->persistency = $persistency; + $this->user = $sqluser; + $this->server = $sqlserver . (($port) ? ':' . $port : ''); + $this->dbname = $database; + + $connect = $database; + + // support for "easy connect naming" + if ($sqlserver !== '' && $sqlserver !== '/') + { + if (substr($sqlserver, -1, 1) == '/') + { + $sqlserver == substr($sqlserver, 0, -1); + } + $connect = $sqlserver . (($port) ? ':' . $port : '') . '/' . $database; + } + + if ($new_link) + { + if (!function_exists('ocinlogon')) + { + $this->connect_error = 'ocinlogon function does not exist, is oci extension installed?'; + return $this->sql_error(''); + } + $this->db_connect_id = @ocinlogon($this->user, $sqlpassword, $connect, 'UTF8'); + } + else if ($this->persistency) + { + if (!function_exists('ociplogon')) + { + $this->connect_error = 'ociplogon function does not exist, is oci extension installed?'; + return $this->sql_error(''); + } + $this->db_connect_id = @ociplogon($this->user, $sqlpassword, $connect, 'UTF8'); + } + else + { + if (!function_exists('ocilogon')) + { + $this->connect_error = 'ocilogon function does not exist, is oci extension installed?'; + return $this->sql_error(''); + } + $this->db_connect_id = @ocilogon($this->user, $sqlpassword, $connect, 'UTF8'); + } + + return ($this->db_connect_id) ? $this->db_connect_id : $this->sql_error(''); + } + + /** + * {@inheritDoc} + */ + function sql_server_info($raw = false, $use_cache = true) + { + /** + * force $use_cache false. I didn't research why the caching code below is commented out + * but I assume its because the Oracle extension provides a direct method to access it + * without a query. + */ + + $use_cache = false; +/* + global $cache; + + if (empty($cache) || ($this->sql_server_version = $cache->get('oracle_version')) === false) + { + $result = @ociparse($this->db_connect_id, 'SELECT * FROM v$version WHERE banner LIKE \'Oracle%\''); + @ociexecute($result, OCI_DEFAULT); + @ocicommit($this->db_connect_id); + + $row = array(); + @ocifetchinto($result, $row, OCI_ASSOC + OCI_RETURN_NULLS); + @ocifreestatement($result); + $this->sql_server_version = trim($row['BANNER']); + + $cache->put('oracle_version', $this->sql_server_version); + } +*/ + $this->sql_server_version = @ociserverversion($this->db_connect_id); + + return $this->sql_server_version; + } + + /** + * SQL Transaction + * @access private + */ + function _sql_transaction($status = 'begin') + { + switch ($status) + { + case 'begin': + return true; + break; + + case 'commit': + return @ocicommit($this->db_connect_id); + break; + + case 'rollback': + return @ocirollback($this->db_connect_id); + break; + } + + return true; + } + + /** + * Oracle specific code to handle the fact that it does not compare columns properly + * @access private + */ + function _rewrite_col_compare($args) + { + if (sizeof($args) == 4) + { + if ($args[2] == '=') + { + return '(' . $args[0] . ' OR (' . $args[1] . ' is NULL AND ' . $args[3] . ' is NULL))'; + } + else if ($args[2] == '<>') + { + // really just a fancy way of saying foo <> bar or (foo is NULL XOR bar is NULL) but SQL has no XOR :P + return '(' . $args[0] . ' OR ((' . $args[1] . ' is NULL AND ' . $args[3] . ' is NOT NULL) OR (' . $args[1] . ' is NOT NULL AND ' . $args[3] . ' is NULL)))'; + } + } + else + { + return $this->_rewrite_where($args[0]); + } + } + + /** + * Oracle specific code to handle it's lack of sanity + * @access private + */ + function _rewrite_where($where_clause) + { + preg_match_all('/\s*(AND|OR)?\s*([\w_.()]++)\s*(?:(=|<[=>]?|>=?|LIKE)\s*((?>\'(?>[^\']++|\'\')*+\'|[\d-.()]+))|((NOT )?IN\s*\((?>\'(?>[^\']++|\'\')*+\',? ?|[\d-.]+,? ?)*+\)))/', $where_clause, $result, PREG_SET_ORDER); + $out = ''; + foreach ($result as $val) + { + if (!isset($val[5])) + { + if ($val[4] !== "''") + { + $out .= $val[0]; + } + else + { + $out .= ' ' . $val[1] . ' ' . $val[2]; + if ($val[3] == '=') + { + $out .= ' is NULL'; + } + else if ($val[3] == '<>') + { + $out .= ' is NOT NULL'; + } + } + } + else + { + $in_clause = array(); + $sub_exp = substr($val[5], strpos($val[5], '(') + 1, -1); + $extra = false; + preg_match_all('/\'(?>[^\']++|\'\')*+\'|[\d-.]++/', $sub_exp, $sub_vals, PREG_PATTERN_ORDER); + $i = 0; + foreach ($sub_vals[0] as $sub_val) + { + // two things: + // 1) This determines if an empty string was in the IN clausing, making us turn it into a NULL comparison + // 2) This fixes the 1000 list limit that Oracle has (ORA-01795) + if ($sub_val !== "''") + { + $in_clause[(int) $i++/1000][] = $sub_val; + } + else + { + $extra = true; + } + } + if (!$extra && $i < 1000) + { + $out .= $val[0]; + } + else + { + $out .= ' ' . $val[1] . '('; + $in_array = array(); + + // constuct each IN() clause + foreach ($in_clause as $in_values) + { + $in_array[] = $val[2] . ' ' . (isset($val[6]) ? $val[6] : '') . 'IN(' . implode(', ', $in_values) . ')'; + } + + // Join the IN() clauses against a few ORs (IN is just a nicer OR anyway) + $out .= implode(' OR ', $in_array); + + // handle the empty string case + if ($extra) + { + $out .= ' OR ' . $val[2] . ' is ' . (isset($val[6]) ? $val[6] : '') . 'NULL'; + } + $out .= ')'; + + unset($in_array, $in_clause); + } + } + } + + return $out; + } + + /** + * {@inheritDoc} + */ + function sql_query($query = '', $cache_ttl = 0) + { + if ($query != '') + { + global $cache; + + // EXPLAIN only in extra debug mode + if (defined('DEBUG')) + { + $this->sql_report('start', $query); + } + else if (defined('PHPBB_DISPLAY_LOAD_TIME')) + { + $this->curtime = microtime(true); + } + + $this->last_query_text = $query; + $this->query_result = ($cache && $cache_ttl) ? $cache->sql_load($query) : false; + $this->sql_add_num_queries($this->query_result); + + if ($this->query_result === false) + { + $in_transaction = false; + if (!$this->transaction) + { + $this->sql_transaction('begin'); + } + else + { + $in_transaction = true; + } + + $array = array(); + + // We overcome Oracle's 4000 char limit by binding vars + if (strlen($query) > 4000) + { + if (preg_match('/^(INSERT INTO[^(]++)\\(([^()]+)\\) VALUES[^(]++\\((.*?)\\)$/sU', $query, $regs)) + { + if (strlen($regs[3]) > 4000) + { + $cols = explode(', ', $regs[2]); + + preg_match_all('/\'(?:[^\']++|\'\')*+\'|[\d-.]+/', $regs[3], $vals, PREG_PATTERN_ORDER); + +/* The code inside this comment block breaks clob handling, but does allow the + database restore script to work. If you want to allow no posts longer than 4KB + and/or need the db restore script, uncomment this. + + + if (sizeof($cols) !== sizeof($vals)) + { + // Try to replace some common data we know is from our restore script or from other sources + $regs[3] = str_replace("'||chr(47)||'", '/', $regs[3]); + $_vals = explode(', ', $regs[3]); + + $vals = array(); + $is_in_val = false; + $i = 0; + $string = ''; + + foreach ($_vals as $value) + { + if (strpos($value, "'") === false && !$is_in_val) + { + $vals[$i++] = $value; + continue; + } + + if (substr($value, -1) === "'") + { + $vals[$i] = $string . (($is_in_val) ? ', ' : '') . $value; + $string = ''; + $is_in_val = false; + + if ($vals[$i][0] !== "'") + { + $vals[$i] = "''" . $vals[$i]; + } + $i++; + continue; + } + else + { + $string .= (($is_in_val) ? ', ' : '') . $value; + $is_in_val = true; + } + } + + if ($string) + { + // New value if cols != value + $vals[(sizeof($cols) !== sizeof($vals)) ? $i : $i - 1] .= $string; + } + + $vals = array(0 => $vals); + } +*/ + + $inserts = $vals[0]; + unset($vals); + + foreach ($inserts as $key => $value) + { + if (!empty($value) && $value[0] === "'" && strlen($value) > 4002) // check to see if this thing is greater than the max + 'x2 + { + $inserts[$key] = ':' . strtoupper($cols[$key]); + $array[$inserts[$key]] = str_replace("''", "'", substr($value, 1, -1)); + } + } + + $query = $regs[1] . '(' . $regs[2] . ') VALUES (' . implode(', ', $inserts) . ')'; + } + } + else if (preg_match_all('/^(UPDATE [\\w_]++\\s+SET )([\\w_]++\\s*=\\s*(?:\'(?:[^\']++|\'\')*+\'|[\d-.]+)(?:,\\s*[\\w_]++\\s*=\\s*(?:\'(?:[^\']++|\'\')*+\'|[\d-.]+))*+)\\s+(WHERE.*)$/s', $query, $data, PREG_SET_ORDER)) + { + if (strlen($data[0][2]) > 4000) + { + $update = $data[0][1]; + $where = $data[0][3]; + preg_match_all('/([\\w_]++)\\s*=\\s*(\'(?:[^\']++|\'\')*+\'|[\d-.]++)/', $data[0][2], $temp, PREG_SET_ORDER); + unset($data); + + $cols = array(); + foreach ($temp as $value) + { + if (!empty($value[2]) && $value[2][0] === "'" && strlen($value[2]) > 4002) // check to see if this thing is greater than the max + 'x2 + { + $cols[] = $value[1] . '=:' . strtoupper($value[1]); + $array[$value[1]] = str_replace("''", "'", substr($value[2], 1, -1)); + } + else + { + $cols[] = $value[1] . '=' . $value[2]; + } + } + + $query = $update . implode(', ', $cols) . ' ' . $where; + unset($cols); + } + } + } + + switch (substr($query, 0, 6)) + { + case 'DELETE': + if (preg_match('/^(DELETE FROM [\w_]++ WHERE)((?:\s*(?:AND|OR)?\s*[\w_]+\s*(?:(?:=|<>)\s*(?>\'(?>[^\']++|\'\')*+\'|[\d-.]+)|(?:NOT )?IN\s*\((?>\'(?>[^\']++|\'\')*+\',? ?|[\d-.]+,? ?)*+\)))*+)$/', $query, $regs)) + { + $query = $regs[1] . $this->_rewrite_where($regs[2]); + unset($regs); + } + break; + + case 'UPDATE': + if (preg_match('/^(UPDATE [\\w_]++\\s+SET [\\w_]+\s*=\s*(?:\'(?:[^\']++|\'\')*+\'|[\d-.]++|:\w++)(?:, [\\w_]+\s*=\s*(?:\'(?:[^\']++|\'\')*+\'|[\d-.]++|:\w++))*+\\s+WHERE)(.*)$/s', $query, $regs)) + { + $query = $regs[1] . $this->_rewrite_where($regs[2]); + unset($regs); + } + break; + + case 'SELECT': + $query = preg_replace_callback('/([\w_.]++)\s*(?:(=|<>)\s*(?>\'(?>[^\']++|\'\')*+\'|[\d-.]++|([\w_.]++))|(?:NOT )?IN\s*\((?>\'(?>[^\']++|\'\')*+\',? ?|[\d-.]++,? ?)*+\))/', array($this, '_rewrite_col_compare'), $query); + break; + } + + $this->query_result = @ociparse($this->db_connect_id, $query); + + foreach ($array as $key => $value) + { + @ocibindbyname($this->query_result, $key, $array[$key], -1); + } + + $success = @ociexecute($this->query_result, OCI_DEFAULT); + + if (!$success) + { + $this->sql_error($query); + $this->query_result = false; + } + else + { + if (!$in_transaction) + { + $this->sql_transaction('commit'); + } + } + + if (defined('DEBUG')) + { + $this->sql_report('stop', $query); + } + else if (defined('PHPBB_DISPLAY_LOAD_TIME')) + { + $this->sql_time += microtime(true) - $this->curtime; + } + + 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) + { + $this->open_queries[(int) $this->query_result] = $this->query_result; + } + } + else if (defined('DEBUG')) + { + $this->sql_report('fromcache', $query); + } + } + else + { + return false; + } + + return $this->query_result; + } + + /** + * Build LIMIT query + */ + function _sql_query_limit($query, $total, $offset = 0, $cache_ttl = 0) + { + $this->query_result = false; + + $query = 'SELECT * FROM (SELECT /*+ FIRST_ROWS */ rownum AS xrownum, a.* FROM (' . $query . ') a WHERE rownum <= ' . ($offset + $total) . ') WHERE xrownum >= ' . $offset; + + return $this->sql_query($query, $cache_ttl); + } + + /** + * {@inheritDoc} + */ + function sql_affectedrows() + { + return ($this->query_result) ? @ocirowcount($this->query_result) : false; + } + + /** + * {@inheritDoc} + */ + function sql_fetchrow($query_id = false) + { + global $cache; + + if ($query_id === false) + { + $query_id = $this->query_result; + } + + if ($cache && $cache->sql_exists($query_id)) + { + return $cache->sql_fetchrow($query_id); + } + + if ($query_id !== false) + { + $row = array(); + $result = @ocifetchinto($query_id, $row, OCI_ASSOC + OCI_RETURN_NULLS); + + if (!$result || !$row) + { + return false; + } + + $result_row = array(); + foreach ($row as $key => $value) + { + // Oracle treats empty strings as null + if (is_null($value)) + { + $value = ''; + } + + // OCI->CLOB? + if (is_object($value)) + { + $value = $value->load(); + } + + $result_row[strtolower($key)] = $value; + } + + return $result_row; + } + + return false; + } + + /** + * {@inheritDoc} + */ + function sql_rowseek($rownum, &$query_id) + { + global $cache; + + if ($query_id === false) + { + $query_id = $this->query_result; + } + + if ($cache && $cache->sql_exists($query_id)) + { + return $cache->sql_rowseek($rownum, $query_id); + } + + if ($query_id === false) + { + return false; + } + + // Reset internal pointer + @ociexecute($query_id, OCI_DEFAULT); + + // We do not fetch the row for rownum == 0 because then the next resultset would be the second row + for ($i = 0; $i < $rownum; $i++) + { + if (!$this->sql_fetchrow($query_id)) + { + return false; + } + } + + return true; + } + + /** + * {@inheritDoc} + */ + function sql_nextid() + { + $query_id = $this->query_result; + + if ($query_id !== false && $this->last_query_text != '') + { + if (preg_match('#^INSERT[\t\n ]+INTO[\t\n ]+([a-z0-9\_\-]+)#is', $this->last_query_text, $tablename)) + { + $query = 'SELECT ' . $tablename[1] . '_seq.currval FROM DUAL'; + $stmt = @ociparse($this->db_connect_id, $query); + @ociexecute($stmt, OCI_DEFAULT); + + $temp_result = @ocifetchinto($stmt, $temp_array, OCI_ASSOC + OCI_RETURN_NULLS); + @ocifreestatement($stmt); + + if ($temp_result) + { + return $temp_array['CURRVAL']; + } + else + { + return false; + } + } + } + + return false; + } + + /** + * {@inheritDoc} + */ + function sql_freeresult($query_id = false) + { + global $cache; + + if ($query_id === false) + { + $query_id = $this->query_result; + } + + if ($cache && !is_object($query_id) && $cache->sql_exists($query_id)) + { + return $cache->sql_freeresult($query_id); + } + + if (isset($this->open_queries[(int) $query_id])) + { + unset($this->open_queries[(int) $query_id]); + return @ocifreestatement($query_id); + } + + return false; + } + + /** + * {@inheritDoc} + */ + function sql_escape($msg) + { + return str_replace(array("'", "\0"), array("''", ''), $msg); + } + + /** + * Build LIKE expression + * @access private + */ + function _sql_like_expression($expression) + { + return $expression . " ESCAPE '\\'"; + } + + /** + * Build NOT LIKE expression + * @access private + */ + function _sql_not_like_expression($expression) + { + return $expression . " ESCAPE '\\'"; + } + + function _sql_custom_build($stage, $data) + { + return $data; + } + + function _sql_bit_and($column_name, $bit, $compare = '') + { + return 'BITAND(' . $column_name . ', ' . (1 << $bit) . ')' . (($compare) ? ' ' . $compare : ''); + } + + function _sql_bit_or($column_name, $bit, $compare = '') + { + return 'BITOR(' . $column_name . ', ' . (1 << $bit) . ')' . (($compare) ? ' ' . $compare : ''); + } + + /** + * return sql error array + * @access private + */ + function _sql_error() + { + if (function_exists('ocierror')) + { + $error = @ocierror(); + $error = (!$error) ? @ocierror($this->query_result) : $error; + $error = (!$error) ? @ocierror($this->db_connect_id) : $error; + + if ($error) + { + $this->last_error_result = $error; + } + else + { + $error = (isset($this->last_error_result) && $this->last_error_result) ? $this->last_error_result : array(); + } + } + else + { + $error = array( + 'message' => $this->connect_error, + 'code' => '', + ); + } + + return $error; + } + + /** + * Close sql connection + * @access private + */ + function _sql_close() + { + return @ocilogoff($this->db_connect_id); + } + + /** + * Build db-specific report + * @access private + */ + function _sql_report($mode, $query = '') + { + switch ($mode) + { + case 'start': + + $html_table = false; + + // Grab a plan table, any will do + $sql = "SELECT table_name + FROM USER_TABLES + WHERE table_name LIKE '%PLAN_TABLE%'"; + $stmt = ociparse($this->db_connect_id, $sql); + ociexecute($stmt); + $result = array(); + + if (ocifetchinto($stmt, $result, OCI_ASSOC + OCI_RETURN_NULLS)) + { + $table = $result['TABLE_NAME']; + + // This is the statement_id that will allow us to track the plan + $statement_id = substr(md5($query), 0, 30); + + // Remove any stale plans + $stmt2 = ociparse($this->db_connect_id, "DELETE FROM $table WHERE statement_id='$statement_id'"); + ociexecute($stmt2); + ocifreestatement($stmt2); + + // Explain the plan + $sql = "EXPLAIN PLAN + SET STATEMENT_ID = '$statement_id' + FOR $query"; + $stmt2 = ociparse($this->db_connect_id, $sql); + ociexecute($stmt2); + ocifreestatement($stmt2); + + // Get the data from the plan + $sql = "SELECT operation, options, object_name, object_type, cardinality, cost + FROM plan_table + START WITH id = 0 AND statement_id = '$statement_id' + CONNECT BY PRIOR id = parent_id + AND statement_id = '$statement_id'"; + $stmt2 = ociparse($this->db_connect_id, $sql); + ociexecute($stmt2); + + $row = array(); + while (ocifetchinto($stmt2, $row, OCI_ASSOC + OCI_RETURN_NULLS)) + { + $html_table = $this->sql_report('add_select_row', $query, $html_table, $row); + } + + ocifreestatement($stmt2); + + // Remove the plan we just made, we delete them on request anyway + $stmt2 = ociparse($this->db_connect_id, "DELETE FROM $table WHERE statement_id='$statement_id'"); + ociexecute($stmt2); + ocifreestatement($stmt2); + } + + ocifreestatement($stmt); + + if ($html_table) + { + $this->html_hold .= '</table>'; + } + + break; + + case 'fromcache': + $endtime = explode(' ', microtime()); + $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)) + { + // Take the time spent on parsing rows into account + } + @ocifreestatement($result); + + $splittime = explode(' ', microtime()); + $splittime = $splittime[0] + $splittime[1]; + + $this->sql_report('record_fromcache', $query, $endtime, $splittime); + + break; + } + } +} diff --git a/phpBB/phpbb/db/driver/postgres.php b/phpBB/phpbb/db/driver/postgres.php new file mode 100644 index 0000000000..a3b9aa4c6b --- /dev/null +++ b/phpBB/phpbb/db/driver/postgres.php @@ -0,0 +1,490 @@ +<?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\driver; + +/** +* PostgreSQL Database Abstraction Layer +* Minimum Requirement is Version 8.3+ +*/ +class postgres extends \phpbb\db\driver\driver +{ + var $multi_insert = true; + var $last_query_text = ''; + var $connect_error = ''; + + /** + * {@inheritDoc} + */ + function sql_connect($sqlserver, $sqluser, $sqlpassword, $database, $port = false, $persistency = false, $new_link = false) + { + $connect_string = ''; + + if ($sqluser) + { + $connect_string .= "user=$sqluser "; + } + + if ($sqlpassword) + { + $connect_string .= "password=$sqlpassword "; + } + + if ($sqlserver) + { + // $sqlserver can carry a port separated by : for compatibility reasons + // If $sqlserver has more than one : it's probably an IPv6 address. + // In this case we only allow passing a port via the $port variable. + if (substr_count($sqlserver, ':') === 1) + { + list($sqlserver, $port) = explode(':', $sqlserver); + } + + if ($sqlserver !== 'localhost') + { + $connect_string .= "host=$sqlserver "; + } + + if ($port) + { + $connect_string .= "port=$port "; + } + } + + $schema = ''; + + if ($database) + { + $this->dbname = $database; + if (strpos($database, '.') !== false) + { + list($database, $schema) = explode('.', $database); + } + $connect_string .= "dbname=$database"; + } + + $this->persistency = $persistency; + + if ($this->persistency) + { + if (!function_exists('pg_pconnect')) + { + $this->connect_error = 'pg_pconnect function does not exist, is pgsql extension installed?'; + return $this->sql_error(''); + } + $collector = new \phpbb\error_collector; + $collector->install(); + $this->db_connect_id = (!$new_link) ? @pg_pconnect($connect_string) : @pg_pconnect($connect_string, PGSQL_CONNECT_FORCE_NEW); + } + else + { + if (!function_exists('pg_connect')) + { + $this->connect_error = 'pg_connect function does not exist, is pgsql extension installed?'; + return $this->sql_error(''); + } + $collector = new \phpbb\error_collector; + $collector->install(); + $this->db_connect_id = (!$new_link) ? @pg_connect($connect_string) : @pg_connect($connect_string, PGSQL_CONNECT_FORCE_NEW); + } + + $collector->uninstall(); + + if ($this->db_connect_id) + { + if ($schema !== '') + { + @pg_query($this->db_connect_id, 'SET search_path TO ' . $schema); + } + return $this->db_connect_id; + } + + $this->connect_error = $collector->format_errors(); + return $this->sql_error(''); + } + + /** + * {@inheritDoc} + */ + function sql_server_info($raw = false, $use_cache = true) + { + global $cache; + + 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); + + $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); + } + } + + return ($raw) ? $this->sql_server_version : 'PostgreSQL ' . $this->sql_server_version; + } + + /** + * SQL Transaction + * @access private + */ + function _sql_transaction($status = 'begin') + { + switch ($status) + { + case 'begin': + return @pg_query($this->db_connect_id, 'BEGIN'); + break; + + case 'commit': + return @pg_query($this->db_connect_id, 'COMMIT'); + break; + + case 'rollback': + return @pg_query($this->db_connect_id, 'ROLLBACK'); + break; + } + + return true; + } + + /** + * {@inheritDoc} + */ + function sql_query($query = '', $cache_ttl = 0) + { + if ($query != '') + { + global $cache; + + // EXPLAIN only in extra debug mode + if (defined('DEBUG')) + { + $this->sql_report('start', $query); + } + else if (defined('PHPBB_DISPLAY_LOAD_TIME')) + { + $this->curtime = microtime(true); + } + + $this->last_query_text = $query; + $this->query_result = ($cache && $cache_ttl) ? $cache->sql_load($query) : false; + $this->sql_add_num_queries($this->query_result); + + if ($this->query_result === false) + { + if (($this->query_result = @pg_query($this->db_connect_id, $query)) === false) + { + $this->sql_error($query); + } + + if (defined('DEBUG')) + { + $this->sql_report('stop', $query); + } + else if (defined('PHPBB_DISPLAY_LOAD_TIME')) + { + $this->sql_time += microtime(true) - $this->curtime; + } + + 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) + { + $this->open_queries[(int) $this->query_result] = $this->query_result; + } + } + else if (defined('DEBUG')) + { + $this->sql_report('fromcache', $query); + } + } + else + { + return false; + } + + return $this->query_result; + } + + /** + * Build db-specific query data + * @access private + */ + function _sql_custom_build($stage, $data) + { + return $data; + } + + /** + * Build LIMIT query + */ + function _sql_query_limit($query, $total, $offset = 0, $cache_ttl = 0) + { + $this->query_result = false; + + // if $total is set to 0 we do not want to limit the number of rows + if ($total == 0) + { + $total = 'ALL'; + } + + $query .= "\n LIMIT $total OFFSET $offset"; + + return $this->sql_query($query, $cache_ttl); + } + + /** + * {@inheritDoc} + */ + function sql_affectedrows() + { + return ($this->query_result) ? @pg_affected_rows($this->query_result) : false; + } + + /** + * {@inheritDoc} + */ + function sql_fetchrow($query_id = false) + { + global $cache; + + if ($query_id === false) + { + $query_id = $this->query_result; + } + + if ($cache && $cache->sql_exists($query_id)) + { + return $cache->sql_fetchrow($query_id); + } + + return ($query_id !== false) ? @pg_fetch_assoc($query_id, null) : false; + } + + /** + * {@inheritDoc} + */ + function sql_rowseek($rownum, &$query_id) + { + global $cache; + + if ($query_id === false) + { + $query_id = $this->query_result; + } + + if ($cache && $cache->sql_exists($query_id)) + { + return $cache->sql_rowseek($rownum, $query_id); + } + + return ($query_id !== false) ? @pg_result_seek($query_id, $rownum) : false; + } + + /** + * {@inheritDoc} + */ + function sql_nextid() + { + $query_id = $this->query_result; + + if ($query_id !== false && $this->last_query_text != '') + { + if (preg_match("/^INSERT[\t\n ]+INTO[\t\n ]+([a-z0-9\_\-]+)/is", $this->last_query_text, $tablename)) + { + $query = "SELECT currval('" . $tablename[1] . "_seq') AS last_value"; + $temp_q_id = @pg_query($this->db_connect_id, $query); + + if (!$temp_q_id) + { + return false; + } + + $temp_result = @pg_fetch_assoc($temp_q_id, null); + @pg_free_result($query_id); + + return ($temp_result) ? $temp_result['last_value'] : false; + } + } + + return false; + } + + /** + * {@inheritDoc} + */ + function sql_freeresult($query_id = false) + { + global $cache; + + if ($query_id === false) + { + $query_id = $this->query_result; + } + + if ($cache && !is_object($query_id) && $cache->sql_exists($query_id)) + { + return $cache->sql_freeresult($query_id); + } + + if (isset($this->open_queries[(int) $query_id])) + { + unset($this->open_queries[(int) $query_id]); + return @pg_free_result($query_id); + } + + return false; + } + + /** + * {@inheritDoc} + */ + function sql_escape($msg) + { + return @pg_escape_string($msg); + } + + /** + * Build LIKE expression + * @access private + */ + function _sql_like_expression($expression) + { + return $expression; + } + + /** + * Build NOT LIKE expression + * @access private + */ + function _sql_not_like_expression($expression) + { + return $expression; + } + + /** + * {@inheritDoc} + */ + function cast_expr_to_bigint($expression) + { + return 'CAST(' . $expression . ' as DECIMAL(255, 0))'; + } + + /** + * {@inheritDoc} + */ + function cast_expr_to_string($expression) + { + return 'CAST(' . $expression . ' as VARCHAR(255))'; + } + + /** + * return sql error array + * @access private + */ + function _sql_error() + { + // pg_last_error only works when there is an established connection. + // Connection errors have to be tracked by us manually. + if ($this->db_connect_id) + { + $message = @pg_last_error($this->db_connect_id); + } + else + { + $message = $this->connect_error; + } + + return array( + 'message' => $message, + 'code' => '' + ); + } + + /** + * Close sql connection + * @access private + */ + function _sql_close() + { + return @pg_close($this->db_connect_id); + } + + /** + * Build db-specific report + * @access private + */ + function _sql_report($mode, $query = '') + { + switch ($mode) + { + case 'start': + + $explain_query = $query; + if (preg_match('/UPDATE ([a-z0-9_]+).*?WHERE(.*)/s', $query, $m)) + { + $explain_query = 'SELECT * FROM ' . $m[1] . ' WHERE ' . $m[2]; + } + else if (preg_match('/DELETE FROM ([a-z0-9_]+).*?WHERE(.*)/s', $query, $m)) + { + $explain_query = 'SELECT * FROM ' . $m[1] . ' WHERE ' . $m[2]; + } + + if (preg_match('/^SELECT/', $explain_query)) + { + $html_table = false; + + if ($result = @pg_query($this->db_connect_id, "EXPLAIN $explain_query")) + { + while ($row = @pg_fetch_assoc($result, null)) + { + $html_table = $this->sql_report('add_select_row', $query, $html_table, $row); + } + } + @pg_free_result($result); + + if ($html_table) + { + $this->html_hold .= '</table>'; + } + } + + break; + + case 'fromcache': + $endtime = explode(' ', microtime()); + $endtime = $endtime[0] + $endtime[1]; + + $result = @pg_query($this->db_connect_id, $query); + while ($void = @pg_fetch_assoc($result, null)) + { + // Take the time spent on parsing rows into account + } + @pg_free_result($result); + + $splittime = explode(' ', microtime()); + $splittime = $splittime[0] + $splittime[1]; + + $this->sql_report('record_fromcache', $query, $endtime, $splittime); + + break; + } + } +} diff --git a/phpBB/phpbb/db/driver/sqlite.php b/phpBB/phpbb/db/driver/sqlite.php new file mode 100644 index 0000000000..d5da0e2438 --- /dev/null +++ b/phpBB/phpbb/db/driver/sqlite.php @@ -0,0 +1,378 @@ +<?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\driver; + +/** +* Sqlite Database Abstraction Layer +* Minimum Requirement: 2.8.2+ +*/ +class sqlite extends \phpbb\db\driver\driver +{ + var $connect_error = ''; + + /** + * {@inheritDoc} + */ + function sql_connect($sqlserver, $sqluser, $sqlpassword, $database, $port = false, $persistency = false, $new_link = false) + { + $this->persistency = $persistency; + $this->user = $sqluser; + $this->server = $sqlserver . (($port) ? ':' . $port : ''); + $this->dbname = $database; + + $error = ''; + if ($this->persistency) + { + if (!function_exists('sqlite_popen')) + { + $this->connect_error = 'sqlite_popen function does not exist, is sqlite extension installed?'; + return $this->sql_error(''); + } + $this->db_connect_id = @sqlite_popen($this->server, 0666, $error); + } + else + { + if (!function_exists('sqlite_open')) + { + $this->connect_error = 'sqlite_open function does not exist, is sqlite extension installed?'; + return $this->sql_error(''); + } + $this->db_connect_id = @sqlite_open($this->server, 0666, $error); + } + + if ($this->db_connect_id) + { + @sqlite_query('PRAGMA short_column_names = 1', $this->db_connect_id); +// @sqlite_query('PRAGMA encoding = "UTF-8"', $this->db_connect_id); + } + + return ($this->db_connect_id) ? true : array('message' => $error); + } + + /** + * {@inheritDoc} + */ + function sql_server_info($raw = false, $use_cache = true) + { + global $cache; + + 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); + + $this->sql_server_version = (!empty($row['version'])) ? $row['version'] : 0; + + if (!empty($cache) && $use_cache) + { + $cache->put('sqlite_version', $this->sql_server_version); + } + } + + return ($raw) ? $this->sql_server_version : 'SQLite ' . $this->sql_server_version; + } + + /** + * SQL Transaction + * @access private + */ + function _sql_transaction($status = 'begin') + { + switch ($status) + { + case 'begin': + return @sqlite_query('BEGIN', $this->db_connect_id); + break; + + case 'commit': + return @sqlite_query('COMMIT', $this->db_connect_id); + break; + + case 'rollback': + return @sqlite_query('ROLLBACK', $this->db_connect_id); + break; + } + + return true; + } + + /** + * {@inheritDoc} + */ + function sql_query($query = '', $cache_ttl = 0) + { + if ($query != '') + { + global $cache; + + // EXPLAIN only in extra debug mode + if (defined('DEBUG')) + { + $this->sql_report('start', $query); + } + else if (defined('PHPBB_DISPLAY_LOAD_TIME')) + { + $this->curtime = microtime(true); + } + + $this->query_result = ($cache && $cache_ttl) ? $cache->sql_load($query) : false; + $this->sql_add_num_queries($this->query_result); + + if ($this->query_result === false) + { + if (($this->query_result = @sqlite_query($query, $this->db_connect_id)) === false) + { + $this->sql_error($query); + } + + if (defined('DEBUG')) + { + $this->sql_report('stop', $query); + } + else if (defined('PHPBB_DISPLAY_LOAD_TIME')) + { + $this->sql_time += microtime(true) - $this->curtime; + } + + 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) + { + $this->open_queries[(int) $this->query_result] = $this->query_result; + } + } + else if (defined('DEBUG')) + { + $this->sql_report('fromcache', $query); + } + } + else + { + return false; + } + + return $this->query_result; + } + + /** + * Build LIMIT query + */ + function _sql_query_limit($query, $total, $offset = 0, $cache_ttl = 0) + { + $this->query_result = false; + + // if $total is set to 0 we do not want to limit the number of rows + if ($total == 0) + { + $total = -1; + } + + $query .= "\n LIMIT " . ((!empty($offset)) ? $offset . ', ' . $total : $total); + + return $this->sql_query($query, $cache_ttl); + } + + /** + * {@inheritDoc} + */ + function sql_affectedrows() + { + return ($this->db_connect_id) ? @sqlite_changes($this->db_connect_id) : false; + } + + /** + * {@inheritDoc} + */ + function sql_fetchrow($query_id = false) + { + global $cache; + + if ($query_id === false) + { + $query_id = $this->query_result; + } + + if ($cache && $cache->sql_exists($query_id)) + { + return $cache->sql_fetchrow($query_id); + } + + return ($query_id !== false) ? @sqlite_fetch_array($query_id, SQLITE_ASSOC) : false; + } + + /** + * {@inheritDoc} + */ + function sql_rowseek($rownum, &$query_id) + { + global $cache; + + if ($query_id === false) + { + $query_id = $this->query_result; + } + + if ($cache && $cache->sql_exists($query_id)) + { + return $cache->sql_rowseek($rownum, $query_id); + } + + return ($query_id !== false) ? @sqlite_seek($query_id, $rownum) : false; + } + + /** + * {@inheritDoc} + */ + function sql_nextid() + { + return ($this->db_connect_id) ? @sqlite_last_insert_rowid($this->db_connect_id) : false; + } + + /** + * {@inheritDoc} + */ + function sql_freeresult($query_id = false) + { + global $cache; + + if ($query_id === false) + { + $query_id = $this->query_result; + } + + if ($cache && !is_object($query_id) && $cache->sql_exists($query_id)) + { + return $cache->sql_freeresult($query_id); + } + + return true; + } + + /** + * {@inheritDoc} + */ + function sql_escape($msg) + { + return @sqlite_escape_string($msg); + } + + /** + * {@inheritDoc} + * + * For SQLite an underscore is a not-known character... this may change with SQLite3 + */ + function sql_like_expression($expression) + { + // Unlike LIKE, GLOB is unfortunately case sensitive. + // We only catch * and ? here, not the character map possible on file globbing. + $expression = str_replace(array(chr(0) . '_', chr(0) . '%'), array(chr(0) . '?', chr(0) . '*'), $expression); + + $expression = str_replace(array('?', '*'), array("\?", "\*"), $expression); + $expression = str_replace(array(chr(0) . "\?", chr(0) . "\*"), array('?', '*'), $expression); + + return 'GLOB \'' . $this->sql_escape($expression) . '\''; + } + + /** + * {@inheritDoc} + * + * For SQLite an underscore is a not-known character... + */ + function sql_not_like_expression($expression) + { + // Unlike NOT LIKE, NOT GLOB is unfortunately case sensitive. + // We only catch * and ? here, not the character map possible on file globbing. + $expression = str_replace(array(chr(0) . '_', chr(0) . '%'), array(chr(0) . '?', chr(0) . '*'), $expression); + + $expression = str_replace(array('?', '*'), array("\?", "\*"), $expression); + $expression = str_replace(array(chr(0) . "\?", chr(0) . "\*"), array('?', '*'), $expression); + + return 'NOT GLOB \'' . $this->sql_escape($expression) . '\''; + } + + /** + * return sql error array + * @access private + */ + function _sql_error() + { + if (function_exists('sqlite_error_string')) + { + $error = array( + 'message' => @sqlite_error_string(@sqlite_last_error($this->db_connect_id)), + 'code' => @sqlite_last_error($this->db_connect_id), + ); + } + else + { + $error = array( + 'message' => $this->connect_error, + 'code' => '', + ); + } + + return $error; + } + + /** + * Build db-specific query data + * @access private + */ + function _sql_custom_build($stage, $data) + { + return $data; + } + + /** + * Close sql connection + * @access private + */ + function _sql_close() + { + return @sqlite_close($this->db_connect_id); + } + + /** + * Build db-specific report + * @access private + */ + function _sql_report($mode, $query = '') + { + switch ($mode) + { + case 'start': + break; + + case 'fromcache': + $endtime = explode(' ', microtime()); + $endtime = $endtime[0] + $endtime[1]; + + $result = @sqlite_query($query, $this->db_connect_id); + while ($void = @sqlite_fetch_array($result, SQLITE_ASSOC)) + { + // Take the time spent on parsing rows into account + } + + $splittime = explode(' ', microtime()); + $splittime = $splittime[0] + $splittime[1]; + + $this->sql_report('record_fromcache', $query, $endtime, $splittime); + + break; + } + } +} diff --git a/phpBB/phpbb/db/driver/sqlite3.php b/phpBB/phpbb/db/driver/sqlite3.php new file mode 100644 index 0000000000..cc3352af34 --- /dev/null +++ b/phpBB/phpbb/db/driver/sqlite3.php @@ -0,0 +1,405 @@ +<?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\driver; + +/** +* SQLite3 Database Abstraction Layer +* Minimum Requirement: 3.6.15+ +*/ +class sqlite3 extends \phpbb\db\driver\driver +{ + /** + * @var string Stores errors during connection setup in case the driver is not available + */ + protected $connect_error = ''; + + /** + * @var \SQLite3 The SQLite3 database object to operate against + */ + protected $dbo = null; + + /** + * {@inheritDoc} + */ + public function sql_connect($sqlserver, $sqluser, $sqlpassword, $database, $port = false, $persistency = false, $new_link = false) + { + $this->persistency = false; + $this->user = $sqluser; + $this->server = $sqlserver . (($port) ? ':' . $port : ''); + $this->dbname = $database; + + if (!class_exists('SQLite3', false)) + { + $this->connect_error = 'SQLite3 not found, is the extension installed?'; + return $this->sql_error(''); + } + + try + { + $this->dbo = new \SQLite3($this->server, SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE); + $this->dbo->busyTimeout(60000); + $this->db_connect_id = true; + } + catch (\Exception $e) + { + $this->connect_error = $e->getMessage(); + return array('message' => $this->connect_error); + } + + return true; + } + + /** + * {@inheritDoc} + */ + public function sql_server_info($raw = false, $use_cache = true) + { + global $cache; + + if (!$use_cache || empty($cache) || ($this->sql_server_version = $cache->get('sqlite_version')) === false) + { + $version = \SQLite3::version(); + + $this->sql_server_version = $version['versionString']; + + if (!empty($cache) && $use_cache) + { + $cache->put('sqlite_version', $this->sql_server_version); + } + } + + return ($raw) ? $this->sql_server_version : 'SQLite ' . $this->sql_server_version; + } + + /** + * SQL Transaction + * + * @param string $status Should be one of the following strings: + * begin, commit, rollback + * @return bool Success/failure of the transaction query + */ + protected function _sql_transaction($status = 'begin') + { + switch ($status) + { + case 'begin': + return $this->dbo->exec('BEGIN IMMEDIATE'); + break; + + case 'commit': + return $this->dbo->exec('COMMIT'); + break; + + case 'rollback': + return $this->dbo->exec('ROLLBACK'); + break; + } + + return true; + } + + /** + * {@inheritDoc} + */ + public function sql_query($query = '', $cache_ttl = 0) + { + if ($query != '') + { + global $cache; + + // EXPLAIN only in extra debug mode + if (defined('DEBUG')) + { + $this->sql_report('start', $query); + } + else if (defined('PHPBB_DISPLAY_LOAD_TIME')) + { + $this->curtime = microtime(true); + } + + $this->last_query_text = $query; + $this->query_result = ($cache && $cache_ttl) ? $cache->sql_load($query) : false; + $this->sql_add_num_queries($this->query_result); + + if ($this->query_result === false) + { + if (($this->query_result = @$this->dbo->query($query)) === false) + { + $this->sql_error($query); + } + + if (defined('DEBUG')) + { + $this->sql_report('stop', $query); + } + else if (defined('PHPBB_DISPLAY_LOAD_TIME')) + { + $this->sql_time += microtime(true) - $this->curtime; + } + + if ($cache && $cache_ttl) + { + $this->query_result = $cache->sql_save($this, $query, $this->query_result, $cache_ttl); + } + } + else if (defined('DEBUG')) + { + $this->sql_report('fromcache', $query); + } + } + else + { + return false; + } + + return $this->query_result; + } + + /** + * Build LIMIT query + * + * @param string $query The SQL query to execute + * @param int $total The number of rows to select + * @param int $offset + * @param int $cache_ttl Either 0 to avoid caching or + * the time in seconds which the result shall be kept in cache + * @return mixed Buffered, seekable result handle, false on error + */ + protected function _sql_query_limit($query, $total, $offset = 0, $cache_ttl = 0) + { + $this->query_result = false; + + // if $total is set to 0 we do not want to limit the number of rows + if ($total == 0) + { + $total = -1; + } + + $query .= "\n LIMIT " . ((!empty($offset)) ? $offset . ', ' . $total : $total); + + return $this->sql_query($query, $cache_ttl); + } + + /** + * {@inheritDoc} + */ + public function sql_affectedrows() + { + return ($this->db_connect_id) ? $this->dbo->changes() : false; + } + + /** + * {@inheritDoc} + */ + public function sql_fetchrow($query_id = false) + { + global $cache; + + if ($query_id === false) + { + $query_id = $this->query_result; + } + + if ($cache && !is_object($query_id) && $cache->sql_exists($query_id)) + { + return $cache->sql_fetchrow($query_id); + } + + return is_object($query_id) ? $query_id->fetchArray(SQLITE3_ASSOC) : false; + } + + /** + * {@inheritDoc} + */ + public function sql_nextid() + { + return ($this->db_connect_id) ? $this->dbo->lastInsertRowID() : false; + } + + /** + * {@inheritDoc} + */ + public function sql_freeresult($query_id = false) + { + global $cache; + + if ($query_id === false) + { + $query_id = $this->query_result; + } + + if ($cache && !is_object($query_id) && $cache->sql_exists($query_id)) + { + return $cache->sql_freeresult($query_id); + } + + if ($query_id) + { + return @$query_id->finalize(); + } + } + + /** + * {@inheritDoc} + */ + public function sql_escape($msg) + { + return \SQLite3::escapeString($msg); + } + + /** + * {@inheritDoc} + * + * For SQLite an underscore is an unknown character. + */ + public function sql_like_expression($expression) + { + // Unlike LIKE, GLOB is unfortunately case sensitive. + // We only catch * and ? here, not the character map possible on file globbing. + $expression = str_replace(array(chr(0) . '_', chr(0) . '%'), array(chr(0) . '?', chr(0) . '*'), $expression); + + $expression = str_replace(array('?', '*'), array("\?", "\*"), $expression); + $expression = str_replace(array(chr(0) . "\?", chr(0) . "\*"), array('?', '*'), $expression); + + return 'GLOB \'' . $this->sql_escape($expression) . '\''; + } + + /** + * {@inheritDoc} + * + * For SQLite an underscore is an unknown character. + */ + public function sql_not_like_expression($expression) + { + // Unlike NOT LIKE, NOT GLOB is unfortunately case sensitive + // We only catch * and ? here, not the character map possible on file globbing. + $expression = str_replace(array(chr(0) . '_', chr(0) . '%'), array(chr(0) . '?', chr(0) . '*'), $expression); + + $expression = str_replace(array('?', '*'), array("\?", "\*"), $expression); + $expression = str_replace(array(chr(0) . "\?", chr(0) . "\*"), array('?', '*'), $expression); + + return 'NOT GLOB \'' . $this->sql_escape($expression) . '\''; + } + + /** + * return sql error array + * + * @return array + */ + protected function _sql_error() + { + if (class_exists('SQLite3', false) && isset($this->dbo)) + { + $error = array( + 'message' => $this->dbo->lastErrorMsg(), + 'code' => $this->dbo->lastErrorCode(), + ); + } + else + { + $error = array( + 'message' => $this->connect_error, + 'code' => '', + ); + } + + return $error; + } + + /** + * Build db-specific query data + * + * @param string $stage Available stages: FROM, WHERE + * @param mixed $data A string containing the CROSS JOIN query or an array of WHERE clauses + * + * @return string The db-specific query fragment + */ + protected function _sql_custom_build($stage, $data) + { + return $data; + } + + /** + * Close sql connection + * + * @return bool False if failure + */ + protected function _sql_close() + { + return $this->dbo->close(); + } + + /** + * Build db-specific report + * + * @param string $mode Available modes: display, start, stop, + * add_select_row, fromcache, record_fromcache + * @param string $query The Query that should be explained + * @return mixed Either a full HTML page, boolean or null + */ + protected function _sql_report($mode, $query = '') + { + switch ($mode) + { + case 'start': + + $explain_query = $query; + if (preg_match('/UPDATE ([a-z0-9_]+).*?WHERE(.*)/s', $query, $m)) + { + $explain_query = 'SELECT * FROM ' . $m[1] . ' WHERE ' . $m[2]; + } + else if (preg_match('/DELETE FROM ([a-z0-9_]+).*?WHERE(.*)/s', $query, $m)) + { + $explain_query = 'SELECT * FROM ' . $m[1] . ' WHERE ' . $m[2]; + } + + if (preg_match('/^SELECT/', $explain_query)) + { + $html_table = false; + + if ($result = $this->dbo->query("EXPLAIN QUERY PLAN $explain_query")) + { + while ($row = $result->fetchArray(SQLITE3_ASSOC)) + { + $html_table = $this->sql_report('add_select_row', $query, $html_table, $row); + } + } + + if ($html_table) + { + $this->html_hold .= '</table>'; + } + } + + break; + + case 'fromcache': + $endtime = explode(' ', microtime()); + $endtime = $endtime[0] + $endtime[1]; + + $result = $this->dbo->query($query); + while ($void = $result->fetchArray(SQLITE3_ASSOC)) + { + // Take the time spent on parsing rows into account + } + + $splittime = explode(' ', microtime()); + $splittime = $splittime[0] + $splittime[1]; + + $this->sql_report('record_fromcache', $query, $endtime, $splittime); + + break; + } + } +} |