Blame | Last modification | View Log | RSS feed
<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed');/*** CodeIgniter** An open source application development framework for PHP 4.3.2 or newer** @package CodeIgniter* @author ExpressionEngine Dev Team* @copyright Copyright (c) 2008, EllisLab, Inc.* @license http://codeigniter.com/user_guide/license.html* @link http://codeigniter.com* @since Version 1.0* @filesource*/// ------------------------------------------------------------------------/*** Database Driver Class** This is the platform-independent base DB implementation class.* This class will not be called directly. Rather, the adapter* class for the specific database will extend and instantiate it.** @package CodeIgniter* @subpackage Drivers* @category Database* @author ExpressionEngine Dev Team* @link http://codeigniter.com/user_guide/database/*/class CI_DB_driver {var $username;var $password;var $hostname;var $database;var $dbdriver = 'mysql';var $dbprefix = '';var $char_set = 'utf8';var $dbcollat = 'utf8_general_ci';var $autoinit = TRUE; // Whether to automatically initialize the DBvar $swap_pre = '';var $port = '';var $pconnect = FALSE;var $conn_id = FALSE;var $result_id = FALSE;var $db_debug = FALSE;var $benchmark = 0;var $query_count = 0;var $bind_marker = '?';var $save_queries = TRUE;var $queries = array();var $query_times = array();var $data_cache = array();var $trans_enabled = TRUE;var $trans_strict = TRUE;var $_trans_depth = 0;var $_trans_status = TRUE; // Used with transactions to determine if a rollback should occurvar $cache_on = FALSE;var $cachedir = '';var $cache_autodel = FALSE;var $CACHE; // The cache class object// Private variablesvar $_protect_identifiers = TRUE;var $_reserved_identifiers = array('*'); // Identifiers that should NOT be escaped// These are use with Oraclevar $stmt_id;var $curs_id;var $limit_used;/*** Constructor. Accepts one parameter containing the database* connection settings.** @param array*/function CI_DB_driver($params){if (is_array($params)){foreach ($params as $key => $val){$this->$key = $val;}}log_message('debug', 'Database Driver Class Initialized');}// --------------------------------------------------------------------/*** Initialize Database Settings** @access private Called by the constructor* @param mixed* @return void*/function initialize(){// If an existing connection resource is available// there is no need to connect and select the databaseif (is_resource($this->conn_id) OR is_object($this->conn_id)){return TRUE;}// ----------------------------------------------------------------// Connect to the database and set the connection ID$this->conn_id = ($this->pconnect == FALSE) ? $this->db_connect() : $this->db_pconnect();// No connection resource? Throw an errorif ( ! $this->conn_id){log_message('error', 'Unable to connect to the database');if ($this->db_debug){$this->display_error('db_unable_to_connect');}return FALSE;}// ----------------------------------------------------------------// Select the DB... assuming a database name is specified in the config fileif ($this->database != ''){if ( ! $this->db_select()){log_message('error', 'Unable to select database: '.$this->database);if ($this->db_debug){$this->display_error('db_unable_to_select', $this->database);}return FALSE;}else{// We've selected the DB. Now we set the character setif ( ! $this->db_set_charset($this->char_set, $this->dbcollat)){return FALSE;}return TRUE;}}return TRUE;}// --------------------------------------------------------------------/*** Set client character set** @access public* @param string* @param string* @return resource*/function db_set_charset($charset, $collation){if ( ! $this->_db_set_charset($this->char_set, $this->dbcollat)){log_message('error', 'Unable to set database connection charset: '.$this->char_set);if ($this->db_debug){$this->display_error('db_unable_to_set_charset', $this->char_set);}return FALSE;}return TRUE;}// --------------------------------------------------------------------/*** The name of the platform in use (mysql, mssql, etc...)** @access public* @return string*/function platform(){return $this->dbdriver;}// --------------------------------------------------------------------/*** Database Version Number. Returns a string containing the* version of the database being used** @access public* @return string*/function version(){if (FALSE === ($sql = $this->_version())){if ($this->db_debug){return $this->display_error('db_unsupported_function');}return FALSE;}if ($this->dbdriver == 'oci8'){return $sql;}$query = $this->query($sql);return $query->row('ver');}// --------------------------------------------------------------------/*** Execute the query** Accepts an SQL string as input and returns a result object upon* successful execution of a "read" type query. Returns boolean TRUE* upon successful execution of a "write" type query. Returns boolean* FALSE upon failure, and if the $db_debug variable is set to TRUE* will raise an error.** @access public* @param string An SQL query string* @param array An array of binding data* @return mixed*/function query($sql, $binds = FALSE, $return_object = TRUE){if ($sql == ''){if ($this->db_debug){log_message('error', 'Invalid query: '.$sql);return $this->display_error('db_invalid_query');}return FALSE;}// Verify table prefix and replace if necessaryif ( ($this->dbprefix != '' AND $this->swap_pre != '') AND ($this->dbprefix != $this->swap_pre) ){$sql = preg_replace("/(\W)".$this->swap_pre."(\S+?)/", "\\1".$this->dbprefix."\\2", $sql);}// Is query caching enabled? If the query is a "read type"// we will load the caching class and return the previously// cached query if it existsif ($this->cache_on == TRUE AND stristr($sql, 'SELECT')){if ($this->_cache_init()){$this->load_rdriver();if (FALSE !== ($cache = $this->CACHE->read($sql))){return $cache;}}}// Compile binds if neededif ($binds !== FALSE){$sql = $this->compile_binds($sql, $binds);}// Save the query for debuggingif ($this->save_queries == TRUE){$this->queries[] = $sql;}// Start the Query Timer$time_start = list($sm, $ss) = explode(' ', microtime());// Run the Queryif (FALSE === ($this->result_id = $this->simple_query($sql))){if ($this->save_queries == TRUE){$this->query_times[] = 0;}// This will trigger a rollback if transactions are being used$this->_trans_status = FALSE;if ($this->db_debug){// grab the error number and message now, as we might run some// additional queries before displaying the error$error_no = $this->_error_number();$error_msg = $this->_error_message();// We call this function in order to roll-back queries// if transactions are enabled. If we don't call this here// the error message will trigger an exit, causing the// transactions to remain in limbo.$this->trans_complete();// Log and display errorslog_message('error', 'Query error: '.$error_msg);return $this->display_error(array('Error Number: '.$error_no,$error_msg,$sql));}return FALSE;}// Stop and aggregate the query time results$time_end = list($em, $es) = explode(' ', microtime());$this->benchmark += ($em + $es) - ($sm + $ss);if ($this->save_queries == TRUE){$this->query_times[] = ($em + $es) - ($sm + $ss);}// Increment the query counter$this->query_count++;// Was the query a "write" type?// If so we'll simply return trueif ($this->is_write_type($sql) === TRUE){// If caching is enabled we'll auto-cleanup any// existing files related to this particular URIif ($this->cache_on == TRUE AND $this->cache_autodel == TRUE AND $this->_cache_init()){$this->CACHE->delete();}return TRUE;}// Return TRUE if we don't need to create a result object// Currently only the Oracle driver uses this when stored// procedures are usedif ($return_object !== TRUE){return TRUE;}// Load and instantiate the result driver$driver = $this->load_rdriver();$RES = new $driver();$RES->conn_id = $this->conn_id;$RES->result_id = $this->result_id;if ($this->dbdriver == 'oci8'){$RES->stmt_id = $this->stmt_id;$RES->curs_id = NULL;$RES->limit_used = $this->limit_used;$this->stmt_id = FALSE;}// oci8 vars must be set before calling this$RES->num_rows = $RES->num_rows();// Is query caching enabled? If so, we'll serialize the// result object and save it to a cache file.if ($this->cache_on == TRUE AND $this->_cache_init()){// We'll create a new instance of the result object// only without the platform specific driver since// we can't use it with cached data (the query result// resource ID won't be any good once we've cached the// result object, so we'll have to compile the data// and save it)$CR = new CI_DB_result();$CR->num_rows = $RES->num_rows();$CR->result_object = $RES->result_object();$CR->result_array = $RES->result_array();// Reset these since cached objects can not utilize resource IDs.$CR->conn_id = NULL;$CR->result_id = NULL;$this->CACHE->write($sql, $CR);}return $RES;}// --------------------------------------------------------------------/*** Load the result drivers** @access public* @return string the name of the result class*/function load_rdriver(){$driver = 'CI_DB_'.$this->dbdriver.'_result';if ( ! class_exists($driver)){include_once(BASEPATH.'database/DB_result'.EXT);include_once(BASEPATH.'database/drivers/'.$this->dbdriver.'/'.$this->dbdriver.'_result'.EXT);}return $driver;}// --------------------------------------------------------------------/*** Simple Query* This is a simplified version of the query() function. Internally* we only use it when running transaction commands since they do* not require all the features of the main query() function.** @access public* @param string the sql query* @return mixed*/function simple_query($sql){if ( ! $this->conn_id){$this->initialize();}return $this->_execute($sql);}// --------------------------------------------------------------------/*** Disable Transactions* This permits transactions to be disabled at run-time.** @access public* @return void*/function trans_off(){$this->trans_enabled = FALSE;}// --------------------------------------------------------------------/*** Enable/disable Transaction Strict Mode* When strict mode is enabled, if you are running multiple groups of* transactions, if one group fails all groups will be rolled back.* If strict mode is disabled, each group is treated autonomously, meaning* a failure of one group will not affect any others** @access public* @return void*/function trans_strict($mode = TRUE){$this->trans_strict = is_bool($mode) ? $mode : TRUE;}// --------------------------------------------------------------------/*** Start Transaction** @access public* @return void*/function trans_start($test_mode = FALSE){if ( ! $this->trans_enabled){return FALSE;}// When transactions are nested we only begin/commit/rollback the outermost onesif ($this->_trans_depth > 0){$this->_trans_depth += 1;return;}$this->trans_begin($test_mode);}// --------------------------------------------------------------------/*** Complete Transaction** @access public* @return bool*/function trans_complete(){if ( ! $this->trans_enabled){return FALSE;}// When transactions are nested we only begin/commit/rollback the outermost onesif ($this->_trans_depth > 1){$this->_trans_depth -= 1;return TRUE;}// The query() function will set this flag to FALSE in the event that a query failedif ($this->_trans_status === FALSE){$this->trans_rollback();// If we are NOT running in strict mode, we will reset// the _trans_status flag so that subsequent groups of transactions// will be permitted.if ($this->trans_strict === FALSE){$this->_trans_status = TRUE;}log_message('debug', 'DB Transaction Failure');return FALSE;}$this->trans_commit();return TRUE;}// --------------------------------------------------------------------/*** Lets you retrieve the transaction flag to determine if it has failed** @access public* @return bool*/function trans_status(){return $this->_trans_status;}// --------------------------------------------------------------------/*** Compile Bindings** @access public* @param string the sql statement* @param array an array of bind data* @return string*/function compile_binds($sql, $binds){if (strpos($sql, $this->bind_marker) === FALSE){return $sql;}if ( ! is_array($binds)){$binds = array($binds);}// Get the sql segments around the bind markers$segments = explode($this->bind_marker, $sql);// The count of bind should be 1 less then the count of segments// If there are more bind arguments trim it downif (count($binds) >= count($segments)) {$binds = array_slice($binds, 0, count($segments)-1);}// Construct the binded query$result = $segments[0];$i = 0;foreach ($binds as $bind){$result .= $this->escape($bind);$result .= $segments[++$i];}return $result;}// --------------------------------------------------------------------/*** Determines if a query is a "write" type.** @access public* @param string An SQL query string* @return boolean*/function is_write_type($sql){if ( ! preg_match('/^\s*"?(SET|INSERT|UPDATE|DELETE|REPLACE|CREATE|DROP|LOAD DATA|COPY|ALTER|GRANT|REVOKE|LOCK|UNLOCK)\s+/i', $sql)){return FALSE;}return TRUE;}// --------------------------------------------------------------------/*** Calculate the aggregate query elapsed time** @access public* @param integer The number of decimal places* @return integer*/function elapsed_time($decimals = 6){return number_format($this->benchmark, $decimals);}// --------------------------------------------------------------------/*** Returns the total number of queries** @access public* @return integer*/function total_queries(){return $this->query_count;}// --------------------------------------------------------------------/*** Returns the last query that was executed** @access public* @return void*/function last_query(){return end($this->queries);}// --------------------------------------------------------------------/*** "Smart" Escape String** Escapes data based on type* Sets boolean and null types** @access public* @param string* @return integer*/function escape($str){switch (gettype($str)){case 'string' : $str = "'".$this->escape_str($str)."'";break;case 'boolean' : $str = ($str === FALSE) ? 0 : 1;break;default : $str = ($str === NULL) ? 'NULL' : $str;break;}return $str;}// --------------------------------------------------------------------/*** Primary** Retrieves the primary key. It assumes that the row in the first* position is the primary key** @access public* @param string the table name* @return string*/function primary($table = ''){$fields = $this->list_fields($table);if ( ! is_array($fields)){return FALSE;}return current($fields);}// --------------------------------------------------------------------/*** Returns an array of table names** @access public* @return array*/function list_tables($constrain_by_prefix = FALSE){// Is there a cached result?if (isset($this->data_cache['table_names'])){return $this->data_cache['table_names'];}if (FALSE === ($sql = $this->_list_tables($constrain_by_prefix))){if ($this->db_debug){return $this->display_error('db_unsupported_function');}return FALSE;}$retval = array();$query = $this->query($sql);if ($query->num_rows() > 0){foreach($query->result_array() as $row){if (isset($row['TABLE_NAME'])){$retval[] = $row['TABLE_NAME'];}else{$retval[] = array_shift($row);}}}$this->data_cache['table_names'] = $retval;return $this->data_cache['table_names'];}// --------------------------------------------------------------------/*** Determine if a particular table exists* @access public* @return boolean*/function table_exists($table_name){return ( ! in_array($this->_protect_identifiers($table_name, TRUE, FALSE, FALSE), $this->list_tables())) ? FALSE : TRUE;}// --------------------------------------------------------------------/*** Fetch MySQL Field Names** @access public* @param string the table name* @return array*/function list_fields($table = ''){// Is there a cached result?if (isset($this->data_cache['field_names'][$table])){return $this->data_cache['field_names'][$table];}if ($table == ''){if ($this->db_debug){return $this->display_error('db_field_param_missing');}return FALSE;}if (FALSE === ($sql = $this->_list_columns($this->_protect_identifiers($table, TRUE, NULL, FALSE)))){if ($this->db_debug){return $this->display_error('db_unsupported_function');}return FALSE;}$query = $this->query($sql);$retval = array();foreach($query->result_array() as $row){if (isset($row['COLUMN_NAME'])){$retval[] = $row['COLUMN_NAME'];}else{$retval[] = current($row);}}$this->data_cache['field_names'][$table] = $retval;return $this->data_cache['field_names'][$table];}// --------------------------------------------------------------------/*** Determine if a particular field exists* @access public* @param string* @param string* @return boolean*/function field_exists($field_name, $table_name){return ( ! in_array($field_name, $this->list_fields($table_name))) ? FALSE : TRUE;}// --------------------------------------------------------------------/*** Returns an object with field data** @access public* @param string the table name* @return object*/function field_data($table = ''){if ($table == ''){if ($this->db_debug){return $this->display_error('db_field_param_missing');}return FALSE;}$query = $this->query($this->_field_data($this->_protect_identifiers($table, TRUE, NULL, FALSE)));return $query->field_data();}// --------------------------------------------------------------------/*** Generate an insert string** @access public* @param string the table upon which the query will be performed* @param array an associative array data of key/values* @return string*/function insert_string($table, $data){$fields = array();$values = array();foreach($data as $key => $val){$fields[] = $this->_escape_identifiers($key);$values[] = $this->escape($val);}return $this->_insert($this->_protect_identifiers($table, TRUE, NULL, FALSE), $fields, $values);}// --------------------------------------------------------------------/*** Generate an update string** @access public* @param string the table upon which the query will be performed* @param array an associative array data of key/values* @param mixed the "where" statement* @return string*/function update_string($table, $data, $where){if ($where == ''){return false;}$fields = array();foreach($data as $key => $val){$fields[$this->_protect_identifiers($key)] = $this->escape($val);}if ( ! is_array($where)){$dest = array($where);}else{$dest = array();foreach ($where as $key => $val){$prefix = (count($dest) == 0) ? '' : ' AND ';if ($val !== ''){if ( ! $this->_has_operator($key)){$key .= ' =';}$val = ' '.$this->escape($val);}$dest[] = $prefix.$key.$val;}}return $this->_update($this->_protect_identifiers($table, TRUE, NULL, FALSE), $fields, $dest);}// --------------------------------------------------------------------/*** Tests whether the string has an SQL operator** @access private* @param string* @return bool*/function _has_operator($str){$str = trim($str);if ( ! preg_match("/(\s|<|>|!|=|is null|is not null)/i", $str)){return FALSE;}return TRUE;}// --------------------------------------------------------------------/*** Enables a native PHP function to be run, using a platform agnostic wrapper.** @access public* @param string the function name* @param mixed any parameters needed by the function* @return mixed*/function call_function($function){$driver = ($this->dbdriver == 'postgre') ? 'pg_' : $this->dbdriver.'_';if (FALSE === strpos($driver, $function)){$function = $driver.$function;}if ( ! function_exists($function)){if ($this->db_debug){return $this->display_error('db_unsupported_function');}return FALSE;}else{$args = (func_num_args() > 1) ? array_splice(func_get_args(), 1) : null;return call_user_func_array($function, $args);}}// --------------------------------------------------------------------/*** Set Cache Directory Path** @access public* @param string the path to the cache directory* @return void*/function cache_set_path($path = ''){$this->cachedir = $path;}// --------------------------------------------------------------------/*** Enable Query Caching** @access public* @return void*/function cache_on(){$this->cache_on = TRUE;return TRUE;}// --------------------------------------------------------------------/*** Disable Query Caching** @access public* @return void*/function cache_off(){$this->cache_on = FALSE;return FALSE;}// --------------------------------------------------------------------/*** Delete the cache files associated with a particular URI** @access public* @return void*/function cache_delete($segment_one = '', $segment_two = ''){if ( ! $this->_cache_init()){return FALSE;}return $this->CACHE->delete($segment_one, $segment_two);}// --------------------------------------------------------------------/*** Delete All cache files** @access public* @return void*/function cache_delete_all(){if ( ! $this->_cache_init()){return FALSE;}return $this->CACHE->delete_all();}// --------------------------------------------------------------------/*** Initialize the Cache Class** @access private* @return void*/function _cache_init(){if (is_object($this->CACHE) AND class_exists('CI_DB_Cache')){return TRUE;}if ( ! @include(BASEPATH.'database/DB_cache'.EXT)){return $this->cache_off();}$this->CACHE = new CI_DB_Cache($this); // pass db object to support multiple db connections and returned db objectsreturn TRUE;}// --------------------------------------------------------------------/*** Close DB Connection** @access public* @return void*/function close(){if (is_resource($this->conn_id) OR is_object($this->conn_id)){$this->_close($this->conn_id);}$this->conn_id = FALSE;}// --------------------------------------------------------------------/*** Display an error message** @access public* @param string the error message* @param string any "swap" values* @param boolean whether to localize the message* @return string sends the application/error_db.php template*/function display_error($error = '', $swap = '', $native = FALSE){$LANG =& load_class('Language');$LANG->load('db');$heading = $LANG->line('db_error_heading');if ($native == TRUE){$message = $error;}else{$message = ( ! is_array($error)) ? array(str_replace('%s', $swap, $LANG->line($error))) : $error;}$error =& load_class('Exceptions');echo $error->show_error($heading, $message, 'error_db');exit;}// --------------------------------------------------------------------/*** Protect Identifiers** This function adds backticks if appropriate based on db type** @access private* @param mixed the item to escape* @return mixed the item with backticks*/function protect_identifiers($item, $prefix_single = FALSE){return $this->_protect_identifiers($item, $prefix_single);}// --------------------------------------------------------------------/*** Protect Identifiers** This function is used extensively by the Active Record class, and by* a couple functions in this class.* It takes a column or table name (optionally with an alias) and inserts* the table prefix onto it. Some logic is necessary in order to deal with* column names that include the path. Consider a query like this:** SELECT * FROM hostname.database.table.column AS c FROM hostname.database.table** Or a query with aliasing:** SELECT m.member_id, m.member_name FROM members AS m** Since the column name can include up to four segments (host, DB, table, column)* or also have an alias prefix, we need to do a bit of work to figure this out and* insert the table prefix (if it exists) in the proper position, and escape only* the correct identifiers.** @access private* @param string* @param bool* @param mixed* @param bool* @return string*/function _protect_identifiers($item, $prefix_single = FALSE, $protect_identifiers = NULL, $field_exists = TRUE){if ( ! is_bool($protect_identifiers)){$protect_identifiers = $this->_protect_identifiers;}// Convert tabs or multiple spaces into single spaces$item = preg_replace('/[\t| ]+/', ' ', $item);// If the item has an alias declaration we remove it and set it aside.// Basically we remove everything to the right of the first space$alias = '';if (strpos($item, ' ') !== FALSE){$alias = strstr($item, " ");$item = substr($item, 0, - strlen($alias));}// Break the string apart if it contains periods, then insert the table prefix// in the correct location, assuming the period doesn't indicate that we're dealing// with an alias. While we're at it, we will escape the componentsif (strpos($item, '.') !== FALSE){$parts = explode('.', $item);// Does the first segment of the exploded item match// one of the aliases previously identified? If so,// we have nothing more to do other then escape the itemif (in_array($parts[0], $this->ar_aliased_tables)){if ($protect_identifiers === TRUE){foreach ($parts as $key => $val){if ( ! in_array($val, $this->_reserved_identifiers)){$parts[$key] = $this->_escape_identifiers($val);}}$item = implode('.', $parts);}return $item.$alias;}// Is there a table prefix defined in the config file? If not, no need to do anythingif ($this->dbprefix != ''){// We now add the table prefix based on some logic.// Do we have 4 segments (hostname.database.table.column)?// If so, we add the table prefix to the column name in the 3rd segment.if (isset($parts[3])){$i = 2;}// Do we have 3 segments (database.table.column)?// If so, we add the table prefix to the column name in 2nd positionelseif (isset($parts[2])){$i = 1;}// Do we have 2 segments (table.column)?// If so, we add the table prefix to the column name in 1st segmentelse{$i = 0;}// This flag is set when the supplied $item does not contain a field name.// This can happen when this function is being called from a JOIN.if ($field_exists == FALSE){$i++;}// We only add the table prefix if it does not already existif (substr($parts[$i], 0, strlen($this->dbprefix)) != $this->dbprefix){$parts[$i] = $this->dbprefix.$parts[$i];}// Put the parts back together$item = implode('.', $parts);}if ($protect_identifiers === TRUE){$item = $this->_escape_identifiers($item);}return $item.$alias;}// This is basically a bug fix for queries that use MAX, MIN, etc.// If a parenthesis is found we know that we do not need to// escape the data or add a prefix. There's probably a more graceful// way to deal with this, but I'm not thinking of it -- Rickif (strpos($item, '(') !== FALSE){return $item.$alias;}// Is there a table prefix? If not, no need to insert itif ($this->dbprefix != ''){// Do we prefix an item with no segments?if ($prefix_single == TRUE AND substr($item, 0, strlen($this->dbprefix)) != $this->dbprefix){$item = $this->dbprefix.$item;}}if ($protect_identifiers === TRUE AND ! in_array($item, $this->_reserved_identifiers)){$item = $this->_escape_identifiers($item);}return $item.$alias;}}/* End of file DB_driver.php *//* Location: ./system/database/DB_driver.php */