4 +-----------------------------------------------------------------------+
5 | program/include/rcube_mdb2.php |
7 | This file is part of the Roundcube Webmail client |
8 | Copyright (C) 2005-2009, The Roundcube Dev Team |
9 | Licensed under the GNU GPL |
12 | PEAR:DB wrapper class that implements PEAR MDB2 functions |
13 | See http://pear.php.net/package/MDB2 |
15 +-----------------------------------------------------------------------+
16 | Author: Lukas Kahwe Smith <smith@pooteeweet.org> |
17 +-----------------------------------------------------------------------+
19 $Id: rcube_mdb2.php 4810 2011-05-27 11:02:51Z alec $
25 * Database independent query interface
27 * This is a wrapper for the PEAR::MDB2 class
30 * @author David Saez Padros <david@ols.es>
31 * @author Thomas Bruederli <roundcube@gmail.com>
32 * @author Lukas Kahwe Smith <smith@pooteeweet.org>
34 * @link http://pear.php.net/package/MDB2
38 var $db_dsnw; // DSN for write operations
39 var $db_dsnr; // DSN for read operations
40 var $db_connected = false; // Already connected ?
41 var $db_mode = ''; // Connection mode
42 var $db_handle = 0; // Connection handle
43 var $db_error = false;
44 var $db_error_msg = '';
46 private $debug_mode = false;
47 private $write_failure = false;
48 private $a_query_results = array('dummy');
49 private $last_res_id = 0;
56 * @param string $db_dsnw DSN for read/write operations
57 * @param string $db_dsnr Optional DSN for read only operations
59 function __construct($db_dsnw, $db_dsnr='', $pconn=false)
64 $this->db_dsnw = $db_dsnw;
65 $this->db_dsnr = $db_dsnr;
66 $this->db_pconn = $pconn;
68 $dsn_array = MDB2::parseDSN($db_dsnw);
69 $this->db_provider = $dsn_array['phptype'];
74 * Connect to specific database
76 * @param string $dsn DSN for DB connections
77 * @return MDB2 PEAR database handle
80 private function dsn_connect($dsn)
82 // Use persistent connections if available
84 'persistent' => $this->db_pconn,
85 'emulate_prepared' => $this->debug_mode,
86 'debug' => $this->debug_mode,
87 'debug_handler' => array($this, 'debug_handler'),
88 'portability' => MDB2_PORTABILITY_ALL ^ MDB2_PORTABILITY_EMPTY_TO_NULL);
90 if ($this->db_provider == 'pgsql') {
91 $db_options['disable_smart_seqname'] = true;
92 $db_options['seqname_format'] = '%s';
95 $dbh = MDB2::connect($dsn, $db_options);
97 if (MDB2::isError($dbh)) {
98 $this->db_error = true;
99 $this->db_error_msg = $dbh->getMessage();
101 raise_error(array('code' => 500, 'type' => 'db',
102 'line' => __LINE__, 'file' => __FILE__,
103 'message' => $dbh->getUserInfo()), true, false);
105 else if ($this->db_provider == 'sqlite') {
106 $dsn_array = MDB2::parseDSN($dsn);
107 if (!filesize($dsn_array['database']) && !empty($this->sqlite_initials))
108 $this->_sqlite_create_database($dbh, $this->sqlite_initials);
110 else if ($this->db_provider!='mssql' && $this->db_provider!='sqlsrv')
111 $dbh->setCharset('utf8');
118 * Connect to appropiate database depending on the operation
120 * @param string $mode Connection mode (r|w)
123 function db_connect($mode)
126 if ($this->db_connected) {
127 // connected to read-write db, current connection is ok
128 if ($this->db_mode == 'w' && !$this->write_failure)
131 // no replication, current connection is ok for read and write
132 if (empty($this->db_dsnr) || $this->db_dsnw == $this->db_dsnr) {
133 $this->db_mode = 'w';
137 // Same mode, current connection is ok
138 if ($this->db_mode == $mode)
142 $dsn = ($mode == 'r') ? $this->db_dsnr : $this->db_dsnw;
144 $this->db_handle = $this->dsn_connect($dsn);
145 $this->db_connected = !PEAR::isError($this->db_handle);
147 if ($this->db_connected)
148 $this->db_mode = $mode;
153 * Activate/deactivate debug mode
155 * @param boolean $dbg True if SQL queries should be logged
158 function set_debug($dbg = true)
160 $this->debug_mode = $dbg;
161 if ($this->db_connected) {
162 $this->db_handle->setOption('debug', $dbg);
163 $this->db_handle->setOption('emulate_prepared', $dbg);
169 * Getter for error state
171 * @param boolean True on error
176 return $this->db_error ? $this->db_error_msg : false;
181 * Connection state checker
183 * @param boolean True if in connected state
186 function is_connected()
188 return PEAR::isError($this->db_handle) ? false : $this->db_connected;
193 * Is database replication configured?
194 * This returns true if dsnw != dsnr
196 function is_replicated()
198 return !empty($this->db_dsnr) && $this->db_dsnw != $this->db_dsnr;
203 * Execute a SQL query
205 * @param string SQL query to execute
206 * @param mixed Values to be inserted in query
207 * @return number Query handle identifier
212 $params = func_get_args();
213 $query = array_shift($params);
215 // Support one argument of type array, instead of n arguments
216 if (count($params) == 1 && is_array($params[0]))
217 $params = $params[0];
219 return $this->_query($query, 0, 0, $params);
224 * Execute a SQL query with limits
226 * @param string SQL query to execute
227 * @param number Offset for LIMIT statement
228 * @param number Number of rows for LIMIT statement
229 * @param mixed Values to be inserted in query
230 * @return number Query handle identifier
233 function limitquery()
235 $params = func_get_args();
236 $query = array_shift($params);
237 $offset = array_shift($params);
238 $numrows = array_shift($params);
240 return $this->_query($query, $offset, $numrows, $params);
245 * Execute a SQL query with limits
247 * @param string $query SQL query to execute
248 * @param number $offset Offset for LIMIT statement
249 * @param number $numrows Number of rows for LIMIT statement
250 * @param array $params Values to be inserted in query
251 * @return number Query handle identifier
254 private function _query($query, $offset, $numrows, $params)
257 $mode = (strtolower(substr(trim($query),0,6)) == 'select') ? 'r' : 'w';
259 // don't event attempt to connect if previous write-operation failed
260 if ($this->write_failure && $mode == 'w')
263 $this->db_connect($mode);
265 // check connection before proceeding
266 if (!$this->is_connected())
269 if ($this->db_provider == 'sqlite')
270 $this->_sqlite_prepare();
272 if ($numrows || $offset)
273 $result = $this->db_handle->setLimit($numrows,$offset);
276 $result = $mode == 'r' ? $this->db_handle->query($query) : $this->db_handle->exec($query);
278 $params = (array)$params;
279 $q = $this->db_handle->prepare($query, null, $mode=='w' ? MDB2_PREPARE_MANIP : null);
280 if ($this->db_handle->isError($q)) {
281 $this->db_error = true;
282 $this->db_error_msg = $q->userinfo;
284 raise_error(array('code' => 500, 'type' => 'db',
285 'line' => __LINE__, 'file' => __FILE__,
286 'message' => $this->db_error_msg), true, false);
291 $result = $q->execute($params);
296 // remember that write-operation failed
297 if ($mode == 'w' && ($result === false || PEAR::isError($result)))
298 $this->write_failure = true;
300 // add result, even if it's an error
301 return $this->_add_result($result);
306 * Get number of rows for a SQL query
307 * If no query handle is specified, the last query will be taken as reference
309 * @param number $res_id Optional query handle identifier
310 * @return mixed Number of rows or false on failure
313 function num_rows($res_id=null)
315 if (!$this->db_connected)
318 if ($result = $this->_get_result($res_id))
319 return $result->numRows();
326 * Get number of affected rows for the last query
328 * @param number $res_id Optional query handle identifier
329 * @return mixed Number of rows or false on failure
332 function affected_rows($res_id = null)
334 if (!$this->db_connected)
337 return $this->_get_result($res_id);
342 * Get last inserted record ID
343 * For Postgres databases, a sequence name is required
345 * @param string $table Table name (to find the incremented sequence)
346 * @return mixed ID or false on failure
349 function insert_id($table = '')
351 if (!$this->db_connected || $this->db_mode == 'r')
355 if ($this->db_provider == 'pgsql')
356 // find sequence name
357 $table = get_sequence_name($table);
359 // resolve table name
360 $table = get_table_name($table);
363 $id = $this->db_handle->lastInsertID($table);
365 return $this->db_handle->isError($id) ? null : $id;
370 * Get an associative array for one row
371 * If no query handle is specified, the last query will be taken as reference
373 * @param number $res_id Optional query handle identifier
374 * @return mixed Array with col values or false on failure
377 function fetch_assoc($res_id=null)
379 $result = $this->_get_result($res_id);
380 return $this->_fetch_row($result, MDB2_FETCHMODE_ASSOC);
385 * Get an index array for one row
386 * If no query handle is specified, the last query will be taken as reference
388 * @param number $res_id Optional query handle identifier
389 * @return mixed Array with col values or false on failure
392 function fetch_array($res_id=null)
394 $result = $this->_get_result($res_id);
395 return $this->_fetch_row($result, MDB2_FETCHMODE_ORDERED);
400 * Get col values for a result row
402 * @param MDB2_Result_Common Query $result result handle
403 * @param number $mode Fetch mode identifier
404 * @return mixed Array with col values or false on failure
407 private function _fetch_row($result, $mode)
409 if ($result === false || PEAR::isError($result) || !$this->is_connected())
412 return $result->fetchRow($mode);
417 * Wrapper for the SHOW TABLES command
419 * @return array List of all tables of the current database
423 function list_tables()
425 // get tables if not cached
426 if (!$this->tables) {
427 $this->db_handle->loadModule('Manager');
428 if (!PEAR::isError($result = $this->db_handle->listTables()))
429 $this->tables = $result;
431 $this->tables = array();
434 return $this->tables;
439 * Wrapper for SHOW COLUMNS command
441 * @param string Table name
442 * @return array List of table cols
444 function list_cols($table)
446 $this->db_handle->loadModule('Manager');
447 if (!PEAR::isError($result = $this->db_handle->listTableFields($table))) {
456 * Formats input so it can be safely used in a query
458 * @param mixed $input Value to quote
459 * @param string $type Type of data
460 * @return string Quoted/converted string for use in query
463 function quote($input, $type = null)
465 // handle int directly for better performance
466 if ($type == 'integer')
467 return intval($input);
469 // create DB handle if not available
470 if (!$this->db_handle)
471 $this->db_connect('r');
473 return $this->db_connected ? $this->db_handle->quote($input, $type) : addslashes($input);
478 * Quotes a string so it can be safely used as a table or column name
480 * @param string $str Value to quote
481 * @return string Quoted string for use in query
482 * @deprecated Replaced by rcube_MDB2::quote_identifier
483 * @see rcube_mdb2::quote_identifier
486 function quoteIdentifier($str)
488 return $this->quote_identifier($str);
493 * Quotes a string so it can be safely used as a table or column name
495 * @param string $str Value to quote
496 * @return string Quoted string for use in query
499 function quote_identifier($str)
501 if (!$this->db_handle)
502 $this->db_connect('r');
504 return $this->db_connected ? $this->db_handle->quoteIdentifier($str) : $str;
511 * @param string $str The string to be escaped
512 * @return string The escaped string
516 function escapeSimple($str)
518 if (!$this->db_handle)
519 $this->db_connect('r');
521 return $this->db_handle->escape($str);
526 * Return SQL function for current time and date
528 * @return string SQL function to use in query
533 switch($this->db_provider) {
545 * Return list of elements for use with SQL's IN clause
547 * @param array $arr Input array
548 * @param string $type Type of data
549 * @return string Comma-separated list of quoted values for use in query
552 function array2list($arr, $type = null)
555 return $this->quote($arr, $type);
557 foreach ($arr as $idx => $item)
558 $arr[$idx] = $this->quote($item, $type);
560 return implode(',', $arr);
565 * Return SQL statement to convert a field value into a unix timestamp
567 * This method is deprecated and should not be used anymore due to limitations
568 * of timestamp functions in Mysql (year 2038 problem)
570 * @param string $field Field name
571 * @return string SQL statement to use in query
574 function unixtimestamp($field)
576 switch($this->db_provider) {
578 return "EXTRACT (EPOCH FROM $field)";
582 return "DATEDIFF(second, '19700101', $field) + DATEDIFF(second, GETDATE(), GETUTCDATE())";
585 return "UNIX_TIMESTAMP($field)";
591 * Return SQL statement to convert from a unix timestamp
593 * @param string $timestamp Field name
594 * @return string SQL statement to use in query
597 function fromunixtime($timestamp)
599 return date("'Y-m-d H:i:s'", $timestamp);
604 * Return SQL statement for case insensitive LIKE
606 * @param string $column Field name
607 * @param string $value Search value
608 * @return string SQL statement to use in query
611 function ilike($column, $value)
613 // TODO: use MDB2's matchPattern() function
614 switch($this->db_provider) {
616 return $this->quote_identifier($column).' ILIKE '.$this->quote($value);
618 return $this->quote_identifier($column).' LIKE '.$this->quote($value);
623 * Abstract SQL statement for value concatenation
625 * @return string SQL statement to be used in query
628 function concat(/* col1, col2, ... */)
631 $args = func_get_args();
633 switch($this->db_provider) {
647 return $func . '(' . join($delim, $args) . ')';
652 * Encodes non-UTF-8 characters in string/array/object (recursive)
654 * @param mixed $input Data to fix
655 * @return mixed Properly UTF-8 encoded data
658 function encode($input)
660 if (is_object($input)) {
661 foreach (get_object_vars($input) as $idx => $value)
662 $input->$idx = $this->encode($value);
665 else if (is_array($input)) {
666 foreach ($input as $idx => $value)
667 $input[$idx] = $this->encode($value);
671 return utf8_encode($input);
676 * Decodes encoded UTF-8 string/object/array (recursive)
678 * @param mixed $input Input data
679 * @return mixed Decoded data
682 function decode($input)
684 if (is_object($input)) {
685 foreach (get_object_vars($input) as $idx => $value)
686 $input->$idx = $this->decode($value);
689 else if (is_array($input)) {
690 foreach ($input as $idx => $value)
691 $input[$idx] = $this->decode($value);
695 return utf8_decode($input);
700 * Adds a query result and returns a handle ID
702 * @param object $res Query handle
703 * @return mixed Handle ID
706 private function _add_result($res)
709 if (PEAR::isError($res)) {
710 $this->db_error = true;
711 $this->db_error_msg = $res->getMessage();
712 raise_error(array('code' => 500, 'type' => 'db',
713 'line' => __LINE__, 'file' => __FILE__,
714 'message' => $res->getMessage() . " Query: "
715 . substr(preg_replace('/[\r\n]+\s*/', ' ', $res->userinfo), 0, 512)),
719 $res_id = sizeof($this->a_query_results);
720 $this->last_res_id = $res_id;
721 $this->a_query_results[$res_id] = $res;
727 * Resolves a given handle ID and returns the according query handle
728 * If no ID is specified, the last resource handle will be returned
730 * @param number $res_id Handle ID
731 * @return mixed Resource handle or false on failure
734 private function _get_result($res_id = null)
737 $res_id = $this->last_res_id;
739 if (isset($this->a_query_results[$res_id]))
740 if (!PEAR::isError($this->a_query_results[$res_id]))
741 return $this->a_query_results[$res_id];
748 * Create a sqlite database from a file
750 * @param MDB2 $dbh SQLite database handle
751 * @param string $file_name File path to use for DB creation
754 private function _sqlite_create_database($dbh, $file_name)
756 if (empty($file_name) || !is_string($file_name))
759 $data = file_get_contents($file_name);
762 if (!sqlite_exec($dbh->connection, $data, $error) || MDB2::isError($dbh))
763 raise_error(array('code' => 500, 'type' => 'db',
764 'line' => __LINE__, 'file' => __FILE__,
765 'message' => $error), true, false);
770 * Add some proprietary database functions to the current SQLite handle
771 * in order to make it MySQL compatible
775 private function _sqlite_prepare()
777 include_once(INSTALL_PATH . 'program/include/rcube_sqlite.inc');
779 // we emulate via callback some missing MySQL function
780 sqlite_create_function($this->db_handle->connection,
781 'from_unixtime', 'rcube_sqlite_from_unixtime');
782 sqlite_create_function($this->db_handle->connection,
783 'unix_timestamp', 'rcube_sqlite_unix_timestamp');
784 sqlite_create_function($this->db_handle->connection,
785 'now', 'rcube_sqlite_now');
786 sqlite_create_function($this->db_handle->connection,
787 'md5', 'rcube_sqlite_md5');
792 * Debug handler for the MDB2
794 function debug_handler(&$db, $scope, $message, $context = array())
796 if ($scope != 'prepare') {
797 $debug_output = sprintf('%s(%d): %s;',
798 $scope, $db->db_index, rtrim($message, ';'));
799 write_log('sql', $debug_output);
803 } // end class rcube_db