alecpl
2009-05-02 50d515a7d690f6017d97017f1370705e1a214c27
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                     |
cbbef3 8  | Copyright (C) 2005-2009, 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
9f23f0 81     $db_options = array(
981472 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',
9f23f0 86         'portability' => MDB2_PORTABILITY_ALL ^ MDB2_PORTABILITY_EMPTY_TO_NULL);
A 87
88     if ($this->db_provider == 'pgsql') {
e905db 89       $db_options['disable_smart_seqname'] = true;
T 90       $db_options['seqname_format'] = '%s';
91     }
9f23f0 92
A 93     $dbh = MDB2::connect($dsn, $db_options);
d2a9db 94
7eaf7a 95     if (MDB2::isError($dbh))
d2a9db 96       {
T 97       $this->db_error = TRUE;
98       $this->db_error_msg = $dbh->getMessage();
99       
7eaf7a 100       raise_error(array('code' => 500, 'type' => 'db', 'line' => __LINE__,
T 101         'file' => __FILE__, 'message' => $dbh->getUserInfo()), TRUE, FALSE);
d2a9db 102       }
T 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       }
b66d40 109     else
T 110       $dbh->setCharset('utf8');
d2a9db 111
T 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
981472 152
T 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)
0ce1a6 162     {
981472 163       $this->db_handle->setOption('debug', $dbg);
0ce1a6 164       $this->db_handle->setOption('emulate_prepared', $dbg);
T 165     }
981472 166   }
d2a9db 167
T 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 ?
bac356 231     if (strtolower(substr(trim($query),0,6))=='select')
d2a9db 232       $mode='r';
T 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))
f45ec7 251         {
d2a9db 252         $this->db_error = TRUE;
T 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);
f45ec7 257         }
d2a9db 258       else
f45ec7 259         {
d2a9db 260         $result = $q->execute($params);
T 261         $q->free();
f45ec7 262         }
d2a9db 263       }
T 264
265     // add result, even if it's an error
266     return $this->_add_result($result);
f45ec7 267     }
S 268
ccfda8 269
d2a9db 270   /**
T 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)
10a699 279     {
d2a9db 280     if (!$this->db_handle)
T 281       return FALSE;
ccfda8 282
d2a9db 283     if ($result = $this->_get_result($res_id))
T 284       return $result->numRows();
285     else
286       return FALSE;
10a699 287     }
T 288
289
d2a9db 290   /**
9c5bee 291    * Get number of affected rows for the last query
d2a9db 292    *
9c5bee 293    * @param  number  Optional query handle identifier
d2a9db 294    * @return mixed   Number of rows or FALSE on failure
T 295    * @access public
296    */
9c5bee 297   function affected_rows($res_id = null)
d2a9db 298     {
T 299     if (!$this->db_handle)
300       return FALSE;
ccfda8 301
9c5bee 302     return (int) $this->_get_result($res_id);
d2a9db 303     }
ccfda8 304
d2a9db 305
T 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   /**
9c5bee 354    * Get col values for a result row
d2a9db 355    *
T 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     {
9c5bee 363     if ($result === FALSE || PEAR::isError($result))
d2a9db 364       return FALSE;
T 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
9c5bee 398    * @see            rcube_mdb2::quote_identifier
d2a9db 399    * @access public
T 400    */
401   function quoteIdentifier($str)
9c5bee 402     {
d2a9db 403     return $this->quote_identifier($str);
9c5bee 404     }
ccfda8 405
10a699 406
d2a9db 407   /**
T 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)
f45ec7 415     {
d2a9db 416     if (!$this->db_handle)
T 417       $this->db_connect('r');
418
419     return $this->db_handle->quoteIdentifier($str);
f45ec7 420     }
S 421
8f4dcb 422   /**
T 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
f45ec7 438
d2a9db 439   /**
7139e3 440    * Return SQL function for current time and date
T 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   /**
d2a9db 459    * Return SQL statement to convert a field value into a unix timestamp
T 460    *
461    * @param  string  Field name
462    * @return string  SQL statement to use in query
463    * @access public
464    */
465   function unixtimestamp($field)
f45ec7 466     {
d2a9db 467     switch($this->db_provider)
T 468       {
469       case 'pgsql':
470         return "EXTRACT (EPOCH FROM $field)";
471         break;
472
7139e3 473       case 'mssql':
T 474         return "datediff(s, '1970-01-01 00:00:00', $field)";
475
d2a9db 476       default:
T 477         return "UNIX_TIMESTAMP($field)";
478       }
f45ec7 479     }
S 480
481
d2a9db 482   /**
T 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)
f45ec7 490     {
d2a9db 491     switch($this->db_provider)
T 492       {
493       case 'mysqli':
494       case 'mysql':
495       case 'sqlite':
3978ab 496         return sprintf("FROM_UNIXTIME(%d)", $timestamp);
f45ec7 497
d2a9db 498       default:
T 499         return date("'Y-m-d H:i:s'", $timestamp);
500       }
f45ec7 501     }
S 502
d2a9db 503
T 504   /**
d8d416 505    * Return SQL statement for case insensitive LIKE
A 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   /**
d2a9db 526    * Adds a query result and returns a handle ID
T 527    *
528    * @param  object  Query handle
9c5bee 529    * @return mixed   Handle ID
d2a9db 530    * @access private
T 531    */
532   function _add_result($res)
f45ec7 533     {
d2a9db 534     // sql error occured
T 535     if (PEAR::isError($res))
536       {
9c5bee 537       $this->db_error = TRUE;
A 538       $this->db_error_msg = $res->getMessage();
d2a9db 539       raise_error(array('code' => 500, 'type' => 'db', 'line' => __LINE__, 'file' => __FILE__,
9c5bee 540             'message' => $res->getMessage() . " Query: " 
A 541         . substr(preg_replace('/[\r\n]+\s*/', ' ', $res->userinfo), 0, 512)),
542         TRUE, FALSE);
d2a9db 543       }
9c5bee 544     
A 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;
f45ec7 549     }
d2a9db 550
T 551
552   /**
553    * Resolves a given handle ID and returns the according query handle
9c5bee 554    * If no ID is specified, the last resource handle will be returned
d2a9db 555    *
T 556    * @param  number  Handle ID
9c5bee 557    * @return mixed   Resource handle or FALSE on failure
d2a9db 558    * @access private
T 559    */
560   function _get_result($res_id=NULL)
561     {
562     if ($res_id==NULL)
563       $res_id = $this->last_res_id;
564
9c5bee 565     if (isset($this->a_query_results[$res_id]))
A 566       if (!PEAR::isError($this->a_query_results[$res_id]))
567         return $this->a_query_results[$res_id];
568     
569     return FALSE;
d2a9db 570     }
T 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
ff73e0 585     $data = file_get_contents($file_name);
d2a9db 586
T 587     if (strlen($data))
50d515 588       if (!sqlite_exec($dbh->connection, $data, $error) || MDB2::isError($dbh)) 
A 589         raise_error(array('code' => 500, 'type' => 'db',
590         'line' => __LINE__, 'file' => __FILE__, 'message' => $error), TRUE, FALSE); 
d2a9db 591     }
T 592
593
594   /**
595    * Add some proprietary database functions to the current SQLite handle
596    * in order to make it MySQL compatible
597    *
598    * @access private
599    */
600   function _sqlite_prepare()
601     {
602     include_once('include/rcube_sqlite.inc');
603
604     // we emulate via callback some missing MySQL function
605     sqlite_create_function($this->db_handle->connection, "from_unixtime", "rcube_sqlite_from_unixtime");
606     sqlite_create_function($this->db_handle->connection, "unix_timestamp", "rcube_sqlite_unix_timestamp");
607     sqlite_create_function($this->db_handle->connection, "now", "rcube_sqlite_now");
608     sqlite_create_function($this->db_handle->connection, "md5", "rcube_sqlite_md5");
609     }
610
611
612   }  // end class rcube_db
f45ec7 613
981472 614
T 615 /* this is our own debug handler for the MDB2 connection */
616 function mdb2_debug_handler(&$db, $scope, $message, $context = array())
617 {
618   if ($scope != 'prepare')
619   {
620     $debug_output = $scope . '('.$db->db_index.'): ';
621     $debug_output .= $message . $db->getOption('log_line_break');
622     write_log('sqllog', $debug_output);
623   }
624 }
625
626