source: subversion/trunk/roundcubemail/program/include/rcube_mdb2.php @ 5395

Last change on this file since 5395 was 5395, checked in by alec, 20 months ago
  • Fix handling of DB connection failures. Detect failure on connection level instead of on query level. Fixes issue when one write query failed, next queries were not executed.
  • Changed 'var' to 'public'.
  • Property svn:eol-style set to native
  • Property svn:keywords set to Id
File size: 22.2 KB
Line 
1<?php
2
3/*
4 +-----------------------------------------------------------------------+
5 | program/include/rcube_mdb2.php                                        |
6 |                                                                       |
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                                            |
10 |                                                                       |
11 | PURPOSE:                                                              |
12 |   PEAR:DB wrapper class that implements PEAR MDB2 functions           |
13 |   See http://pear.php.net/package/MDB2                                |
14 |                                                                       |
15 +-----------------------------------------------------------------------+
16 | Author: Lukas Kahwe Smith <smith@pooteeweet.org>                      |
17 +-----------------------------------------------------------------------+
18
19 $Id$
20
21*/
22
23
24/**
25 * Database independent query interface
26 *
27 * This is a wrapper for the PEAR::MDB2 class
28 *
29 * @package    Database
30 * @author     David Saez Padros <david@ols.es>
31 * @author     Thomas Bruederli <roundcube@gmail.com>
32 * @author     Lukas Kahwe Smith <smith@pooteeweet.org>
33 * @version    1.18
34 * @link       http://pear.php.net/package/MDB2
35 */
36class rcube_mdb2
37{
38    public $db_dsnw;               // DSN for write operations
39    public $db_dsnr;               // DSN for read operations
40    public $db_connected = false;  // Already connected ?
41    public $db_mode = '';          // Connection mode
42    public $db_handle = 0;         // Connection handle
43    public $db_error = false;
44    public $db_error_msg = '';
45
46    private $debug_mode = false;
47    private $conn_failure = false;
48    private $a_query_results = array('dummy');
49    private $last_res_id = 0;
50    private $tables;
51
52
53    /**
54     * Object constructor
55     *
56     * @param  string $db_dsnw DSN for read/write operations
57     * @param  string $db_dsnr Optional DSN for read only operations
58     */
59    function __construct($db_dsnw, $db_dsnr='', $pconn=false)
60    {
61        if (empty($db_dsnr))
62            $db_dsnr = $db_dsnw;
63
64        $this->db_dsnw = $db_dsnw;
65        $this->db_dsnr = $db_dsnr;
66        $this->db_pconn = $pconn;
67
68        $dsn_array = MDB2::parseDSN($db_dsnw);
69        $this->db_provider = $dsn_array['phptype'];
70    }
71
72
73    /**
74     * Connect to specific database
75     *
76     * @param  string $dsn  DSN for DB connections
77     * @return MDB2 PEAR database handle
78     * @access private
79     */
80    private function dsn_connect($dsn)
81    {
82        // Use persistent connections if available
83        $db_options = array(
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);
89
90        if ($this->db_provider == 'pgsql') {
91            $db_options['disable_smart_seqname'] = true;
92            $db_options['seqname_format'] = '%s';
93        }
94
95        $dbh = MDB2::connect($dsn, $db_options);
96
97        if (MDB2::isError($dbh)) {
98            $this->db_error = true;
99            $this->db_error_msg = $dbh->getMessage();
100
101            raise_error(array('code' => 500, 'type' => 'db',
102                'line' => __LINE__, 'file' => __FILE__,
103                'message' => $dbh->getUserInfo()), true, false);
104        }
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);
109        }
110        else if ($this->db_provider!='mssql' && $this->db_provider!='sqlsrv')
111            $dbh->setCharset('utf8');
112
113        return $dbh;
114    }
115
116
117    /**
118     * Connect to appropiate database depending on the operation
119     *
120     * @param  string $mode Connection mode (r|w)
121     * @access public
122     */
123    function db_connect($mode)
124    {
125        // previous connection failed, don't attempt to connect again
126        if ($this->conn_failure) {
127            return;
128        }
129
130        // no replication
131        if ($this->db_dsnw == $this->db_dsnr) {
132            $mode = 'w';
133        }
134
135        // Already connected
136        if ($this->db_connected) {
137            // connected to db with the same or "higher" mode
138            if ($this->db_mode == 'w' || $this->db_mode == $mode) {
139                return;
140            }
141        }
142
143        $dsn = ($mode == 'r') ? $this->db_dsnr : $this->db_dsnw;
144
145        $this->db_handle    = $this->dsn_connect($dsn);
146        $this->db_connected = !PEAR::isError($this->db_handle);
147
148        if ($this->db_connected)
149            $this->db_mode = $mode;
150        else
151            $this->conn_failure = true;
152    }
153
154
155    /**
156     * Activate/deactivate debug mode
157     *
158     * @param boolean $dbg True if SQL queries should be logged
159     * @access public
160     */
161    function set_debug($dbg = true)
162    {
163        $this->debug_mode = $dbg;
164        if ($this->db_connected) {
165            $this->db_handle->setOption('debug', $dbg);
166            $this->db_handle->setOption('emulate_prepared', $dbg);
167        }
168    }
169
170
171    /**
172     * Getter for error state
173     *
174     * @param  boolean  True on error
175     * @access public
176     */
177    function is_error()
178    {
179        return $this->db_error ? $this->db_error_msg : false;
180    }
181
182
183    /**
184     * Connection state checker
185     *
186     * @param  boolean  True if in connected state
187     * @access public
188     */
189    function is_connected()
190    {
191        return PEAR::isError($this->db_handle) ? false : $this->db_connected;
192    }
193
194
195    /**
196     * Is database replication configured?
197     * This returns true if dsnw != dsnr
198     */
199    function is_replicated()
200    {
201      return !empty($this->db_dsnr) && $this->db_dsnw != $this->db_dsnr;
202    }
203
204
205    /**
206     * Execute a SQL query
207     *
208     * @param  string  SQL query to execute
209     * @param  mixed   Values to be inserted in query
210     * @return number  Query handle identifier
211     * @access public
212     */
213    function query()
214    {
215        $params = func_get_args();
216        $query = array_shift($params);
217
218        // Support one argument of type array, instead of n arguments
219        if (count($params) == 1 && is_array($params[0]))
220            $params = $params[0];
221
222        return $this->_query($query, 0, 0, $params);
223    }
224
225
226    /**
227     * Execute a SQL query with limits
228     *
229     * @param  string  SQL query to execute
230     * @param  number  Offset for LIMIT statement
231     * @param  number  Number of rows for LIMIT statement
232     * @param  mixed   Values to be inserted in query
233     * @return number  Query handle identifier
234     * @access public
235     */
236    function limitquery()
237    {
238        $params  = func_get_args();
239        $query   = array_shift($params);
240        $offset  = array_shift($params);
241        $numrows = array_shift($params);
242
243        return $this->_query($query, $offset, $numrows, $params);
244    }
245
246
247    /**
248     * Execute a SQL query with limits
249     *
250     * @param  string $query   SQL query to execute
251     * @param  number $offset  Offset for LIMIT statement
252     * @param  number $numrows Number of rows for LIMIT statement
253     * @param  array  $params  Values to be inserted in query
254     * @return number  Query handle identifier
255     * @access private
256     */
257    private function _query($query, $offset, $numrows, $params)
258    {
259        // Read or write ?
260        $mode = (strtolower(substr(trim($query),0,6)) == 'select') ? 'r' : 'w';
261
262        $this->db_connect($mode);
263
264        // check connection before proceeding
265        if (!$this->is_connected())
266            return null;
267
268        if ($this->db_provider == 'sqlite')
269            $this->_sqlite_prepare();
270
271        if ($numrows || $offset)
272            $result = $this->db_handle->setLimit($numrows,$offset);
273
274        if (empty($params))
275            $result = $mode == 'r' ? $this->db_handle->query($query) : $this->db_handle->exec($query);
276        else {
277            $params = (array)$params;
278            $q = $this->db_handle->prepare($query, null, $mode=='w' ? MDB2_PREPARE_MANIP : null);
279            if ($this->db_handle->isError($q)) {
280                $this->db_error = true;
281                $this->db_error_msg = $q->userinfo;
282
283                raise_error(array('code' => 500, 'type' => 'db',
284                    'line' => __LINE__, 'file' => __FILE__,
285                    'message' => $this->db_error_msg), true, false);
286
287                $result = false;
288            }
289            else {
290                $result = $q->execute($params);
291                $q->free();
292            }
293        }
294
295        // add result, even if it's an error
296        return $this->_add_result($result);
297    }
298
299
300    /**
301     * Get number of rows for a SQL query
302     * If no query handle is specified, the last query will be taken as reference
303     *
304     * @param  number $res_id  Optional query handle identifier
305     * @return mixed   Number of rows or false on failure
306     * @access public
307     */
308    function num_rows($res_id=null)
309    {
310        if (!$this->db_connected)
311            return false;
312
313        if ($result = $this->_get_result($res_id))
314            return $result->numRows();
315        else
316            return false;
317    }
318
319
320    /**
321     * Get number of affected rows for the last query
322     *
323     * @param  number $res_id Optional query handle identifier
324     * @return mixed   Number of rows or false on failure
325     * @access public
326     */
327    function affected_rows($res_id = null)
328    {
329        if (!$this->db_connected)
330            return false;
331
332        return $this->_get_result($res_id);
333    }
334
335
336    /**
337     * Get last inserted record ID
338     * For Postgres databases, a sequence name is required
339     *
340     * @param  string $table  Table name (to find the incremented sequence)
341     * @return mixed   ID or false on failure
342     * @access public
343     */
344    function insert_id($table = '')
345    {
346        if (!$this->db_connected || $this->db_mode == 'r')
347            return false;
348
349        if ($table) {
350            if ($this->db_provider == 'pgsql')
351                // find sequence name
352                $table = get_sequence_name($table);
353            else
354                // resolve table name
355                $table = get_table_name($table);
356        }
357
358        $id = $this->db_handle->lastInsertID($table);
359
360        return $this->db_handle->isError($id) ? null : $id;
361    }
362
363
364    /**
365     * Get an associative array for one row
366     * If no query handle is specified, the last query will be taken as reference
367     *
368     * @param  number $res_id Optional query handle identifier
369     * @return mixed   Array with col values or false on failure
370     * @access public
371     */
372    function fetch_assoc($res_id=null)
373    {
374        $result = $this->_get_result($res_id);
375        return $this->_fetch_row($result, MDB2_FETCHMODE_ASSOC);
376    }
377
378
379    /**
380     * Get an index array for one row
381     * If no query handle is specified, the last query will be taken as reference
382     *
383     * @param  number $res_id  Optional query handle identifier
384     * @return mixed   Array with col values or false on failure
385     * @access public
386     */
387    function fetch_array($res_id=null)
388    {
389        $result = $this->_get_result($res_id);
390        return $this->_fetch_row($result, MDB2_FETCHMODE_ORDERED);
391    }
392
393
394    /**
395     * Get col values for a result row
396     *
397     * @param  MDB2_Result_Common Query $result result handle
398     * @param  number                   $mode   Fetch mode identifier
399     * @return mixed   Array with col values or false on failure
400     * @access private
401     */
402    private function _fetch_row($result, $mode)
403    {
404        if ($result === false || PEAR::isError($result) || !$this->is_connected())
405            return false;
406
407        return $result->fetchRow($mode);
408    }
409
410
411    /**
412     * Wrapper for the SHOW TABLES command
413     *
414     * @return array List of all tables of the current database
415     * @access public
416     * @since 0.4-beta
417     */
418    function list_tables()
419    {
420        // get tables if not cached
421        if (!$this->tables) {
422            $this->db_handle->loadModule('Manager');
423            if (!PEAR::isError($result = $this->db_handle->listTables()))
424                $this->tables = $result;
425            else
426                $this->tables = array();
427        }
428
429        return $this->tables;
430    }
431
432
433    /**
434     * Wrapper for SHOW COLUMNS command
435     *
436     * @param string Table name
437     * @return array List of table cols
438     */
439    function list_cols($table)
440    {
441        $this->db_handle->loadModule('Manager');
442        if (!PEAR::isError($result = $this->db_handle->listTableFields($table))) {
443            return $result;
444        }
445
446        return null;
447    }
448
449
450    /**
451     * Formats input so it can be safely used in a query
452     *
453     * @param  mixed  $input  Value to quote
454     * @param  string $type   Type of data
455     * @return string  Quoted/converted string for use in query
456     * @access public
457     */
458    function quote($input, $type = null)
459    {
460        // handle int directly for better performance
461        if ($type == 'integer')
462            return intval($input);
463
464        // create DB handle if not available
465        if (!$this->db_handle)
466            $this->db_connect('r');
467
468        return $this->db_connected ? $this->db_handle->quote($input, $type) : addslashes($input);
469    }
470
471
472    /**
473     * Quotes a string so it can be safely used as a table or column name
474     *
475     * @param  string $str Value to quote
476     * @return string  Quoted string for use in query
477     * @deprecated     Replaced by rcube_MDB2::quote_identifier
478     * @see            rcube_mdb2::quote_identifier
479     * @access public
480     */
481    function quoteIdentifier($str)
482    {
483        return $this->quote_identifier($str);
484    }
485
486
487    /**
488     * Quotes a string so it can be safely used as a table or column name
489     *
490     * @param  string $str Value to quote
491     * @return string  Quoted string for use in query
492     * @access public
493     */
494    function quote_identifier($str)
495    {
496        if (!$this->db_handle)
497            $this->db_connect('r');
498
499        return $this->db_connected ? $this->db_handle->quoteIdentifier($str) : $str;
500    }
501
502
503    /**
504     * Escapes a string
505     *
506     * @param  string $str The string to be escaped
507     * @return string  The escaped string
508     * @access public
509     * @since  0.1.1
510     */
511    function escapeSimple($str)
512    {
513        if (!$this->db_handle)
514            $this->db_connect('r');
515
516        return $this->db_handle->escape($str);
517    }
518
519
520    /**
521     * Return SQL function for current time and date
522     *
523     * @return string SQL function to use in query
524     * @access public
525     */
526    function now()
527    {
528        switch ($this->db_provider) {
529            case 'mssql':
530            case 'sqlsrv':
531                return "getdate()";
532
533            default:
534                return "now()";
535        }
536    }
537
538
539    /**
540     * Return list of elements for use with SQL's IN clause
541     *
542     * @param  array  $arr  Input array
543     * @param  string $type Type of data
544     * @return string Comma-separated list of quoted values for use in query
545     * @access public
546     */
547    function array2list($arr, $type = null)
548    {
549        if (!is_array($arr))
550            return $this->quote($arr, $type);
551
552        foreach ($arr as $idx => $item)
553            $arr[$idx] = $this->quote($item, $type);
554
555        return implode(',', $arr);
556    }
557
558
559    /**
560     * Return SQL statement to convert a field value into a unix timestamp
561     *
562     * This method is deprecated and should not be used anymore due to limitations
563     * of timestamp functions in Mysql (year 2038 problem)
564     *
565     * @param  string $field Field name
566     * @return string  SQL statement to use in query
567     * @deprecated
568     */
569    function unixtimestamp($field)
570    {
571        switch($this->db_provider) {
572            case 'pgsql':
573                return "EXTRACT (EPOCH FROM $field)";
574
575            case 'mssql':
576            case 'sqlsrv':
577                return "DATEDIFF(second, '19700101', $field) + DATEDIFF(second, GETDATE(), GETUTCDATE())";
578
579            default:
580                return "UNIX_TIMESTAMP($field)";
581        }
582    }
583
584
585    /**
586     * Return SQL statement to convert from a unix timestamp
587     *
588     * @param  string $timestamp Field name
589     * @return string  SQL statement to use in query
590     * @access public
591     */
592    function fromunixtime($timestamp)
593    {
594        return date("'Y-m-d H:i:s'", $timestamp);
595    }
596
597
598    /**
599     * Return SQL statement for case insensitive LIKE
600     *
601     * @param  string $column  Field name
602     * @param  string $value   Search value
603     * @return string  SQL statement to use in query
604     * @access public
605     */
606    function ilike($column, $value)
607    {
608        // TODO: use MDB2's matchPattern() function
609        switch($this->db_provider) {
610            case 'pgsql':
611                return $this->quote_identifier($column).' ILIKE '.$this->quote($value);
612            default:
613                return $this->quote_identifier($column).' LIKE '.$this->quote($value);
614        }
615    }
616
617    /**
618     * Abstract SQL statement for value concatenation
619     *
620     * @return string SQL statement to be used in query
621     * @access public
622     */
623    function concat(/* col1, col2, ... */)
624    {
625        $func = '';
626        $args = func_get_args();
627
628        switch($this->db_provider) {
629            case 'mysql':
630            case 'mysqli':
631                $func = 'CONCAT';
632                $delim = ', ';
633                break;
634            case 'mssql':
635            case 'sqlsrv':
636                $delim = ' + ';
637                break;
638            default:
639                $delim = ' || ';
640        }
641
642        return $func . '(' . join($delim, $args) . ')';
643    }
644
645
646    /**
647     * Encodes non-UTF-8 characters in string/array/object (recursive)
648     *
649     * @param  mixed  $input Data to fix
650     * @return mixed  Properly UTF-8 encoded data
651     * @access public
652     */
653    function encode($input)
654    {
655        if (is_object($input)) {
656            foreach (get_object_vars($input) as $idx => $value)
657                $input->$idx = $this->encode($value);
658            return $input;
659        }
660        else if (is_array($input)) {
661            foreach ($input as $idx => $value)
662                $input[$idx] = $this->encode($value);
663            return $input;     
664        }
665
666        return utf8_encode($input);
667    }
668
669
670    /**
671     * Decodes encoded UTF-8 string/object/array (recursive)
672     *
673     * @param  mixed $input Input data
674     * @return mixed  Decoded data
675     * @access public
676     */
677    function decode($input)
678    {
679        if (is_object($input)) {
680            foreach (get_object_vars($input) as $idx => $value)
681                $input->$idx = $this->decode($value);
682            return $input;
683        }
684        else if (is_array($input)) {
685            foreach ($input as $idx => $value)
686                $input[$idx] = $this->decode($value);
687            return $input;     
688        }
689
690        return utf8_decode($input);
691    }
692
693
694    /**
695     * Adds a query result and returns a handle ID
696     *
697     * @param  object $res Query handle
698     * @return mixed   Handle ID
699     * @access private
700     */
701    private function _add_result($res)
702    {
703        // sql error occured
704        if (PEAR::isError($res)) {
705            $this->db_error = true;
706            $this->db_error_msg = $res->getMessage();
707            raise_error(array('code' => 500, 'type' => 'db',
708                'line' => __LINE__, 'file' => __FILE__,
709                'message' => $res->getMessage() . " Query: " 
710                . substr(preg_replace('/[\r\n]+\s*/', ' ', $res->userinfo), 0, 512)),
711                true, false);
712        }
713
714        $res_id = sizeof($this->a_query_results);
715        $this->last_res_id = $res_id;
716        $this->a_query_results[$res_id] = $res;
717        return $res_id;
718    }
719
720
721    /**
722     * Resolves a given handle ID and returns the according query handle
723     * If no ID is specified, the last resource handle will be returned
724     *
725     * @param  number $res_id Handle ID
726     * @return mixed   Resource handle or false on failure
727     * @access private
728     */
729    private function _get_result($res_id = null)
730    {
731        if ($res_id == null)
732            $res_id = $this->last_res_id;
733
734        if (isset($this->a_query_results[$res_id]))
735            if (!PEAR::isError($this->a_query_results[$res_id]))
736                return $this->a_query_results[$res_id];
737
738        return false;
739    }
740
741
742    /**
743     * Create a sqlite database from a file
744     *
745     * @param  MDB2   $dbh       SQLite database handle
746     * @param  string $file_name File path to use for DB creation
747     * @access private
748     */
749    private function _sqlite_create_database($dbh, $file_name)
750    {
751        if (empty($file_name) || !is_string($file_name))
752            return;
753
754        $data = file_get_contents($file_name);
755
756        if (strlen($data))
757            if (!sqlite_exec($dbh->connection, $data, $error) || MDB2::isError($dbh)) 
758                raise_error(array('code' => 500, 'type' => 'db',
759                    'line' => __LINE__, 'file' => __FILE__,
760                    'message' => $error), true, false); 
761    }
762
763
764    /**
765     * Add some proprietary database functions to the current SQLite handle
766     * in order to make it MySQL compatible
767     *
768     * @access private
769     */
770    private function _sqlite_prepare()
771    {
772        include_once(INSTALL_PATH . 'program/include/rcube_sqlite.inc');
773
774        // we emulate via callback some missing MySQL function
775        sqlite_create_function($this->db_handle->connection,
776            'from_unixtime', 'rcube_sqlite_from_unixtime');
777        sqlite_create_function($this->db_handle->connection,
778            'unix_timestamp', 'rcube_sqlite_unix_timestamp');
779        sqlite_create_function($this->db_handle->connection,
780            'now', 'rcube_sqlite_now');
781        sqlite_create_function($this->db_handle->connection,
782            'md5', 'rcube_sqlite_md5');
783    }
784
785
786    /**
787     * Debug handler for the MDB2
788     */
789    function debug_handler(&$db, $scope, $message, $context = array())
790    {
791        if ($scope != 'prepare') {
792            $debug_output = sprintf('%s(%d): %s;',
793                $scope, $db->db_index, rtrim($message, ';'));
794            write_log('sql', $debug_output);
795        }
796    }
797
798}  // end class rcube_db
Note: See TracBrowser for help on using the repository browser.