mirror of
https://gitlab.com/davical-project/davical.git
synced 2026-01-27 00:33:34 +00:00
478 lines
14 KiB
PHP
478 lines
14 KiB
PHP
<?php
|
|
/**
|
|
* @package awl
|
|
* @subpackage AWLDB
|
|
* @author Andrew McMillan <andrew@morphoss.com>
|
|
* @copyright Morphoss Ltd
|
|
* @license http://gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
|
* @compatibility Requires PHP 5.1 or later
|
|
*/
|
|
|
|
require_once('AwlDatabase.php');
|
|
|
|
/**
|
|
* Database query class and associated functions
|
|
*
|
|
* This subpackage provides some functions that are useful around database
|
|
* activity and an AwlQuery class to simplify handling of database queries.
|
|
*
|
|
* The class is intended to be a very lightweight wrapper with no pretentions
|
|
* towards database independence, but it does include some features that have
|
|
* proved useful in developing and debugging web-based applications:
|
|
* - All queries are timed, and an expected time can be provided.
|
|
* - Parameters replaced into the SQL will be escaped correctly in order to
|
|
* minimise the chances of SQL injection errors.
|
|
* - Queries which fail, or which exceed their expected execution time, will
|
|
* be logged for potential further analysis.
|
|
* - Debug logging of queries may be enabled globally, or restricted to
|
|
* particular sets of queries.
|
|
* - Simple syntax for iterating through a result set.
|
|
*
|
|
* This class is intended as a transitional mechanism for moving from the
|
|
* PostgreSQL-specific PgQuery class to something which uses PDO in a more
|
|
* replaceable manner.
|
|
*
|
|
*/
|
|
|
|
/**
|
|
* Connect to the database defined in the $c->db_connect[] (or $c->pg_connect) arrays
|
|
*/
|
|
function _awl_connect_configured_database() {
|
|
global $c, $_awl_dbconn;
|
|
|
|
/**
|
|
* Attempt to connect to the configured connect strings
|
|
*/
|
|
$_awl_dbconn = false;
|
|
|
|
if ( isset($c->db_connect) ) {
|
|
$connection_strings = $c->db_connect;
|
|
}
|
|
elseif ( isset($c->pg_connect) ) {
|
|
$connection_strings = $c->pg_connect;
|
|
}
|
|
|
|
foreach( $connection_strings AS $k => $v ) {
|
|
$dbuser = null;
|
|
$dbpass = null;
|
|
if ( is_array($v) ) {
|
|
$dsn = $v['dsn'];
|
|
if ( isset($v['dbuser']) ) $dbuser = $v['dbuser'];
|
|
if ( isset($v['dbpass']) ) $dbpass = $v['dbpass'];
|
|
}
|
|
elseif ( preg_match( '/^(\S+:)?(.*)( user=(\S+))?( password=(\S+))?$/', $v, $matches ) ) {
|
|
$dsn = $matches[2];
|
|
if ( isset($matches[1]) && $matches[1] != '' ) {
|
|
$dsn = $matches[1] . $dsn;
|
|
}
|
|
else {
|
|
$dsn = 'pgsql:' . $dsn;
|
|
}
|
|
if ( isset($matches[4]) && $matches[4] != '' ) $dbuser = $matches[4];
|
|
if ( isset($matches[6]) && $matches[6] != '' ) $dbpass = $matches[6];
|
|
}
|
|
if ( $_awl_dbconn = new AwlDatabase( $dsn, $dbuser, $dbpass ) ) break;
|
|
}
|
|
|
|
if ( ! $_awl_dbconn ) {
|
|
echo <<<EOERRMSG
|
|
<html><head><title>Database Connection Failure</title></head><body>
|
|
<h1>Database Error</h1>
|
|
<h3>Could not connect to database</h3>
|
|
</body>
|
|
</html>
|
|
EOERRMSG;
|
|
exit;
|
|
}
|
|
|
|
if ( isset($c->db_schema) && $c->db_schema != '' ) {
|
|
$_awl_dbconn->SetSearchPath( $c->db_schema . ',public' );
|
|
}
|
|
|
|
$c->_awl_dbversion = $_awl_dbconn->GetVersion();
|
|
}
|
|
|
|
|
|
if ( !function_exists('duration') ) {
|
|
/**
|
|
* A duration (in decimal seconds) between two times which are the result of calls to microtime()
|
|
*
|
|
* This simple function is used by the AwlQuery class because the
|
|
* microtime function doesn't return a decimal time, so a simple
|
|
* subtraction is not sufficient.
|
|
*
|
|
* @param microtime $t1 start time
|
|
* @param microtime $t2 end time
|
|
* @return double difference
|
|
*/
|
|
function duration( $t1, $t2 ) {
|
|
list ( $ms1, $s1 ) = explode ( " ", $t1 ); // Format times - by spliting seconds and microseconds
|
|
list ( $ms2, $s2 ) = explode ( " ", $t2 );
|
|
$s1 = $s2 - $s1;
|
|
$s1 = $s1 + ( $ms2 -$ms1 );
|
|
return $s1; // Return duration of time
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* The AwlQuery Class.
|
|
*
|
|
* This class builds and executes SQL Queries and traverses the
|
|
* set of results returned from the query.
|
|
*
|
|
* <b>Example usage</b>
|
|
* <code>
|
|
* $sql = "SELECT * FROM mytable WHERE mytype = ?";
|
|
* $qry = new AwlQuery( $sql, $myunsanitisedtype );
|
|
* if ( $qry->Exec("typeselect", __line__, __file__ )
|
|
* && $qry->rows > 0 )
|
|
* {
|
|
* while( $row = $qry->Fetch() ) {
|
|
* do_something_with($row);
|
|
* }
|
|
* }
|
|
* </code>
|
|
*
|
|
* @package awl
|
|
*/
|
|
class AwlQuery
|
|
{
|
|
/**#@+
|
|
* @access private
|
|
*/
|
|
/**
|
|
* Our database connection, normally copied from a global one
|
|
* @var resource
|
|
*/
|
|
protected $connection;
|
|
|
|
/**
|
|
* The original query string
|
|
* @var string
|
|
*/
|
|
protected $querystring;
|
|
|
|
/**
|
|
* The current array of bound parameters
|
|
* @var array
|
|
*/
|
|
protected $bound_parameters;
|
|
|
|
/**
|
|
* The PDO statement handle, or null if we don't have one yet.
|
|
* @var string
|
|
*/
|
|
protected $sth;
|
|
|
|
/**
|
|
* Result of the last execution
|
|
* @var resource
|
|
*/
|
|
protected $result;
|
|
|
|
/**
|
|
* number of current row - use accessor to get/set
|
|
* @var int
|
|
*/
|
|
protected $rownum = null;
|
|
|
|
/**
|
|
* number of rows from pg_numrows - use accessor to get value
|
|
* @var int
|
|
*/
|
|
protected $rows;
|
|
|
|
/**
|
|
* The Database error information, if the query fails.
|
|
* @var string
|
|
*/
|
|
protected $error_info;
|
|
|
|
/**
|
|
* Stores the query execution time - used to deal with long queries.
|
|
* should be read-only
|
|
* @var string
|
|
*/
|
|
protected $execution_time;
|
|
|
|
/**#@-*/
|
|
|
|
/**#@+
|
|
* @access public
|
|
*/
|
|
/**
|
|
* Where we called this query from so we can find it in our code!
|
|
* Debugging may also be selectively enabled for a $location.
|
|
* @var string
|
|
*/
|
|
public $location;
|
|
|
|
/**
|
|
* How long the query should take before a warning is issued.
|
|
*
|
|
* This is writable, but a method to set it might be a better interface.
|
|
* The default is 0.3 seconds.
|
|
* @var double
|
|
*/
|
|
public $query_time_warning = 0.3;
|
|
/**#@-*/
|
|
|
|
|
|
/**
|
|
* Constructor
|
|
* @param string The query string in PDO syntax with replacable '?' characters or bindable parameters.
|
|
* @param mixed The values to replace into the SQL string.
|
|
* @return The AwlQuery object
|
|
*/
|
|
function __construct() {
|
|
global $_awl_dbconn;
|
|
$this->rows = null;
|
|
$this->execution_time = 0;
|
|
$this->error_info = null;
|
|
$this->rownum = -1;
|
|
if ( isset($_awl_dbconn) ) $this->connection = $_awl_dbconn;
|
|
else $this->connection = null;
|
|
|
|
$argc = func_num_args();
|
|
$args = func_get_args();
|
|
|
|
$this->querystring = array_shift($args);
|
|
if ( 1 < $argc ) {
|
|
if ( is_array($args[0]) )
|
|
$this->Bind($args[0]);
|
|
else
|
|
$this->Bind($args);
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
|
|
/**
|
|
* Use a different database connection for this query
|
|
* @param resource $new_connection The database connection to use.
|
|
*/
|
|
function SetConnection( $new_connection ) {
|
|
$this->connection = $new_connection;
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Log query, optionally with file and line location of the caller.
|
|
*
|
|
* This function should not really be used outside of AwlQuery. For a more
|
|
* useful generic logging interface consider calling dbg_error_log(...);
|
|
*
|
|
* @param string $locn A string identifying the calling location.
|
|
* @param string $tag A tag string, e.g. identifying the type of event.
|
|
* @param string $string The information to be logged.
|
|
* @param int $line The line number where the logged event occurred.
|
|
* @param string $file The file name where the logged event occurred.
|
|
*/
|
|
function _log_query( $locn, $tag, $string, $line = 0, $file = "") {
|
|
// replace more than one space with one space
|
|
$string = preg_replace('/\s+/', ' ', $string);
|
|
|
|
if ( ($tag == 'QF' || $tag == 'SQ') && ( $line != 0 && $file != "" ) ) {
|
|
dbg_error_log( "LOG-$locn", " Query: %s: %s in '%s' on line %d", ($tag == 'QF' ? 'Error' : 'Possible slow query'), $tag, $file, $line );
|
|
}
|
|
|
|
while( strlen( $string ) > 0 ) {
|
|
dbg_error_log( "LOG-$locn", " Query: %s: %s", $tag, substr( $string, 0, 240) );
|
|
$string = substr( "$string", 240 );
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Quote the given string so it can be safely used within string delimiters
|
|
* in a query. To be avoided, in general.
|
|
*
|
|
* @param mixed $str Data to be converted to a string suitable for including as a value in SQL.
|
|
* @return string NULL, TRUE, FALSE, a plain number, or the original string quoted and with ' and \ characters escaped
|
|
*/
|
|
function quote($str = null) {
|
|
if ( !isset($this->connection) ) {
|
|
_awl_connect_configured_database();
|
|
$this->connection = $GLOBALS['_awl_dbconn'];
|
|
}
|
|
return $this->connection->Quote($str);
|
|
}
|
|
|
|
|
|
/**
|
|
* Bind some parameters
|
|
*/
|
|
function Bind() {
|
|
$args = func_get_args();
|
|
|
|
if ( gettype($args[0]) == 'array' ) {
|
|
$this->bound_parameters = $args[0];
|
|
/** @TODO: perhaps we should WARN here if there is more than 1 argument */
|
|
}
|
|
else {
|
|
$this->bound_parameters = $args;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Tell the database to prepare the query that we will execute
|
|
*/
|
|
function Prepare() {
|
|
if ( !isset($this->connection) ) {
|
|
_awl_connect_configured_database();
|
|
$this->connection = $GLOBALS['_awl_dbconn'];
|
|
}
|
|
$this->sth = $this->connection->prepare( $this->querystring );
|
|
if ( ! $this->sth ) {
|
|
$this->error_info = $this->connection->errorInfo();
|
|
}
|
|
else $this->error_info = null;
|
|
}
|
|
|
|
|
|
/**
|
|
* Return the query string we are planning to execute
|
|
*/
|
|
function QueryString() {
|
|
return $this->querystring;
|
|
}
|
|
|
|
|
|
/**
|
|
* Return the parameters we are planning to substitute into the query string
|
|
*/
|
|
function Parameters() {
|
|
return $this->bound_parameters;
|
|
}
|
|
|
|
|
|
/**
|
|
* Return the count of rows retrieved/affected
|
|
*/
|
|
function rows() {
|
|
return $this->rows;
|
|
}
|
|
|
|
|
|
/**
|
|
* Execute the query, logging any debugging.
|
|
*
|
|
* <b>Example</b>
|
|
* So that you can nicely enable/disable the queries for a particular class, you
|
|
* could use some of PHPs magic constants in your call.
|
|
* <code>
|
|
* $qry->Exec(__CLASS__, __LINE__, __FILE__);
|
|
* </code>
|
|
*
|
|
*
|
|
* @param string $location The name of the location for enabling debugging or just
|
|
* to help our children find the source of a problem.
|
|
* @param int $line The line number where Exec was called
|
|
* @param string $file The file where Exec was called
|
|
* @return resource The actual result of the query (FWIW)
|
|
*/
|
|
function Exec( $location = '', $line = 0, $file = '' ) {
|
|
global $debuggroups, $c;
|
|
$this->location = trim($location);
|
|
if ( $this->location == "" ) $this->location = substr($_SERVER['PHP_SELF'],1);
|
|
|
|
if ( isset($debuggroups['querystring']) || isset($c->dbg['querystring']) || isset($c->dbg['ALL']) ) {
|
|
$this->_log_query( $this->location, 'DBGQ', $this->querystring, $line, $file );
|
|
if ( isset($this->bound_parameters) && !isset($this->sth) ) {
|
|
foreach( $this->bound_parameters AS $k => $v ) {
|
|
$this->_log_query( $this->location, 'DBGQ', sprintf(' "%s" => "%s"', $k, $v), $line, $file );
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( isset($this->bound_parameters) && !isset($this->sth) ) {
|
|
$this->Prepare();
|
|
}
|
|
|
|
|
|
$success = true;
|
|
$t1 = microtime(true); // get start time
|
|
if ( isset($this->sth) && $this->sth !== false ) {
|
|
if ( ! $this->sth->execute( $this->bound_parameters ) ) {
|
|
$this->error_info = $this->sth->errorInfo();
|
|
$success = false;
|
|
}
|
|
else $this->error_info = null;
|
|
}
|
|
else if ( $this->sth !== false ) {
|
|
/** Ensure we have a connection to the database */
|
|
if ( !isset($this->connection) ) {
|
|
_awl_connect_configured_database();
|
|
$this->connection = $GLOBALS['_awl_dbconn'];
|
|
}
|
|
$this->sth = $this->connection->query( $this->querystring );
|
|
if ( ! $this->sth ) {
|
|
$this->error_info = $this->connection->errorInfo();
|
|
$success = false;
|
|
}
|
|
else $this->error_info = null;
|
|
}
|
|
if ( $success ) $this->rows = $this->sth->rowCount();
|
|
$t2 = microtime(true); // get end time
|
|
$i_took = $t2 - $t1;
|
|
$c->total_query_time += $i_took;
|
|
$this->execution_time = sprintf( "%2.06lf", $i_took);
|
|
|
|
if ( ! $success ) {
|
|
// query failed
|
|
$this->errorstring = sprintf( 'SQL error "%s" - %s"', $this->error_info[0], (isset($this->error_info[2]) ? $this->error_info[2] : ''));
|
|
$this->_log_query( $this->location, 'QF', $this->errorstring, $line, $file );
|
|
$this->_log_query( $this->location, 'QF', $this->querystring, $line, $file );
|
|
if ( isset($this->bound_parameters) && !isset($this->sth) ) {
|
|
foreach( $this->bound_parameters AS $k => $v ) {
|
|
$this->_log_query( $this->location, 'QF', sprintf(' "%s" => "%s"', $k, $v), $line, $file );
|
|
}
|
|
}
|
|
}
|
|
elseif ( $this->execution_time > $this->query_time_warning ) {
|
|
// if execution time is too long
|
|
$this->_log_query( $this->location, 'SQ', "Took: $this->execution_time for $this->querystring", $line, $file ); // SQ == Slow Query :-)
|
|
}
|
|
elseif ( isset($debuggroups[$this->location]) || isset($c->dbg[strtolower($this->location)]) || isset($c->dbg['ALL']) ) {
|
|
// query successful, but we're debugging and want to know how long it took anyway
|
|
$this->_log_query( $this->location, 'DBGQ', "Took: $this->execution_time for $this->querystring to find $this->rows rows.", $line, $file );
|
|
}
|
|
|
|
return $success;
|
|
}
|
|
|
|
|
|
/**
|
|
* Fetch the next row from the query results
|
|
* @param boolean $as_array True if thing to be returned is array
|
|
* @return mixed query row
|
|
*/
|
|
function Fetch($as_array = false) {
|
|
global $c, $debuggroups;
|
|
|
|
if ( ( isset($debuggroups["$this->location"]) && $debuggroups["$this->location"] > 2 )
|
|
|| (isset($c) && is_object($c) && ( isset($c->dbg[strtolower($this->location)]) && isset($c->dbg[strtolower($this->location)]) )
|
|
|| isset($c->dbg['ALL']) ) ) {
|
|
$this->_log_query( $this->location, "Fetch", "$this->result Rows: $this->rows, Rownum: $this->rownum");
|
|
}
|
|
if ( ! $this->sth || $this->rows == 0 ) return false; // no results
|
|
if ( $this->rownum == null ) $this->rownum = -1;
|
|
if ( ($this->rownum + 1) >= $this->rows ) return false; // reached the end of results
|
|
|
|
$this->rownum++;
|
|
if ( isset($debuggroups["$this->location"]) && $debuggroups["$this->location"] > 1 ) {
|
|
$this->_log_query( $this->location, "Fetch", "Fetching row $this->rownum" );
|
|
}
|
|
$row = $this->sth->fetch( ($as_array ? PDO::FETCH_NUM : PDO::FETCH_OBJ) );
|
|
|
|
return $row;
|
|
}
|
|
|
|
|
|
}
|
|
|