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