]> git.donarmstrong.com Git - roundcube.git/blob - program/include/rcube_mdb2.inc
3f13a75e10e326f203bc0b05cd9f777e24c20a3f
[roundcube.git] / program / include / rcube_mdb2.inc
1 <?php
2
3 /*
4  +-----------------------------------------------------------------------+
5  | program/include/rcube_mdb2.inc                                        |
6  |                                                                       |
7  | This file is part of the RoundCube Webmail client                     |
8  | Copyright (C) 2005-2007, 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.inc 1255 2008-04-05 12:49:21Z thomasb $
20
21 */
22
23
24 /**
25  * Obtain the PEAR::DB class that is used for abstraction
26  */
27 require_once('MDB2.php');
28
29
30 /**
31  * Database independent query interface
32  *
33  * This is a wrapper for the PEAR::MDB2 class
34  *
35  * @package    Database
36  * @author     David Saez Padros <david@ols.es>
37  * @author     Thomas Bruederli <roundcube@gmail.com>
38  * @author     Lukas Kahwe Smith <smith@pooteeweet.org>
39  * @version    1.16
40  * @link       http://pear.php.net/package/MDB2
41  */
42 class rcube_mdb2
43   {
44   var $db_dsnw;               // DSN for write operations
45   var $db_dsnr;               // DSN for read operations
46   var $db_connected = false;  // Already connected ?
47   var $db_mode = '';          // Connection mode
48   var $db_handle = 0;         // Connection handle
49   var $db_error = false;
50   var $db_error_msg = '';
51   var $debug_mode = false;
52
53   var $a_query_results = array('dummy');
54   var $last_res_id = 0;
55
56
57   /**
58    * Object constructor
59    *
60    * @param  string  DSN for read/write operations
61    * @param  string  Optional DSN for read only operations
62    */
63   function __construct($db_dsnw, $db_dsnr='', $pconn=false)
64     {
65     if ($db_dsnr=='')
66       $db_dsnr=$db_dsnw;
67
68     $this->db_dsnw = $db_dsnw;
69     $this->db_dsnr = $db_dsnr;
70     $this->db_pconn = $pconn;
71     
72     $dsn_array = MDB2::parseDSN($db_dsnw);
73     $this->db_provider = $dsn_array['phptype'];
74     }
75
76
77   /**
78    * PHP 4 object constructor
79    *
80    * @see  rcube_mdb2::__construct
81    */
82   function rcube_db($db_dsnw,$db_dsnr='')
83     {
84     $this->__construct($db_dsnw,$db_dsnr);
85     }
86
87
88   /**
89    * Connect to specific database
90    *
91    * @param  string  DSN for DB connections
92    * @return object  PEAR database handle
93    * @access private
94    */
95   function dsn_connect($dsn)
96     {
97     // Use persistent connections if available
98     $dbh = MDB2::connect($dsn, array(
99         'persistent' => $this->db_pconn,
100         'emulate_prepared' => $this->debug_mode,
101         'debug' => $this->debug_mode,
102         'debug_handler' => 'mdb2_debug_handler',
103         'portability' => MDB2_PORTABILITY_ALL ^ MDB2_PORTABILITY_EMPTY_TO_NULL));
104
105     if (MDB2::isError($dbh))
106       {
107       $this->db_error = TRUE;
108       $this->db_error_msg = $dbh->getMessage();
109       
110       raise_error(array('code' => 500, 'type' => 'db', 'line' => __LINE__,
111         'file' => __FILE__, 'message' => $dbh->getUserInfo()), TRUE, FALSE);
112       }
113     else if ($this->db_provider=='sqlite')
114       {
115       $dsn_array = MDB2::parseDSN($dsn);
116       if (!filesize($dsn_array['database']) && !empty($this->sqlite_initials))
117         $this->_sqlite_create_database($dbh, $this->sqlite_initials);
118       }
119     else
120       $dbh->setCharset('utf8');
121
122     return $dbh;
123     }
124
125
126   /**
127    * Connect to appropiate databse
128    * depending on the operation
129    *
130    * @param  string  Connection mode (r|w)
131    * @access public
132    */
133   function db_connect($mode)
134     {
135     $this->db_mode = $mode;
136
137     // Already connected
138     if ($this->db_connected)
139       {
140       // no replication, current connection is ok
141       if ($this->db_dsnw==$this->db_dsnr)
142         return;
143
144       // connected to master, current connection is ok
145       if ($this->db_mode=='w')
146         return;
147
148       // Same mode, current connection is ok
149       if ($this->db_mode==$mode)
150         return;
151       }
152
153     if ($mode=='r')
154       $dsn = $this->db_dsnr;
155     else
156       $dsn = $this->db_dsnw;
157
158     $this->db_handle = $this->dsn_connect($dsn);
159     $this->db_connected = true;
160     }
161
162
163   /**
164    * Activate/deactivate debug mode
165    *
166    * @param boolean True if SQL queries should be logged
167    */
168   function set_debug($dbg = true)
169   {
170     $this->debug_mode = $dbg;
171     if ($this->db_connected)
172     {
173       $this->db_handle->setOption('debug', $dbg);
174       $this->db_handle->setOption('emulate_prepared', $dbg);
175     }
176   }
177
178     
179   /**
180    * Getter for error state
181    *
182    * @param  boolean  True on error
183    */
184   function is_error()
185     {
186     return $this->db_error ? $this->db_error_msg : FALSE;
187     }
188     
189
190   /**
191    * Execute a SQL query
192    *
193    * @param  string  SQL query to execute
194    * @param  mixed   Values to be inserted in query
195    * @return number  Query handle identifier
196    * @access public
197    */
198   function query()
199     {
200     $params = func_get_args();
201     $query = array_shift($params);
202
203     return $this->_query($query, 0, 0, $params);
204     }
205
206
207   /**
208    * Execute a SQL query with limits
209    *
210    * @param  string  SQL query to execute
211    * @param  number  Offset for LIMIT statement
212    * @param  number  Number of rows for LIMIT statement
213    * @param  mixed   Values to be inserted in query
214    * @return number  Query handle identifier
215    * @access public
216    */
217   function limitquery()
218     {
219     $params = func_get_args();
220     $query = array_shift($params);
221     $offset = array_shift($params);
222     $numrows = array_shift($params);
223
224     return $this->_query($query, $offset, $numrows, $params);
225     }
226
227
228   /**
229    * Execute a SQL query with limits
230    *
231    * @param  string  SQL query to execute
232    * @param  number  Offset for LIMIT statement
233    * @param  number  Number of rows for LIMIT statement
234    * @param  array   Values to be inserted in query
235    * @return number  Query handle identifier
236    * @access private
237    */
238   function _query($query, $offset, $numrows, $params)
239     {
240     // Read or write ?
241     if (strtolower(trim(substr($query,0,6)))=='select')
242       $mode='r';
243     else
244       $mode='w';
245
246     $this->db_connect($mode);
247
248     if ($this->db_provider == 'sqlite')
249       $this->_sqlite_prepare();
250
251     if ($numrows || $offset)
252       $result = $this->db_handle->setLimit($numrows,$offset);
253
254     if (empty($params))
255         $result = $this->db_handle->query($query);
256     else
257       {
258       $params = (array)$params;
259       $q = $this->db_handle->prepare($query);
260       if ($this->db_handle->isError($q))
261         {
262         $this->db_error = TRUE;
263         $this->db_error_msg = $q->userinfo;
264
265         raise_error(array('code' => 500, 'type' => 'db', 'line' => __LINE__, 'file' => __FILE__,
266                           'message' => $this->db_error_msg), TRUE, TRUE);
267         }
268       else
269         {
270         $result = $q->execute($params);
271         $q->free();
272         }
273       }
274
275     // add result, even if it's an error
276     return $this->_add_result($result);
277     }
278
279
280   /**
281    * Get number of rows for a SQL query
282    * If no query handle is specified, the last query will be taken as reference
283    *
284    * @param  number  Optional query handle identifier
285    * @return mixed   Number of rows or FALSE on failure
286    * @access public
287    */
288   function num_rows($res_id=NULL)
289     {
290     if (!$this->db_handle)
291       return FALSE;
292
293     if ($result = $this->_get_result($res_id))
294       return $result->numRows();
295     else
296       return FALSE;
297     }
298
299
300   /**
301    * Get number of affected rows fort he last query
302    *
303    * @return mixed   Number of rows or FALSE on failure
304    * @access public
305    */
306   function affected_rows($result = null)
307     {
308     if (!$this->db_handle)
309       return FALSE;
310
311     return $this->_get_result($result);
312     }
313
314
315   /**
316    * Get last inserted record ID
317    * For Postgres databases, a sequence name is required
318    *
319    * @param  string  Sequence name for increment
320    * @return mixed   ID or FALSE on failure
321    * @access public
322    */
323   function insert_id($sequence = '')
324     {
325     if (!$this->db_handle || $this->db_mode=='r')
326       return FALSE;
327
328     return $this->db_handle->lastInsertID($sequence);
329     }
330
331
332   /**
333    * Get an associative array for one row
334    * If no query handle is specified, the last query will be taken as reference
335    *
336    * @param  number  Optional query handle identifier
337    * @return mixed   Array with col values or FALSE on failure
338    * @access public
339    */
340   function fetch_assoc($res_id=NULL)
341     {
342     $result = $this->_get_result($res_id);
343     return $this->_fetch_row($result, MDB2_FETCHMODE_ASSOC);
344     }
345
346
347   /**
348    * Get an index array for one row
349    * If no query handle is specified, the last query will be taken as reference
350    *
351    * @param  number  Optional query handle identifier
352    * @return mixed   Array with col values or FALSE on failure
353    * @access public
354    */
355   function fetch_array($res_id=NULL)
356     {
357     $result = $this->_get_result($res_id);
358     return $this->_fetch_row($result, MDB2_FETCHMODE_ORDERED);
359     }
360
361
362   /**
363    * Get co values for a result row
364    *
365    * @param  object  Query result handle
366    * @param  number  Fetch mode identifier
367    * @return mixed   Array with col values or FALSE on failure
368    * @access private
369    */
370   function _fetch_row($result, $mode)
371     {
372     if (PEAR::isError($result))
373       {
374       raise_error(array('code' => 500, 'type' => 'db', 'line' => __LINE__, 'file' => __FILE__,
375                         'message' => $this->db_link->getMessage()), TRUE, FALSE);
376       return FALSE;
377       }
378
379     return $result->fetchRow($mode);
380     }
381
382
383   /**
384    * Formats input so it can be safely used in a query
385    *
386    * @param  mixed   Value to quote
387    * @return string  Quoted/converted string for use in query
388    * @access public
389    */
390   function quote($input, $type = null)
391     {
392     // create DB handle if not available
393     if (!$this->db_handle)
394       $this->db_connect('r');
395
396     // escape pear identifier chars
397     $rep_chars = array('?' => '\?',
398                        '!' => '\!',
399                        '&' => '\&');
400
401     return $this->db_handle->quote($input, $type);
402     }
403
404
405   /**
406    * Quotes a string so it can be safely used as a table or column name
407    *
408    * @param  string  Value to quote
409    * @return string  Quoted string for use in query
410    * @deprecated     Replaced by rcube_MDB2::quote_identifier
411    * @see            rcube_MDB2::quote_identifier
412    * @access public
413    */
414   function quoteIdentifier($str)
415         {
416     return $this->quote_identifier($str);
417         }
418
419
420   /**
421    * Quotes a string so it can be safely used as a table or column name
422    *
423    * @param  string  Value to quote
424    * @return string  Quoted string for use in query
425    * @access public
426    */
427   function quote_identifier($str)
428     {
429     if (!$this->db_handle)
430       $this->db_connect('r');
431
432     return $this->db_handle->quoteIdentifier($str);
433     }
434
435   /**
436    * Escapes a string
437    *
438    * @param  string  The string to be escaped
439    * @return string  The escaped string
440    * @access public
441    * @since  0.1.1
442    */
443   function escapeSimple($str)
444     {
445     if (!$this->db_handle)
446       $this->db_connect('r');
447    
448     return $this->db_handle->escape($str);
449     }
450
451
452   /**
453    * Return SQL function for current time and date
454    *
455    * @return string SQL function to use in query
456    * @access public
457    */
458   function now()
459     {
460     switch($this->db_provider)
461       {
462       case 'mssql':
463         return "getdate()";
464
465       default:
466         return "now()";
467       }
468     }
469
470
471   /**
472    * Return SQL statement to convert a field value into a unix timestamp
473    *
474    * @param  string  Field name
475    * @return string  SQL statement to use in query
476    * @access public
477    */
478   function unixtimestamp($field)
479     {
480     switch($this->db_provider)
481       {
482       case 'pgsql':
483         return "EXTRACT (EPOCH FROM $field)";
484         break;
485
486       case 'mssql':
487         return "datediff(s, '1970-01-01 00:00:00', $field)";
488
489       default:
490         return "UNIX_TIMESTAMP($field)";
491       }
492     }
493
494
495   /**
496    * Return SQL statement to convert from a unix timestamp
497    *
498    * @param  string  Field name
499    * @return string  SQL statement to use in query
500    * @access public
501    */
502   function fromunixtime($timestamp)
503     {
504     switch($this->db_provider)
505       {
506       case 'mysqli':
507       case 'mysql':
508       case 'sqlite':
509         return "FROM_UNIXTIME($timestamp)";
510
511       default:
512         return date("'Y-m-d H:i:s'", $timestamp);
513       }
514     }
515
516
517   /**
518    * Adds a query result and returns a handle ID
519    *
520    * @param  object  Query handle
521    * @return mixed   Handle ID or FALE on failure
522    * @access private
523    */
524   function _add_result($res)
525     {
526     // sql error occured
527     if (PEAR::isError($res))
528       {
529       raise_error(array('code' => 500, 'type' => 'db', 'line' => __LINE__, 'file' => __FILE__,
530                         'message' => $res->getMessage() . " Query: " . substr(preg_replace('/[\r\n]+\s*/', ' ', $res->userinfo), 0, 512)), TRUE, FALSE);
531       return FALSE;
532       }
533     else
534       {
535       $res_id = sizeof($this->a_query_results);
536       $this->a_query_results[$res_id] = $res;
537       $this->last_res_id = $res_id;
538       return $res_id;
539       }
540     }
541
542
543   /**
544    * Resolves a given handle ID and returns the according query handle
545    * If no ID is specified, the last ressource handle will be returned
546    *
547    * @param  number  Handle ID
548    * @return mixed   Ressource handle or FALE on failure
549    * @access private
550    */
551   function _get_result($res_id=NULL)
552     {
553     if ($res_id==NULL)
554       $res_id = $this->last_res_id;
555
556      if ($res_id && isset($this->a_query_results[$res_id]))
557        return $this->a_query_results[$res_id];
558      else
559        return FALSE;
560     }
561
562
563   /**
564    * Create a sqlite database from a file
565    *
566    * @param  object  SQLite database handle
567    * @param  string  File path to use for DB creation
568    * @access private
569    */
570   function _sqlite_create_database($dbh, $file_name)
571     {
572     if (empty($file_name) || !is_string($file_name))
573       return;
574
575     $data = '';
576     if ($fd = fopen($file_name, 'r'))
577       {
578       $data = fread($fd, filesize($file_name));
579       fclose($fd);
580       }
581
582     if (strlen($data))
583       sqlite_exec($dbh->connection, $data);
584     }
585
586
587   /**
588    * Add some proprietary database functions to the current SQLite handle
589    * in order to make it MySQL compatible
590    *
591    * @access private
592    */
593   function _sqlite_prepare()
594     {
595     include_once('include/rcube_sqlite.inc');
596
597     // we emulate via callback some missing MySQL function
598     sqlite_create_function($this->db_handle->connection, "from_unixtime", "rcube_sqlite_from_unixtime");
599     sqlite_create_function($this->db_handle->connection, "unix_timestamp", "rcube_sqlite_unix_timestamp");
600     sqlite_create_function($this->db_handle->connection, "now", "rcube_sqlite_now");
601     sqlite_create_function($this->db_handle->connection, "md5", "rcube_sqlite_md5");
602     }
603
604
605   }  // end class rcube_db
606
607
608 /* this is our own debug handler for the MDB2 connection */
609 function mdb2_debug_handler(&$db, $scope, $message, $context = array())
610 {
611   if ($scope != 'prepare')
612   {
613     $debug_output = $scope . '('.$db->db_index.'): ';
614     $debug_output .= $message . $db->getOption('log_line_break');
615     write_log('sqllog', $debug_output);
616   }
617 }
618
619
620 ?>