diff -r 8be996c3740d -r 112debff64bd includes/dbal.php
--- a/includes/dbal.php Wed Dec 12 21:46:28 2007 -0500
+++ b/includes/dbal.php Sat Dec 15 18:10:14 2007 -0500
@@ -124,6 +124,12 @@
{
$this->enable_errorhandler();
+ define('ENANO_DBLAYER', 'MYSQL');
+ define('ENANO_SQLFUNC_LOWERCASE', 'lcase');
+ define('ENANO_SQL_MULTISTRING_PRFIX', '');
+ define('ENANO_SQL_BOOLEAN_TRUE', 'true');
+ define('ENANO_SQL_BOOLEAN_FALSE', 'false');
+
if ( defined('IN_ENANO_INSTALL') && !defined('IN_ENANO_UPGRADE') )
{
@include(ENANO_ROOT.'/config.new.php');
@@ -778,4 +784,747 @@
}
}
+class postgresql {
+ var $num_queries, $query_backtrace, $query_times, $query_sources, $latest_result, $latest_query, $_conn, $sql_stack_fields, $sql_stack_values, $debug;
+ var $row = array();
+ var $rowset = array();
+ var $errhandler;
+
+ function enable_errorhandler()
+ {
+ // echo "DBAL: enabling error handler
";
+ if ( function_exists('debug_backtrace') )
+ {
+ $this->errhandler = set_error_handler('db_error_handler');
+ }
+ }
+
+ function disable_errorhandler()
+ {
+ // echo "DBAL: disabling error handler
";
+ if ( $this->errhandler )
+ {
+ set_error_handler($this->errhandler);
+ }
+ else
+ {
+ restore_error_handler();
+ }
+ }
+
+ function sql_backtrace()
+ {
+ return implode("\n-------------------------------------------------------------------\n", $this->query_backtrace);
+ }
+
+ function ensure_connection()
+ {
+ if(!$this->_conn)
+ {
+ $this->connect();
+ }
+ }
+
+ function _die($t = '') {
+ if(defined('ENANO_HEADERS_SENT')) {
+ ob_clean();
+ }
+ header('HTTP/1.1 500 Internal Server Error');
+ $bt = $this->latest_query; // $this->sql_backtrace();
+ $e = htmlspecialchars(pg_last_error());
+ if($e=='') $e='<none>';
+ $t = ( !empty($t) ) ? $t : '<No error description provided>';
+ global $email;
+ $email_info = ( defined('ENANO_CONFIG_FETCHED') && is_object($email) ) ? ', at <' . $email->jscode() . $email->encryptEmail(getConfig('contact_email')) . '>' : '';
+ $internal_text = '
We apologize for the inconveience, but an error occurred in the Enano database layer. Please report the full text of this page to the administrator of this site' . $email_info . '.
+Description or location of error: '.$t.'
+ Error returned by PostgreSQL extension: ' . $e . '
+ Most recent SQL query:
'.$bt.''; + if(defined('ENANO_CONFIG_FETCHED')) die_semicritical('Database error', $internal_text); + else grinding_halt('Database error', $internal_text); + exit; + } + + function die_json() + { + $e = addslashes(htmlspecialchars(pg_last_error())); + $q = addslashes($this->latest_query); + $t = "{'mode':'error','error':'An error occurred during database query.\nQuery was:\n $q\n\nError returned by PostgreSQL: $e'}"; + die($t); + } + + function get_error($t = '') { + header('HTTP/1.1 500 Internal Server Error'); + $bt = $this->sql_backtrace(); + $e = htmlspecialchars(pg_last_error()); + if($e=='') $e='<none>'; + global $email; + $email_info = ( defined('ENANO_CONFIG_FETCHED') && is_object($email) ) ? ', at <' . $email->jscode() . $email->encryptEmail(getConfig('contact_email')) . '>' : ''; + $internal_text = '
We apologize for the inconveience, but an error occurred in the Enano database layer. Please report the full text of this page to the administrator of this site' . $email_info . '.
+Description or location of error: '.$t.'
+ Error returned by MySQL extension: ' . $e . '
+ Most recent SQL query:
'.$bt.''; + return $internal_text; + } + + function connect() + { + $this->enable_errorhandler(); + + define('ENANO_DBLAYER', 'PGSQL'); + define('ENANO_SQLFUNC_LOWERCASE', 'lower'); + define('ENANO_SQL_MULTISTRING_PRFIX', 'E'); + define('ENANO_SQL_BOOLEAN_TRUE', '1'); + define('ENANO_SQL_BOOLEAN_FALSE', '0'); + + if ( defined('IN_ENANO_INSTALL') && !defined('IN_ENANO_UPGRADE') ) + { + @include(ENANO_ROOT.'/config.new.php'); + } + else + { + @include(ENANO_ROOT.'/config.php'); + } + + if ( isset($crypto_key) ) + unset($crypto_key); // Get this sucker out of memory fast + + if ( !defined('ENANO_INSTALLED') && !defined('MIDGET_INSTALLED') && !defined('IN_ENANO_INSTALL') ) + { + // scriptPath isn't set yet - we need to autodetect it to avoid infinite redirects + if ( !defined('scriptPath') ) + { + if ( isset($_SERVER['PATH_INFO']) && !preg_match('/index\.php$/', $_SERVER['PATH_INFO']) ) + { + $_SERVER['REQUEST_URI'] = preg_replace(';' . preg_quote($_SERVER['PATH_INFO']) . '$;', '', $_SERVER['REQUEST_URI']); + } + if ( !preg_match('/\.php$/', $_SERVER['REQUEST_URI']) ) + { + // user requested http://foo/enano as opposed to http://foo/enano/index.php + $_SERVER['REQUEST_URI'] .= '/index.php'; + } + $sp = dirname($_SERVER['REQUEST_URI']); + if($sp == '/' || $sp == '\\') $sp = ''; + define('scriptPath', $sp); + define('contentPath', "$sp/index.php?title="); + } + $loc = scriptPath . '/install.php'; + // header("Location: $loc"); + redirect($loc, 'Enano not installed', 'We can\'t seem to find an Enano installation (valid config file). You will be transferred to the installation wizard momentarily...', 3); + exit; + } + $this->_conn = @pg_connect("host=$dbhost port=5432 dbname=$dbname user=$dbuser password=$dbpasswd"); + unset($dbuser); + unset($dbpasswd); // Security + + if ( !$this->_conn ) + { + grinding_halt('Enano is having a problem', '
Error: couldn\'t connect to PostgreSQL.
'.pg_last_error().'
Enano has caught and prevented an SQL injection attempt. Your IP address has been recorded and the administrator has been notified.
Query was:
'.htmlspecialchars($q).''); + } + + $time_start = microtime_float(); + $r = pg_query($q); + $this->query_times[$q] = microtime_float() - $time_start; + $this->latest_result = $r; + $this->disable_errorhandler(); + return $r; + } + + function sql_unbuffered_query($q) + { + $this->enable_errorhandler(); + + $this->num_queries++; + $this->query_backtrace[] = '(UNBUFFERED) ' . $q; + $this->latest_query = $q; + // First make sure we have a connection + if ( !$this->_conn ) + { + $this->_die('A database connection has not yet been established.'); + } + // Does this query look malicious? + if ( !$this->check_query($q) ) + { + $this->report_query($q); + grinding_halt('SQL Injection attempt', '
Enano has caught and prevented an SQL injection attempt. Your IP address has been recorded and the administrator has been notified.
Query was:
'.htmlspecialchars($q).''); + } + + $time_start = microtime_float(); + $r = pg_query($q); + $this->query_times[$q] = microtime_float() - $time_start; + $this->latest_result = $r; + $this->disable_errorhandler(); + return $r; + } + + /** + * Checks a SQL query for possible signs of injection attempts + * @param string $q the query to check + * @return bool true if query passed check, otherwise false + */ + + function check_query($q, $debug = false) + { + if($debug) echo "\$db->check_query(): checking query: ".htmlspecialchars($q).'
' . print_r($match, true) . ''; + return false; + } + return true; + } + + /** + * Set the internal result pointer to X + * @param int $pos The number of the row + * @param resource $result The MySQL result resource - if not given, the latest cached query is assumed + * @return true on success, false on failure + */ + + function sql_data_seek($pos, $result = false) + { + $this->enable_errorhandler(); + if(!$result) + $result = $this->latest_result; + if(!$result) + { + $this->disable_errorhandler(); + return false; + } + if(pg_result_seek($result, $pos)) + { + $this->disable_errorhandler(); + return true; + } + else + { + $this->disable_errorhandler(); + return false; + } + } + + /** + * Reports a bad query to the admin + * @param string $query the naughty query + * @access private + */ + + function report_query($query) + { + global $session; + if(is_object($session) && defined('ENANO_MAINSTREAM')) + $username = $session->username; + else + $username = 'Unavailable'; + $query = $this->escape($query); + $q = $this->sql_query('INSERT INTO '.table_prefix.'logs(log_type, action, time_id, date_string, page_text, author, edit_summary) + VALUES(\'security\', \'sql_inject\', '.time().', \'\', \''.$query.'\', \''.$username.'\', \''.$_SERVER['REMOTE_ADDR'].'\');'); + } + + /** + * Returns the ID of the row last inserted. + * @return int + */ + + function insert_id() + { + return @pg_last_oid(); + } + + function fetchrow($r = false) { + $this->enable_errorhandler(); + if(!$this->_conn) return false; + if(!$r) $r = $this->latest_result; + if(!$r) $this->_die('$db->fetchrow(): an invalid MySQL resource was passed.'); + $row = pg_fetch_assoc($r); + $this->disable_errorhandler(); + return $row; + } + + function fetchrow_num($r = false) { + $this->enable_errorhandler(); + if(!$r) $r = $this->latest_result; + if(!$r) $this->_die('$db->fetchrow(): an invalid MySQL resource was passed.'); + $row = pg_fetch_row($r); + $this->disable_errorhandler(); + return $row; + } + + function numrows($r = false) { + $this->enable_errorhandler(); + if(!$r) $r = $this->latest_result; + if(!$r) $this->_die('$db->fetchrow(): an invalid MySQL resource was passed.'); + $n = pg_num_rows($r); + $this->disable_errorhandler(); + return $n; + } + + function escape($str) + { + $this->enable_errorhandler(); + $str = pg_escape_string($str); + $this->disable_errorhandler(); + return $str; + } + + function free_result($result = false) + { + $this->enable_errorhandler(); + if(!$result) + $result = $this->latest_result; + if(!$result) + { + $this->disable_errorhandler(); + return null; + } + pg_free_result($result); + $this->disable_errorhandler(); + return null; + } + + function close() { + pg_close($this->_conn); + unset($this->_conn); + } + + // phpBB DBAL compatibility + function sql_fetchrow($r = false) + { + return $this->fetchrow($r); + } + function sql_freeresult($r = false) + { + if(!$this->_conn) return false; + if(!$r) $r = $this->latest_result; + if(!$r) $this->_die('$db->fetchrow(): an invalid MySQL resource was passed.'); + $this->free_result($r); + } + function sql_numrows($r = false) + { + return $this->numrows(); + } + function sql_affectedrows($r = false, $f, $n) + { + if(!$this->_conn) return false; + if(!$r) $r = $this->latest_result; + if(!$r) $this->_die('$db->fetchrow(): an invalid MySQL resource was passed.'); + return pg_affected_rows(); + } + + function sql_type_cast(&$value) + { + if ( is_float($value) ) + { + return doubleval($value); + } + if ( is_integer($value) || is_bool($value) ) + { + return intval($value); + } + if ( is_string($value) || empty($value) ) + { + return '\'' . $this->sql_escape_string($value) . '\''; + } + // uncastable var : let's do a basic protection on it to prevent sql injection attempt + return '\'' . $this->sql_escape_string(htmlspecialchars($value)) . '\''; + } + + function sql_statement(&$fields, $fields_inc='') + { + // init result + $this->sql_fields = $this->sql_values = $this->sql_update = ''; + if ( empty($fields) && empty($fields_inc) ) + { + return; + } + + // process + if ( !empty($fields) ) + { + $first = true; + foreach ( $fields as $field => $value ) + { + // field must contain a field name + if ( !empty($field) && is_string($field) ) + { + $value = $this->sql_type_cast($value); + $this->sql_fields .= ( $first ? '' : ', ' ) . $field; + $this->sql_values .= ( $first ? '' : ', ' ) . $value; + $this->sql_update .= ( $first ? '' : ', ' ) . $field . ' = ' . $value; + $first = false; + } + } + } + if ( !empty($fields_inc) ) + { + foreach ( $fields_inc as $field => $indent ) + { + if ( $indent != 0 ) + { + $this->sql_update .= (empty($this->sql_update) ? '' : ', ') . $field . ' = ' . $field . ($indent < 0 ? ' - ' : ' + ') . abs($indent); + } + } + } + } + + function sql_stack_reset($id='') + { + if ( empty($id) ) + { + $this->sql_stack_fields = array(); + $this->sql_stack_values = array(); + } + else + { + $this->sql_stack_fields[$id] = array(); + $this->sql_stack_values[$id] = array(); + } + } + + function sql_stack_statement(&$fields, $id='') + { + $this->sql_statement($fields); + if ( empty($id) ) + { + $this->sql_stack_fields = $this->sql_fields; + $this->sql_stack_values[] = '(' . $this->sql_values . ')'; + } + else + { + $this->sql_stack_fields[$id] = $this->sql_fields; + $this->sql_stack_values[$id][] = '(' . $this->sql_values . ')'; + } + } + + function sql_stack_insert($table, $transaction=false, $line='', $file='', $break_on_error=true, $id='') + { + if ( (empty($id) && empty($this->sql_stack_values)) || (!empty($id) && empty($this->sql_stack_values[$id])) ) + { + return false; + } + switch( SQL_LAYER ) + { + case 'mysql': + case 'mysql4': + if ( empty($id) ) + { + $sql = 'INSERT INTO ' . $table . ' + (' . $this->sql_stack_fields . ') VALUES ' . implode(",\n", $this->sql_stack_values); + } + else + { + $sql = 'INSERT INTO ' . $table . ' + (' . $this->sql_stack_fields[$id] . ') VALUES ' . implode(",\n", $this->sql_stack_values[$id]); + } + $this->sql_stack_reset($id); + return $this->sql_query($sql, $transaction, $line, $file, $break_on_error); + break; + default: + $count_sql_stack_values = empty($id) ? count($this->sql_stack_values) : count($this->sql_stack_values[$id]); + $result = !empty($count_sql_stack_values); + for ( $i = 0; $i < $count_sql_stack_values; $i++ ) + { + if ( empty($id) ) + { + $sql = 'INSERT INTO ' . $table . ' + (' . $this->sql_stack_fields . ') VALUES ' . $this->sql_stack_values[$i]; + } + else + { + $sql = 'INSERT INTO ' . $table . ' + (' . $this->sql_stack_fields[$id] . ') VALUES ' . $this->sql_stack_values[$id][$i]; + } + $result &= $this->sql_query($sql, $transaction, $line, $file, $break_on_error); + } + $this->sql_stack_reset($id); + return $result; + break; + } + } + + function sql_subquery($field, $sql, $line='', $file='', $break_on_error=true, $type=TYPE_INT) + { + // sub-queries doable + $this->sql_get_version(); + if ( !in_array(SQL_LAYER, array('mysql', 'mysql4')) || (($this->sql_version[0] + ($this->sql_version[1] / 100)) >= 4.01) ) + { + return $sql; + } + + // no sub-queries + $ids = array(); + $result = $this->sql_query(trim($sql), false, $line, $file, $break_on_error); + while ( $row = $this->sql_fetchrow($result) ) + { + $ids[] = $type == TYPE_INT ? intval($row[$field]) : '\'' . $this->sql_escape_string($row[$field]) . '\''; + } + $this->sql_freeresult($result); + return empty($ids) ? 'NULL' : implode(', ', $ids); + } + + function sql_col_id($expr, $alias) + { + $this->sql_get_version(); + return in_array(SQL_LAYER, array('mysql', 'mysql4')) && (($this->sql_version[0] + ($this->sql_version[1] / 100)) <= 4.01) ? $alias : $expr; + } + + function sql_get_version() + { + if ( empty($this->sql_version) ) + { + $this->sql_version = array(0, 0, 0); + switch ( SQL_LAYER ) + { + case 'mysql': + case 'mysql4': + if ( function_exists('mysql_get_server_info') ) + { + $lo_version = explode('-', mysql_get_server_info()); + $this->sql_version = explode('.', $lo_version[0]); + $this->sql_version = array(intval($this->sql_version[0]), intval($this->sql_version[1]), intval($this->sql_version[2]), $lo_version[1]); + } + break; + + case 'postgresql': + case 'mssql': + case 'mssql-odbc': + default: + break; + } + } + return $this->sql_version; + } + + function sql_error() + { + if ( $this->_conn ) + { + return mysql_error(); + } + else + { + return array(); + } + } + function sql_escape_string($t) + { + return mysql_real_escape_string($t); + } + function sql_close() + { + $this->close(); + } + function sql_fetchrowset($query_id = 0) + { + if( !$query_id ) + { + $query_id = $this->query_result; + } + + if( $query_id ) + { + unset($this->rowset[$query_id]); + unset($this->row[$query_id]); + + while($this->rowset[$query_id] = mysql_fetch_array($query_id, MYSQL_ASSOC)) + { + $result[] = $this->rowset[$query_id]; + } + + return $result; + } + else + { + return false; + } + } + /** + * Generates and outputs a report of all the SQL queries made during execution. Should only be called after everything's over with. + */ + + function sql_report() + { + global $db, $session, $paths, $template, $plugins; // Common objects + if ( !$session->get_permissions('mod_misc') ) + { + die_friendly('Access denied', '
You are not authorized to generate a SQL backtrace.
'); + } + // Create copies of variables that may be changed after header is called + $backtrace = $this->query_backtrace; + $times = $this->query_times; + $template->header(); + echo 'SQL backtrace for a normal page load of ' . htmlspecialchars($paths->cpage['urlname']) . ' | +|
---|---|
+ | |
Query: | +' . htmlspecialchars($query) . ' |
+
Time: | +' . number_format($this->query_times[$query], 6) . ' seconds | +
Unbuffered: | +' . ( $unbuffered ? 'Yes' : 'No' ) . ' | +
Called from: | +' . $this->query_sources[$query] . ' | +
+ Total time taken for SQL queries: ' . round( $query_time_total, 6 ) . ' seconds + | +