<?php
/**
* @version	$Id: db_connection.php 14241 2011-03-16 20:24:35Z alex $
* @package	In-Portal
* @copyright	Copyright (C) 1997 - 2009 Intechnic. All rights reserved.
* @license      GNU/GPL
* In-Portal is Open Source software.
* This means that this software may have been modified pursuant
* the GNU General Public License, and as distributed it includes
* or is derivative of works licensed under the GNU General Public License
* or other free or open source software licenses.
* See http://www.in-portal.org/license for copyright notices and details.
*/

	defined('FULL_PATH') or die('restricted access!');

	/**
	 * Multi database connection class
	 *
	 */
	class kDBConnection {

		/**
		* Holds reference to global KernelApplication instance
		* @access public
		* @var kApplication
		*/
		var $Application;

		/**
		 * Current database type
		 *
		 * @var string
		 * @access private
		 */
		var $dbType = 'mysql';

		/**
		 * Created connection handle
		 *
		 * @var resource
		 * @access private
		 */
		var $connectionID = null;

		/**
		 * Connection parameters, that were used
		 *
		 * @var Array
		 */
		var $connectionParams = Array ('host' => '', 'user' => '', 'pass' => '', 'db' => '');

		/**
		 * Handle of currenty processed recordset
		 *
		 * @var resource
		 * @access private
		 */
		var $queryID = null;

		/**
		 * DB type specific function mappings
		 *
		 * @var Array
		 * @access private
		 */
		var $metaFunctions = Array();

		/**
		 * Function to handle sql errors
		 *
		 * @var string
		 * @access private
		 */
		var $errorHandler = '';

		/**
		 * Error code
		 *
		 * @var int
		 * @access private
		 */
		var $errorCode = 0;

		/**
		 * Error message
		 *
		 * @var string
		 * @access private
		 */
		var $errorMessage = '';

		/**
		 * Defines if database connection
		 * operations should generate debug
		 * information
		 *
		 * @var bool
		 */
		var $debugMode = false;

		/**
		 * Save query execution statistics
		 *
		 * @var bool
		 */
		var $_captureStatistics = false;

		/**
		 * Last query to database
		 *
		 * @var string
		 */
		var $lastQuery = '';

		/**
		 * Total processed queries count
		 *
		 * @var int
		 */
		var $_queryCount = 0;

		/**
		 * Total time, used for serving queries
		 *
		 * @var Array
		 */
		var $_queryTime = 0;

		/**
		 * Indicates, that next database query could be cached, when memory caching is enabled
		 *
		 * @var bool
		 */
		var $nextQueryCachable = false;

		/**
		 * Initializes connection class with
		 * db type to used in future
		 *
		 * @param string $dbType
		 * @return DBConnection
		 * @access public
		 */
		function kDBConnection($dbType, $errorHandler = '')
		{
			$this->dbType = $dbType;
//			$this->initMetaFunctions();
			if (!$errorHandler) {
				$this->errorHandler = Array(&$this, 'handleError');
			}
			else {
				$this->errorHandler = $errorHandler;
			}

			$this->_captureStatistics = defined('DBG_CAPTURE_STATISTICS') && DBG_CAPTURE_STATISTICS && !(defined('ADMIN') && ADMIN);

			if (class_exists('kApplication')) {
				// prevents "Fatal Error" on 2nd installation step (when database is empty)
				$this->Application =& kApplication::Instance();
			}
		}

		/**
		 * Set's custom error
		 *
		 * @param int $code
		 * @param string $msg
		 * @access public
		 */
		function setError($code, $msg)
		{
			$this->errorCode = $code;
			$this->errorMessage = $msg;
		}

		/**
		 * Checks if previous query execution
		 * raised an error.
		 *
		 * @return bool
		 * @access public
		 */
		function hasError()
		{
			return $this->errorCode != 0;
		}

		/**
		 * Caches function specific to requested
		 * db type
		 *
		 * @access private
		 */
		function initMetaFunctions()
		{
			$ret = Array();
			switch ($this->dbType)
			{
				case 'mysql':
					 $ret = Array(); // only define functions, that name differs from "dbType_<meta_name>"

					break;


			}
			$this->metaFunctions = $ret;
		}

		/**
		 * Get's function for specific db type
		 * based on it's meta name
		 *
		 * @param string $name
		 * @return string
		 * @access private
		 */
		function getMetaFunction($name)
		{
			/*if (!isset($this->metaFunctions[$name])) {
				$this->metaFunctions[$name] = $name;
			}*/

			return $this->dbType.'_'.$name;
		}


		/**
		 * Try to connect to database server
		 * using specified parameters and set
		 * database to $db if connection made
		 *
		 * @param string $host
		 * @param string $user
		 * @param string $pass
		 * @param string $db
		 * @access public
		 */
		function Connect($host, $user, $pass, $db, $force_new = false, $retry = false)
		{
			$this->connectionParams = Array ('host' => $host, 'user' => $user, 'pass' => $pass, 'db' => $db);

			$func = $this->getMetaFunction('connect');
			$this->connectionID = $func($host, $user, $pass, $force_new);

			if ($this->connectionID) {
				if (defined('DBG_SQL_MODE')) {
					$this->Query('SET sql_mode = \''.DBG_SQL_MODE.'\'');
				}

				if (defined('SQL_COLLATION') && defined('SQL_CHARSET')) {
					$this->Query('SET NAMES \''.SQL_CHARSET.'\' COLLATE \''.SQL_COLLATION.'\'');
				}

				$this->setError(0, ''); // reset error
				$this->setDB($db);
			}

			// process error (fatal in most cases)
			$func = $this->getMetaFunction('errno');
			$this->errorCode = $this->connectionID ? $func($this->connectionID) : $func();

			if ( !$this->hasError() ) {
				return true;
			}

			$func = $this->getMetaFunction('error');
			$this->errorMessage = $this->connectionID ? $func($this->connectionID) : $func();
			$error_msg = 'Database connection failed, please check your connection settings.<br/>Error (' . $this->errorCode . '): ' . $this->errorMessage;

			trigger_error($error_msg, (defined('IS_INSTALL') && IS_INSTALL) || $retry ? E_USER_WARNING : E_USER_ERROR);

			return false;
		}

		function ReConnect($force_new = false)
		{
			$retry_count = 0;

			$func = $this->getMetaFunction('close');
			$func($this->connectionID);

			while ($retry_count < 3) {
				sleep(5); // wait 5 seconds before each reconnect attempt

				$connected = $this->Connect(
					$this->connectionParams['host'],
					$this->connectionParams['user'],
					$this->connectionParams['pass'],
					$this->connectionParams['db'],
					$force_new, true
				);

				if ($connected) {
					break;
				}

				$retry_count++;
			}

			return $connected;
		}

		/**
		 * Shows error message from previous operation
		 * if it failed
		 *
		 * @access private
		 */
		function showError($sql = '', $key_field = null, $no_debug = false)
		{
			static $retry_count = 0;
			
			$func = $this->getMetaFunction('errno');

			if (!$this->connectionID) {
				// no connection while doing mysql_query
				$this->errorCode = $func();

				if ( $this->hasError() ) {
					$func = $this->getMetaFunction('error');
					$this->errorMessage = $func();

					$ret = $this->callErrorHandler($sql);

					if (!$ret) {
						exit;
					}
				}

				return false;
			}

			// checking if there was an error during last mysql_query
			$this->errorCode = $func($this->connectionID);

			if ( $this->hasError() ) {
				$func = $this->getMetaFunction('error');
				$this->errorMessage = $func($this->connectionID);

				$ret = $this->callErrorHandler($sql);

				if ( ($this->errorCode == 2006 || $this->errorCode == 2013) && ($retry_count < 3) ) {
					// #2006 - MySQL server has gone away
					// #2013 - Lost connection to MySQL server during query
					$retry_count++;

					if ( $this->ReConnect() ) {
						return $this->Query($sql, $key_field, $no_debug);
					}
				}

				if (!$ret) {
					exit;
				}
			}
			else {
				$retry_count = 0;
			}

			return false;
		}

		function callErrorHandler($sql)
		{
			if (is_array($this->errorHandler)) {
				$func = $this->errorHandler[1];
				$ret = $this->errorHandler[0]->$func($this->errorCode, $this->errorMessage, $sql);
			}
			else {
				$func = $this->errorHandler;
				$ret = $func($this->errorCode, $this->errorMessage, $sql);
			}

			return $ret;
		}

		/**
		 * Default error handler for sql errors
		 *
		 * @param int $code
		 * @param string $msg
		 * @param string $sql
		 * @return bool
		 * @access private
		 */
		function handleError($code, $msg, $sql)
		{
			echo '<strong>Processing SQL</strong>: ' . $sql . '<br/>';
			echo '<strong>Error (' . $code . '):</strong> ' . $msg . '<br/>';

			return false;
		}

		/**
		 * Set's database name for connection
		 * to $new_name
		 *
		 * @param string $new_name
		 * @return bool
		 * @access public
		 */
		function setDB($new_name)
		{
			if (!$this->connectionID) return false;
			$func = $this->getMetaFunction('select_db');
			return $func($new_name, $this->connectionID);
		}

		/**
		 * Returns first field of first line
		 * of recordset if query ok or false
		 * otherwise
		 *
		 * @param string $sql
		 * @param int $offset
		 * @return string
		 * @access public
		 */
		function GetOne($sql, $offset = 0)
		{
			$row = $this->GetRow($sql, $offset);
			if(!$row) return false;

			return array_shift($row);
		}

		/**
		 * Returns first row of recordset
		 * if query ok, false otherwise
		 *
		 * @param string $sql
		 * @param int $offset
		 * @return Array
		 * @access public
		 */
		function GetRow($sql, $offset = 0)
		{
			$sql .= ' '.$this->getLimitClause($offset, 1);
			$ret = $this->Query($sql);
			if(!$ret) return false;

			return array_shift($ret);
		}

		/**
		 * Returns 1st column of recordset as
		 * one-dimensional array or false otherwise
		 * Optional parameter $key_field can be used
		 * to set field name to be used as resulting
		 * array key
		 *
		 * @param string $sql
		 * @param string $key_field
		 * @return Array
		 * @access public
		 */
		function GetCol($sql, $key_field = null)
		{
			$rows = $this->Query($sql);
			if (!$rows) return $rows;

			$i = 0; $row_count = count($rows);
			$ret = Array();
			if (isset($key_field)) {
				while ($i < $row_count) {
					$ret[$rows[$i][$key_field]] = array_shift($rows[$i]);
					$i++;
				}
			}
			else {
				while ($i < $row_count) {
					$ret[] = array_shift($rows[$i]);
					$i++;
				}
			}
			return $ret;
		}

		/**
		 * Queries db with $sql query supplied
		 * and returns rows selected if any, false
		 * otherwise. Optional parameter $key_field
		 * allows to set one of the query fields
		 * value as key in string array.
		 *
		 * @param string $sql
		 * @param string $key_field
		 * @return Array
		 */
		function Query($sql, $key_field = null, $no_debug = false)
		{
			$this->lastQuery = $sql;
			if (!$no_debug) {
				$this->_queryCount++;
			}

			if ($this->debugMode && !$no_debug) {
				return $this->debugQuery($sql,$key_field);
			}
			$query_func = $this->getMetaFunction('query');

			// set 1st checkpoint: begin
			if ($this->_captureStatistics) {
				$start_time = getmicrotime();
			}
			// set 1st checkpoint: end

			$this->setError(0, ''); // reset error
			$this->queryID = $query_func($sql,$this->connectionID);
			if (is_resource($this->queryID)) {
				$ret = Array();
				$fetch_func = $this->getMetaFunction('fetch_assoc');
				if (isset($key_field)) {
					while (($row = $fetch_func($this->queryID))) {
						$ret[$row[$key_field]] = $row;
					}
				}
				else {
					while (($row = $fetch_func($this->queryID))) {
						$ret[] = $row;
					}
				}

				// set 2nd checkpoint: begin
				if ($this->_captureStatistics) {
					$query_time = getmicrotime() - $start_time;
					if ($query_time > DBG_MAX_SQL_TIME && !$no_debug) {
						$this->Application->logSlowQuery($sql, $query_time);
					}
					$this->_queryTime += $query_time;
				}
				// set 2nd checkpoint: end

				$this->Destroy();
				return $ret;
			}
			else {
				// set 2nd checkpoint: begin
				if ($this->_captureStatistics) {
					$this->_queryTime += getmicrotime() - $start_time;
				}
				// set 2nd checkpoint: end
			}

			return $this->showError($sql, $key_field, $no_debug);
		}

		function ChangeQuery($sql)
		{
			$this->Query($sql);
			return $this->errorCode == 0 ? true : false;
		}

		function debugQuery($sql, $key_field = null)
		{
			global $debugger;
			$query_func = $this->getMetaFunction('query');

			// set 1st checkpoint: begin
			$profileSQLs = defined('DBG_SQL_PROFILE') && DBG_SQL_PROFILE;
			if ($profileSQLs) {
				$queryID = $debugger->generateID();
				$debugger->profileStart('sql_'.$queryID, $debugger->formatSQL($sql));
			}
			// set 1st checkpoint: end

			$this->setError(0, ''); // reset error
			$this->queryID = $query_func($sql, $this->connectionID);

			if( is_resource($this->queryID) )
			{
				$ret = Array();
				$fetch_func = $this->getMetaFunction('fetch_assoc');
				if( isset($key_field) )
				{
					while( ($row = $fetch_func($this->queryID)) )
					{
						$ret[$row[$key_field]] = $row;
					}
				}
				else
				{
					while( ($row = $fetch_func($this->queryID)) )
					{
						$ret[] = $row;
					}
				}

				// set 2nd checkpoint: begin
				if ($profileSQLs) {
					$first_cell = count($ret) == 1 && count(current($ret)) == 1 ? current(current($ret)) : null;

					if (strlen($first_cell) > 200) {
					    $first_cell = substr($first_cell, 0, 50) . ' ...';
					}

					$debugger->profileFinish('sql_'.$queryID, null, null, $this->getAffectedRows(), $first_cell, $this->_queryCount, $this->nextQueryCachable);
					$debugger->profilerAddTotal('sql', 'sql_'.$queryID);
					$this->nextQueryCachable = false;
				}
				// set 2nd checkpoint: end

				$this->Destroy();
				return $ret;
			}
			else {
				// set 2nd checkpoint: begin
				if ($profileSQLs) {
					$debugger->profileFinish('sql_'.$queryID, null, null, $this->getAffectedRows(), null, $this->_queryCount, $this->nextQueryCachable);
					$debugger->profilerAddTotal('sql', 'sql_'.$queryID);
					$this->nextQueryCachable = false;
				}
				// set 2nd checkpoint: end
			}

			return $this->showError($sql, $key_field);
		}

		/**
		 * Free memory used to hold recordset handle
		 *
		 * @access private
		 */
		function Destroy()
		{
			if($this->queryID)
			{
				$free_func = $this->getMetaFunction('free_result');
				$free_func($this->queryID);
				$this->queryID = null;
			}
		}

		/**
		 * Returns auto increment field value from
		 * insert like operation if any, zero otherwise
		 *
		 * @return int
		 * @access public
		 */
		function getInsertID()
		{
			$func = $this->getMetaFunction('insert_id');
			return $func($this->connectionID);
		}

		/**
		 * Returns row count affected by last query
		 *
		 * @return int
		 * @access public
		 */
		function getAffectedRows()
		{
			$func = $this->getMetaFunction('affected_rows');
			return $func($this->connectionID);
		}

		/**
		 * Returns LIMIT sql clause part for specific db
		 *
		 * @param int $offset
		 * @param int $rows
		 * @return string
		 * @access private
		 */
		function getLimitClause($offset, $rows)
		{
			if(!($rows > 0)) return '';

			switch ($this->dbType) {

				default:
					return 'LIMIT '.$offset.','.$rows;
					break;
			}
		}

		/**
		 * If it's a string, adds quotes and backslashes (only work since PHP 4.3.0)
		 * Otherwise returns as-is
		 *
		 * @param mixed $string
		 *
		 * @return string
		 */
		function qstr($string)
		{
			if ( is_null($string) ) {
				return 'NULL';
			}

			# This will also quote numeric values. This should be harmless,
			# and protects against weird problems that occur when they really
			# _are_ strings such as article titles and string->number->string
			# conversion is not 1:1.
			return "'" . mysql_real_escape_string($string, $this->connectionID) . "'";
		}

		/**
		 * Escapes strings (only work since PHP 4.3.0)
		 *
		 * @param mixed $string
		 *
		 * @return string
		 */
		function escape($string)
		{
			if ( is_null($string) ) {
				return 'NULL';
			}

			$string = mysql_real_escape_string($string, $this->connectionID);

			// prevent double-escaping of MySQL wildcard symbols ("%" and  "_") in case if they were already escaped
			return str_replace(Array ('\\\\%', '\\\\_'), Array ('\\%', '\\_'), $string);
		}

		/**
		 * Returns last error code occured
		 *
		 * @return int
		 */
		function getErrorCode()
		{
			return $this->errorCode;
		}

		/**
		 * Returns last error message
		 *
		 * @return string
		 * @access public
		 */
		function getErrorMsg()
		{
			return $this->errorMessage;
		}

		/**
		 * Performs insert of given data (useful with small number of queries)
		 * or stores it to perform multiple insert later (useful with large number of queries)
		 *
		 * @param Array $fields_hash
		 * @param string $table
		 * @param string $type
		 * @param bool $insert_now
		 * @return bool
		 */
		function doInsert($fields_hash, $table, $type = 'INSERT', $insert_now = true)
		{
			static $value_sqls = Array ();

			if ($insert_now) {
				$fields_sql = '`' . implode('`,`', array_keys($fields_hash)) . '`';
			}

			$values_sql = '';
			foreach ($fields_hash as $field_name => $field_value) {
				$values_sql .= $this->qstr($field_value) . ',';
			}

			// don't use preg here, as it may fail when string is too long
			$value_sqls[] = rtrim($values_sql, ',');

			$insert_result = true;
			if ($insert_now) {
				$insert_count = count($value_sqls);
				if (($insert_count > 1) && ($value_sqls[$insert_count - 1] == $value_sqls[$insert_count - 2])) {
					// last two records are the same
					array_pop($value_sqls);
				}

				$sql = strtoupper($type) . ' INTO `' . $table . '` (' . $fields_sql . ') VALUES (' . implode('),(', $value_sqls) . ')';
				$insert_result = $this->ChangeQuery($sql);
				$value_sqls = Array ();
			}

			return $insert_result;
		}

		function doUpdate($fields_hash, $table, $key_clause)
		{
			if (!$fields_hash) return true;

			$fields_sql = '';
			foreach ($fields_hash as $field_name => $field_value) {
				$fields_sql .= '`'.$field_name.'` = ' . $this->qstr($field_value) . ',';
			}

			// don't use preg here, as it may fail when string is too long
			$fields_sql = rtrim($fields_sql, ',');

			$sql = 'UPDATE `'.$table.'` SET '.$fields_sql.' WHERE '.$key_clause;

			return $this->ChangeQuery($sql);
		}

		/**
		 * Allows to detect table's presense in database
		 *
		 * @param string $table_name
		 * @return bool
		 */
		function TableFound($table_name)
		{
			static $table_found = Array();

			if (!preg_match('/^'.preg_quote(TABLE_PREFIX, '/').'(.*)/', $table_name)) {
				$table_name = TABLE_PREFIX.$table_name;
			}

			if (!isset($table_found[$table_name])) {
				$table_found[$table_name] = $this->Query('SHOW TABLES LIKE "'.$table_name.'"');
			}

			return $table_found[$table_name];
		}

		/**
		 * Returns query processing statistics
		 *
		 * @return Array
		 */
		function getQueryStatistics()
		{
			return Array ('time' => $this->_queryTime, 'count' => $this->_queryCount);
		}
	}