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