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