<?php/**
* PDO-based database layer
*
* @see http://www.php.net/manual/en/book.pdo.php
*
* @package API - Database
* @copyright (c) Cotonti Team
* @license https://github.com/Cotonti/Cotonti/blob/master/License.txt
*/defined('COT_CODE') or die('Wrong URL');/**
* Cotonti Database Connection class.
* A compact extension to standard PHP PDO class with slight Cotonti-specific needs,
* handy functions and query builder.
*
* @see http://www.php.net/manual/en/class.pdo.php
*
* @property-read int $affectedRows Number of rows affected by the most recent query
* @property-read int $count Total query count
* @property-read int $timeCount Total query execution time
*/class CotDB extends PDO {/**
* Number of rows affected by the most recent query
* @var int
*/private$_affected_rows=0;/**
* Total query count
* @var int
*/private$_count=0;/**
* Prepare statements by itself. Used with MySQL client API versions prior to 5.1
* @var bool
*/private$_prepare_itself=false;/**
* Total query execution time
* @var int
*/private$_tcount=0;/**
* Timer start microtime
* @var string
*/private$_xtime=0;/**
* Table names registry
* @var array
*/private$_tables=array();/**
* Creates a PDO instance to represent a connection to the requested database.
*
* @param string $dsn The Data Source Name, or DSN, contains the information required to connect to the database.
* @param string $username The user name for the DSN string.
* @param string $passwd The password for the DSN string.
* @param array $options A key=>value array of driver-specific connection options.
* @see http://www.php.net/manual/en/pdo.construct.php
*/publicfunction __construct($dsn,$username,$passwd,$options=array()){global$cfg;if(!empty($cfg['mysqlcharset'])&&version_compare(PHP_VERSION,'5.3.0','!=')){$collation_query="SET NAMES '{$cfg['mysqlcharset']}'";if(!empty($cfg['mysqlcollate'])){$collation_query.=" COLLATE '{$cfg['mysqlcollate']}'";}$options[PDO::MYSQL_ATTR_INIT_COMMAND]=$collation_query;} parent::__construct($dsn,$username,$passwd,$options);$this->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);if(!method_exists($this,'prepare')){$this->_prepare_itself =true;}}/**
* Provides access to properties
* @param string $name Property name
* @return mixed Property value
*/publicfunction __get($name){switch($name){case'affectedRows':return$this->_affected_rows;break;case'count':return$this->_count;break;case'timeCount':return$this->_tcount;break;default:returnisset($this->_tables[$name]) ? $this->_tables[$name]:null;}}/**
* Binds parameters to a statement
*
* @param PDOStatement $statement PDO statement
* @param array $parameters Array of parameters, numeric or associative
*/privatefunction _bindParams($statement,$parameters){$is_numeric=is_int(key($parameters));foreach($parametersas$key=>$val){$type=is_int($val) ? PDO::PARAM_INT: PDO::PARAM_STR;$is_numeric ? $statement->bindValue($key+1,$val,$type):$statement->bindValue($key,$val,$type);}}/**
* Parses PDO exception message and returns its components and status
*
* @param PDOException $e PDO Exception
* @param string $err_code Output error code parameter
* @param string $err_message Output error message parameter
* @return bool TRUE for error cases, FALSE for notifications and warnings
*/privatefunction _parseError(PDOException $e,&$err_code,&$err_message){$pdo_message=$e->getMessage();if(preg_match('#SQLSTATE\[(\w+)\].*?: (.*)#',$pdo_message,$matches)){$err_code=$matches[1];$err_message=$matches[2];}else{$err_code=$e->getCode();$err_message=$pdo_message;}return$err_code>'02';}/**
* Prepares a parametrized query on client side
*
* @param string $query Query being prepared
* @param array $parameters Associative or numeric array of parameters
* @return string Array with placeholders substituted
*/privatefunction _prepare($query,$parameters=array()){if(count($parameters)>0){foreach($parametersas$key=>$val){$placeholder=is_int($key) ? '?':':'.$key;$value=is_int($val) ? $val:$this->quote($val);$pos=strpos($query,$placeholder);if($pos!==false){$query=substr_replace($query,$value,$pos,strlen($placeholder));}}}return$query;}/**
* Starts query execution timer
*/privatefunction _startTimer(){global$cfg;$this->_count++;if($cfg['showsqlstats']||$cfg['debug_mode']){$this->_xtime =microtime();}}/**
* Stops query execution timer
*/privatefunction _stopTimer($query){global$cfg,$sys;if($cfg['showsqlstats']||$cfg['debug_mode']){$ytime=microtime();$xtime=explode(' ',$this->_xtime);$ytime=explode(' ',$ytime);$this->_tcount +=$ytime[1]+$ytime[0]-$xtime[1]-$xtime[0];if($cfg['devmode']||$cfg['debug_mode']){$calls='';$bt=debug_backtrace();for($i=sizeof($bt)-1;$i>0;$i--){$call=(($bt[$i]['object']&&$bt[$i]['class']) ? $bt[$i]['class'].$bt[$i]['type']:'').$bt[$i]['function'].'();';$calls.=(empty($calls)?'':"\n → ").basename($bt[$i]['file']).' ['.$bt[$i]['line'].']: '.$call;}$sys['devmode']['queries'][]=array($this->_count,$ytime[1]+$ytime[0]-$xtime[1]-$xtime[0],$query,$calls);$sys['devmode']['timeline'][]=$xtime[1]+$xtime[0]-$sys['starttime'];}}}/**
* Returns total number of records contained in a table
* @param string $table_name Table name
* @return int
*/publicfunction countRows($table_name){return$this->query("SELECT COUNT(*) FROM `$table_name`")->fetchColumn();}/**
* Performs simple SQL DELETE query and returns number of removed items.
*
* @param string $table_name Table name
* @param string $condition Body of WHERE clause
* @param array $parameters Array of statement input parameters, see http://www.php.net/manual/en/pdostatement.execute.php
* @return int Number of records removed on success or FALSE on error
*/publicfunction delete($table_name,$condition='',$parameters=array()){$query=empty($condition) ? "DELETE FROM `$table_name`":"DELETE FROM `$table_name` WHERE $condition";if(!is_array($parameters)){$parameters=array($parameters);}$this->_startTimer(); try
{if(count($parameters)>0){if($this->_prepare_itself){$res=$this->exec($this->_prepare($query,$parameters));}else{$stmt=$this->prepare($query);$this->_bindParams($stmt,$parameters);$stmt->execute();$res=$stmt->rowCount();}}else{$res=$this->exec($query);}} catch (PDOException $err){if($this->_parseError($err,$err_code,$err_message)){ cot_diefatal('SQL error '.$err_code.': '.$err_message);}}$this->_stopTimer($query);return$res;}/**
* Checks if a field exists in a table
*
* @param string $table_name Table name
* @param string $field_name Field name
* @return bool TRUE if the field exists, FALSE otherwise
*/function fieldExists($table_name,$field_name){return$this->query("SHOW COLUMNS FROM `$table_name` WHERE Field = ".$this->quote($field_name))->rowCount()==1;}/**
* Checks if an index with the same index name or column order exists
*
* @param string $table_name Table name
* @param string $index_name Index/Key name
* @param mixed $index_columns Either a string for a single column name or an array for single/multiple columns. No column check will be preformed if left empty.
* @return bool TRUE if the index name or column order exists, FALSE otherwise
*/function indexExists($table_name,$index_name,$index_columns=array()){if(empty($index_columns)){return(bool)$this->query("SHOW INDEXES FROM `$table_name` WHERE Key_name=".$this->quote($index_name))->rowCount();}$existing_indexes=$this->query("SHOW INDEXES FROM `$table_name`")->fetchAll();if(!empty($index_columns)&&!is_array($index_columns)){$index_columns=array($index_columns);}$exists=false;$index_list=array();foreach($existing_indexesas$existing_index){$index_list[$existing_index['Key_name']][$existing_index['Seq_in_index']-1]=$existing_index['Column_name'];}foreach($index_listas$list_index=>$list_columns){if($list_index==$index_name){$exists=true;break;}if(count(array_diff_assoc($index_columns,$list_columns))===0&&count($index_columns)===count($list_columns)){$exists=true;break;}}return$exists;}/**
* Adds an index on a table
*
* @param string $table_name Table name
* @param string $index_name Index/Key name
* @param mixed Either a string for a single column name or an array for single/multiple columns. $index_name will be used if empty.
* @return int Number of rows affected
*/function addIndex($table_name,$index_name,$index_columns=array()){if(empty($index_columns)){$index_columns=array($index_name);}if(!is_array($index_columns)){$index_columns=array($index_columns);}return$this->query("ALTER TABLE `$table_name` ADD INDEX `$index_name` (`".implode('`,`',$index_columns)."`)")->rowCount();}/**
* Performs SQL INSERT on simple data array. Array keys must match table keys, optionally you can specify
* key prefix as third parameter. Strings get quoted and escaped automatically.
* Ints and floats must be typecasted.
* You can use special values in the array:
* - PHP NULL => SQL NULL
* - 'NOW()' => SQL NOW()
* Performs single row INSERT if $data is an associative array,
* performs multi-row INSERT if $data is a 2D array (numeric => assoc)
*
* @param string $table_name Table name
* @param array $data Associative or 2D array containing data for insertion.
* @param bool $insert_null Insert SQL NULL for empty values rather than ignoring them.
* @param bool $ignore Ignore duplicate key errors on insert
* @param array $update_fields List of fields to be updated with ON DUPLICATE KEY UPDATE
* @return int The number of affected records
*/publicfunction insert($table_name,$data,$insert_null=false,$ignore=false,$update_fields=array()){if(!is_array($data)){return0;}$keys='';$vals='';// Check the array type$arr_keys=array_keys($data);$multiline=is_numeric($arr_keys[0]);// Build the queryif($multiline){$rowset=&$data;}else{$rowset=array($data);}$keys_built=false;$cnt=count($rowset);for($i=0;$i<$cnt;$i++){$vals.=($i>0) ? ',(':'(';$j=0;if(is_array($rowset[$i])){foreach($rowset[$i]as$key=>$val){if(is_null($val)&&!$insert_null){continue;}if($j>0)$vals.=',';if(!$keys_built){if($j>0)$keys.=',';$keys.="`$key`";}if(is_null($val)||$val==='NULL'){$vals.='NULL';}elseif(is_bool($val)){$vals.=$val ? 'TRUE':'FALSE';}elseif($val==='NOW()'){$vals.='NOW()';}elseif(is_int($val)||is_float($val)){$vals.=$val;}else{$vals.=$this->quote($val);}$j++;}}$vals.=')';$keys_built=true;}if(!empty($keys)&&!empty($vals)){$ignore=$ignore ? 'IGNORE':'';$query="INSERT $ignore INTO `$table_name` ($keys) VALUES $vals";if(count($update_fields)>0){$query.=' ON DUPLICATE KEY UPDATE';$j=0;foreach($update_fieldsas$key){if($j>0)$query.=',';$query.=" `$key` = VALUES(`$key`)";$j++;}}$this->_startTimer(); try
{$res=$this->exec($query);} catch (PDOException $err){if($this->_parseError($err,$err_code,$err_message)){ cot_diefatal('SQL error '.$err_code.': '.$err_message);}}$this->_stopTimer($query);return$res;}return0;}/**
* Prepares a param for use in SQL query without wrapping it with quotes
* @param string $str Param string
* @return string Escaped param
*/publicfunction prep($str){returnpreg_replace("#^'(.*)'\$#",'$1',$this->quote($str));}/**
* Registers an unprefixed table name in table names registry
* @param string $table_name Table name without a prefix, e.g. 'pages'
*/publicfunction registerTable($table_name){if(!isset($GLOBALS['db_'.$table_name])){$GLOBALS['db_'.$table_name]=$GLOBALS['db_x'].$table_name;}$this->_tables[$table_name]=$GLOBALS['db_'.$table_name];}/**
* Runs an SQL script containing multiple queries.
*
* @param string $script SQL script body, containing formatted queries separated by semicolons and newlines
* @param resource $conn Custom connection handle
* @return string Error message if an error occurs or empty string on success
*/publicfunction runScript($script){global$db_x;// Remove comments$script=preg_replace('#^/\*.*?\*/#m','',$script);$script=preg_replace('#^--.*?$#m','',$script);// Run queries separated by ; at the end of line$queries=preg_split('#;\r?\n#',$script);foreach($queriesas$query){$query=trim($query);if(!empty($query)){if($db_x!='cot_'&&preg_match('#`cot_(\w+)`#',$query,$mt)){if(isset($GLOBALS['db_'.$mt[1]])){$table_name=$GLOBALS['db_'.$mt[1]];$query=str_replace($mt[0],"`$table_name`",$query);}else{$query=str_replace('`cot_','`'.$db_x,$query);}}$result=$this->query($query);if(!$result){return$this->error.'<br />'.htmlspecialchars($query).'<hr />';}elseif($result instanceof PDOStatement){$result->closeCursor();}}}return'';}/**
* 1) If called with one parameter:
* Works like PDO::query()
* Executes an SQL statement in a single function call, returning the result set (if any) returned by the statement as a PDOStatement object.
* 2) If called with second parameter as array of input parameter bindings:
* Works like PDO::prepare()->execute()
* Prepares an SQL statement and executes it.
* @see http://www.php.net/manual/en/pdo.query.php
* @see http://www.php.net/manual/en/pdo.prepare.php
* @param string $query The SQL statement to prepare and execute.
* @param array $parameters An array of values to be binded as input parameters to the query. PHP int parameters will beconsidered as PDO::PARAM_INT, others as PDO::PARAM_STR.
* @return PDOStatement
*/publicfunction query($query,$parameters=array()){if(!is_array($parameters)){$parameters=array($parameters);}$this->_startTimer(); try
{if(count($parameters)>0){if($this->_prepare_itself){$result= parent::query($this->_prepare($query,$parameters));}else{$result= parent::prepare($query);$this->_bindParams($result,$parameters);$result->execute();}}else{$result= parent::query($query);}} catch (PDOException $err){if($this->_parseError($err,$err_code,$err_message)){ cot_diefatal('SQL error '.$err_code.': '.$err_message);}}$this->_stopTimer($query);// In Cotonti we use PDO::FETCH_ASSOC by default to save memory$result->setFetchMode(PDO::FETCH_ASSOC);$this->_affected_rows =$result->rowCount();return$result;}/**
* Performs SQL UPDATE with simple data array. Array keys must match table keys, optionally you can specify
* key prefix as fourth parameter. Strings get quoted and escaped automatically.
* Ints and floats must be typecasted.
* You can use special values in the array:
* - PHP NULL => SQL NULL
* - 'NOW()' => SQL NOW()
*
* @param string $table_name Table name
* @param array $data Associative array containing data for update
* @param string $condition Body of SQL WHERE clause
* @param array $parameters Array of statement input parameters, see http://www.php.net/manual/en/pdostatement.execute.php
* @param bool $update_null Nullify cells which have null values in the array. By default they are skipped
* @return int The number of affected records or FALSE on error
*/publicfunction update($table_name,$data,$condition='',$parameters=array(),$update_null=false){if(!is_array($data)){return0;}$upd='';if(!is_array($parameters)){$parameters=array($parameters);}if($this->_prepare_itself &&!empty($condition)&&count($parameters)>0){$condition=$this->_prepare($condition,$parameters);$parameters=array();}$condition=empty($condition) ? '':'WHERE '.$condition;foreach($dataas$key=>$val){if(is_null($val)&&!$update_null){continue;}$upd.="`$key`=";if(is_null($val)||$val==='NULL'){$upd.='NULL,';}elseif(is_bool($val)){$upd.=$val ? 'TRUE,':'FALSE,';}elseif($val==='NOW()'){$upd.='NOW(),';}elseif(is_int($val)||is_float($val)){$upd.=$val.',';}else{$upd.=$this->quote($val).',';}}if(!empty($upd)){$upd=mb_substr($upd,0,-1);$query="UPDATE `$table_name` SET $upd$condition";$this->_startTimer(); try
{if(count($parameters)>0){$stmt=$this->prepare($query);$this->_bindParams($stmt,$parameters);$stmt->execute();$res=$stmt->rowCount();}else{$res=$this->exec($query);}} catch (PDOException $err){if($this->_parseError($err,$err_code,$err_message)){ cot_diefatal('SQL error '.$err_code.': '.$err_message);}}$this->_stopTimer($query);return$res;}return0;}}