]> git.donarmstrong.com Git - roundcube.git/blob - program/include/rcube_mdb2.php
Imported Upstream version 0.5.2+dfsg
[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 4643 2011-04-11 12:24:00Z 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         return date("'Y-m-d H:i:s'", $timestamp);
557     }
558
559
560     /**
561      * Return SQL statement for case insensitive LIKE
562      *
563      * @param  string $column  Field name
564      * @param  string $value   Search value
565      * @return string  SQL statement to use in query
566      * @access public
567      */
568     function ilike($column, $value)
569     {
570         // TODO: use MDB2's matchPattern() function
571         switch($this->db_provider) {
572             case 'pgsql':
573                 return $this->quote_identifier($column).' ILIKE '.$this->quote($value);
574             default:
575                 return $this->quote_identifier($column).' LIKE '.$this->quote($value);
576         }
577     }
578
579
580     /**
581      * Encodes non-UTF-8 characters in string/array/object (recursive)
582      *
583      * @param  mixed  $input Data to fix
584      * @return mixed  Properly UTF-8 encoded data
585      * @access public
586      */
587     function encode($input)
588     {
589         if (is_object($input)) {
590             foreach (get_object_vars($input) as $idx => $value)
591                 $input->$idx = $this->encode($value);
592             return $input;
593         }
594         else if (is_array($input)) {
595             foreach ($input as $idx => $value)
596                 $input[$idx] = $this->encode($value);
597             return $input;      
598         }
599
600         return utf8_encode($input);
601     }
602
603
604     /**
605      * Decodes encoded UTF-8 string/object/array (recursive)
606      *
607      * @param  mixed $input Input data
608      * @return mixed  Decoded data
609      * @access public
610      */
611     function decode($input)
612     {
613         if (is_object($input)) {
614             foreach (get_object_vars($input) as $idx => $value)
615                 $input->$idx = $this->decode($value);
616             return $input;
617         }
618         else if (is_array($input)) {
619             foreach ($input as $idx => $value)
620                 $input[$idx] = $this->decode($value);
621             return $input;      
622         }
623
624         return utf8_decode($input);
625     }
626
627
628     /**
629      * Adds a query result and returns a handle ID
630      *
631      * @param  object $res Query handle
632      * @return mixed   Handle ID
633      * @access private
634      */
635     private function _add_result($res)
636     {
637         // sql error occured
638         if (PEAR::isError($res)) {
639             $this->db_error = true;
640             $this->db_error_msg = $res->getMessage();
641             raise_error(array('code' => 500, 'type' => 'db',
642                 'line' => __LINE__, 'file' => __FILE__,
643                 'message' => $res->getMessage() . " Query: " 
644                 . substr(preg_replace('/[\r\n]+\s*/', ' ', $res->userinfo), 0, 512)),
645                 true, false);
646         }
647
648         $res_id = sizeof($this->a_query_results);
649         $this->last_res_id = $res_id;
650         $this->a_query_results[$res_id] = $res;
651         return $res_id;
652     }
653
654
655     /**
656      * Resolves a given handle ID and returns the according query handle
657      * If no ID is specified, the last resource handle will be returned
658      *
659      * @param  number $res_id Handle ID
660      * @return mixed   Resource handle or false on failure
661      * @access private
662      */
663     private function _get_result($res_id = null)
664     {
665         if ($res_id == null)
666             $res_id = $this->last_res_id;
667
668         if (isset($this->a_query_results[$res_id]))
669             if (!PEAR::isError($this->a_query_results[$res_id]))
670                 return $this->a_query_results[$res_id];
671
672         return false;
673     }
674
675
676     /**
677      * Create a sqlite database from a file
678      *
679      * @param  MDB2   $dbh       SQLite database handle
680      * @param  string $file_name File path to use for DB creation
681      * @access private
682      */
683     private function _sqlite_create_database($dbh, $file_name)
684     {
685         if (empty($file_name) || !is_string($file_name))
686             return;
687
688         $data = file_get_contents($file_name);
689
690         if (strlen($data))
691             if (!sqlite_exec($dbh->connection, $data, $error) || MDB2::isError($dbh)) 
692                 raise_error(array('code' => 500, 'type' => 'db',
693                     'line' => __LINE__, 'file' => __FILE__,
694                     'message' => $error), true, false); 
695     }
696
697
698     /**
699      * Add some proprietary database functions to the current SQLite handle
700      * in order to make it MySQL compatible
701      *
702      * @access private
703      */
704     private function _sqlite_prepare()
705     {
706         include_once('include/rcube_sqlite.inc');
707
708         // we emulate via callback some missing MySQL function
709         sqlite_create_function($this->db_handle->connection,
710             'from_unixtime', 'rcube_sqlite_from_unixtime');
711         sqlite_create_function($this->db_handle->connection,
712             'unix_timestamp', 'rcube_sqlite_unix_timestamp');
713         sqlite_create_function($this->db_handle->connection,
714             'now', 'rcube_sqlite_now');
715         sqlite_create_function($this->db_handle->connection,
716             'md5', 'rcube_sqlite_md5');
717     }
718
719 }  // end class rcube_db
720
721
722 /* this is our own debug handler for the MDB2 connection */
723 function mdb2_debug_handler(&$db, $scope, $message, $context = array())
724 {
725     if ($scope != 'prepare') {
726         $debug_output = sprintf('%s(%d): %s;',
727             $scope, $db->db_index, rtrim($message, ';'));
728         write_log('sql', $debug_output);
729     }
730 }
731