Handle htmlspecialchars() differences between PHP < 5.4 vs >= 5.4
[awl.git] / inc / AwlQuery.php
blobc9fe5531005c39353cf18231dccf1b8ceca3eb7a
1 <?php
2 /**
3 * @package awl
4 * @subpackage AwlDatabase
5 * @author Andrew McMillan <andrew@morphoss.com>
6 * @copyright Morphoss Ltd
7 * @license http://gnu.org/copyleft/gpl.html GNU GPL v3 or later
8 * @compatibility Requires PHP 5.1 or later
9 */
11 require_once('AwlDatabase.php');
13 /**
14 * Database query class and associated functions
16 * This subpackage provides some functions that are useful around database
17 * activity and an AwlQuery class to simplify handling of database queries.
19 * The class is intended to be a very lightweight wrapper with no pretentions
20 * towards database independence, but it does include some features that have
21 * proved useful in developing and debugging web-based applications:
22 * - All queries are timed, and an expected time can be provided.
23 * - Parameters replaced into the SQL will be escaped correctly in order to
24 * minimise the chances of SQL injection errors.
25 * - Queries which fail, or which exceed their expected execution time, will
26 * be logged for potential further analysis.
27 * - Debug logging of queries may be enabled globally, or restricted to
28 * particular sets of queries.
29 * - Simple syntax for iterating through a result set.
31 * This class is intended as a transitional mechanism for moving from the
32 * PostgreSQL-specific Pg Query class to something which uses PDO in a more
33 * replaceable manner.
37 /**
38 * Connect to the database defined in the $c->db_connect[] (or $c->pg_connect) arrays
40 function _awl_connect_configured_database() {
41 global $c, $_awl_dbconn;
43 /**
44 * Attempt to connect to the configured connect strings
46 $_awl_dbconn = false;
48 if ( isset($c->db_connect) ) {
49 $connection_strings = $c->db_connect;
51 elseif ( isset($c->pg_connect) ) {
52 $connection_strings = $c->pg_connect;
55 foreach( $connection_strings AS $k => $v ) {
56 $dbuser = null;
57 $dbpass = null;
58 if ( is_array($v) ) {
59 $dsn = $v['dsn'];
60 if ( isset($v['dbuser']) ) $dbuser = $v['dbuser'];
61 if ( isset($v['dbpass']) ) $dbpass = $v['dbpass'];
63 elseif ( preg_match( '/^(\S+:)?(.*)( user=(\S+))?( password=(\S+))?$/', $v, $matches ) ) {
64 $dsn = $matches[2];
65 if ( isset($matches[1]) && $matches[1] != '' ) {
66 $dsn = $matches[1] . $dsn;
68 else {
69 $dsn = 'pgsql:' . $dsn;
71 if ( isset($matches[4]) && $matches[4] != '' ) $dbuser = $matches[4];
72 if ( isset($matches[6]) && $matches[6] != '' ) $dbpass = $matches[6];
74 if ( $_awl_dbconn = new AwlDatabase( $dsn, $dbuser, $dbpass, (isset($c->use_persistent) && $c->use_persistent ? array(PDO::ATTR_PERSISTENT => true) : null) ) ) break;
77 if ( ! $_awl_dbconn ) {
78 echo <<<EOERRMSG
79 <html><head><title>Database Connection Failure</title></head><body>
80 <h1>Database Error</h1>
81 <h3>Could not connect to database</h3>
82 </body>
83 </html>
84 EOERRMSG;
85 exit;
88 if ( isset($c->db_schema) && $c->db_schema != '' ) {
89 $_awl_dbconn->SetSearchPath( $c->db_schema . ',public' );
92 $c->_awl_dbversion = $_awl_dbconn->GetVersion();
96 /**
97 * The AwlQuery Class.
99 * This class builds and executes SQL Queries and traverses the
100 * set of results returned from the query.
102 * <b>Example usage</b>
103 * <code>
104 * $sql = "SELECT * FROM mytable WHERE mytype = ?";
105 * $qry = new AwlQuery( $sql, $myunsanitisedtype );
106 * if ( $qry->Exec("typeselect", __line__, __file__ )
107 * && $qry->rows > 0 )
109 * while( $row = $qry->Fetch() ) {
110 * do_something_with($row);
113 * </code>
115 * @package awl
117 class AwlQuery
119 /**#@+
120 * @access private
123 * Our database connection, normally copied from a global one
124 * @var resource
126 protected $connection;
129 * The original query string
130 * @var string
132 protected $querystring;
135 * The actual query string, after we've replaced parameters in it
136 * @var string
138 protected $bound_querystring;
141 * The current array of bound parameters
142 * @var array
144 protected $bound_parameters;
147 * The PDO statement handle, or null if we don't have one yet.
148 * @var string
150 protected $sth;
153 * Result of the last execution
154 * @var resource
156 protected $result;
159 * number of current row - use accessor to get/set
160 * @var int
162 protected $rownum = null;
165 * number of rows from pg_numrows - use accessor to get value
166 * @var int
168 protected $rows;
171 * The Database error information, if the query fails.
172 * @var string
174 protected $error_info;
177 * Stores the query execution time - used to deal with long queries.
178 * should be read-only
179 * @var string
181 protected $execution_time;
183 /**#@-*/
185 /**#@+
186 * @access public
189 * Where we called this query from so we can find it in our code!
190 * Debugging may also be selectively enabled for a $location.
191 * @var string
193 public $location;
196 * How long the query should take before a warning is issued.
198 * This is writable, but a method to set it might be a better interface.
199 * The default is 0.3 seconds.
200 * @var double
202 public $query_time_warning = 0.3;
203 /**#@-*/
207 * Constructor
208 * @param string The query string in PDO syntax with replacable '?' characters or bindable parameters.
209 * @param mixed The values to replace into the SQL string.
210 * @return The AwlQuery object
212 function __construct() {
213 global $_awl_dbconn;
214 $this->rows = null;
215 $this->execution_time = 0;
216 $this->error_info = null;
217 $this->rownum = -1;
218 if ( isset($_awl_dbconn) ) $this->connection = $_awl_dbconn;
219 else $this->connection = null;
221 $argc = func_num_args();
222 $args = func_get_args();
224 $this->querystring = array_shift($args);
225 if ( 1 < $argc ) {
226 if ( is_array($args[0]) )
227 $this->bound_parameters = $args[0];
228 else
229 $this->bound_parameters = $args;
230 // print_r( $this->bound_parameters );
233 return $this;
238 * Use a different database connection for this query
239 * @param resource $new_connection The database connection to use.
241 function SetConnection( $new_connection, $options = null ) {
242 if ( is_string($new_connection) || is_array($new_connection) ) {
243 $dbuser = null;
244 $dbpass = null;
245 if ( is_array($new_connection) ) {
246 $dsn = $new_connection['dsn'];
247 if ( isset($new_connection['dbuser']) ) $dbuser = $new_connection['dbuser'];
248 if ( isset($new_connection['dbpass']) ) $dbpass = $new_connection['dbpass'];
250 elseif ( preg_match( '/^(\S+:)?(.*)( user=(\S+))?( password=(\S+))?$/', $new_connection, $matches ) ) {
251 $dsn = $matches[2];
252 if ( isset($matches[1]) && $matches[1] != '' ) {
253 $dsn = $matches[1] . $dsn;
255 else {
256 $dsn = 'pgsql:' . $dsn;
258 if ( isset($matches[4]) && $matches[4] != '' ) $dbuser = $matches[4];
259 if ( isset($matches[6]) && $matches[6] != '' ) $dbpass = $matches[6];
261 if ( $new_connection = new AwlDatabase( $dsn, $dbuser, $dbpass, $options ) ) break;
263 $this->connection = $new_connection;
264 return $new_connection;
270 * Get the current database connection for this query
272 function GetConnection() {
273 return $this->connection;
278 * Log query, optionally with file and line location of the caller.
280 * This function should not really be used outside of AwlQuery. For a more
281 * useful generic logging interface consider calling dbg_error_log(...);
283 * @param string $locn A string identifying the calling location.
284 * @param string $tag A tag string, e.g. identifying the type of event.
285 * @param string $string The information to be logged.
286 * @param int $line The line number where the logged event occurred.
287 * @param string $file The file name where the logged event occurred.
289 function _log_query( $locn, $tag, $string, $line = 0, $file = "") {
290 // replace more than one space with one space
291 $string = preg_replace('/\s+/', ' ', $string);
293 if ( ($tag == 'QF' || $tag == 'SQ') && ( $line != 0 && $file != "" ) ) {
294 dbg_error_log( "LOG-$locn", " Query: %s: %s in '%s' on line %d", ($tag == 'QF' ? 'Error' : 'Possible slow query'), $tag, $file, $line );
297 while( strlen( $string ) > 0 ) {
298 dbg_error_log( "LOG-$locn", " Query: %s: %s", $tag, substr( $string, 0, 240) );
299 $string = substr( "$string", 240 );
305 * Quote the given string so it can be safely used within string delimiters
306 * in a query. To be avoided, in general.
308 * @param mixed $str Data to be converted to a string suitable for including as a value in SQL.
309 * @return string NULL, TRUE, FALSE, a plain number, or the original string quoted and with ' and \ characters escaped
311 public static function quote($str = null) {
312 global $_awl_dbconn;
313 if ( !isset($_awl_dbconn) ) {
314 _awl_connect_configured_database();
316 return $_awl_dbconn->Quote($str);
321 * Bind some parameters. This can be called in three ways:
322 * 1) As Bind(':key','value), when using named parameters
323 * 2) As Bind('value'), when using ? placeholders
324 * 3) As Bind(array()), to overwrite the existing bound parameters. The array may
325 * be ':name' => 'value' pairs or ordinal values, depending on whether the SQL
326 * is using ':name' or '?' style placeholders.
328 * @param mixed $args See details above.
330 function Bind() {
331 $argc = func_num_args();
332 $args = func_get_args();
334 if ( $argc == 1 ) {
335 if ( gettype($args[0]) == 'array' ) {
336 $this->bound_parameters = $args[0];
338 else {
339 $this->bound_parameters[] = $args[0];
342 else {
343 $this->bound_parameters[$args[0]] = $args[1];
349 * Tell the database to prepare the query that we will execute
351 function Prepare() {
352 global $c;
354 if ( isset($this->sth) ) return; // Already prepared
355 if ( isset($c->expand_pdo_parameters) && $c->expand_pdo_parameters ) return; // No-op if we're expanding internally
357 if ( !isset($this->connection) ) {
358 _awl_connect_configured_database();
359 $this->connection = $GLOBALS['_awl_dbconn'];
362 $this->sth = $this->connection->prepare( $this->querystring );
364 if ( ! $this->sth ) {
365 $this->error_info = $this->connection->errorInfo();
367 else $this->error_info = null;
371 * Tell the database to execute the query
373 function Execute() {
374 global $c;
376 if ( !isset($this->connection) ) {
377 _awl_connect_configured_database();
378 $this->connection = $GLOBALS['_awl_dbconn'];
380 if ( !is_object($this->connection) ) throw new Exception('Database not connected.');
382 if ( isset($c->expand_pdo_parameters) && $c->expand_pdo_parameters ) {
383 $this->bound_querystring = $this->querystring;
384 if ( isset($this->bound_parameters) ) {
385 $this->bound_querystring = $this->connection->ReplaceParameters($this->querystring,$this->bound_parameters);
386 // printf( "\n=============================================================== OQ\n%s\n", $this->querystring);
387 // printf( "\n=============================================================== QQ\n%s\n", $this->bound_querystring);
388 // print_r( $this->bound_parameters );
390 $t1 = microtime(true); // get start time
391 $execute_result = $this->sth = $this->connection->query($this->bound_querystring);
393 else {
394 $t1 = microtime(true); // get start time
395 $execute_result = $this->sth = $this->connection->prepare($this->querystring);
396 if ( $this->sth ) $execute_result = $this->sth->execute($this->bound_parameters);
397 // printf( "\n=============================================================== OQ\n%s\n", $this->querystring);
398 // print_r( $this->bound_parameters );
400 $this->bound_querystring = null;
402 if ( $execute_result === false ) {
403 $this->error_info = $this->connection->errorInfo();
404 return false;
406 $this->rows = $this->sth->rowCount();
408 $i_took = microtime(true) - $t1;
409 $c->total_query_time += $i_took;
410 $this->execution_time = sprintf( "%2.06lf", $i_took);
412 $this->error_info = null;
413 return true;
418 * Return the query string we are planning to execute
420 function QueryString() {
421 return $this->querystring;
426 * Return the parameters we are planning to substitute into the query string
428 function Parameters() {
429 return $this->bound_parameters;
434 * Return the count of rows retrieved/affected
436 function rows() {
437 return $this->rows;
442 * Return the current rownum in the retrieved set
444 function rownum() {
445 return $this->rownum;
450 * Returns the current state of a transaction, indicating if we have begun a transaction, whether the transaction
451 * has failed, or if we are not in a transaction.
452 * @return int 0 = not started, 1 = in progress, -1 = error pending rollback/commit
454 function TransactionState() {
455 global $_awl_dbconn;
456 if ( !isset($this->connection) ) {
457 if ( !isset($_awl_dbconn) ) _awl_connect_configured_database();
458 $this->connection = $_awl_dbconn;
460 return $this->connection->TransactionState();
465 * Wrap the parent DB class Begin() so we can $qry->Begin() sometime before we $qry->Exec()
467 public function Begin() {
468 global $_awl_dbconn;
469 if ( !isset($this->connection) ) {
470 if ( !isset($_awl_dbconn) ) _awl_connect_configured_database();
471 $this->connection = $_awl_dbconn;
473 return $this->connection->Begin();
478 * Wrap the parent DB class Commit() so we can $qry->Commit() sometime after we $qry->Exec()
480 public function Commit() {
481 if ( !isset($this->connection) ) {
482 trigger_error("Cannot commit a transaction without an active statement.", E_USER_ERROR);
484 return $this->connection->Commit();
489 * Wrap the parent DB class Rollback() so we can $qry->Rollback() sometime after we $qry->Exec()
491 public function Rollback() {
492 if ( !isset($this->connection) ) {
493 trigger_error("Cannot rollback a transaction without an active statement.", E_USER_ERROR);
495 return $this->connection->Rollback();
500 * Simple SetSql() class which will reset the object with the querystring from the first argument.
501 * @param string The query string in PDO syntax with replacable '?' characters or bindable parameters.
503 public function SetSql( $sql ) {
504 $this->rows = null;
505 $this->execution_time = 0;
506 $this->error_info = null;
507 $this->rownum = -1;
508 $this->bound_parameters = null;
509 $this->bound_querystring = null;
510 $this->sth = null;
512 $this->querystring = $sql;
517 * Simple QDo() class which will re-use this query for whatever was passed in, and execute it
518 * returning the result of the Exec() call. We can't call it Do() since that's a reserved word...
519 * @param string The query string in PDO syntax with replacable '?' characters or bindable parameters.
520 * @param mixed The values to replace into the SQL string.
521 * @return boolean Success (true) or Failure (false)
523 public function QDo() {
524 $argc = func_num_args();
525 $args = func_get_args();
527 $this->SetSql( array_shift($args) );
528 if ( 1 < $argc ) {
529 if ( is_array($args[0]) )
530 $this->bound_parameters = $args[0];
531 else
532 $this->bound_parameters = $args;
535 return $this->Exec();
540 * Execute the query, logging any debugging.
542 * <b>Example</b>
543 * So that you can nicely enable/disable the queries for a particular class, you
544 * could use some of PHPs magic constants in your call.
545 * <code>
546 * $qry->Exec(__CLASS__, __LINE__, __FILE__);
547 * </code>
550 * @param string $location The name of the location for enabling debugging or just
551 * to help our children find the source of a problem.
552 * @param int $line The line number where Exec was called
553 * @param string $file The file where Exec was called
554 * @return boolean Success (true) or Failure (false)
556 function Exec( $location = null, $line = null, $file = null ) {
557 global $c;
558 if ( isset($location) ) $this->location = trim($location);
559 if ( !isset($this->location) || $this->location == "" ) $this->location = substr($_SERVER['PHP_SELF'],1);
561 if ( isset($line) ) $this->location_line = intval($line);
562 else if ( isset($this->location_line) ) $line = $this->location_line;
564 if ( isset($file) ) $this->location_file = trim($file);
565 else if ( isset($this->location_file) ) $file = $this->location_file;
567 if ( isset($c->dbg['querystring']) || isset($c->dbg['ALL']) ) {
568 $this->_log_query( $this->location, 'DBGQ', $this->querystring, $line, $file );
569 if ( isset($this->bound_parameters) && !isset($this->sth) ) {
570 foreach( $this->bound_parameters AS $k => $v ) {
571 $this->_log_query( $this->location, 'DBGQ', sprintf(' "%s" => "%s"', $k, $v), $line, $file );
576 if ( isset($this->bound_parameters) ) {
577 $this->Prepare();
580 $success = $this->Execute();
582 if ( ! $success ) {
583 // query failed
584 $this->errorstring = sprintf( 'SQL error "%s" - %s"', $this->error_info[0], (isset($this->error_info[2]) ? $this->error_info[2] : ''));
585 if ( isset($c->dbg['print_query_errors']) && $c->dbg['print_query_errors'] ) {
586 printf( "\n=====================\n" );
587 printf( "%s[%d] QF: %s\n", $file, $line, $this->errorstring);
588 printf( "%s\n", $this->querystring );
589 if ( isset($this->bound_parameters) ) {
590 foreach( $this->bound_parameters AS $k => $v ) {
591 printf( " %-18s \t=> '%s'\n", "'$k'", $v );
594 printf( ".....................\n" );
596 $this->_log_query( $this->location, 'QF', $this->errorstring, $line, $file );
597 $this->_log_query( $this->location, 'QF', $this->querystring, $line, $file );
598 if ( isset($this->bound_parameters) && ! ( isset($c->dbg['querystring']) || isset($c->dbg['ALL']) ) ) {
599 foreach( $this->bound_parameters AS $k => $v ) {
600 dbg_error_log( 'LOG-'.$this->location, ' Query: QF: "%s" => "%s"', $k, $v);
604 elseif ( $this->execution_time > $this->query_time_warning ) {
605 // if execution time is too long
606 $this->_log_query( $this->location, 'SQ', "Took: $this->execution_time for $this->querystring", $line, $file ); // SQ == Slow Query :-)
608 elseif ( isset($c->dbg['querystring']) || isset($c->dbg[strtolower($this->location)]) || isset($c->dbg['ALL']) ) {
609 // query successful, but we're debugging and want to know how long it took anyway
610 $this->_log_query( $this->location, 'DBGQ', "Took: $this->execution_time to find $this->rows rows.", $line, $file );
613 return $success;
618 * Fetch the next row from the query results
619 * @param boolean $as_array True if thing to be returned is array
620 * @return mixed query row
622 function Fetch($as_array = false) {
624 if ( ! $this->sth || $this->rows == 0 ) return false; // no results
625 if ( $this->rownum == null ) $this->rownum = -1;
626 if ( ($this->rownum + 1) >= $this->rows ) return false; // reached the end of results
628 $this->rownum++;
629 $row = $this->sth->fetch( ($as_array ? PDO::FETCH_NUM : PDO::FETCH_OBJ) );
631 return $row;
636 * Get any error information from the last query
638 function getErrorInfo() {
639 return $this->error_info;