includes/dbal.php
author Dan
Thu, 17 Jan 2008 02:03:33 -0500
changeset 349 fdaf9070566c
parent 348 87e08a6e4fec
child 352 9d7225c0db6d
permissions -rw-r--r--
More progress on the installer. At this point it can install and import the language, but does not rename config files. Still much work to be done, most notably localization and creation of MySQL users and databases.

<?php

/*
 * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
 * Version 1.1.1
 * Copyright (C) 2006-2007 Dan Fuhry
 *
 * This program is Free Software; you can redistribute and/or modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for details.
 */
 
function db_error_handler($errno, $errstr, $errfile = false, $errline = false, $errcontext = Array() )
{
  if ( !defined('ENANO_DEBUG') )
    return;
  $e = error_reporting(0);
  error_reporting($e);
  if ( $e < $errno )
    return;
  $errtype = 'Notice';
  switch ( $errno )
  {
    case E_ERROR: case E_USER_ERROR: case E_CORE_ERROR: case E_COMPILE_ERROR: $errtype = 'Error'; break;
    case E_WARNING: case E_USER_WARNING: case E_CORE_WARNING: case E_COMPILE_WARNING: $errtype = 'Warning'; break;
  }
  $debug = debug_backtrace();
  if ( !isset($debug[0]['file']) )
    return false;
  $debug = $debug[0]['file'] . ', line ' . $debug[0]['line'];
  echo "<b>$errtype:</b> $errstr<br />Error source:<pre>$debug</pre>";
}
 
class mysql {
  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<br />";
    if ( function_exists('debug_backtrace') )
    {
      $this->errhandler = set_error_handler('db_error_handler');
    }
  }
  
  function disable_errorhandler()
  {
    // echo "DBAL: disabling error handler<br />";
    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(mysql_error());
    if($e=='') $e='&lt;none&gt;';
    $t = ( !empty($t) ) ? $t : '&lt;No error description provided&gt;';
    global $email;
    $email_info = ( defined('ENANO_CONFIG_FETCHED') && is_object($email) ) ? ', at &lt;' . $email->jscode() . $email->encryptEmail(getConfig('contact_email')) . '&gt;' : '';
    $internal_text = '<h3>The site was unable to finish serving your request.</h3>
                      <p>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 . '.</p>
                      <p>Description or location of error: '.$t.'<br />
                      Error returned by MySQL extension: ' . $e . '<br />
                      Most recent SQL query:</p>
                      <pre>'.$bt.'</pre>';
    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(mysql_error()));
    $q = addslashes($this->latest_query);
    $t = "{'mode':'error','error':'An error occurred during database query.\nQuery was:\n  $q\n\nError returned by MySQL: $e'}";
    die($t);
  }
  
  function get_error($t = '') {
    header('HTTP/1.1 500 Internal Server Error');
    $bt = $this->sql_backtrace();
    $e = htmlspecialchars(mysql_error());
    if($e=='') $e='&lt;none&gt;';
    global $email;
    $email_info = ( defined('ENANO_CONFIG_FETCHED') && is_object($email) ) ? ', at &lt;' . $email->jscode() . $email->encryptEmail(getConfig('contact_email')) . '&gt;' : '';
    $internal_text = '<h3>The site was unable to finish serving your request.</h3>
                      <p>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 . '.</p>
                      <p>Description or location of error: '.$t.'<br />
                      Error returned by MySQL extension: ' . $e . '<br />
                      Most recent SQL query:</p>
                      <pre>'.$bt.'</pre>';
    return $internal_text;
  }
  
  function connect($manual_credentials = false, $dbhost = false, $dbuser = false, $dbpasswd = false, $dbname = false)
  {
    $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 ( !$manual_credentials )
    {
      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/index.php';
        define('IN_ENANO_INSTALL', 1);
        $GLOBALS['lang'] = new Language('eng');
        global $lang;
        $lang->load_file('./language/english/core.json');
        $lang->load_file('./language/english/install.json');
        // 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...', 0);
        exit;
      }
    }
    
    $this->_conn = @mysql_connect($dbhost, $dbuser, $dbpasswd);
    unset($dbuser);
    unset($dbpasswd); // Security
    
    if ( !$this->_conn && !$manual_credentials )
    {
      grinding_halt('Enano is having a problem', '<p>Error: couldn\'t connect to MySQL.<br />'.mysql_error().'</p>');
    }
    else if ( !$this->_conn && $manual_credentials )
    {
      return false;
    }
    
    // Reset some variables
    $this->query_backtrace = array();
    $this->query_times = array();
    $this->query_sources = array();
    $this->num_queries = 0;
    
    $this->debug = ( defined('ENANO_DEBUG') );
    
    $q = $this->sql_query('USE `'.$dbname.'`;');
    
    if ( !$q )
      $this->_die('The database could not be selected.');
    
    // We're in!
    $this->disable_errorhandler();
    return true;
  }
  
  function sql_query($q)
  {
    $this->enable_errorhandler();
    
    if ( $this->debug && function_exists('debug_backtrace') )
    {
      $backtrace = @debug_backtrace();
      if ( is_array($backtrace) )
      {
        $bt = $backtrace[0];
        if ( isset($backtrace[1]['class']) )
        {
          if ( $backtrace[1]['class'] == 'sessionManager' )
          {
            $bt = $backtrace[1];
          }
        }
        $this->query_sources[$q] = substr($bt['file'], strlen(ENANO_ROOT) + 1) . ', line ' . $bt['line'];
      }
      unset($backtrace);
    }
    
    $this->num_queries++;
    $this->query_backtrace[] = $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', '<p>Enano has caught and prevented an SQL injection attempt. Your IP address has been recorded and the administrator has been notified.</p><p>Query was:</p><pre>'.htmlspecialchars($q).'</pre>');
    }
    
    $time_start = microtime_float();
    $r = mysql_query($q, $this->_conn);
    $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', '<p>Enano has caught and prevented an SQL injection attempt. Your IP address has been recorded and the administrator has been notified.</p><p>Query was:</p><pre>'.htmlspecialchars($q).'</pre>');
    }
    
    $time_start = microtime_float();
    $r = mysql_unbuffered_query($q, $this->_conn);
    $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-&gt;check_query(): checking query: ".htmlspecialchars($q).'<br />'."\n";
    $sz = strlen($q);
    $quotechar = false;
    $quotepos  = 0;
    $prev_is_quote = false;
    $just_started = false;
    for ( $i = 0; $i < strlen($q); $i++, $c = substr($q, $i, 1) )
    {
      $next = substr($q, $i+1, 1);
      $next2 = substr($q, $i+2, 1);
      $prev = substr($q, $i-1, 1);
      $prev2 = substr($q, $i-2, 1);
      if(isset($c) && in_array($c, Array('"', "'", '`')))
      {
        if($quotechar)
        {
          if (
              ( $quotechar == $c && $quotechar != $next && ( $quotechar != $prev || $just_started ) && $prev != '\\') ||
              ( $prev2 == '\\' && $prev == $quotechar && $quotechar == $c )
            )
          {
            $quotechar = false;
            if($debug) echo('$db-&gt;check_query(): just finishing a quote section, quoted string: '.htmlspecialchars(substr($q, $quotepos, $i - $quotepos + 1)) . '<br />');
            $q = substr($q, 0, $quotepos) . 'SAFE_QUOTE' . substr($q, $i + 1, strlen($q));
            if($debug) echo('$db-&gt;check_query(): Filtered query: '.$q.'<br />');
            $i = $quotepos;
          }
        }
        else
        {
          $quotechar = $c;
          $quotepos  = $i;
          $just_started = true;
        }
        if($debug) echo '$db-&gt;check_query(): found quote char as pos: '.$i.'<br />';
        continue;
      }
      $just_started = false;
    }
    if(substr(trim($q), strlen(trim($q))-1, 1) == ';') $q = substr(trim($q), 0, strlen(trim($q))-1);
    for($i=0;$i<strlen($q);$i++,$c=substr($q, $i, 1))
    {
      if ( 
           ( ( $c == ';' && $i != $sz-1 ) || $c . substr($q, $i+1, 1) == '--' )
        || ( in_array($c, Array('"', "'", '`')) )
         ) // Don't permit semicolons in mid-query, and never allow comments
      {
        // Injection attempt!
        if($debug)
        {
          $e = '';
          for($j=$i-5;$j<$i+5;$j++)
          {
            if($j == $i) $e .= '<span style="color: red; text-decoration: underline;">' . $c . '</span>';
            else $e .= $c;
          }
          echo 'Injection attempt caught at pos: '.$i.'<br />';
        }
        return false;
      }
    }
    if ( preg_match('/[\s]+(SAFE_QUOTE|[\S]+)=\\1($|[\s]+)/', $q, $match) )
    {
      if ( $debug ) echo 'Found always-true test in query, injection attempt caught, match:<br />' . '<pre>' . print_r($match, true) . '</pre>';
      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(mysql_data_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 @mysql_insert_id();
  }
  
  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 = mysql_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 = mysql_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 = mysql_num_rows($r);
    $this->disable_errorhandler();
    return $n;
  }
  
  function escape($str)
  {
    $this->enable_errorhandler();
    $str = mysql_real_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;
    }
    mysql_free_result($result);
    $this->disable_errorhandler();
    return null;
  }
  
  function close() {
    mysql_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.');
    mysql_free_result($r);
  }
  function sql_numrows($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.');
    return mysql_num_rows($r);
  }
  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 mysql_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()
	{
    return mysql_error();
	}
  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', '<p>You are not authorized to generate a SQL backtrace.</p>');
    }
    // Create copies of variables that may be changed after header is called
    $backtrace = $this->query_backtrace;
    $times = $this->query_times;
    $template->header();
    echo '<h3>SQL query log and timetable</h3>';
    echo '<div class="tblholder">
            <table border="0" cellspacing="1" cellpadding="4">';
    $i = 0;
    foreach ( $backtrace as $query )
    {
      $i++;
      $unbuffered = false;
      if ( substr($query, 0, 13) == '(UNBUFFERED) ' )
      {
        $query = substr($query, 13);
        $unbuffered = true;
      }
      if ( $i == 1 )
      {
        echo '<tr>
                <th colspan="2">SQL backtrace for a normal page load of ' . htmlspecialchars($paths->cpage['urlname']) . '</th>
              </tr>';
      }
      else
      {
        echo '<tr>
                <th class="subhead" colspan="2">&nbsp;</th>
              </tr>';
      }
      echo '<tr>
              <td class="row2">Query:</td>
              <td class="row1"><pre>' . htmlspecialchars($query) . '</pre></td>
            </tr>
            <tr>
              <td class="row2">Time:</td>
              <td class="row1">' . number_format($this->query_times[$query], 6) . ' seconds</td>
            </tr>
            <tr>
              <td class="row2">Unbuffered:</td>
              <td class="row1">' . ( $unbuffered ? 'Yes' : 'No' ) . '</td>
            </tr>';
      if ( isset($this->query_sources[$query]) )
      {
        echo '<tr>
                <td class="row2">Called from:</td>
                <td class="row1">' . $this->query_sources[$query] . '</td>
              </tr>';
      }
    }
    if ( function_exists('array_sum') )
    {
      $query_time_total = array_sum($this->query_times);
      echo '<tr>
              <th class="subhead" colspan="2">
                Total time taken for SQL queries: ' . round( $query_time_total, 6 ) . ' seconds
              </th>
            </tr>';
    }
    echo '  </table>
          </div>';
    $template->footer();
  }
}

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<br />";
    if ( function_exists('debug_backtrace') )
    {
      $this->errhandler = set_error_handler('db_error_handler');
    }
  }
  
  function disable_errorhandler()
  {
    // echo "DBAL: disabling error handler<br />";
    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='&lt;none&gt;';
    $t = ( !empty($t) ) ? $t : '&lt;No error description provided&gt;';
    global $email;
    $email_info = ( defined('ENANO_CONFIG_FETCHED') && is_object($email) ) ? ', at &lt;' . $email->jscode() . $email->encryptEmail(getConfig('contact_email')) . '&gt;' : '';
    $internal_text = '<h3>The site was unable to finish serving your request.</h3>
                      <p>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 . '.</p>
                      <p>Description or location of error: '.$t.'<br />
                      Error returned by PostgreSQL extension: ' . $e . '<br />
                      Most recent SQL query:</p>
                      <pre>'.$bt.'</pre>';
    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='&lt;none&gt;';
    global $email;
    $email_info = ( defined('ENANO_CONFIG_FETCHED') && is_object($email) ) ? ', at &lt;' . $email->jscode() . $email->encryptEmail(getConfig('contact_email')) . '&gt;' : '';
    $internal_text = '<h3>The site was unable to finish serving your request.</h3>
                      <p>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 . '.</p>
                      <p>Description or location of error: '.$t.'<br />
                      Error returned by MySQL extension: ' . $e . '<br />
                      Most recent SQL query:</p>
                      <pre>'.$bt.'</pre>';
    return $internal_text;
  }
  
  function connect($manual_credentials = false, $dbhost = false, $dbuser = false, $dbpasswd = false, $dbname = false)
  {
    $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 ( !$manual_credentials )
    {
      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 && !$manual_credentials )
    {
      grinding_halt('Enano is having a problem', '<p>Error: couldn\'t connect to PostgreSQL.<br />'.pg_last_error().'</p>');
    }
    else if ( !$this->_conn && $manual_credentials )
    {
      return false;
    }
    
    // Reset some variables
    $this->query_backtrace = array();
    $this->query_times = array();
    $this->query_sources = array();
    $this->num_queries = 0;
    
    $this->debug = ( defined('ENANO_DEBUG') );
    
    // We're in!
    $this->disable_errorhandler();
    return true;
  }
  
  function sql_query($q)
  {
    $this->enable_errorhandler();
    
    if ( $this->debug && function_exists('debug_backtrace') )
    {
      $backtrace = @debug_backtrace();
      if ( is_array($backtrace) )
      {
        $bt = $backtrace[0];
        if ( isset($backtrace[1]['class']) )
        {
          if ( $backtrace[1]['class'] == 'sessionManager' )
          {
            $bt = $backtrace[1];
          }
        }
        $this->query_sources[$q] = substr($bt['file'], strlen(ENANO_ROOT) + 1) . ', line ' . $bt['line'];
      }
      unset($backtrace);
    }
    
    $this->num_queries++;
    $this->query_backtrace[] = $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', '<p>Enano has caught and prevented an SQL injection attempt. Your IP address has been recorded and the administrator has been notified.</p><p>Query was:</p><pre>'.htmlspecialchars($q).'</pre>');
    }
    
    $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', '<p>Enano has caught and prevented an SQL injection attempt. Your IP address has been recorded and the administrator has been notified.</p><p>Query was:</p><pre>'.htmlspecialchars($q).'</pre>');
    }
    
    $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-&gt;check_query(): checking query: ".htmlspecialchars($q).'<br />'."\n";
    $sz = strlen($q);
    $quotechar = false;
    $quotepos  = 0;
    $prev_is_quote = false;
    $just_started = false;
    for ( $i = 0; $i < strlen($q); $i++, $c = substr($q, $i, 1) )
    {
      $next = substr($q, $i+1, 1);
      $next2 = substr($q, $i+2, 1);
      $prev = substr($q, $i-1, 1);
      $prev2 = substr($q, $i-2, 1);
      if(isset($c) && in_array($c, Array('"', "'", '`')))
      {
        if($quotechar)
        {
          if (
              ( $quotechar == $c && $quotechar != $next && ( $quotechar != $prev || $just_started ) && $prev != '\\') ||
              ( $prev2 == '\\' && $prev == $quotechar && $quotechar == $c )
            )
          {
            $quotechar = false;
            if($debug) echo('$db-&gt;check_query(): just finishing a quote section, quoted string: '.htmlspecialchars(substr($q, $quotepos, $i - $quotepos + 1)) . '<br />');
            $q = substr($q, 0, $quotepos) . 'SAFE_QUOTE' . substr($q, $i + 1, strlen($q));
            if($debug) echo('$db-&gt;check_query(): Filtered query: '.$q.'<br />');
            $i = $quotepos;
          }
        }
        else
        {
          $quotechar = $c;
          $quotepos  = $i;
          $just_started = true;
        }
        if($debug) echo '$db-&gt;check_query(): found quote char as pos: '.$i.'<br />';
        continue;
      }
      $just_started = false;
    }
    if(substr(trim($q), strlen(trim($q))-1, 1) == ';') $q = substr(trim($q), 0, strlen(trim($q))-1);
    for($i=0;$i<strlen($q);$i++,$c=substr($q, $i, 1))
    {
      if ( 
           ( ( $c == ';' && $i != $sz-1 ) || $c . substr($q, $i+1, 1) == '--' )
        || ( in_array($c, Array('"', "'", '`')) )
         ) // Don't permit semicolons in mid-query, and never allow comments
      {
        // Injection attempt!
        if($debug)
        {
          $e = '';
          for($j=$i-5;$j<$i+5;$j++)
          {
            if($j == $i) $e .= '<span style="color: red; text-decoration: underline;">' . $c . '</span>';
            else $e .= $c;
          }
          echo 'Injection attempt caught at pos: '.$i.'<br />';
        }
        return false;
      }
    }
    if ( preg_match('/[\s]+(SAFE_QUOTE|[\S]+)=\\1($|[\s]+)/', $q, $match) )
    {
      if ( $debug ) echo 'Found always-true test in query, injection attempt caught, match:<br />' . '<pre>' . print_r($match, true) . '</pre>';
      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 pg_last_error();
		}
		else
		{
			return ( defined('IN_ENANO_INSTALL') ) ? $GLOBALS["lang"]->get('dbpgsql_msg_err_auth') : 'Access to the database was denied. Ensure that your database exists and that your username and password are correct.';
		}
	}
  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', '<p>You are not authorized to generate a SQL backtrace.</p>');
    }
    // Create copies of variables that may be changed after header is called
    $backtrace = $this->query_backtrace;
    $times = $this->query_times;
    $template->header();
    echo '<h3>SQL query log and timetable</h3>';
    echo '<div class="tblholder">
            <table border="0" cellspacing="1" cellpadding="4">';
    $i = 0;
    foreach ( $backtrace as $query )
    {
      $i++;
      $unbuffered = false;
      if ( substr($query, 0, 13) == '(UNBUFFERED) ' )
      {
        $query = substr($query, 13);
        $unbuffered = true;
      }
      if ( $i == 1 )
      {
        echo '<tr>
                <th colspan="2">SQL backtrace for a normal page load of ' . htmlspecialchars($paths->cpage['urlname']) . '</th>
              </tr>';
      }
      else
      {
        echo '<tr>
                <th class="subhead" colspan="2">&nbsp;</th>
              </tr>';
      }
      echo '<tr>
              <td class="row2">Query:</td>
              <td class="row1"><pre>' . htmlspecialchars($query) . '</pre></td>
            </tr>
            <tr>
              <td class="row2">Time:</td>
              <td class="row1">' . number_format($this->query_times[$query], 6) . ' seconds</td>
            </tr>
            <tr>
              <td class="row2">Unbuffered:</td>
              <td class="row1">' . ( $unbuffered ? 'Yes' : 'No' ) . '</td>
            </tr>';
      if ( isset($this->query_sources[$query]) )
      {
        echo '<tr>
                <td class="row2">Called from:</td>
                <td class="row1">' . $this->query_sources[$query] . '</td>
              </tr>';
      }
    }
    if ( function_exists('array_sum') )
    {
      $query_time_total = array_sum($this->query_times);
      echo '<tr>
              <th class="subhead" colspan="2">
                Total time taken for SQL queries: ' . round( $query_time_total, 6 ) . ' seconds
              </th>
            </tr>';
    }
    echo '  </table>
          </div>';
    $template->footer();
  }
}

?>