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