]> git.donarmstrong.com Git - roundcube.git/blob - program/include/rcube_mdb2.php
Imported Upstream version 0.2~stable
[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-2008, 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 1978 2008-10-14 12:49:44Z thomasb $
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 fort he last query
292    *
293    * @return mixed   Number of rows or FALSE on failure
294    * @access public
295    */
296   function affected_rows($result = null)
297     {
298     if (!$this->db_handle)
299       return FALSE;
300
301     return $this->_get_result($result);
302     }
303
304
305   /**
306    * Get last inserted record ID
307    * For Postgres databases, a sequence name is required
308    *
309    * @param  string  Sequence name for increment
310    * @return mixed   ID or FALSE on failure
311    * @access public
312    */
313   function insert_id($sequence = '')
314     {
315     if (!$this->db_handle || $this->db_mode=='r')
316       return FALSE;
317
318     return $this->db_handle->lastInsertID($sequence);
319     }
320
321
322   /**
323    * Get an associative array for one row
324    * If no query handle is specified, the last query will be taken as reference
325    *
326    * @param  number  Optional query handle identifier
327    * @return mixed   Array with col values or FALSE on failure
328    * @access public
329    */
330   function fetch_assoc($res_id=NULL)
331     {
332     $result = $this->_get_result($res_id);
333     return $this->_fetch_row($result, MDB2_FETCHMODE_ASSOC);
334     }
335
336
337   /**
338    * Get an index array for one row
339    * If no query handle is specified, the last query will be taken as reference
340    *
341    * @param  number  Optional query handle identifier
342    * @return mixed   Array with col values or FALSE on failure
343    * @access public
344    */
345   function fetch_array($res_id=NULL)
346     {
347     $result = $this->_get_result($res_id);
348     return $this->_fetch_row($result, MDB2_FETCHMODE_ORDERED);
349     }
350
351
352   /**
353    * Get co values for a result row
354    *
355    * @param  object  Query result handle
356    * @param  number  Fetch mode identifier
357    * @return mixed   Array with col values or FALSE on failure
358    * @access private
359    */
360   function _fetch_row($result, $mode)
361     {
362     if (PEAR::isError($result))
363       {
364       raise_error(array('code' => 500, 'type' => 'db', 'line' => __LINE__, 'file' => __FILE__,
365                         'message' => $this->db_link->getMessage()), TRUE, FALSE);
366       return FALSE;
367       }
368
369     return $result->fetchRow($mode);
370     }
371
372
373   /**
374    * Formats input so it can be safely used in a query
375    *
376    * @param  mixed   Value to quote
377    * @return string  Quoted/converted string for use in query
378    * @access public
379    */
380   function quote($input, $type = null)
381     {
382     // create DB handle if not available
383     if (!$this->db_handle)
384       $this->db_connect('r');
385
386     // escape pear identifier chars
387     $rep_chars = array('?' => '\?',
388                        '!' => '\!',
389                        '&' => '\&');
390
391     return $this->db_handle->quote($input, $type);
392     }
393
394
395   /**
396    * Quotes a string so it can be safely used as a table or column name
397    *
398    * @param  string  Value to quote
399    * @return string  Quoted string for use in query
400    * @deprecated     Replaced by rcube_MDB2::quote_identifier
401    * @see            rcube_MDB2::quote_identifier
402    * @access public
403    */
404   function quoteIdentifier($str)
405         {
406     return $this->quote_identifier($str);
407         }
408
409
410   /**
411    * Quotes a string so it can be safely used as a table or column name
412    *
413    * @param  string  Value to quote
414    * @return string  Quoted string for use in query
415    * @access public
416    */
417   function quote_identifier($str)
418     {
419     if (!$this->db_handle)
420       $this->db_connect('r');
421
422     return $this->db_handle->quoteIdentifier($str);
423     }
424
425   /**
426    * Escapes a string
427    *
428    * @param  string  The string to be escaped
429    * @return string  The escaped string
430    * @access public
431    * @since  0.1.1
432    */
433   function escapeSimple($str)
434     {
435     if (!$this->db_handle)
436       $this->db_connect('r');
437    
438     return $this->db_handle->escape($str);
439     }
440
441
442   /**
443    * Return SQL function for current time and date
444    *
445    * @return string SQL function to use in query
446    * @access public
447    */
448   function now()
449     {
450     switch($this->db_provider)
451       {
452       case 'mssql':
453         return "getdate()";
454
455       default:
456         return "now()";
457       }
458     }
459
460
461   /**
462    * Return SQL statement to convert a field value into a unix timestamp
463    *
464    * @param  string  Field name
465    * @return string  SQL statement to use in query
466    * @access public
467    */
468   function unixtimestamp($field)
469     {
470     switch($this->db_provider)
471       {
472       case 'pgsql':
473         return "EXTRACT (EPOCH FROM $field)";
474         break;
475
476       case 'mssql':
477         return "datediff(s, '1970-01-01 00:00:00', $field)";
478
479       default:
480         return "UNIX_TIMESTAMP($field)";
481       }
482     }
483
484
485   /**
486    * Return SQL statement to convert from a unix timestamp
487    *
488    * @param  string  Field name
489    * @return string  SQL statement to use in query
490    * @access public
491    */
492   function fromunixtime($timestamp)
493     {
494     switch($this->db_provider)
495       {
496       case 'mysqli':
497       case 'mysql':
498       case 'sqlite':
499         return sprintf("FROM_UNIXTIME(%d)", $timestamp);
500
501       default:
502         return date("'Y-m-d H:i:s'", $timestamp);
503       }
504     }
505
506
507   /**
508    * Return SQL statement for case insensitive LIKE
509    *
510    * @param  string  Field name
511    * @param  string  Search value
512    * @return string  SQL statement to use in query
513    * @access public
514    */
515   function ilike($column, $value)
516     {
517     // TODO: use MDB2's matchPattern() function
518     switch($this->db_provider)
519       {
520       case 'pgsql':
521         return $this->quote_identifier($column).' ILIKE '.$this->quote($value);
522       default:
523         return $this->quote_identifier($column).' LIKE '.$this->quote($value);
524       }
525     }
526
527
528   /**
529    * Adds a query result and returns a handle ID
530    *
531    * @param  object  Query handle
532    * @return mixed   Handle ID or FALE on failure
533    * @access private
534    */
535   function _add_result($res)
536     {
537     // sql error occured
538     if (PEAR::isError($res))
539       {
540       raise_error(array('code' => 500, 'type' => 'db', 'line' => __LINE__, 'file' => __FILE__,
541                         'message' => $res->getMessage() . " Query: " . substr(preg_replace('/[\r\n]+\s*/', ' ', $res->userinfo), 0, 512)), TRUE, FALSE);
542       return FALSE;
543       }
544     else
545       {
546       $res_id = sizeof($this->a_query_results);
547       $this->a_query_results[$res_id] = $res;
548       $this->last_res_id = $res_id;
549       return $res_id;
550       }
551     }
552
553
554   /**
555    * Resolves a given handle ID and returns the according query handle
556    * If no ID is specified, the last ressource handle will be returned
557    *
558    * @param  number  Handle ID
559    * @return mixed   Ressource handle or FALE on failure
560    * @access private
561    */
562   function _get_result($res_id=NULL)
563     {
564     if ($res_id==NULL)
565       $res_id = $this->last_res_id;
566
567      if ($res_id && isset($this->a_query_results[$res_id]))
568        return $this->a_query_results[$res_id];
569      else
570        return FALSE;
571     }
572
573
574   /**
575    * Create a sqlite database from a file
576    *
577    * @param  object  SQLite database handle
578    * @param  string  File path to use for DB creation
579    * @access private
580    */
581   function _sqlite_create_database($dbh, $file_name)
582     {
583     if (empty($file_name) || !is_string($file_name))
584       return;
585
586     $data = file_get_contents($file_name);
587
588     if (strlen($data))
589       sqlite_exec($dbh->connection, $data);
590     }
591
592
593   /**
594    * Add some proprietary database functions to the current SQLite handle
595    * in order to make it MySQL compatible
596    *
597    * @access private
598    */
599   function _sqlite_prepare()
600     {
601     include_once('include/rcube_sqlite.inc');
602
603     // we emulate via callback some missing MySQL function
604     sqlite_create_function($this->db_handle->connection, "from_unixtime", "rcube_sqlite_from_unixtime");
605     sqlite_create_function($this->db_handle->connection, "unix_timestamp", "rcube_sqlite_unix_timestamp");
606     sqlite_create_function($this->db_handle->connection, "now", "rcube_sqlite_now");
607     sqlite_create_function($this->db_handle->connection, "md5", "rcube_sqlite_md5");
608     }
609
610
611   }  // end class rcube_db
612
613
614 /* this is our own debug handler for the MDB2 connection */
615 function mdb2_debug_handler(&$db, $scope, $message, $context = array())
616 {
617   if ($scope != 'prepare')
618   {
619     $debug_output = $scope . '('.$db->db_index.'): ';
620     $debug_output .= $message . $db->getOption('log_line_break');
621     write_log('sqllog', $debug_output);
622   }
623 }
624
625