]> git.donarmstrong.com Git - roundcube.git/blob - program/include/rcube_mdb2.php
Imported Upstream version 0.2.1
[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 2237 2009-01-17 01:55:39Z till $
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
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    * Execute a SQL query
182    *
183    * @param  string  SQL query to execute
184    * @param  mixed   Values to be inserted in query
185    * @return number  Query handle identifier
186    * @access public
187    */
188   function query()
189     {
190     $params = func_get_args();
191     $query = array_shift($params);
192
193     return $this->_query($query, 0, 0, $params);
194     }
195
196
197   /**
198    * Execute a SQL query with limits
199    *
200    * @param  string  SQL query to execute
201    * @param  number  Offset for LIMIT statement
202    * @param  number  Number of rows for LIMIT statement
203    * @param  mixed   Values to be inserted in query
204    * @return number  Query handle identifier
205    * @access public
206    */
207   function limitquery()
208     {
209     $params = func_get_args();
210     $query = array_shift($params);
211     $offset = array_shift($params);
212     $numrows = array_shift($params);
213
214     return $this->_query($query, $offset, $numrows, $params);
215     }
216
217
218   /**
219    * Execute a SQL query with limits
220    *
221    * @param  string  SQL query to execute
222    * @param  number  Offset for LIMIT statement
223    * @param  number  Number of rows for LIMIT statement
224    * @param  array   Values to be inserted in query
225    * @return number  Query handle identifier
226    * @access private
227    */
228   function _query($query, $offset, $numrows, $params)
229     {
230     // Read or write ?
231     if (strtolower(trim(substr($query,0,6)))=='select')
232       $mode='r';
233     else
234       $mode='w';
235
236     $this->db_connect($mode);
237
238     if ($this->db_provider == 'sqlite')
239       $this->_sqlite_prepare();
240
241     if ($numrows || $offset)
242       $result = $this->db_handle->setLimit($numrows,$offset);
243
244     if (empty($params))
245         $result = $this->db_handle->query($query);
246     else
247       {
248       $params = (array)$params;
249       $q = $this->db_handle->prepare($query);
250       if ($this->db_handle->isError($q))
251         {
252         $this->db_error = TRUE;
253         $this->db_error_msg = $q->userinfo;
254
255         raise_error(array('code' => 500, 'type' => 'db', 'line' => __LINE__, 'file' => __FILE__,
256                           'message' => $this->db_error_msg), TRUE, TRUE);
257         }
258       else
259         {
260         $result = $q->execute($params);
261         $q->free();
262         }
263       }
264
265     // add result, even if it's an error
266     return $this->_add_result($result);
267     }
268
269
270   /**
271    * Get number of rows for a SQL query
272    * If no query handle is specified, the last query will be taken as reference
273    *
274    * @param  number  Optional query handle identifier
275    * @return mixed   Number of rows or FALSE on failure
276    * @access public
277    */
278   function num_rows($res_id=NULL)
279     {
280     if (!$this->db_handle)
281       return FALSE;
282
283     if ($result = $this->_get_result($res_id))
284       return $result->numRows();
285     else
286       return FALSE;
287     }
288
289
290   /**
291    * Get number of affected rows for the last query
292    *
293    * @param  number  Optional query handle identifier
294    * @return mixed   Number of rows or FALSE on failure
295    * @access public
296    */
297   function affected_rows($res_id = null)
298     {
299     if (!$this->db_handle)
300       return FALSE;
301
302     return (int) $this->_get_result($res_id);
303     }
304
305
306   /**
307    * Get last inserted record ID
308    * For Postgres databases, a sequence name is required
309    *
310    * @param  string  Sequence name for increment
311    * @return mixed   ID or FALSE on failure
312    * @access public
313    */
314   function insert_id($sequence = '')
315     {
316     if (!$this->db_handle || $this->db_mode=='r')
317       return FALSE;
318
319     return $this->db_handle->lastInsertID($sequence);
320     }
321
322
323   /**
324    * Get an associative array for one row
325    * If no query handle is specified, the last query will be taken as reference
326    *
327    * @param  number  Optional query handle identifier
328    * @return mixed   Array with col values or FALSE on failure
329    * @access public
330    */
331   function fetch_assoc($res_id=NULL)
332     {
333     $result = $this->_get_result($res_id);
334     return $this->_fetch_row($result, MDB2_FETCHMODE_ASSOC);
335     }
336
337
338   /**
339    * Get an index array for one row
340    * If no query handle is specified, the last query will be taken as reference
341    *
342    * @param  number  Optional query handle identifier
343    * @return mixed   Array with col values or FALSE on failure
344    * @access public
345    */
346   function fetch_array($res_id=NULL)
347     {
348     $result = $this->_get_result($res_id);
349     return $this->_fetch_row($result, MDB2_FETCHMODE_ORDERED);
350     }
351
352
353   /**
354    * Get col values for a result row
355    *
356    * @param  object  Query result handle
357    * @param  number  Fetch mode identifier
358    * @return mixed   Array with col values or FALSE on failure
359    * @access private
360    */
361   function _fetch_row($result, $mode)
362     {
363     if ($result === FALSE || PEAR::isError($result))
364       return FALSE;
365
366     return $result->fetchRow($mode);
367     }
368
369
370   /**
371    * Formats input so it can be safely used in a query
372    *
373    * @param  mixed   Value to quote
374    * @return string  Quoted/converted string for use in query
375    * @access public
376    */
377   function quote($input, $type = null)
378     {
379     // create DB handle if not available
380     if (!$this->db_handle)
381       $this->db_connect('r');
382
383     // escape pear identifier chars
384     $rep_chars = array('?' => '\?',
385                        '!' => '\!',
386                        '&' => '\&');
387
388     return $this->db_handle->quote($input, $type);
389     }
390
391
392   /**
393    * Quotes a string so it can be safely used as a table or column name
394    *
395    * @param  string  Value to quote
396    * @return string  Quoted string for use in query
397    * @deprecated     Replaced by rcube_MDB2::quote_identifier
398    * @see            rcube_mdb2::quote_identifier
399    * @access public
400    */
401   function quoteIdentifier($str)
402     {
403     return $this->quote_identifier($str);
404     }
405
406
407   /**
408    * Quotes a string so it can be safely used as a table or column name
409    *
410    * @param  string  Value to quote
411    * @return string  Quoted string for use in query
412    * @access public
413    */
414   function quote_identifier($str)
415     {
416     if (!$this->db_handle)
417       $this->db_connect('r');
418
419     return $this->db_handle->quoteIdentifier($str);
420     }
421
422   /**
423    * Escapes a string
424    *
425    * @param  string  The string to be escaped
426    * @return string  The escaped string
427    * @access public
428    * @since  0.1.1
429    */
430   function escapeSimple($str)
431     {
432     if (!$this->db_handle)
433       $this->db_connect('r');
434    
435     return $this->db_handle->escape($str);
436     }
437
438
439   /**
440    * Return SQL function for current time and date
441    *
442    * @return string SQL function to use in query
443    * @access public
444    */
445   function now()
446     {
447     switch($this->db_provider)
448       {
449       case 'mssql':
450         return "getdate()";
451
452       default:
453         return "now()";
454       }
455     }
456
457
458   /**
459    * Return SQL statement to convert a field value into a unix timestamp
460    *
461    * @param  string  Field name
462    * @return string  SQL statement to use in query
463    * @access public
464    */
465   function unixtimestamp($field)
466     {
467     switch($this->db_provider)
468       {
469       case 'pgsql':
470         return "EXTRACT (EPOCH FROM $field)";
471         break;
472
473       case 'mssql':
474         return "datediff(s, '1970-01-01 00:00:00', $field)";
475
476       default:
477         return "UNIX_TIMESTAMP($field)";
478       }
479     }
480
481
482   /**
483    * Return SQL statement to convert from a unix timestamp
484    *
485    * @param  string  Field name
486    * @return string  SQL statement to use in query
487    * @access public
488    */
489   function fromunixtime($timestamp)
490     {
491     switch($this->db_provider)
492       {
493       case 'mysqli':
494       case 'mysql':
495       case 'sqlite':
496         return sprintf("FROM_UNIXTIME(%d)", $timestamp);
497
498       default:
499         return date("'Y-m-d H:i:s'", $timestamp);
500       }
501     }
502
503
504   /**
505    * Return SQL statement for case insensitive LIKE
506    *
507    * @param  string  Field name
508    * @param  string  Search value
509    * @return string  SQL statement to use in query
510    * @access public
511    */
512   function ilike($column, $value)
513     {
514     // TODO: use MDB2's matchPattern() function
515     switch($this->db_provider)
516       {
517       case 'pgsql':
518         return $this->quote_identifier($column).' ILIKE '.$this->quote($value);
519       default:
520         return $this->quote_identifier($column).' LIKE '.$this->quote($value);
521       }
522     }
523
524
525   /**
526    * Adds a query result and returns a handle ID
527    *
528    * @param  object  Query handle
529    * @return mixed   Handle ID
530    * @access private
531    */
532   function _add_result($res)
533     {
534     // sql error occured
535     if (PEAR::isError($res))
536       {
537       $this->db_error = TRUE;
538       $this->db_error_msg = $res->getMessage();
539       raise_error(array('code' => 500, 'type' => 'db', 'line' => __LINE__, 'file' => __FILE__,
540             'message' => $res->getMessage() . " Query: " 
541             . substr(preg_replace('/[\r\n]+\s*/', ' ', $res->userinfo), 0, 512)),
542             TRUE, FALSE);
543       }
544     
545     $res_id = sizeof($this->a_query_results);
546     $this->last_res_id = $res_id;
547     $this->a_query_results[$res_id] = $res;
548     return $res_id;
549     }
550
551
552   /**
553    * Resolves a given handle ID and returns the according query handle
554    * If no ID is specified, the last resource handle will be returned
555    *
556    * @param  number  Handle ID
557    * @return mixed   Resource handle or FALSE on failure
558    * @access private
559    */
560   function _get_result($res_id=NULL)
561     {
562     if ($res_id==NULL)
563       $res_id = $this->last_res_id;
564
565     if (isset($this->a_query_results[$res_id]))
566       if (!PEAR::isError($this->a_query_results[$res_id]))
567         return $this->a_query_results[$res_id];
568     
569     return FALSE;
570     }
571
572
573   /**
574    * Create a sqlite database from a file
575    *
576    * @param  object  SQLite database handle
577    * @param  string  File path to use for DB creation
578    * @access private
579    */
580   function _sqlite_create_database($dbh, $file_name)
581     {
582     if (empty($file_name) || !is_string($file_name))
583       return;
584
585     $data = file_get_contents($file_name);
586
587     if (strlen($data))
588       sqlite_exec($dbh->connection, $data);
589     }
590
591
592   /**
593    * Add some proprietary database functions to the current SQLite handle
594    * in order to make it MySQL compatible
595    *
596    * @access private
597    */
598   function _sqlite_prepare()
599     {
600     include_once('include/rcube_sqlite.inc');
601
602     // we emulate via callback some missing MySQL function
603     sqlite_create_function($this->db_handle->connection, "from_unixtime", "rcube_sqlite_from_unixtime");
604     sqlite_create_function($this->db_handle->connection, "unix_timestamp", "rcube_sqlite_unix_timestamp");
605     sqlite_create_function($this->db_handle->connection, "now", "rcube_sqlite_now");
606     sqlite_create_function($this->db_handle->connection, "md5", "rcube_sqlite_md5");
607     }
608
609
610   }  // end class rcube_db
611
612
613 /* this is our own debug handler for the MDB2 connection */
614 function mdb2_debug_handler(&$db, $scope, $message, $context = array())
615 {
616   if ($scope != 'prepare')
617   {
618     $debug_output = $scope . '('.$db->db_index.'): ';
619     $debug_output .= $message . $db->getOption('log_line_break');
620     write_log('sqllog', $debug_output);
621   }
622 }
623
624